Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 179 additions & 11 deletions crates/uv-bin-install/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,11 @@ impl Binary {
.unwrap(),
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_URL}/{name}.ndjson")).unwrap(),
],
Self::Uv => {
vec![
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_URL}/{name}.ndjson"))
.unwrap(),
]
}
Self::Uv => vec![
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_MIRROR}/{name}.ndjson"))
.unwrap(),
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_URL}/{name}.ndjson")).unwrap(),
],
}
}

Expand Down Expand Up @@ -393,11 +392,13 @@ impl RetriableError for Error {

/// Returns `true` if trying an alternative URL makes sense after this error.
///
/// All errors arising from downloading (including streaming during extraction)
/// qualify.
/// Download and streaming failures qualify, as do malformed manifest responses.
fn should_try_next_url(&self) -> bool {
match self {
Self::Download { .. } | Self::ManifestFetch { .. } => true,
Self::Download { .. }
| Self::ManifestFetch { .. }
| Self::ManifestParse(..)
| Self::ManifestUtf8(..) => true,
Self::Stream { .. } => true,
Self::RetriedError { err, .. } => err.should_try_next_url(),
err => {
Expand Down Expand Up @@ -856,13 +857,98 @@ where

#[cfg(test)]
mod tests {
use std::io::Write;
use std::io::{Read, Write};
use std::net::TcpListener;
use uv_client::{BaseClientBuilder, retryable_on_request_failure};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{self, Sender};
use std::thread::JoinHandle;
use std::time::Duration;
use uv_client::{BaseClientBuilder, fetch_with_url_fallback, retryable_on_request_failure};
use uv_redacted::DisplaySafeUrl;

use super::*;

fn spawn_http_server(
Comment thread
zsol marked this conversation as resolved.
Outdated
response: String,
) -> (DisplaySafeUrl, Arc<AtomicUsize>, Sender<()>, JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let addr = listener.local_addr().unwrap();
let requests = Arc::new(AtomicUsize::new(0));
let requests_clone = Arc::clone(&requests);
let (shutdown_tx, shutdown_rx) = mpsc::channel();
let handle = std::thread::spawn(move || {
loop {
if shutdown_rx.try_recv().is_ok() {
return;
}

match listener.accept() {
Ok((mut stream, _)) => {
requests_clone.fetch_add(1, Ordering::SeqCst);
// Drain the request; we don't inspect it in these tests.
let mut buf = [0u8; 4096];
let _ = stream.read(&mut buf);
stream.write_all(response.as_bytes()).unwrap();
return;
}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(10));
}
Err(err) => panic!("failed to accept connection: {err}"),
}
}
});
(
DisplaySafeUrl::parse(&format!("http://{addr}/uv.ndjson")).unwrap(),
requests,
shutdown_tx,
handle,
)
}

fn manifest_response(body: &str) -> String {
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/x-ndjson\r\n\r\n{body}",
body.len()
)
}

fn not_found_response() -> String {
"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n".to_string()
}

fn uv_manifest_line(version: &str, platform: &str) -> String {
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
format!(
Comment thread
zsol marked this conversation as resolved.
"{{\"version\":\"{version}\",\"date\":\"2025-01-01T00:00:00Z\",\"artifacts\":[{{\"platform\":\"{platform}\",\"url\":\"https://github.com/astral-sh/uv/releases/download/{version}/uv-{platform}.{extension}\",\"archive_format\":\"{extension}\"}}]}}\n"
)
}

async fn resolve_version_from_manifest_urls(
urls: &[DisplaySafeUrl],
constraints: Option<&VersionSpecifiers>,
) -> Result<ResolvedVersion, Error> {
let platform = Platform::from_env().unwrap();
let platform_name = platform.as_cargo_dist_triple();
let client_builder = BaseClientBuilder::default().retries(0);
let retry_policy = client_builder.retry_policy();
let client = client_builder.build();

fetch_with_url_fallback(urls, retry_policy, "manifest for `uv`", |url| {
fetch_and_find_matching_version(
Binary::Uv,
constraints,
None,
&platform_name,
url,
&client,
)
})
.await
}

#[test]
fn test_uv_download_urls() {
let urls = Binary::Uv
Expand All @@ -886,6 +972,88 @@ mod tests {
);
}

#[tokio::test]
async fn test_manifest_falls_back_on_404() {
let platform = Platform::from_env().unwrap();
let platform_name = platform.as_cargo_dist_triple();
let (mirror_url, mirror_requests, mirror_shutdown, mirror_handle) =
spawn_http_server(not_found_response());
let (canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
spawn_http_server(manifest_response(&uv_manifest_line(
"1.2.3",
&platform_name,
)));

let resolved = resolve_version_from_manifest_urls(&[mirror_url, canonical_url], None)
.await
.expect("404 from mirror should fall back to canonical manifest");

let _ = mirror_shutdown.send(());
let _ = canonical_shutdown.send(());
mirror_handle.join().unwrap();
canonical_handle.join().unwrap();

assert_eq!(resolved.version, Version::new([1, 2, 3]));
assert_eq!(mirror_requests.load(Ordering::SeqCst), 1);
assert_eq!(canonical_requests.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn test_manifest_falls_back_on_parse_error() {
let platform = Platform::from_env().unwrap();
let platform_name = platform.as_cargo_dist_triple();
let (mirror_url, mirror_requests, mirror_shutdown, mirror_handle) =
spawn_http_server(manifest_response("{not json}\n"));
let (canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
spawn_http_server(manifest_response(&uv_manifest_line(
"1.2.3",
&platform_name,
)));

let resolved = resolve_version_from_manifest_urls(&[mirror_url, canonical_url], None)
.await
.expect("parse failure from mirror should fall back to canonical manifest");

let _ = mirror_shutdown.send(());
let _ = canonical_shutdown.send(());
mirror_handle.join().unwrap();
canonical_handle.join().unwrap();

assert_eq!(resolved.version, Version::new([1, 2, 3]));
assert_eq!(mirror_requests.load(Ordering::SeqCst), 1);
assert_eq!(canonical_requests.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn test_manifest_no_matching_version_does_not_fallback() {
let platform = Platform::from_env().unwrap();
let platform_name = platform.as_cargo_dist_triple();
let (mirror_url, mirror_requests, mirror_shutdown, mirror_handle) = spawn_http_server(
manifest_response(&uv_manifest_line("1.2.3", &platform_name)),
);
let (canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
spawn_http_server(manifest_response(&uv_manifest_line(
"9.9.9",
&platform_name,
)));
let constraints =
VersionSpecifiers::from(VersionSpecifier::equals_version(Version::new([9, 9, 9])));

let err =
resolve_version_from_manifest_urls(&[mirror_url, canonical_url], Some(&constraints))
.await
.expect_err("no matching version should not fall back to canonical manifest");

let _ = mirror_shutdown.send(());
let _ = canonical_shutdown.send(());
mirror_handle.join().unwrap();
canonical_handle.join().unwrap();

assert!(matches!(err, Error::NoMatchingVersion { .. }));
assert_eq!(mirror_requests.load(Ordering::SeqCst), 1);
assert_eq!(canonical_requests.load(Ordering::SeqCst), 0);
}

/// Verify that `should_try_next_url` returns `true` even for streaming errors
/// that `retryable_on_request_failure` does not recognise as transient.
///
Expand Down
Loading