mirror of https://github.com/astral-sh/uv
Add tests for IO Error retries (#13627)
Often, HTTP requests don't fail due to server errors, but from spurious network errors such as connection resets. reqwest surfaces these as `io::Error`, and we have to handle their retrying separately. Companion PR: https://github.com/LukeMathWalker/wiremock-rs/pull/159
This commit is contained in:
parent
62ed17b230
commit
e10881d49c
|
|
@ -6740,8 +6740,7 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wiremock"
|
name = "wiremock"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/astral-sh/wiremock-rs?rev=b79b69f62521df9f83a54e866432397562eae789#b79b69f62521df9f83a54e866432397562eae789"
|
||||||
checksum = "101681b74cd87b5899e87bcf5a64e83334dd313fcd3053ea72e6dba18928e301"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ windows-core = { version = "0.59.0" }
|
||||||
windows-registry = { version = "0.5.0" }
|
windows-registry = { version = "0.5.0" }
|
||||||
windows-result = { version = "0.3.0" }
|
windows-result = { version = "0.3.0" }
|
||||||
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] }
|
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] }
|
||||||
wiremock = { version = "0.6.2" }
|
wiremock = { git = "https://github.com/astral-sh/wiremock-rs", rev = "b79b69f62521df9f83a54e866432397562eae789" }
|
||||||
xz2 = { version = "0.1.7" }
|
xz2 = { version = "0.1.7" }
|
||||||
zip = { version = "2.2.3", default-features = false, features = ["deflate", "zstd", "bzip2", "lzma", "xz"] }
|
zip = { version = "2.2.3", default-features = false, features = ["deflate", "zstd", "bzip2", "lzma", "xz"] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::env;
|
use std::{env, io};
|
||||||
|
|
||||||
use assert_fs::fixture::{FileWriteStr, PathChild};
|
use assert_fs::fixture::{ChildPath, FileWriteStr, PathChild};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use wiremock::matchers::method;
|
use wiremock::matchers::method;
|
||||||
|
|
@ -8,24 +8,47 @@ use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||||
|
|
||||||
use crate::common::{TestContext, uv_snapshot};
|
use crate::common::{TestContext, uv_snapshot};
|
||||||
|
|
||||||
/// Check the simple index error message when the server returns HTTP status 500, a retryable error.
|
fn connection_reset(_request: &wiremock::Request) -> io::Error {
|
||||||
#[tokio::test]
|
io::Error::new(io::ErrorKind::ConnectionReset, "Connection reset by peer")
|
||||||
async fn simple_http_500() {
|
}
|
||||||
let context = TestContext::new("3.12");
|
|
||||||
|
|
||||||
|
/// Answers with a retryable HTTP status 500.
|
||||||
|
async fn http_error_server() -> (MockServer, String) {
|
||||||
let server = MockServer::start().await;
|
let server = MockServer::start().await;
|
||||||
Mock::given(method("GET"))
|
Mock::given(method("GET"))
|
||||||
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
|
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
|
||||||
.mount(&server)
|
.mount(&server)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mock_server_uri = server.uri();
|
let mock_server_uri = server.uri();
|
||||||
|
(server, mock_server_uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Answers with a retryable connection reset IO error.
|
||||||
|
async fn io_error_server() -> (MockServer, String) {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.respond_with_err(connection_reset)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mock_server_uri = server.uri();
|
||||||
|
(server, mock_server_uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the simple index error message when the server returns HTTP status 500, a retryable error.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn simple_http_500() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
|
||||||
|
|
||||||
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
uv_snapshot!(filters, context
|
uv_snapshot!(filters, context
|
||||||
.pip_install()
|
.pip_install()
|
||||||
.arg("tqdm")
|
.arg("tqdm")
|
||||||
.arg("--index-url")
|
.arg("--index-url")
|
||||||
.arg(server.uri()), @r"
|
.arg(&mock_server_uri), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -36,17 +59,38 @@ async fn simple_http_500() {
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check the simple index error message when the server returns a retryable IO error.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn simple_io_err() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
|
||||||
|
|
||||||
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
|
uv_snapshot!(filters, context
|
||||||
|
.pip_install()
|
||||||
|
.arg("tqdm")
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg(&mock_server_uri), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to fetch: `[SERVER]/tqdm/`
|
||||||
|
Caused by: Request failed after 3 retries
|
||||||
|
Caused by: error sending request for url ([SERVER]/tqdm/)
|
||||||
|
Caused by: client error (SendRequest)
|
||||||
|
Caused by: connection closed before message completed
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
/// Check the find links error message when the server returns HTTP status 500, a retryable error.
|
/// Check the find links error message when the server returns HTTP status 500, a retryable error.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn find_links_http_500() {
|
async fn find_links_http_500() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
let server = MockServer::start().await;
|
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
|
||||||
Mock::given(method("GET"))
|
|
||||||
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
let mock_server_uri = server.uri();
|
|
||||||
|
|
||||||
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
uv_snapshot!(filters, context
|
uv_snapshot!(filters, context
|
||||||
|
|
@ -54,7 +98,7 @@ async fn find_links_http_500() {
|
||||||
.arg("tqdm")
|
.arg("tqdm")
|
||||||
.arg("--no-index")
|
.arg("--no-index")
|
||||||
.arg("--find-links")
|
.arg("--find-links")
|
||||||
.arg(server.uri()), @r"
|
.arg(&mock_server_uri), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -66,18 +110,41 @@ async fn find_links_http_500() {
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check the find links error message when the server returns a retryable IO error.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn find_links_io_error() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
|
||||||
|
|
||||||
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
|
uv_snapshot!(filters, context
|
||||||
|
.pip_install()
|
||||||
|
.arg("tqdm")
|
||||||
|
.arg("--no-index")
|
||||||
|
.arg("--find-links")
|
||||||
|
.arg(&mock_server_uri), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to read `--find-links` URL: [SERVER]/
|
||||||
|
Caused by: Failed to fetch: `[SERVER]/`
|
||||||
|
Caused by: Request failed after 3 retries
|
||||||
|
Caused by: error sending request for url ([SERVER]/)
|
||||||
|
Caused by: client error (SendRequest)
|
||||||
|
Caused by: connection closed before message completed
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
/// Check the direct package URL error message when the server returns HTTP status 500, a retryable
|
/// Check the direct package URL error message when the server returns HTTP status 500, a retryable
|
||||||
/// error.
|
/// error.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn direct_url_http_500() {
|
async fn direct_url_http_500() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
let server = MockServer::start().await;
|
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
|
||||||
Mock::given(method("GET"))
|
|
||||||
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
let mock_server_uri = server.uri();
|
|
||||||
|
|
||||||
let tqdm_url = format!(
|
let tqdm_url = format!(
|
||||||
"{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"
|
"{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"
|
||||||
|
|
@ -97,22 +164,35 @@ async fn direct_url_http_500() {
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check the Python install error message when the server returns HTTP status 500, a retryable
|
/// Check the direct package URL error message when the server returns a retryable IO error.
|
||||||
/// error.
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn python_install_http_500() {
|
async fn direct_url_io_error() {
|
||||||
let context = TestContext::new("3.12")
|
let context = TestContext::new("3.12");
|
||||||
.with_filtered_python_keys()
|
|
||||||
.with_filtered_exe_suffix()
|
|
||||||
.with_managed_python_dirs();
|
|
||||||
|
|
||||||
let server = MockServer::start().await;
|
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
|
||||||
Mock::given(method("GET"))
|
|
||||||
.respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR))
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
let mock_server_uri = server.uri();
|
|
||||||
|
|
||||||
|
let tqdm_url = format!(
|
||||||
|
"{mock_server_uri}/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl"
|
||||||
|
);
|
||||||
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
|
uv_snapshot!(filters, context
|
||||||
|
.pip_install()
|
||||||
|
.arg(format!("tqdm @ {tqdm_url}")), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
× Failed to download `tqdm @ [SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl`
|
||||||
|
├─▶ Failed to fetch: `[SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl`
|
||||||
|
├─▶ Request failed after 3 retries
|
||||||
|
├─▶ error sending request for url ([SERVER]/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl)
|
||||||
|
├─▶ client error (SendRequest)
|
||||||
|
╰─▶ connection closed before message completed
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_python_downloads_json(context: &TestContext, mock_server_uri: &String) -> ChildPath {
|
||||||
let python_downloads_json = context.temp_dir.child("python_downloads.json");
|
let python_downloads_json = context.temp_dir.child("python_downloads.json");
|
||||||
let interpreter = json!({
|
let interpreter = json!({
|
||||||
"cpython-3.10.0-darwin-aarch64-none": {
|
"cpython-3.10.0-darwin-aarch64-none": {
|
||||||
|
|
@ -135,6 +215,21 @@ async fn python_install_http_500() {
|
||||||
python_downloads_json
|
python_downloads_json
|
||||||
.write_str(&serde_json::to_string(&interpreter).unwrap())
|
.write_str(&serde_json::to_string(&interpreter).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
python_downloads_json
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the Python install error message when the server returns HTTP status 500, a retryable
|
||||||
|
/// error.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn python_install_http_500() {
|
||||||
|
let context = TestContext::new("3.12")
|
||||||
|
.with_filtered_python_keys()
|
||||||
|
.with_filtered_exe_suffix()
|
||||||
|
.with_managed_python_dirs();
|
||||||
|
|
||||||
|
let (_server_drop_guard, mock_server_uri) = http_error_server().await;
|
||||||
|
|
||||||
|
let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri);
|
||||||
|
|
||||||
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
uv_snapshot!(filters, context
|
uv_snapshot!(filters, context
|
||||||
|
|
@ -152,3 +247,35 @@ async fn python_install_http_500() {
|
||||||
Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst)
|
Caused by: HTTP status server error (500 Internal Server Error) for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst)
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check the Python install error message when the server returns a retryable IO error.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn python_install_io_error() {
|
||||||
|
let context = TestContext::new("3.12")
|
||||||
|
.with_filtered_python_keys()
|
||||||
|
.with_filtered_exe_suffix()
|
||||||
|
.with_managed_python_dirs();
|
||||||
|
|
||||||
|
let (_server_drop_guard, mock_server_uri) = io_error_server().await;
|
||||||
|
|
||||||
|
let python_downloads_json = write_python_downloads_json(&context, &mock_server_uri);
|
||||||
|
|
||||||
|
let filters = vec![(mock_server_uri.as_str(), "[SERVER]")];
|
||||||
|
uv_snapshot!(filters, context
|
||||||
|
.python_install()
|
||||||
|
.arg("cpython-3.10.0-darwin-aarch64-none")
|
||||||
|
.arg("--python-downloads-json-url")
|
||||||
|
.arg(python_downloads_json.path()), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Failed to install cpython-3.10.0-macos-aarch64-none
|
||||||
|
Caused by: Failed to download [SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst
|
||||||
|
Caused by: Request failed after 3 retries
|
||||||
|
Caused by: error sending request for url ([SERVER]/astral-sh/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst)
|
||||||
|
Caused by: client error (SendRequest)
|
||||||
|
Caused by: connection closed before message completed
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue