diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index ca73428bf..ea63f5125 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -3,6 +3,9 @@ name = "uv-auth" version = "0.0.1" edition = "2021" +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index 008744ece..f62c69d05 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -215,77 +215,4 @@ impl TrieState { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_trie() { - let credentials1 = Arc::new(Credentials::new( - Some("username1".to_string()), - Some("password1".to_string()), - )); - let credentials2 = Arc::new(Credentials::new( - Some("username2".to_string()), - Some("password2".to_string()), - )); - let credentials3 = Arc::new(Credentials::new( - Some("username3".to_string()), - Some("password3".to_string()), - )); - let credentials4 = Arc::new(Credentials::new( - Some("username4".to_string()), - Some("password4".to_string()), - )); - - let mut trie = UrlTrie::new(); - trie.insert( - &Url::parse("https://burntsushi.net").unwrap(), - credentials1.clone(), - ); - trie.insert( - &Url::parse("https://astral.sh").unwrap(), - credentials2.clone(), - ); - trie.insert( - &Url::parse("https://example.com/foo").unwrap(), - credentials3.clone(), - ); - trie.insert( - &Url::parse("https://example.com/bar").unwrap(), - credentials4.clone(), - ); - - let url = Url::parse("https://burntsushi.net/regex-internals").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials1)); - - let url = Url::parse("https://burntsushi.net/").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials1)); - - let url = Url::parse("https://astral.sh/about").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials2)); - - let url = Url::parse("https://example.com/foo").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials3)); - - let url = Url::parse("https://example.com/foo/").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials3)); - - let url = Url::parse("https://example.com/foo/bar").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials3)); - - let url = Url::parse("https://example.com/bar").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials4)); - - let url = Url::parse("https://example.com/bar/").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials4)); - - let url = Url::parse("https://example.com/bar/foo").unwrap(); - assert_eq!(trie.get(&url), Some(&credentials4)); - - let url = Url::parse("https://example.com/about").unwrap(); - assert_eq!(trie.get(&url), None); - - let url = Url::parse("https://example.com/foobar").unwrap(); - assert_eq!(trie.get(&url), None); - } -} +mod tests; diff --git a/crates/uv-auth/src/cache/tests.rs b/crates/uv-auth/src/cache/tests.rs new file mode 100644 index 000000000..2975e0e23 --- /dev/null +++ b/crates/uv-auth/src/cache/tests.rs @@ -0,0 +1,72 @@ +use super::*; + +#[test] +fn test_trie() { + let credentials1 = Arc::new(Credentials::new( + Some("username1".to_string()), + Some("password1".to_string()), + )); + let credentials2 = Arc::new(Credentials::new( + Some("username2".to_string()), + Some("password2".to_string()), + )); + let credentials3 = Arc::new(Credentials::new( + Some("username3".to_string()), + Some("password3".to_string()), + )); + let credentials4 = Arc::new(Credentials::new( + Some("username4".to_string()), + Some("password4".to_string()), + )); + + let mut trie = UrlTrie::new(); + trie.insert( + &Url::parse("https://burntsushi.net").unwrap(), + credentials1.clone(), + ); + trie.insert( + &Url::parse("https://astral.sh").unwrap(), + credentials2.clone(), + ); + trie.insert( + &Url::parse("https://example.com/foo").unwrap(), + credentials3.clone(), + ); + trie.insert( + &Url::parse("https://example.com/bar").unwrap(), + credentials4.clone(), + ); + + let url = Url::parse("https://burntsushi.net/regex-internals").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials1)); + + let url = Url::parse("https://burntsushi.net/").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials1)); + + let url = Url::parse("https://astral.sh/about").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials2)); + + let url = Url::parse("https://example.com/foo").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials3)); + + let url = Url::parse("https://example.com/foo/").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials3)); + + let url = Url::parse("https://example.com/foo/bar").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials3)); + + let url = Url::parse("https://example.com/bar").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials4)); + + let url = Url::parse("https://example.com/bar/").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials4)); + + let url = Url::parse("https://example.com/bar/foo").unwrap(); + assert_eq!(trie.get(&url), Some(&credentials4)); + + let url = Url::parse("https://example.com/about").unwrap(); + assert_eq!(trie.get(&url), None); + + let url = Url::parse("https://example.com/foobar").unwrap(); + assert_eq!(trie.get(&url), None); +} diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index 0c301dcec..69174f37b 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -230,111 +230,4 @@ impl Credentials { } #[cfg(test)] -mod test { - use insta::assert_debug_snapshot; - - use super::*; - - #[test] - fn from_url_no_credentials() { - let url = &Url::parse("https://example.com/simple/first/").unwrap(); - assert_eq!(Credentials::from_url(url), None); - } - - #[test] - fn from_url_username_and_password() { - let url = &Url::parse("https://example.com/simple/first/").unwrap(); - let mut auth_url = url.clone(); - auth_url.set_username("user").unwrap(); - auth_url.set_password(Some("password")).unwrap(); - let credentials = Credentials::from_url(&auth_url).unwrap(); - assert_eq!(credentials.username(), Some("user")); - assert_eq!(credentials.password(), Some("password")); - } - - #[test] - fn from_url_no_username() { - let url = &Url::parse("https://example.com/simple/first/").unwrap(); - let mut auth_url = url.clone(); - auth_url.set_password(Some("password")).unwrap(); - let credentials = Credentials::from_url(&auth_url).unwrap(); - assert_eq!(credentials.username(), None); - assert_eq!(credentials.password(), Some("password")); - } - - #[test] - fn from_url_no_password() { - let url = &Url::parse("https://example.com/simple/first/").unwrap(); - let mut auth_url = url.clone(); - auth_url.set_username("user").unwrap(); - let credentials = Credentials::from_url(&auth_url).unwrap(); - assert_eq!(credentials.username(), Some("user")); - assert_eq!(credentials.password(), None); - } - - #[test] - fn authenticated_request_from_url() { - let url = Url::parse("https://example.com/simple/first/").unwrap(); - let mut auth_url = url.clone(); - auth_url.set_username("user").unwrap(); - auth_url.set_password(Some("password")).unwrap(); - let credentials = Credentials::from_url(&auth_url).unwrap(); - - let mut request = reqwest::Request::new(reqwest::Method::GET, url); - request = credentials.authenticate(request); - - let mut header = request - .headers() - .get(reqwest::header::AUTHORIZATION) - .expect("Authorization header should be set") - .clone(); - header.set_sensitive(false); - - assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZA==""###); - assert_eq!(Credentials::from_header_value(&header), Some(credentials)); - } - - #[test] - fn authenticated_request_from_url_with_percent_encoded_user() { - let url = Url::parse("https://example.com/simple/first/").unwrap(); - let mut auth_url = url.clone(); - auth_url.set_username("user@domain").unwrap(); - auth_url.set_password(Some("password")).unwrap(); - let credentials = Credentials::from_url(&auth_url).unwrap(); - - let mut request = reqwest::Request::new(reqwest::Method::GET, url); - request = credentials.authenticate(request); - - let mut header = request - .headers() - .get(reqwest::header::AUTHORIZATION) - .expect("Authorization header should be set") - .clone(); - header.set_sensitive(false); - - assert_debug_snapshot!(header, @r###""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""###); - assert_eq!(Credentials::from_header_value(&header), Some(credentials)); - } - - #[test] - fn authenticated_request_from_url_with_percent_encoded_password() { - let url = Url::parse("https://example.com/simple/first/").unwrap(); - let mut auth_url = url.clone(); - auth_url.set_username("user").unwrap(); - auth_url.set_password(Some("password==")).unwrap(); - let credentials = Credentials::from_url(&auth_url).unwrap(); - - let mut request = reqwest::Request::new(reqwest::Method::GET, url); - request = credentials.authenticate(request); - - let mut header = request - .headers() - .get(reqwest::header::AUTHORIZATION) - .expect("Authorization header should be set") - .clone(); - header.set_sensitive(false); - - assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZD09""###); - assert_eq!(Credentials::from_header_value(&header), Some(credentials)); - } -} +mod tests; diff --git a/crates/uv-auth/src/credentials/tests.rs b/crates/uv-auth/src/credentials/tests.rs new file mode 100644 index 000000000..dd4e373ae --- /dev/null +++ b/crates/uv-auth/src/credentials/tests.rs @@ -0,0 +1,106 @@ +use insta::assert_debug_snapshot; + +use super::*; + +#[test] +fn from_url_no_credentials() { + let url = &Url::parse("https://example.com/simple/first/").unwrap(); + assert_eq!(Credentials::from_url(url), None); +} + +#[test] +fn from_url_username_and_password() { + let url = &Url::parse("https://example.com/simple/first/").unwrap(); + let mut auth_url = url.clone(); + auth_url.set_username("user").unwrap(); + auth_url.set_password(Some("password")).unwrap(); + let credentials = Credentials::from_url(&auth_url).unwrap(); + assert_eq!(credentials.username(), Some("user")); + assert_eq!(credentials.password(), Some("password")); +} + +#[test] +fn from_url_no_username() { + let url = &Url::parse("https://example.com/simple/first/").unwrap(); + let mut auth_url = url.clone(); + auth_url.set_password(Some("password")).unwrap(); + let credentials = Credentials::from_url(&auth_url).unwrap(); + assert_eq!(credentials.username(), None); + assert_eq!(credentials.password(), Some("password")); +} + +#[test] +fn from_url_no_password() { + let url = &Url::parse("https://example.com/simple/first/").unwrap(); + let mut auth_url = url.clone(); + auth_url.set_username("user").unwrap(); + let credentials = Credentials::from_url(&auth_url).unwrap(); + assert_eq!(credentials.username(), Some("user")); + assert_eq!(credentials.password(), None); +} + +#[test] +fn authenticated_request_from_url() { + let url = Url::parse("https://example.com/simple/first/").unwrap(); + let mut auth_url = url.clone(); + auth_url.set_username("user").unwrap(); + auth_url.set_password(Some("password")).unwrap(); + let credentials = Credentials::from_url(&auth_url).unwrap(); + + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + request = credentials.authenticate(request); + + let mut header = request + .headers() + .get(reqwest::header::AUTHORIZATION) + .expect("Authorization header should be set") + .clone(); + header.set_sensitive(false); + + assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZA==""###); + assert_eq!(Credentials::from_header_value(&header), Some(credentials)); +} + +#[test] +fn authenticated_request_from_url_with_percent_encoded_user() { + let url = Url::parse("https://example.com/simple/first/").unwrap(); + let mut auth_url = url.clone(); + auth_url.set_username("user@domain").unwrap(); + auth_url.set_password(Some("password")).unwrap(); + let credentials = Credentials::from_url(&auth_url).unwrap(); + + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + request = credentials.authenticate(request); + + let mut header = request + .headers() + .get(reqwest::header::AUTHORIZATION) + .expect("Authorization header should be set") + .clone(); + header.set_sensitive(false); + + assert_debug_snapshot!(header, @r###""Basic dXNlckBkb21haW46cGFzc3dvcmQ=""###); + assert_eq!(Credentials::from_header_value(&header), Some(credentials)); +} + +#[test] +fn authenticated_request_from_url_with_percent_encoded_password() { + let url = Url::parse("https://example.com/simple/first/").unwrap(); + let mut auth_url = url.clone(); + auth_url.set_username("user").unwrap(); + auth_url.set_password(Some("password==")).unwrap(); + let credentials = Credentials::from_url(&auth_url).unwrap(); + + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + request = credentials.authenticate(request); + + let mut header = request + .headers() + .get(reqwest::header::AUTHORIZATION) + .expect("Authorization header should be set") + .clone(); + header.set_sensitive(false); + + assert_debug_snapshot!(header, @r###""Basic dXNlcjpwYXNzd29yZD09""###); + assert_eq!(Credentials::from_header_value(&header), Some(credentials)); +} diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs index 16569d269..bc10c10c7 100644 --- a/crates/uv-auth/src/keyring.rs +++ b/crates/uv-auth/src/keyring.rs @@ -151,133 +151,4 @@ impl KeyringProvider { } #[cfg(test)] -mod test { - use super::*; - use futures::FutureExt; - - #[tokio::test] - async fn fetch_url_no_host() { - let url = Url::parse("file:/etc/bin/").unwrap(); - let keyring = KeyringProvider::empty(); - // Panics due to debug assertion; returns `None` in production - let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, "user")) - .catch_unwind() - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn fetch_url_with_password() { - let url = Url::parse("https://user:password@example.com").unwrap(); - let keyring = KeyringProvider::empty(); - // Panics due to debug assertion; returns `None` in production - let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, url.username())) - .catch_unwind() - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn fetch_url_with_no_username() { - let url = Url::parse("https://example.com").unwrap(); - let keyring = KeyringProvider::empty(); - // Panics due to debug assertion; returns `None` in production - let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, url.username())) - .catch_unwind() - .await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn fetch_url_no_auth() { - let url = Url::parse("https://example.com").unwrap(); - let keyring = KeyringProvider::empty(); - let credentials = keyring.fetch(&url, "user"); - assert!(credentials.await.is_none()); - } - - #[tokio::test] - async fn fetch_url() { - let url = Url::parse("https://example.com").unwrap(); - let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]); - assert_eq!( - keyring.fetch(&url, "user").await, - Some(Credentials::new( - Some("user".to_string()), - Some("password".to_string()) - )) - ); - assert_eq!( - keyring.fetch(&url.join("test").unwrap(), "user").await, - Some(Credentials::new( - Some("user".to_string()), - Some("password".to_string()) - )) - ); - } - - #[tokio::test] - async fn fetch_url_no_match() { - let url = Url::parse("https://example.com").unwrap(); - let keyring = KeyringProvider::dummy([(("other.com", "user"), "password")]); - let credentials = keyring.fetch(&url, "user").await; - assert_eq!(credentials, None); - } - - #[tokio::test] - async fn fetch_url_prefers_url_to_host() { - let url = Url::parse("https://example.com/").unwrap(); - let keyring = KeyringProvider::dummy([ - ((url.join("foo").unwrap().as_str(), "user"), "password"), - ((url.host_str().unwrap(), "user"), "other-password"), - ]); - assert_eq!( - keyring.fetch(&url.join("foo").unwrap(), "user").await, - Some(Credentials::new( - Some("user".to_string()), - Some("password".to_string()) - )) - ); - assert_eq!( - keyring.fetch(&url, "user").await, - Some(Credentials::new( - Some("user".to_string()), - Some("other-password".to_string()) - )) - ); - assert_eq!( - keyring.fetch(&url.join("bar").unwrap(), "user").await, - Some(Credentials::new( - Some("user".to_string()), - Some("other-password".to_string()) - )) - ); - } - - #[tokio::test] - async fn fetch_url_username() { - let url = Url::parse("https://example.com").unwrap(); - let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]); - let credentials = keyring.fetch(&url, "user").await; - assert_eq!( - credentials, - Some(Credentials::new( - Some("user".to_string()), - Some("password".to_string()) - )) - ); - } - - #[tokio::test] - async fn fetch_url_username_no_match() { - let url = Url::parse("https://example.com").unwrap(); - let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "foo"), "password")]); - let credentials = keyring.fetch(&url, "bar").await; - assert_eq!(credentials, None); - - // Still fails if we have `foo` in the URL itself - let url = Url::parse("https://foo@example.com").unwrap(); - let credentials = keyring.fetch(&url, "bar").await; - assert_eq!(credentials, None); - } -} +mod tests; diff --git a/crates/uv-auth/src/keyring/tests.rs b/crates/uv-auth/src/keyring/tests.rs new file mode 100644 index 000000000..6b1e8d6e2 --- /dev/null +++ b/crates/uv-auth/src/keyring/tests.rs @@ -0,0 +1,128 @@ +use super::*; +use futures::FutureExt; + +#[tokio::test] +async fn fetch_url_no_host() { + let url = Url::parse("file:/etc/bin/").unwrap(); + let keyring = KeyringProvider::empty(); + // Panics due to debug assertion; returns `None` in production + let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, "user")) + .catch_unwind() + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn fetch_url_with_password() { + let url = Url::parse("https://user:password@example.com").unwrap(); + let keyring = KeyringProvider::empty(); + // Panics due to debug assertion; returns `None` in production + let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, url.username())) + .catch_unwind() + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn fetch_url_with_no_username() { + let url = Url::parse("https://example.com").unwrap(); + let keyring = KeyringProvider::empty(); + // Panics due to debug assertion; returns `None` in production + let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, url.username())) + .catch_unwind() + .await; + assert!(result.is_err()); +} + +#[tokio::test] +async fn fetch_url_no_auth() { + let url = Url::parse("https://example.com").unwrap(); + let keyring = KeyringProvider::empty(); + let credentials = keyring.fetch(&url, "user"); + assert!(credentials.await.is_none()); +} + +#[tokio::test] +async fn fetch_url() { + let url = Url::parse("https://example.com").unwrap(); + let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]); + assert_eq!( + keyring.fetch(&url, "user").await, + Some(Credentials::new( + Some("user".to_string()), + Some("password".to_string()) + )) + ); + assert_eq!( + keyring.fetch(&url.join("test").unwrap(), "user").await, + Some(Credentials::new( + Some("user".to_string()), + Some("password".to_string()) + )) + ); +} + +#[tokio::test] +async fn fetch_url_no_match() { + let url = Url::parse("https://example.com").unwrap(); + let keyring = KeyringProvider::dummy([(("other.com", "user"), "password")]); + let credentials = keyring.fetch(&url, "user").await; + assert_eq!(credentials, None); +} + +#[tokio::test] +async fn fetch_url_prefers_url_to_host() { + let url = Url::parse("https://example.com/").unwrap(); + let keyring = KeyringProvider::dummy([ + ((url.join("foo").unwrap().as_str(), "user"), "password"), + ((url.host_str().unwrap(), "user"), "other-password"), + ]); + assert_eq!( + keyring.fetch(&url.join("foo").unwrap(), "user").await, + Some(Credentials::new( + Some("user".to_string()), + Some("password".to_string()) + )) + ); + assert_eq!( + keyring.fetch(&url, "user").await, + Some(Credentials::new( + Some("user".to_string()), + Some("other-password".to_string()) + )) + ); + assert_eq!( + keyring.fetch(&url.join("bar").unwrap(), "user").await, + Some(Credentials::new( + Some("user".to_string()), + Some("other-password".to_string()) + )) + ); +} + +#[tokio::test] +async fn fetch_url_username() { + let url = Url::parse("https://example.com").unwrap(); + let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "user"), "password")]); + let credentials = keyring.fetch(&url, "user").await; + assert_eq!( + credentials, + Some(Credentials::new( + Some("user".to_string()), + Some("password".to_string()) + )) + ); +} + +#[tokio::test] +async fn fetch_url_username_no_match() { + let url = Url::parse("https://example.com").unwrap(); + let keyring = KeyringProvider::dummy([((url.host_str().unwrap(), "foo"), "password")]); + let credentials = keyring.fetch(&url, "bar").await; + assert_eq!(credentials, None); + + // Still fails if we have `foo` in the URL itself + let url = Url::parse("https://foo@example.com").unwrap(); + let credentials = keyring.fetch(&url, "bar").await; + assert_eq!(credentials, None); +} diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index cd6b17749..c2651c835 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -407,1084 +407,4 @@ impl AuthMiddleware { } #[cfg(test)] -mod tests { - - use std::io::Write; - - use reqwest::Client; - use tempfile::NamedTempFile; - use test_log::test; - - use url::Url; - use wiremock::matchers::{basic_auth, method, path_regex}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - use super::*; - - type Error = Box; - - async fn start_test_server(username: &'static str, password: &'static str) -> MockServer { - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(basic_auth(username, password)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - - server - } - - fn test_client_builder() -> reqwest_middleware::ClientBuilder { - reqwest_middleware::ClientBuilder::new( - Client::builder() - .build() - .expect("Reqwest client should build"), - ) - } - - #[test(tokio::test)] - async fn test_no_credentials() -> Result<(), Error> { - let server = start_test_server("user", "password").await; - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) - .build(); - - assert_eq!( - client - .get(format!("{}/foo", server.uri())) - .send() - .await? - .status(), - 401 - ); - - assert_eq!( - client - .get(format!("{}/bar", server.uri())) - .send() - .await? - .status(), - 401 - ); - - Ok(()) - } - - /// Without seeding the cache, authenticated requests are not cached - #[test(tokio::test)] - async fn test_credentials_in_url_no_seed() -> Result<(), Error> { - let username = "user"; - let password = "password"; - - let server = start_test_server(username, password).await; - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) - .build(); - - let base_url = Url::parse(&server.uri())?; - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some(password)).unwrap(); - assert_eq!(client.get(url).send().await?.status(), 200); - - // Works for a URL without credentials now - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Subsequent requests should not require credentials" - ); - - assert_eq!( - client - .get(format!("{}/foo", server.uri())) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some("invalid")).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Credentials in the URL should take precedence and fail" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_in_url_seed() -> Result<(), Error> { - let username = "user"; - let password = "password"; - - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - let cache = CredentialsCache::new(); - cache.insert( - &base_url, - Arc::new(Credentials::new( - Some(username.to_string()), - Some(password.to_string()), - )), - ); - - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(cache)) - .build(); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some(password)).unwrap(); - assert_eq!(client.get(url).send().await?.status(), 200); - - // Works for a URL without credentials too - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Requests should not require credentials" - ); - - assert_eq!( - client - .get(format!("{}/foo", server.uri())) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some("invalid")).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Credentials in the URL should take precedence and fail" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_in_url_username_only() -> Result<(), Error> { - let username = "user"; - let password = ""; - - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - let cache = CredentialsCache::new(); - cache.insert( - &base_url, - Arc::new(Credentials::new(Some(username.to_string()), None)), - ); - - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(cache)) - .build(); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(None).unwrap(); - assert_eq!(client.get(url).send().await?.status(), 200); - - // Works for a URL without credentials too - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Requests should not require credentials" - ); - - assert_eq!( - client - .get(format!("{}/foo", server.uri())) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some("invalid")).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Credentials in the URL should take precedence and fail" - ); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Subsequent requests should not use the invalid credentials" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_netrc_file_default_host() -> Result<(), Error> { - let username = "user"; - let password = "password"; - - let mut netrc_file = NamedTempFile::new()?; - writeln!(netrc_file, "default login {username} password {password}")?; - - let server = start_test_server(username, password).await; - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_netrc(Netrc::from_file(netrc_file.path()).ok()), - ) - .build(); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Credentials should be pulled from the netrc file" - ); - - let mut url = Url::parse(&server.uri())?; - url.set_username(username).unwrap(); - url.set_password(Some("invalid")).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Credentials in the URL should take precedence and fail" - ); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Subsequent requests should not use the invalid credentials" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_netrc_file_matching_host() -> Result<(), Error> { - let username = "user"; - let password = "password"; - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - - let mut netrc_file = NamedTempFile::new()?; - writeln!( - netrc_file, - r#"machine {} login {username} password {password}"#, - base_url.host_str().unwrap() - )?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_netrc(Some( - Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), - )), - ) - .build(); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Credentials should be pulled from the netrc file" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some("invalid")).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Credentials in the URL should take precedence and fail" - ); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 200, - "Subsequent requests should not use the invalid credentials" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_netrc_file_mismatched_host() -> Result<(), Error> { - let username = "user"; - let password = "password"; - let server = start_test_server(username, password).await; - - let mut netrc_file = NamedTempFile::new()?; - writeln!( - netrc_file, - r#"machine example.com login {username} password {password}"#, - )?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_netrc(Some( - Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), - )), - ) - .build(); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 401, - "Credentials should not be pulled from the netrc file due to host mismatch" - ); - - let mut url = Url::parse(&server.uri())?; - url.set_username(username).unwrap(); - url.set_password(Some(password)).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 200, - "Credentials in the URL should still work" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_netrc_file_mismatched_username() -> Result<(), Error> { - let username = "user"; - let password = "password"; - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - - let mut netrc_file = NamedTempFile::new()?; - writeln!( - netrc_file, - r#"machine {} login {username} password {password}"#, - base_url.host_str().unwrap() - )?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_netrc(Some( - Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), - )), - ) - .build(); - - let mut url = base_url.clone(); - url.set_username("other-user").unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "The netrc password should not be used due to a username mismatch" - ); - - let mut url = base_url.clone(); - url.set_username("user").unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 200, - "The netrc password should be used for a matching user" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_keyring() -> Result<(), Error> { - let username = "user"; - let password = "password"; - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_keyring(Some(KeyringProvider::dummy([( - ( - format!( - "{}:{}", - base_url.host_str().unwrap(), - base_url.port().unwrap() - ), - username, - ), - password, - )]))), - ) - .build(); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 401, - "Credentials are not pulled from the keyring without a username" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 200, - "Credentials for the username should be pulled from the keyring" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - url.set_password(Some("invalid")).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Password in the URL should take precedence and fail" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - assert_eq!( - client.get(url.clone()).send().await?.status(), - 200, - "Subsequent requests should not use the invalid password" - ); - - let mut url = base_url.clone(); - url.set_username("other_user").unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "Credentials are not pulled from the keyring when given another username" - ); - - Ok(()) - } - - /// We include ports in keyring requests, e.g., `localhost:8000` should be distinct from `localhost`, - /// unless the server is running on a default port, e.g., `localhost:80` is equivalent to `localhost`. - /// We don't unit test the latter case because it's possible to collide with a server a developer is - /// actually running. - #[test(tokio::test)] - async fn test_keyring_includes_non_standard_port() -> Result<(), Error> { - let username = "user"; - let password = "password"; - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_keyring(Some(KeyringProvider::dummy([( - // Omit the port from the keyring entry - (base_url.host_str().unwrap(), username), - password, - )]))), - ) - .build(); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 401, - "We should fail because the port is not present in the keyring entry" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_in_keyring_seed() -> Result<(), Error> { - let username = "user"; - let password = "password"; - - let server = start_test_server(username, password).await; - let base_url = Url::parse(&server.uri())?; - let cache = CredentialsCache::new(); - - // Seed _just_ the username. This cache entry should be ignored and we should - // still find a password via the keyring. - cache.insert( - &base_url, - Arc::new(Credentials::new(Some(username.to_string()), None)), - ); - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some( - KeyringProvider::dummy([( - ( - format!( - "{}:{}", - base_url.host_str().unwrap(), - base_url.port().unwrap() - ), - username, - ), - password, - )]), - ))) - .build(); - - assert_eq!( - client.get(server.uri()).send().await?.status(), - 401, - "Credentials are not pulled from the keyring without a username" - ); - - let mut url = base_url.clone(); - url.set_username(username).unwrap(); - assert_eq!( - client.get(url).send().await?.status(), - 200, - "Credentials for the username should be pulled from the keyring" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> { - let username_1 = "user1"; - let password_1 = "password1"; - let server_1 = start_test_server(username_1, password_1).await; - let base_url_1 = Url::parse(&server_1.uri())?; - - let username_2 = "user2"; - let password_2 = "password2"; - let server_2 = start_test_server(username_2, password_2).await; - let base_url_2 = Url::parse(&server_2.uri())?; - - let cache = CredentialsCache::new(); - // Seed the cache with our credentials - cache.insert( - &base_url_1, - Arc::new(Credentials::new( - Some(username_1.to_string()), - Some(password_1.to_string()), - )), - ); - cache.insert( - &base_url_2, - Arc::new(Credentials::new( - Some(username_2.to_string()), - Some(password_2.to_string()), - )), - ); - - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(cache)) - .build(); - - // Both servers should work - assert_eq!( - client.get(server_1.uri()).send().await?.status(), - 200, - "Requests should not require credentials" - ); - assert_eq!( - client.get(server_2.uri()).send().await?.status(), - 200, - "Requests should not require credentials" - ); - - assert_eq!( - client - .get(format!("{}/foo", server_1.uri())) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - assert_eq!( - client - .get(format!("{}/foo", server_2.uri())) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> { - let username_1 = "user1"; - let password_1 = "password1"; - let server_1 = start_test_server(username_1, password_1).await; - let base_url_1 = Url::parse(&server_1.uri())?; - - let username_2 = "user2"; - let password_2 = "password2"; - let server_2 = start_test_server(username_2, password_2).await; - let base_url_2 = Url::parse(&server_2.uri())?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_keyring(Some(KeyringProvider::dummy([ - ( - ( - format!( - "{}:{}", - base_url_1.host_str().unwrap(), - base_url_1.port().unwrap() - ), - username_1, - ), - password_1, - ), - ( - ( - format!( - "{}:{}", - base_url_2.host_str().unwrap(), - base_url_2.port().unwrap() - ), - username_2, - ), - password_2, - ), - ]))), - ) - .build(); - - // Both servers do not work without a username - assert_eq!( - client.get(server_1.uri()).send().await?.status(), - 401, - "Requests should require a username" - ); - assert_eq!( - client.get(server_2.uri()).send().await?.status(), - 401, - "Requests should require a username" - ); - - let mut url_1 = base_url_1.clone(); - url_1.set_username(username_1).unwrap(); - assert_eq!( - client.get(url_1.clone()).send().await?.status(), - 200, - "Requests with a username should succeed" - ); - assert_eq!( - client.get(server_2.uri()).send().await?.status(), - 401, - "Credentials should not be re-used for the second server" - ); - - let mut url_2 = base_url_2.clone(); - url_2.set_username(username_2).unwrap(); - assert_eq!( - client.get(url_2.clone()).send().await?.status(), - 200, - "Requests with a username should succeed" - ); - - assert_eq!( - client.get(format!("{url_1}/foo")).send().await?.status(), - 200, - "Requests can be to different paths in the same realm" - ); - assert_eq!( - client.get(format!("{url_2}/foo")).send().await?.status(), - 200, - "Requests can be to different paths in the same realm" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> { - let username_1 = "user1"; - let password_1 = "password1"; - let username_2 = "user2"; - let password_2 = "password2"; - - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path_regex("/prefix_1.*")) - .and(basic_auth(username_1, password_1)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .and(path_regex("/prefix_2.*")) - .and(basic_auth(username_2, password_2)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - // Create a third, public prefix - // It will throw a 401 if it receives credentials - Mock::given(method("GET")) - .and(path_regex("/prefix_3.*")) - .and(basic_auth(username_1, password_1)) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path_regex("/prefix_3.*")) - .and(basic_auth(username_2, password_2)) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path_regex("/prefix_3.*")) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - - let base_url = Url::parse(&server.uri())?; - let base_url_1 = base_url.join("prefix_1")?; - let base_url_2 = base_url.join("prefix_2")?; - let base_url_3 = base_url.join("prefix_3")?; - - let cache = CredentialsCache::new(); - - // Seed the cache with our credentials - cache.insert( - &base_url_1, - Arc::new(Credentials::new( - Some(username_1.to_string()), - Some(password_1.to_string()), - )), - ); - cache.insert( - &base_url_2, - Arc::new(Credentials::new( - Some(username_2.to_string()), - Some(password_2.to_string()), - )), - ); - - let client = test_client_builder() - .with(AuthMiddleware::new().with_cache(cache)) - .build(); - - // Both servers should work - assert_eq!( - client.get(base_url_1.clone()).send().await?.status(), - 200, - "Requests should not require credentials" - ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 200, - "Requests should not require credentials" - ); - assert_eq!( - client - .get(base_url.join("prefix_1/foo")?) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - assert_eq!( - client - .get(base_url.join("prefix_2/foo")?) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same realm" - ); - assert_eq!( - client - .get(base_url.join("prefix_1_foo")?) - .send() - .await? - .status(), - 401, - "Requests to paths with a matching prefix but different resource segments should fail" - ); - - assert_eq!( - client.get(base_url_3.clone()).send().await?.status(), - 200, - "Requests to the 'public' prefix should not use credentials" - ); - - Ok(()) - } - - #[test(tokio::test)] - async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> { - let username_1 = "user1"; - let password_1 = "password1"; - let username_2 = "user2"; - let password_2 = "password2"; - - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path_regex("/prefix_1.*")) - .and(basic_auth(username_1, password_1)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .and(path_regex("/prefix_2.*")) - .and(basic_auth(username_2, password_2)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - // Create a third, public prefix - // It will throw a 401 if it receives credentials - Mock::given(method("GET")) - .and(path_regex("/prefix_3.*")) - .and(basic_auth(username_1, password_1)) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path_regex("/prefix_3.*")) - .and(basic_auth(username_2, password_2)) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path_regex("/prefix_3.*")) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - - let base_url = Url::parse(&server.uri())?; - let base_url_1 = base_url.join("prefix_1")?; - let base_url_2 = base_url.join("prefix_2")?; - let base_url_3 = base_url.join("prefix_3")?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_keyring(Some(KeyringProvider::dummy([ - ( - ( - format!( - "{}:{}", - base_url_1.host_str().unwrap(), - base_url_1.port().unwrap() - ), - username_1, - ), - password_1, - ), - ( - ( - format!( - "{}:{}", - base_url_2.host_str().unwrap(), - base_url_2.port().unwrap() - ), - username_2, - ), - password_2, - ), - ]))), - ) - .build(); - - // Both servers do not work without a username - assert_eq!( - client.get(base_url_1.clone()).send().await?.status(), - 401, - "Requests should require a username" - ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 401, - "Requests should require a username" - ); - - let mut url_1 = base_url_1.clone(); - url_1.set_username(username_1).unwrap(); - assert_eq!( - client.get(url_1.clone()).send().await?.status(), - 200, - "Requests with a username should succeed" - ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 401, - "Credentials should not be re-used for the second prefix" - ); - - let mut url_2 = base_url_2.clone(); - url_2.set_username(username_2).unwrap(); - assert_eq!( - client.get(url_2.clone()).send().await?.status(), - 200, - "Requests with a username should succeed" - ); - - assert_eq!( - client - .get(base_url.join("prefix_1/foo")?) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same prefix" - ); - assert_eq!( - client - .get(base_url.join("prefix_2/foo")?) - .send() - .await? - .status(), - 200, - "Requests can be to different paths in the same prefix" - ); - assert_eq!( - client - .get(base_url.join("prefix_1_foo")?) - .send() - .await? - .status(), - 401, - "Requests to paths with a matching prefix but different resource segments should fail" - ); - assert_eq!( - client.get(base_url_3.clone()).send().await?.status(), - 200, - "Requests to the 'public' prefix should not use credentials" - ); - - Ok(()) - } - - /// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of - /// credentials for _every_ request URL at the cost of inconsistent behavior when - /// credentials are not scoped to a realm. - #[test(tokio::test)] - async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username( - ) -> Result<(), Error> { - let username = "user"; - let password_1 = "password1"; - let password_2 = "password2"; - - let server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path_regex("/prefix_1.*")) - .and(basic_auth(username, password_1)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .and(path_regex("/prefix_2.*")) - .and(basic_auth(username, password_2)) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(401)) - .mount(&server) - .await; - - let base_url = Url::parse(&server.uri())?; - let base_url_1 = base_url.join("prefix_1")?; - let base_url_2 = base_url.join("prefix_2")?; - - let client = test_client_builder() - .with( - AuthMiddleware::new() - .with_cache(CredentialsCache::new()) - .with_keyring(Some(KeyringProvider::dummy([ - ((base_url_1.clone(), username), password_1), - ((base_url_2.clone(), username), password_2), - ]))), - ) - .build(); - - // Both servers do not work without a username - assert_eq!( - client.get(base_url_1.clone()).send().await?.status(), - 401, - "Requests should require a username" - ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 401, - "Requests should require a username" - ); - - let mut url_1 = base_url_1.clone(); - url_1.set_username(username).unwrap(); - assert_eq!( - client.get(url_1.clone()).send().await?.status(), - 200, - "The first request with a username will succeed" - ); - assert_eq!( - client.get(base_url_2.clone()).send().await?.status(), - 401, - "Credentials should not be re-used for the second prefix" - ); - assert_eq!( - client - .get(base_url.join("prefix_1/foo")?) - .send() - .await? - .status(), - 200, - "Subsequent requests can be to different paths in the same prefix" - ); - - let mut url_2 = base_url_2.clone(); - url_2.set_username(username).unwrap(); - assert_eq!( - client.get(url_2.clone()).send().await?.status(), - 401, // INCORRECT BEHAVIOR - "A request with the same username and realm for a URL that needs a different password will fail" - ); - assert_eq!( - client - .get(base_url.join("prefix_2/foo")?) - .send() - .await? - .status(), - 401, // INCORRECT BEHAVIOR - "Requests to other paths in the failing prefix will also fail" - ); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-auth/src/middleware/tests.rs b/crates/uv-auth/src/middleware/tests.rs new file mode 100644 index 000000000..f4ad7af25 --- /dev/null +++ b/crates/uv-auth/src/middleware/tests.rs @@ -0,0 +1,1079 @@ +use std::io::Write; + +use reqwest::Client; +use tempfile::NamedTempFile; +use test_log::test; + +use url::Url; +use wiremock::matchers::{basic_auth, method, path_regex}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use super::*; + +type Error = Box; + +async fn start_test_server(username: &'static str, password: &'static str) -> MockServer { + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(basic_auth(username, password)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + server +} + +fn test_client_builder() -> reqwest_middleware::ClientBuilder { + reqwest_middleware::ClientBuilder::new( + Client::builder() + .build() + .expect("Reqwest client should build"), + ) +} + +#[test(tokio::test)] +async fn test_no_credentials() -> Result<(), Error> { + let server = start_test_server("user", "password").await; + let client = test_client_builder() + .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) + .build(); + + assert_eq!( + client + .get(format!("{}/foo", server.uri())) + .send() + .await? + .status(), + 401 + ); + + assert_eq!( + client + .get(format!("{}/bar", server.uri())) + .send() + .await? + .status(), + 401 + ); + + Ok(()) +} + +/// Without seeding the cache, authenticated requests are not cached +#[test(tokio::test)] +async fn test_credentials_in_url_no_seed() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let server = start_test_server(username, password).await; + let client = test_client_builder() + .with(AuthMiddleware::new().with_cache(CredentialsCache::new())) + .build(); + + let base_url = Url::parse(&server.uri())?; + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some(password)).unwrap(); + assert_eq!(client.get(url).send().await?.status(), 200); + + // Works for a URL without credentials now + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Subsequent requests should not require credentials" + ); + + assert_eq!( + client + .get(format!("{}/foo", server.uri())) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some("invalid")).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Credentials in the URL should take precedence and fail" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_in_url_seed() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + let cache = CredentialsCache::new(); + cache.insert( + &base_url, + Arc::new(Credentials::new( + Some(username.to_string()), + Some(password.to_string()), + )), + ); + + let client = test_client_builder() + .with(AuthMiddleware::new().with_cache(cache)) + .build(); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some(password)).unwrap(); + assert_eq!(client.get(url).send().await?.status(), 200); + + // Works for a URL without credentials too + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Requests should not require credentials" + ); + + assert_eq!( + client + .get(format!("{}/foo", server.uri())) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some("invalid")).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Credentials in the URL should take precedence and fail" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_in_url_username_only() -> Result<(), Error> { + let username = "user"; + let password = ""; + + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + let cache = CredentialsCache::new(); + cache.insert( + &base_url, + Arc::new(Credentials::new(Some(username.to_string()), None)), + ); + + let client = test_client_builder() + .with(AuthMiddleware::new().with_cache(cache)) + .build(); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(None).unwrap(); + assert_eq!(client.get(url).send().await?.status(), 200); + + // Works for a URL without credentials too + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Requests should not require credentials" + ); + + assert_eq!( + client + .get(format!("{}/foo", server.uri())) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some("invalid")).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Credentials in the URL should take precedence and fail" + ); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Subsequent requests should not use the invalid credentials" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_netrc_file_default_host() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let mut netrc_file = NamedTempFile::new()?; + writeln!(netrc_file, "default login {username} password {password}")?; + + let server = start_test_server(username, password).await; + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_netrc(Netrc::from_file(netrc_file.path()).ok()), + ) + .build(); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Credentials should be pulled from the netrc file" + ); + + let mut url = Url::parse(&server.uri())?; + url.set_username(username).unwrap(); + url.set_password(Some("invalid")).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Credentials in the URL should take precedence and fail" + ); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Subsequent requests should not use the invalid credentials" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_netrc_file_matching_host() -> Result<(), Error> { + let username = "user"; + let password = "password"; + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + + let mut netrc_file = NamedTempFile::new()?; + writeln!( + netrc_file, + r#"machine {} login {username} password {password}"#, + base_url.host_str().unwrap() + )?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_netrc(Some( + Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), + )), + ) + .build(); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Credentials should be pulled from the netrc file" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some("invalid")).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Credentials in the URL should take precedence and fail" + ); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 200, + "Subsequent requests should not use the invalid credentials" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_netrc_file_mismatched_host() -> Result<(), Error> { + let username = "user"; + let password = "password"; + let server = start_test_server(username, password).await; + + let mut netrc_file = NamedTempFile::new()?; + writeln!( + netrc_file, + r#"machine example.com login {username} password {password}"#, + )?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_netrc(Some( + Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), + )), + ) + .build(); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 401, + "Credentials should not be pulled from the netrc file due to host mismatch" + ); + + let mut url = Url::parse(&server.uri())?; + url.set_username(username).unwrap(); + url.set_password(Some(password)).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 200, + "Credentials in the URL should still work" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_netrc_file_mismatched_username() -> Result<(), Error> { + let username = "user"; + let password = "password"; + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + + let mut netrc_file = NamedTempFile::new()?; + writeln!( + netrc_file, + r#"machine {} login {username} password {password}"#, + base_url.host_str().unwrap() + )?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_netrc(Some( + Netrc::from_file(netrc_file.path()).expect("Test has valid netrc file"), + )), + ) + .build(); + + let mut url = base_url.clone(); + url.set_username("other-user").unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "The netrc password should not be used due to a username mismatch" + ); + + let mut url = base_url.clone(); + url.set_username("user").unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 200, + "The netrc password should be used for a matching user" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_keyring() -> Result<(), Error> { + let username = "user"; + let password = "password"; + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([( + ( + format!( + "{}:{}", + base_url.host_str().unwrap(), + base_url.port().unwrap() + ), + username, + ), + password, + )]))), + ) + .build(); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 401, + "Credentials are not pulled from the keyring without a username" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 200, + "Credentials for the username should be pulled from the keyring" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + url.set_password(Some("invalid")).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Password in the URL should take precedence and fail" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + assert_eq!( + client.get(url.clone()).send().await?.status(), + 200, + "Subsequent requests should not use the invalid password" + ); + + let mut url = base_url.clone(); + url.set_username("other_user").unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "Credentials are not pulled from the keyring when given another username" + ); + + Ok(()) +} + +/// We include ports in keyring requests, e.g., `localhost:8000` should be distinct from `localhost`, +/// unless the server is running on a default port, e.g., `localhost:80` is equivalent to `localhost`. +/// We don't unit test the latter case because it's possible to collide with a server a developer is +/// actually running. +#[test(tokio::test)] +async fn test_keyring_includes_non_standard_port() -> Result<(), Error> { + let username = "user"; + let password = "password"; + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([( + // Omit the port from the keyring entry + (base_url.host_str().unwrap(), username), + password, + )]))), + ) + .build(); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 401, + "We should fail because the port is not present in the keyring entry" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_in_keyring_seed() -> Result<(), Error> { + let username = "user"; + let password = "password"; + + let server = start_test_server(username, password).await; + let base_url = Url::parse(&server.uri())?; + let cache = CredentialsCache::new(); + + // Seed _just_ the username. This cache entry should be ignored and we should + // still find a password via the keyring. + cache.insert( + &base_url, + Arc::new(Credentials::new(Some(username.to_string()), None)), + ); + let client = + test_client_builder() + .with(AuthMiddleware::new().with_cache(cache).with_keyring(Some( + KeyringProvider::dummy([( + ( + format!( + "{}:{}", + base_url.host_str().unwrap(), + base_url.port().unwrap() + ), + username, + ), + password, + )]), + ))) + .build(); + + assert_eq!( + client.get(server.uri()).send().await?.status(), + 401, + "Credentials are not pulled from the keyring without a username" + ); + + let mut url = base_url.clone(); + url.set_username(username).unwrap(); + assert_eq!( + client.get(url).send().await?.status(), + 200, + "Credentials for the username should be pulled from the keyring" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_in_url_multiple_realms() -> Result<(), Error> { + let username_1 = "user1"; + let password_1 = "password1"; + let server_1 = start_test_server(username_1, password_1).await; + let base_url_1 = Url::parse(&server_1.uri())?; + + let username_2 = "user2"; + let password_2 = "password2"; + let server_2 = start_test_server(username_2, password_2).await; + let base_url_2 = Url::parse(&server_2.uri())?; + + let cache = CredentialsCache::new(); + // Seed the cache with our credentials + cache.insert( + &base_url_1, + Arc::new(Credentials::new( + Some(username_1.to_string()), + Some(password_1.to_string()), + )), + ); + cache.insert( + &base_url_2, + Arc::new(Credentials::new( + Some(username_2.to_string()), + Some(password_2.to_string()), + )), + ); + + let client = test_client_builder() + .with(AuthMiddleware::new().with_cache(cache)) + .build(); + + // Both servers should work + assert_eq!( + client.get(server_1.uri()).send().await?.status(), + 200, + "Requests should not require credentials" + ); + assert_eq!( + client.get(server_2.uri()).send().await?.status(), + 200, + "Requests should not require credentials" + ); + + assert_eq!( + client + .get(format!("{}/foo", server_1.uri())) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + assert_eq!( + client + .get(format!("{}/foo", server_2.uri())) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_from_keyring_multiple_realms() -> Result<(), Error> { + let username_1 = "user1"; + let password_1 = "password1"; + let server_1 = start_test_server(username_1, password_1).await; + let base_url_1 = Url::parse(&server_1.uri())?; + + let username_2 = "user2"; + let password_2 = "password2"; + let server_2 = start_test_server(username_2, password_2).await; + let base_url_2 = Url::parse(&server_2.uri())?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([ + ( + ( + format!( + "{}:{}", + base_url_1.host_str().unwrap(), + base_url_1.port().unwrap() + ), + username_1, + ), + password_1, + ), + ( + ( + format!( + "{}:{}", + base_url_2.host_str().unwrap(), + base_url_2.port().unwrap() + ), + username_2, + ), + password_2, + ), + ]))), + ) + .build(); + + // Both servers do not work without a username + assert_eq!( + client.get(server_1.uri()).send().await?.status(), + 401, + "Requests should require a username" + ); + assert_eq!( + client.get(server_2.uri()).send().await?.status(), + 401, + "Requests should require a username" + ); + + let mut url_1 = base_url_1.clone(); + url_1.set_username(username_1).unwrap(); + assert_eq!( + client.get(url_1.clone()).send().await?.status(), + 200, + "Requests with a username should succeed" + ); + assert_eq!( + client.get(server_2.uri()).send().await?.status(), + 401, + "Credentials should not be re-used for the second server" + ); + + let mut url_2 = base_url_2.clone(); + url_2.set_username(username_2).unwrap(); + assert_eq!( + client.get(url_2.clone()).send().await?.status(), + 200, + "Requests with a username should succeed" + ); + + assert_eq!( + client.get(format!("{url_1}/foo")).send().await?.status(), + 200, + "Requests can be to different paths in the same realm" + ); + assert_eq!( + client.get(format!("{url_2}/foo")).send().await?.status(), + 200, + "Requests can be to different paths in the same realm" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_in_url_mixed_authentication_in_realm() -> Result<(), Error> { + let username_1 = "user1"; + let password_1 = "password1"; + let username_2 = "user2"; + let password_2 = "password2"; + + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_1.*")) + .and(basic_auth(username_1, password_1)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_2.*")) + .and(basic_auth(username_2, password_2)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + // Create a third, public prefix + // It will throw a 401 if it receives credentials + Mock::given(method("GET")) + .and(path_regex("/prefix_3.*")) + .and(basic_auth(username_1, password_1)) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path_regex("/prefix_3.*")) + .and(basic_auth(username_2, password_2)) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path_regex("/prefix_3.*")) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + let base_url = Url::parse(&server.uri())?; + let base_url_1 = base_url.join("prefix_1")?; + let base_url_2 = base_url.join("prefix_2")?; + let base_url_3 = base_url.join("prefix_3")?; + + let cache = CredentialsCache::new(); + + // Seed the cache with our credentials + cache.insert( + &base_url_1, + Arc::new(Credentials::new( + Some(username_1.to_string()), + Some(password_1.to_string()), + )), + ); + cache.insert( + &base_url_2, + Arc::new(Credentials::new( + Some(username_2.to_string()), + Some(password_2.to_string()), + )), + ); + + let client = test_client_builder() + .with(AuthMiddleware::new().with_cache(cache)) + .build(); + + // Both servers should work + assert_eq!( + client.get(base_url_1.clone()).send().await?.status(), + 200, + "Requests should not require credentials" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 200, + "Requests should not require credentials" + ); + assert_eq!( + client + .get(base_url.join("prefix_1/foo")?) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + assert_eq!( + client + .get(base_url.join("prefix_2/foo")?) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same realm" + ); + assert_eq!( + client + .get(base_url.join("prefix_1_foo")?) + .send() + .await? + .status(), + 401, + "Requests to paths with a matching prefix but different resource segments should fail" + ); + + assert_eq!( + client.get(base_url_3.clone()).send().await?.status(), + 200, + "Requests to the 'public' prefix should not use credentials" + ); + + Ok(()) +} + +#[test(tokio::test)] +async fn test_credentials_from_keyring_mixed_authentication_in_realm() -> Result<(), Error> { + let username_1 = "user1"; + let password_1 = "password1"; + let username_2 = "user2"; + let password_2 = "password2"; + + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_1.*")) + .and(basic_auth(username_1, password_1)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_2.*")) + .and(basic_auth(username_2, password_2)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + // Create a third, public prefix + // It will throw a 401 if it receives credentials + Mock::given(method("GET")) + .and(path_regex("/prefix_3.*")) + .and(basic_auth(username_1, password_1)) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path_regex("/prefix_3.*")) + .and(basic_auth(username_2, password_2)) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path_regex("/prefix_3.*")) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + let base_url = Url::parse(&server.uri())?; + let base_url_1 = base_url.join("prefix_1")?; + let base_url_2 = base_url.join("prefix_2")?; + let base_url_3 = base_url.join("prefix_3")?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([ + ( + ( + format!( + "{}:{}", + base_url_1.host_str().unwrap(), + base_url_1.port().unwrap() + ), + username_1, + ), + password_1, + ), + ( + ( + format!( + "{}:{}", + base_url_2.host_str().unwrap(), + base_url_2.port().unwrap() + ), + username_2, + ), + password_2, + ), + ]))), + ) + .build(); + + // Both servers do not work without a username + assert_eq!( + client.get(base_url_1.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + + let mut url_1 = base_url_1.clone(); + url_1.set_username(username_1).unwrap(); + assert_eq!( + client.get(url_1.clone()).send().await?.status(), + 200, + "Requests with a username should succeed" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 401, + "Credentials should not be re-used for the second prefix" + ); + + let mut url_2 = base_url_2.clone(); + url_2.set_username(username_2).unwrap(); + assert_eq!( + client.get(url_2.clone()).send().await?.status(), + 200, + "Requests with a username should succeed" + ); + + assert_eq!( + client + .get(base_url.join("prefix_1/foo")?) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same prefix" + ); + assert_eq!( + client + .get(base_url.join("prefix_2/foo")?) + .send() + .await? + .status(), + 200, + "Requests can be to different paths in the same prefix" + ); + assert_eq!( + client + .get(base_url.join("prefix_1_foo")?) + .send() + .await? + .status(), + 401, + "Requests to paths with a matching prefix but different resource segments should fail" + ); + assert_eq!( + client.get(base_url_3.clone()).send().await?.status(), + 200, + "Requests to the 'public' prefix should not use credentials" + ); + + Ok(()) +} + +/// Demonstrates "incorrect" behavior in our cache which avoids an expensive fetch of +/// credentials for _every_ request URL at the cost of inconsistent behavior when +/// credentials are not scoped to a realm. +#[test(tokio::test)] +async fn test_credentials_from_keyring_mixed_authentication_in_realm_same_username( +) -> Result<(), Error> { + let username = "user"; + let password_1 = "password1"; + let password_2 = "password2"; + + let server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_1.*")) + .and(basic_auth(username, password_1)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path_regex("/prefix_2.*")) + .and(basic_auth(username, password_2)) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(401)) + .mount(&server) + .await; + + let base_url = Url::parse(&server.uri())?; + let base_url_1 = base_url.join("prefix_1")?; + let base_url_2 = base_url.join("prefix_2")?; + + let client = test_client_builder() + .with( + AuthMiddleware::new() + .with_cache(CredentialsCache::new()) + .with_keyring(Some(KeyringProvider::dummy([ + ((base_url_1.clone(), username), password_1), + ((base_url_2.clone(), username), password_2), + ]))), + ) + .build(); + + // Both servers do not work without a username + assert_eq!( + client.get(base_url_1.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 401, + "Requests should require a username" + ); + + let mut url_1 = base_url_1.clone(); + url_1.set_username(username).unwrap(); + assert_eq!( + client.get(url_1.clone()).send().await?.status(), + 200, + "The first request with a username will succeed" + ); + assert_eq!( + client.get(base_url_2.clone()).send().await?.status(), + 401, + "Credentials should not be re-used for the second prefix" + ); + assert_eq!( + client + .get(base_url.join("prefix_1/foo")?) + .send() + .await? + .status(), + 200, + "Subsequent requests can be to different paths in the same prefix" + ); + + let mut url_2 = base_url_2.clone(); + url_2.set_username(username).unwrap(); + assert_eq!( + client.get(url_2.clone()).send().await?.status(), + 401, // INCORRECT BEHAVIOR + "A request with the same username and realm for a URL that needs a different password will fail" + ); + assert_eq!( + client + .get(base_url.join("prefix_2/foo")?) + .send() + .await? + .status(), + 401, // INCORRECT BEHAVIOR + "Requests to other paths in the failing prefix will also fail" + ); + + Ok(()) +} diff --git a/crates/uv-auth/src/realm.rs b/crates/uv-auth/src/realm.rs index 92d73d5f8..fcda89ade 100644 --- a/crates/uv-auth/src/realm.rs +++ b/crates/uv-auth/src/realm.rs @@ -59,89 +59,4 @@ impl Display for Realm { } #[cfg(test)] -mod tests { - use url::{ParseError, Url}; - - use crate::Realm; - - #[test] - fn test_should_retain_auth() -> Result<(), ParseError> { - // Exact match (https) - assert_eq!( - Realm::from(&Url::parse("https://example.com")?), - Realm::from(&Url::parse("https://example.com")?) - ); - - // Exact match (with port) - assert_eq!( - Realm::from(&Url::parse("https://example.com:1234")?), - Realm::from(&Url::parse("https://example.com:1234")?) - ); - - // Exact match (http) - assert_eq!( - Realm::from(&Url::parse("http://example.com")?), - Realm::from(&Url::parse("http://example.com")?) - ); - - // Okay, path differs - assert_eq!( - Realm::from(&Url::parse("http://example.com/foo")?), - Realm::from(&Url::parse("http://example.com/bar")?) - ); - - // Okay, default port differs (https) - assert_eq!( - Realm::from(&Url::parse("https://example.com:443")?), - Realm::from(&Url::parse("https://example.com")?) - ); - - // Okay, default port differs (http) - assert_eq!( - Realm::from(&Url::parse("http://example.com:80")?), - Realm::from(&Url::parse("http://example.com")?) - ); - - // Mismatched scheme - assert_ne!( - Realm::from(&Url::parse("https://example.com")?), - Realm::from(&Url::parse("http://example.com")?) - ); - - // Mismatched scheme, we explicitly do not allow upgrade to https - assert_ne!( - Realm::from(&Url::parse("http://example.com")?), - Realm::from(&Url::parse("https://example.com")?) - ); - - // Mismatched host - assert_ne!( - Realm::from(&Url::parse("https://foo.com")?), - Realm::from(&Url::parse("https://bar.com")?) - ); - - // Mismatched port - assert_ne!( - Realm::from(&Url::parse("https://example.com:1234")?), - Realm::from(&Url::parse("https://example.com:5678")?) - ); - - // Mismatched port, with one as default for scheme - assert_ne!( - Realm::from(&Url::parse("https://example.com:443")?), - Realm::from(&Url::parse("https://example.com:5678")?) - ); - assert_ne!( - Realm::from(&Url::parse("https://example.com:1234")?), - Realm::from(&Url::parse("https://example.com:443")?) - ); - - // Mismatched port, with default for a different scheme - assert_ne!( - Realm::from(&Url::parse("https://example.com:80")?), - Realm::from(&Url::parse("https://example.com")?) - ); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-auth/src/realm/tests.rs b/crates/uv-auth/src/realm/tests.rs new file mode 100644 index 000000000..753b37c09 --- /dev/null +++ b/crates/uv-auth/src/realm/tests.rs @@ -0,0 +1,84 @@ +use url::{ParseError, Url}; + +use crate::Realm; + +#[test] +fn test_should_retain_auth() -> Result<(), ParseError> { + // Exact match (https) + assert_eq!( + Realm::from(&Url::parse("https://example.com")?), + Realm::from(&Url::parse("https://example.com")?) + ); + + // Exact match (with port) + assert_eq!( + Realm::from(&Url::parse("https://example.com:1234")?), + Realm::from(&Url::parse("https://example.com:1234")?) + ); + + // Exact match (http) + assert_eq!( + Realm::from(&Url::parse("http://example.com")?), + Realm::from(&Url::parse("http://example.com")?) + ); + + // Okay, path differs + assert_eq!( + Realm::from(&Url::parse("http://example.com/foo")?), + Realm::from(&Url::parse("http://example.com/bar")?) + ); + + // Okay, default port differs (https) + assert_eq!( + Realm::from(&Url::parse("https://example.com:443")?), + Realm::from(&Url::parse("https://example.com")?) + ); + + // Okay, default port differs (http) + assert_eq!( + Realm::from(&Url::parse("http://example.com:80")?), + Realm::from(&Url::parse("http://example.com")?) + ); + + // Mismatched scheme + assert_ne!( + Realm::from(&Url::parse("https://example.com")?), + Realm::from(&Url::parse("http://example.com")?) + ); + + // Mismatched scheme, we explicitly do not allow upgrade to https + assert_ne!( + Realm::from(&Url::parse("http://example.com")?), + Realm::from(&Url::parse("https://example.com")?) + ); + + // Mismatched host + assert_ne!( + Realm::from(&Url::parse("https://foo.com")?), + Realm::from(&Url::parse("https://bar.com")?) + ); + + // Mismatched port + assert_ne!( + Realm::from(&Url::parse("https://example.com:1234")?), + Realm::from(&Url::parse("https://example.com:5678")?) + ); + + // Mismatched port, with one as default for scheme + assert_ne!( + Realm::from(&Url::parse("https://example.com:443")?), + Realm::from(&Url::parse("https://example.com:5678")?) + ); + assert_ne!( + Realm::from(&Url::parse("https://example.com:1234")?), + Realm::from(&Url::parse("https://example.com:443")?) + ); + + // Mismatched port, with default for a different scheme + assert_ne!( + Realm::from(&Url::parse("https://example.com:80")?), + Realm::from(&Url::parse("https://example.com")?) + ); + + Ok(()) +} diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index 7f3ba2323..a46f3122a 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -15,6 +15,7 @@ license = { workspace = true } workspace = true [lib] +doctest = false bench = false [[bench]] diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index a2ade2f70..82986f911 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true authors.workspace = true license.workspace = true +[lib] +doctest = false + [dependencies] uv-distribution-filename = { workspace = true } uv-fs = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index b6c292bf5..ebbc9a71f 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -498,142 +498,4 @@ fn write_record( } #[cfg(test)] -mod tests { - use super::*; - use insta::{assert_snapshot, with_settings}; - use std::str::FromStr; - use tempfile::TempDir; - use uv_normalize::PackageName; - use uv_pep440::Version; - - #[test] - fn test_wheel() { - let filename = WheelFilename { - name: PackageName::from_str("foo").unwrap(), - version: Version::from_str("1.2.3").unwrap(), - build_tag: None, - python_tag: vec!["py2".to_string(), "py3".to_string()], - abi_tag: vec!["none".to_string()], - platform_tag: vec!["any".to_string()], - }; - - with_settings!({ - filters => [(uv_version::version(), "[VERSION]")], - }, { - assert_snapshot!(wheel_info(&filename), @r" - Wheel-Version: 1.0 - Generator: uv [VERSION] - Root-Is-Purelib: true - Tag: py2-none-any - Tag: py3-none-any - "); - }); - } - - #[test] - fn test_record() { - let record = vec![RecordEntry { - path: "uv_backend/__init__.py".to_string(), - hash: "89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865".to_string(), - size: 37, - }]; - - let mut writer = Vec::new(); - write_record(&mut writer, "uv_backend-0.1.0", record).unwrap(); - assert_snapshot!(String::from_utf8(writer).unwrap(), @r" - uv_backend/__init__.py,sha256=89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865,37 - uv_backend-0.1.0/RECORD,, - "); - } - - /// Check that we write deterministic wheels. - #[test] - fn test_determinism() { - let temp1 = TempDir::new().unwrap(); - let uv_backend = Path::new("../../scripts/packages/uv_backend"); - build(uv_backend, temp1.path(), None).unwrap(); - - // Touch the file to check that we don't serialize the last modified date. - fs_err::write( - uv_backend.join("src/uv_backend/__init__.py"), - "def greet():\n print(\"Hello 👋\")\n", - ) - .unwrap(); - - let temp2 = TempDir::new().unwrap(); - build(uv_backend, temp2.path(), None).unwrap(); - - let wheel_filename = "uv_backend-0.1.0-py3-none-any.whl"; - assert_eq!( - fs_err::read(temp1.path().join(wheel_filename)).unwrap(), - fs_err::read(temp2.path().join(wheel_filename)).unwrap() - ); - } - - /// Snapshot all files from the prepare metadata hook. - #[test] - fn test_prepare_metadata() { - let metadata_dir = TempDir::new().unwrap(); - let uv_backend = Path::new("../../scripts/packages/uv_backend"); - metadata(uv_backend, metadata_dir.path()).unwrap(); - - let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) - .into_iter() - .map(|entry| { - entry - .unwrap() - .path() - .strip_prefix(metadata_dir.path()) - .unwrap() - .portable_display() - .to_string() - }) - .filter(|path| !path.is_empty()) - .collect(); - files.sort(); - assert_snapshot!(files.join("\n"), @r" - uv_backend-0.1.0.dist-info - uv_backend-0.1.0.dist-info/METADATA - uv_backend-0.1.0.dist-info/RECORD - uv_backend-0.1.0.dist-info/WHEEL - "); - - let metadata_file = metadata_dir - .path() - .join("uv_backend-0.1.0.dist-info/METADATA"); - assert_snapshot!(fs_err::read_to_string(metadata_file).unwrap(), @r###" - Metadata-Version: 2.3 - Name: uv-backend - Version: 0.1.0 - Summary: Add your description here - Requires-Python: >=3.12 - Description-Content-Type: text/markdown - - # uv_backend - - A simple package to be built with the uv build backend. - "###); - - let record_file = metadata_dir - .path() - .join("uv_backend-0.1.0.dist-info/RECORD"); - assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r###" - uv_backend-0.1.0.dist-info/WHEEL,sha256=70ce44709b6a53e0d0c5a6755b0290179697020f1f867e794f26154fe4825738,79 - uv_backend-0.1.0.dist-info/METADATA,sha256=e4a0d390317d7182f65ea978254c71ed283e0a4242150cf1c99a694b113ff68d,224 - uv_backend-0.1.0.dist-info/RECORD,, - "###); - - let wheel_file = metadata_dir.path().join("uv_backend-0.1.0.dist-info/WHEEL"); - let filters = vec![(uv_version::version(), "[VERSION]")]; - with_settings!({ - filters => filters - }, { - assert_snapshot!(fs_err::read_to_string(wheel_file).unwrap(), @r###" - Wheel-Version: 1.0 - Generator: uv [VERSION] - Root-Is-Purelib: true - Tag: py3-none-any - "###); - }); - } -} +mod tests; diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index df41cbab8..b9d4602db 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -629,406 +629,4 @@ struct BuildSystem { } #[cfg(test)] -mod tests { - use super::*; - use indoc::{formatdoc, indoc}; - use insta::assert_snapshot; - use std::iter; - use tempfile::TempDir; - - fn extend_project(payload: &str) -> String { - formatdoc! {r#" - [project] - name = "hello-world" - version = "0.1.0" - {payload} - - [build-system] - requires = ["uv>=0.4.15,<5"] - build-backend = "uv" - "# - } - } - - fn format_err(err: impl std::error::Error) -> String { - let mut formatted = err.to_string(); - for source in iter::successors(err.source(), |&err| err.source()) { - formatted += &format!("\n Caused by: {source}"); - } - formatted - } - - #[test] - fn valid() { - let temp_dir = TempDir::new().unwrap(); - - fs_err::write( - temp_dir.path().join("Readme.md"), - indoc! {r" - # Foo - - This is the foo library. - "}, - ) - .unwrap(); - - fs_err::write( - temp_dir.path().join("License.txt"), - indoc! {r#" - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - "#}, - ) - .unwrap(); - - let contents = indoc! {r#" - # See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example - - [project] - name = "hello-world" - version = "0.1.0" - description = "A Python package" - readme = "Readme.md" - requires_python = ">=3.12" - license = { file = "License.txt" } - authors = [{ name = "Ferris the crab", email = "ferris@rustacean.net" }] - maintainers = [{ name = "Konsti", email = "konstin@mailbox.org" }] - keywords = ["demo", "example", "package"] - classifiers = [ - "Development Status :: 6 - Mature", - "License :: OSI Approved :: MIT License", - # https://github.com/pypa/trove-classifiers/issues/17 - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - ] - dependencies = ["flask>=3,<4", "sqlalchemy[asyncio]>=2.0.35,<3"] - # We don't support dynamic fields, the default empty array is the only allowed value. - dynamic = [] - - [project.optional-dependencies] - postgres = ["psycopg>=3.2.2,<4"] - mysql = ["pymysql>=1.1.1,<2"] - - [project.urls] - "Homepage" = "https://github.com/astral-sh/uv" - "Repository" = "https://astral.sh" - - [project.scripts] - foo = "foo.cli:__main__" - - [project.gui-scripts] - foo-gui = "foo.gui" - - [project.entry-points.bar_group] - foo-bar = "foo:bar" - - [build-system] - requires = ["uv>=0.4.15,<5"] - build-backend = "uv" - "# - }; - - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); - - assert_snapshot!(metadata.core_metadata_format(), @r###" - Metadata-Version: 2.3 - Name: hello-world - Version: 0.1.0 - Summary: A Python package - Keywords: demo,example,package - Author: Ferris the crab - License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - Classifier: Development Status :: 6 - Mature - Classifier: License :: OSI Approved :: MIT License - Classifier: License :: OSI Approved :: Apache Software License - Classifier: Programming Language :: Python - Requires-Dist: flask>=3,<4 - Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3 - Maintainer: Konsti - Project-URL: Homepage, https://github.com/astral-sh/uv - Project-URL: Repository, https://astral.sh - Provides-Extra: mysql - Provides-Extra: postgres - Description-Content-Type: text/markdown - - # Foo - - This is the foo library. - "###); - - assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###" - [console_scripts] - foo = foo.cli:__main__ - - [gui_scripts] - foo-gui = foo.gui - - [bar_group] - foo-bar = foo:bar - - "###); - } - - #[test] - fn build_system_valid() { - let contents = extend_project(""); - let pyproject_toml = PyProjectToml::parse(&contents).unwrap(); - assert!(pyproject_toml.check_build_system()); - } - - #[test] - fn build_system_no_bound() { - let contents = indoc! {r#" - [project] - name = "hello-world" - version = "0.1.0" - - [build-system] - requires = ["uv"] - build-backend = "uv" - "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system()); - } - - #[test] - fn build_system_multiple_packages() { - let contents = indoc! {r#" - [project] - name = "hello-world" - version = "0.1.0" - - [build-system] - requires = ["uv>=0.4.15,<5", "wheel"] - build-backend = "uv" - "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system()); - } - - #[test] - fn build_system_no_requires_uv() { - let contents = indoc! {r#" - [project] - name = "hello-world" - version = "0.1.0" - - [build-system] - requires = ["setuptools"] - build-backend = "uv" - "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system()); - } - - #[test] - fn build_system_not_uv() { - let contents = indoc! {r#" - [project] - name = "hello-world" - version = "0.1.0" - - [build-system] - requires = ["uv>=0.4.15,<5"] - build-backend = "setuptools" - "#}; - let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system()); - } - - #[test] - fn minimal() { - let contents = extend_project(""); - - let metadata = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap(); - - assert_snapshot!(metadata.core_metadata_format(), @r###" - Metadata-Version: 2.3 - Name: hello-world - Version: 0.1.0 - "###); - } - - #[test] - fn invalid_readme_spec() { - let contents = extend_project(indoc! {r#" - readme = { path = "Readme.md" } - "# - }); - - let err = PyProjectToml::parse(&contents).unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml - Caused by: TOML parse error at line 4, column 10 - | - 4 | readme = { path = "Readme.md" } - | ^^^^^^^^^^^^^^^^^^^^^^ - data did not match any variant of untagged enum Readme - "###); - } - - #[test] - fn missing_readme() { - let contents = extend_project(indoc! {r#" - readme = "Readme.md" - "# - }); - - let err = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap_err(); - // Simplified for windows compatibility. - assert_snapshot!(err.to_string().replace('\\', "/"), @"failed to open file `/do/not/read/Readme.md`"); - } - - #[test] - fn multiline_description() { - let contents = extend_project(indoc! {r#" - description = "Hi :)\nThis is my project" - "# - }); - - let err = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml - Caused by: `project.description` must be a single line - "###); - } - - #[test] - fn mixed_licenses() { - let contents = extend_project(indoc! {r#" - license-files = ["licenses/*"] - license = { text = "MIT" } - "# - }); - - let err = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml - Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string - "###); - } - - #[test] - fn valid_license() { - let contents = extend_project(indoc! {r#" - license = "MIT OR Apache-2.0" - "# - }); - let metadata = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap(); - assert_snapshot!(metadata.core_metadata_format(), @r###" - Metadata-Version: 2.4 - Name: hello-world - Version: 0.1.0 - License-Expression: MIT OR Apache-2.0 - "###); - } - - #[test] - fn invalid_license() { - let contents = extend_project(indoc! {r#" - license = "MIT XOR Apache-2" - "# - }); - let err = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap_err(); - // TODO(konsti): We mess up the indentation in the error. - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml - Caused by: `project.license` is not a valid SPDX expression: `MIT XOR Apache-2` - Caused by: MIT XOR Apache-2 - ^^^ unknown term - "###); - } - - #[test] - fn dynamic() { - let contents = extend_project(indoc! {r#" - dynamic = ["dependencies"] - "# - }); - - let err = PyProjectToml::parse(&contents) - .unwrap() - .to_metadata(Path::new("/do/not/read")) - .unwrap_err(); - assert_snapshot!(format_err(err), @r###" - Invalid pyproject.toml - Caused by: Dynamic metadata is not supported - "###); - } - - fn script_error(contents: &str) -> String { - let err = PyProjectToml::parse(contents) - .unwrap() - .to_entry_points() - .unwrap_err(); - format_err(err) - } - - #[test] - fn invalid_entry_point_group() { - let contents = extend_project(indoc! {r#" - [project.entry-points."a@b"] - foo = "bar" - "# - }); - assert_snapshot!(script_error(&contents), @"Entrypoint groups must consist of letters and numbers separated by dots, invalid group: `a@b`"); - } - - #[test] - fn invalid_entry_point_name() { - let contents = extend_project(indoc! {r#" - [project.scripts] - "a@b" = "bar" - "# - }); - assert_snapshot!(script_error(&contents), @"Entrypoint names must consist of letters, numbers, dots and dashes; invalid name: `a@b`"); - } - - #[test] - fn invalid_entry_point_conflict_scripts() { - let contents = extend_project(indoc! {r#" - [project.entry-points.console_scripts] - foo = "bar" - "# - }); - assert_snapshot!(script_error(&contents), @"Use `project.scripts` instead of `project.entry-points.console_scripts`"); - } - - #[test] - fn invalid_entry_point_conflict_gui_scripts() { - let contents = extend_project(indoc! {r#" - [project.entry-points.gui_scripts] - foo = "bar" - "# - }); - assert_snapshot!(script_error(&contents), @"Use `project.gui-scripts` instead of `project.entry-points.gui_scripts`"); - } -} +mod tests; diff --git a/crates/uv-build-backend/src/metadata/tests.rs b/crates/uv-build-backend/src/metadata/tests.rs new file mode 100644 index 000000000..bc735d625 --- /dev/null +++ b/crates/uv-build-backend/src/metadata/tests.rs @@ -0,0 +1,401 @@ +use super::*; +use indoc::{formatdoc, indoc}; +use insta::assert_snapshot; +use std::iter; +use tempfile::TempDir; + +fn extend_project(payload: &str) -> String { + formatdoc! {r#" + [project] + name = "hello-world" + version = "0.1.0" + {payload} + + [build-system] + requires = ["uv>=0.4.15,<5"] + build-backend = "uv" + "# + } +} + +fn format_err(err: impl std::error::Error) -> String { + let mut formatted = err.to_string(); + for source in iter::successors(err.source(), |&err| err.source()) { + formatted += &format!("\n Caused by: {source}"); + } + formatted +} + +#[test] +fn valid() { + let temp_dir = TempDir::new().unwrap(); + + fs_err::write( + temp_dir.path().join("Readme.md"), + indoc! {r" + # Foo + + This is the foo library. + "}, + ) + .unwrap(); + + fs_err::write( + temp_dir.path().join("License.txt"), + indoc! {r#" + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + "#}, + ) + .unwrap(); + + let contents = indoc! {r#" + # See https://github.com/pypa/sampleproject/blob/main/pyproject.toml for another example + + [project] + name = "hello-world" + version = "0.1.0" + description = "A Python package" + readme = "Readme.md" + requires_python = ">=3.12" + license = { file = "License.txt" } + authors = [{ name = "Ferris the crab", email = "ferris@rustacean.net" }] + maintainers = [{ name = "Konsti", email = "konstin@mailbox.org" }] + keywords = ["demo", "example", "package"] + classifiers = [ + "Development Status :: 6 - Mature", + "License :: OSI Approved :: MIT License", + # https://github.com/pypa/trove-classifiers/issues/17 + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + ] + dependencies = ["flask>=3,<4", "sqlalchemy[asyncio]>=2.0.35,<3"] + # We don't support dynamic fields, the default empty array is the only allowed value. + dynamic = [] + + [project.optional-dependencies] + postgres = ["psycopg>=3.2.2,<4"] + mysql = ["pymysql>=1.1.1,<2"] + + [project.urls] + "Homepage" = "https://github.com/astral-sh/uv" + "Repository" = "https://astral.sh" + + [project.scripts] + foo = "foo.cli:__main__" + + [project.gui-scripts] + foo-gui = "foo.gui" + + [project.entry-points.bar_group] + foo-bar = "foo:bar" + + [build-system] + requires = ["uv>=0.4.15,<5"] + build-backend = "uv" + "# + }; + + let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); + + assert_snapshot!(metadata.core_metadata_format(), @r###" + Metadata-Version: 2.3 + Name: hello-world + Version: 0.1.0 + Summary: A Python package + Keywords: demo,example,package + Author: Ferris the crab + License: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Classifier: Development Status :: 6 - Mature + Classifier: License :: OSI Approved :: MIT License + Classifier: License :: OSI Approved :: Apache Software License + Classifier: Programming Language :: Python + Requires-Dist: flask>=3,<4 + Requires-Dist: sqlalchemy[asyncio]>=2.0.35,<3 + Maintainer: Konsti + Project-URL: Homepage, https://github.com/astral-sh/uv + Project-URL: Repository, https://astral.sh + Provides-Extra: mysql + Provides-Extra: postgres + Description-Content-Type: text/markdown + + # Foo + + This is the foo library. + "###); + + assert_snapshot!(pyproject_toml.to_entry_points().unwrap().unwrap(), @r###" + [console_scripts] + foo = foo.cli:__main__ + + [gui_scripts] + foo-gui = foo.gui + + [bar_group] + foo-bar = foo:bar + + "###); +} + +#[test] +fn build_system_valid() { + let contents = extend_project(""); + let pyproject_toml = PyProjectToml::parse(&contents).unwrap(); + assert!(pyproject_toml.check_build_system()); +} + +#[test] +fn build_system_no_bound() { + let contents = indoc! {r#" + [project] + name = "hello-world" + version = "0.1.0" + + [build-system] + requires = ["uv"] + build-backend = "uv" + "#}; + let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + assert!(!pyproject_toml.check_build_system()); +} + +#[test] +fn build_system_multiple_packages() { + let contents = indoc! {r#" + [project] + name = "hello-world" + version = "0.1.0" + + [build-system] + requires = ["uv>=0.4.15,<5", "wheel"] + build-backend = "uv" + "#}; + let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + assert!(!pyproject_toml.check_build_system()); +} + +#[test] +fn build_system_no_requires_uv() { + let contents = indoc! {r#" + [project] + name = "hello-world" + version = "0.1.0" + + [build-system] + requires = ["setuptools"] + build-backend = "uv" + "#}; + let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + assert!(!pyproject_toml.check_build_system()); +} + +#[test] +fn build_system_not_uv() { + let contents = indoc! {r#" + [project] + name = "hello-world" + version = "0.1.0" + + [build-system] + requires = ["uv>=0.4.15,<5"] + build-backend = "setuptools" + "#}; + let pyproject_toml = PyProjectToml::parse(contents).unwrap(); + assert!(!pyproject_toml.check_build_system()); +} + +#[test] +fn minimal() { + let contents = extend_project(""); + + let metadata = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap(); + + assert_snapshot!(metadata.core_metadata_format(), @r###" + Metadata-Version: 2.3 + Name: hello-world + Version: 0.1.0 + "###); +} + +#[test] +fn invalid_readme_spec() { + let contents = extend_project(indoc! {r#" + readme = { path = "Readme.md" } + "# + }); + + let err = PyProjectToml::parse(&contents).unwrap_err(); + assert_snapshot!(format_err(err), @r###" + Invalid pyproject.toml + Caused by: TOML parse error at line 4, column 10 + | + 4 | readme = { path = "Readme.md" } + | ^^^^^^^^^^^^^^^^^^^^^^ + data did not match any variant of untagged enum Readme + "###); +} + +#[test] +fn missing_readme() { + let contents = extend_project(indoc! {r#" + readme = "Readme.md" + "# + }); + + let err = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap_err(); + // Simplified for windows compatibility. + assert_snapshot!(err.to_string().replace('\\', "/"), @"failed to open file `/do/not/read/Readme.md`"); +} + +#[test] +fn multiline_description() { + let contents = extend_project(indoc! {r#" + description = "Hi :)\nThis is my project" + "# + }); + + let err = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap_err(); + assert_snapshot!(format_err(err), @r###" + Invalid pyproject.toml + Caused by: `project.description` must be a single line + "###); +} + +#[test] +fn mixed_licenses() { + let contents = extend_project(indoc! {r#" + license-files = ["licenses/*"] + license = { text = "MIT" } + "# + }); + + let err = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap_err(); + assert_snapshot!(format_err(err), @r###" + Invalid pyproject.toml + Caused by: When `project.license-files` is defined, `project.license` must be an SPDX expression string + "###); +} + +#[test] +fn valid_license() { + let contents = extend_project(indoc! {r#" + license = "MIT OR Apache-2.0" + "# + }); + let metadata = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap(); + assert_snapshot!(metadata.core_metadata_format(), @r###" + Metadata-Version: 2.4 + Name: hello-world + Version: 0.1.0 + License-Expression: MIT OR Apache-2.0 + "###); +} + +#[test] +fn invalid_license() { + let contents = extend_project(indoc! {r#" + license = "MIT XOR Apache-2" + "# + }); + let err = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap_err(); + // TODO(konsti): We mess up the indentation in the error. + assert_snapshot!(format_err(err), @r###" + Invalid pyproject.toml + Caused by: `project.license` is not a valid SPDX expression: `MIT XOR Apache-2` + Caused by: MIT XOR Apache-2 + ^^^ unknown term + "###); +} + +#[test] +fn dynamic() { + let contents = extend_project(indoc! {r#" + dynamic = ["dependencies"] + "# + }); + + let err = PyProjectToml::parse(&contents) + .unwrap() + .to_metadata(Path::new("/do/not/read")) + .unwrap_err(); + assert_snapshot!(format_err(err), @r###" + Invalid pyproject.toml + Caused by: Dynamic metadata is not supported + "###); +} + +fn script_error(contents: &str) -> String { + let err = PyProjectToml::parse(contents) + .unwrap() + .to_entry_points() + .unwrap_err(); + format_err(err) +} + +#[test] +fn invalid_entry_point_group() { + let contents = extend_project(indoc! {r#" + [project.entry-points."a@b"] + foo = "bar" + "# + }); + assert_snapshot!(script_error(&contents), @"Entrypoint groups must consist of letters and numbers separated by dots, invalid group: `a@b`"); +} + +#[test] +fn invalid_entry_point_name() { + let contents = extend_project(indoc! {r#" + [project.scripts] + "a@b" = "bar" + "# + }); + assert_snapshot!(script_error(&contents), @"Entrypoint names must consist of letters, numbers, dots and dashes; invalid name: `a@b`"); +} + +#[test] +fn invalid_entry_point_conflict_scripts() { + let contents = extend_project(indoc! {r#" + [project.entry-points.console_scripts] + foo = "bar" + "# + }); + assert_snapshot!(script_error(&contents), @"Use `project.scripts` instead of `project.entry-points.console_scripts`"); +} + +#[test] +fn invalid_entry_point_conflict_gui_scripts() { + let contents = extend_project(indoc! {r#" + [project.entry-points.gui_scripts] + foo = "bar" + "# + }); + assert_snapshot!(script_error(&contents), @"Use `project.gui-scripts` instead of `project.entry-points.gui_scripts`"); +} diff --git a/crates/uv-build-backend/src/pep639_glob.rs b/crates/uv-build-backend/src/pep639_glob.rs index b0fb4a2be..aae4d68f2 100644 --- a/crates/uv-build-backend/src/pep639_glob.rs +++ b/crates/uv-build-backend/src/pep639_glob.rs @@ -78,59 +78,4 @@ pub(crate) fn parse_pep639_glob(glob: &str) -> Result } #[cfg(test)] -mod tests { - use super::*; - use insta::assert_snapshot; - - #[test] - fn test_error() { - let parse_err = |glob| parse_pep639_glob(glob).unwrap_err().to_string(); - assert_snapshot!( - parse_err(".."), - @"The parent directory operator (`..`) at position 0 is not allowed in license file globs" - ); - assert_snapshot!( - parse_err("licenses/.."), - @"The parent directory operator (`..`) at position 9 is not allowed in license file globs" - ); - assert_snapshot!( - parse_err("licenses/LICEN!E.txt"), - @"Glob contains invalid character at position 14: `!`" - ); - assert_snapshot!( - parse_err("licenses/LICEN[!C]E.txt"), - @"Glob contains invalid character in range at position 15: `!`" - ); - assert_snapshot!( - parse_err("licenses/LICEN[C?]E.txt"), - @"Glob contains invalid character in range at position 16: `?`" - ); - assert_snapshot!(parse_err("******"), @"Pattern syntax error near position 2: wildcards are either regular `*` or recursive `**`"); - assert_snapshot!( - parse_err(r"licenses\eula.txt"), - @r"Glob contains invalid character at position 8: `\`" - ); - } - - #[test] - fn test_valid() { - let cases = [ - "licenses/*.txt", - "licenses/**/*.txt", - "LICEN[CS]E.txt", - "LICEN?E.txt", - "[a-z].txt", - "[a-z._-].txt", - "*/**", - "LICENSE..txt", - "LICENSE_file-1.txt", - // (google translate) - "licenses/라이센스*.txt", - "licenses/ライセンス*.txt", - "licenses/执照*.txt", - ]; - for case in cases { - parse_pep639_glob(case).unwrap(); - } - } -} +mod tests; diff --git a/crates/uv-build-backend/src/pep639_glob/tests.rs b/crates/uv-build-backend/src/pep639_glob/tests.rs new file mode 100644 index 000000000..1bb02520c --- /dev/null +++ b/crates/uv-build-backend/src/pep639_glob/tests.rs @@ -0,0 +1,54 @@ +use super::*; +use insta::assert_snapshot; + +#[test] +fn test_error() { + let parse_err = |glob| parse_pep639_glob(glob).unwrap_err().to_string(); + assert_snapshot!( + parse_err(".."), + @"The parent directory operator (`..`) at position 0 is not allowed in license file globs" + ); + assert_snapshot!( + parse_err("licenses/.."), + @"The parent directory operator (`..`) at position 9 is not allowed in license file globs" + ); + assert_snapshot!( + parse_err("licenses/LICEN!E.txt"), + @"Glob contains invalid character at position 14: `!`" + ); + assert_snapshot!( + parse_err("licenses/LICEN[!C]E.txt"), + @"Glob contains invalid character in range at position 15: `!`" + ); + assert_snapshot!( + parse_err("licenses/LICEN[C?]E.txt"), + @"Glob contains invalid character in range at position 16: `?`" + ); + assert_snapshot!(parse_err("******"), @"Pattern syntax error near position 2: wildcards are either regular `*` or recursive `**`"); + assert_snapshot!( + parse_err(r"licenses\eula.txt"), + @r"Glob contains invalid character at position 8: `\`" + ); +} + +#[test] +fn test_valid() { + let cases = [ + "licenses/*.txt", + "licenses/**/*.txt", + "LICEN[CS]E.txt", + "LICEN?E.txt", + "[a-z].txt", + "[a-z._-].txt", + "*/**", + "LICENSE..txt", + "LICENSE_file-1.txt", + // (google translate) + "licenses/라이센스*.txt", + "licenses/ライセンス*.txt", + "licenses/执照*.txt", + ]; + for case in cases { + parse_pep639_glob(case).unwrap(); + } +} diff --git a/crates/uv-build-backend/src/tests.rs b/crates/uv-build-backend/src/tests.rs new file mode 100644 index 000000000..c27bd04ad --- /dev/null +++ b/crates/uv-build-backend/src/tests.rs @@ -0,0 +1,137 @@ +use super::*; +use insta::{assert_snapshot, with_settings}; +use std::str::FromStr; +use tempfile::TempDir; +use uv_normalize::PackageName; +use uv_pep440::Version; + +#[test] +fn test_wheel() { + let filename = WheelFilename { + name: PackageName::from_str("foo").unwrap(), + version: Version::from_str("1.2.3").unwrap(), + build_tag: None, + python_tag: vec!["py2".to_string(), "py3".to_string()], + abi_tag: vec!["none".to_string()], + platform_tag: vec!["any".to_string()], + }; + + with_settings!({ + filters => [(uv_version::version(), "[VERSION]")], + }, { + assert_snapshot!(wheel_info(&filename), @r" + Wheel-Version: 1.0 + Generator: uv [VERSION] + Root-Is-Purelib: true + Tag: py2-none-any + Tag: py3-none-any + "); + }); +} + +#[test] +fn test_record() { + let record = vec![RecordEntry { + path: "uv_backend/__init__.py".to_string(), + hash: "89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865".to_string(), + size: 37, + }]; + + let mut writer = Vec::new(); + write_record(&mut writer, "uv_backend-0.1.0", record).unwrap(); + assert_snapshot!(String::from_utf8(writer).unwrap(), @r" + uv_backend/__init__.py,sha256=89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865,37 + uv_backend-0.1.0/RECORD,, + "); +} + +/// Check that we write deterministic wheels. +#[test] +fn test_determinism() { + let temp1 = TempDir::new().unwrap(); + let uv_backend = Path::new("../../scripts/packages/uv_backend"); + build(uv_backend, temp1.path(), None).unwrap(); + + // Touch the file to check that we don't serialize the last modified date. + fs_err::write( + uv_backend.join("src/uv_backend/__init__.py"), + "def greet():\n print(\"Hello 👋\")\n", + ) + .unwrap(); + + let temp2 = TempDir::new().unwrap(); + build(uv_backend, temp2.path(), None).unwrap(); + + let wheel_filename = "uv_backend-0.1.0-py3-none-any.whl"; + assert_eq!( + fs_err::read(temp1.path().join(wheel_filename)).unwrap(), + fs_err::read(temp2.path().join(wheel_filename)).unwrap() + ); +} + +/// Snapshot all files from the prepare metadata hook. +#[test] +fn test_prepare_metadata() { + let metadata_dir = TempDir::new().unwrap(); + let uv_backend = Path::new("../../scripts/packages/uv_backend"); + metadata(uv_backend, metadata_dir.path()).unwrap(); + + let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) + .into_iter() + .map(|entry| { + entry + .unwrap() + .path() + .strip_prefix(metadata_dir.path()) + .unwrap() + .portable_display() + .to_string() + }) + .filter(|path| !path.is_empty()) + .collect(); + files.sort(); + assert_snapshot!(files.join("\n"), @r" + uv_backend-0.1.0.dist-info + uv_backend-0.1.0.dist-info/METADATA + uv_backend-0.1.0.dist-info/RECORD + uv_backend-0.1.0.dist-info/WHEEL + "); + + let metadata_file = metadata_dir + .path() + .join("uv_backend-0.1.0.dist-info/METADATA"); + assert_snapshot!(fs_err::read_to_string(metadata_file).unwrap(), @r###" + Metadata-Version: 2.3 + Name: uv-backend + Version: 0.1.0 + Summary: Add your description here + Requires-Python: >=3.12 + Description-Content-Type: text/markdown + + # uv_backend + + A simple package to be built with the uv build backend. + "###); + + let record_file = metadata_dir + .path() + .join("uv_backend-0.1.0.dist-info/RECORD"); + assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r###" + uv_backend-0.1.0.dist-info/WHEEL,sha256=70ce44709b6a53e0d0c5a6755b0290179697020f1f867e794f26154fe4825738,79 + uv_backend-0.1.0.dist-info/METADATA,sha256=e4a0d390317d7182f65ea978254c71ed283e0a4242150cf1c99a694b113ff68d,224 + uv_backend-0.1.0.dist-info/RECORD,, + "###); + + let wheel_file = metadata_dir.path().join("uv_backend-0.1.0.dist-info/WHEEL"); + let filters = vec![(uv_version::version(), "[VERSION]")]; + with_settings!({ + filters => filters + }, { + assert_snapshot!(fs_err::read_to_string(wheel_file).unwrap(), @r###" + Wheel-Version: 1.0 + Generator: uv [VERSION] + Root-Is-Purelib: true + Tag: py3-none-any + "###); + }); +} diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index 8ad038c62..39e7d953e 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -10,6 +10,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-cache-info/Cargo.toml b/crates/uv-cache-info/Cargo.toml index d1c594a66..ec17a4416 100644 --- a/crates/uv-cache-info/Cargo.toml +++ b/crates/uv-cache-info/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-cache-key/Cargo.toml b/crates/uv-cache-key/Cargo.toml index 098a8f24d..7816c1d13 100644 --- a/crates/uv-cache-key/Cargo.toml +++ b/crates/uv-cache-key/Cargo.toml @@ -10,6 +10,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-cache-key/src/canonical_url.rs b/crates/uv-cache-key/src/canonical_url.rs index 0a866f9fa..9cc26d94c 100644 --- a/crates/uv-cache-key/src/canonical_url.rs +++ b/crates/uv-cache-key/src/canonical_url.rs @@ -181,144 +181,4 @@ impl std::fmt::Display for RepositoryUrl { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn user_credential_does_not_affect_cache_key() -> Result<(), url::ParseError> { - let mut hasher = CacheKeyHasher::new(); - CanonicalUrl::parse("https://example.com/pypa/sample-namespace-packages.git@2.0.0")? - .cache_key(&mut hasher); - let hash_without_creds = hasher.finish(); - - let mut hasher = CacheKeyHasher::new(); - CanonicalUrl::parse( - "https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0", - )? - .cache_key(&mut hasher); - let hash_with_creds = hasher.finish(); - assert_eq!( - hash_without_creds, hash_with_creds, - "URLs with no user credentials should hash the same as URLs with different user credentials", - ); - - let mut hasher = CacheKeyHasher::new(); - CanonicalUrl::parse( - "https://user:bar@example.com/pypa/sample-namespace-packages.git@2.0.0", - )? - .cache_key(&mut hasher); - let hash_with_creds = hasher.finish(); - assert_eq!( - hash_without_creds, hash_with_creds, - "URLs with different user credentials should hash the same", - ); - - let mut hasher = CacheKeyHasher::new(); - CanonicalUrl::parse("https://:bar@example.com/pypa/sample-namespace-packages.git@2.0.0")? - .cache_key(&mut hasher); - let hash_with_creds = hasher.finish(); - assert_eq!( - hash_without_creds, hash_with_creds, - "URLs with no username, though with a password, should hash the same as URLs with different user credentials", - ); - - let mut hasher = CacheKeyHasher::new(); - CanonicalUrl::parse("https://user:@example.com/pypa/sample-namespace-packages.git@2.0.0")? - .cache_key(&mut hasher); - let hash_with_creds = hasher.finish(); - assert_eq!( - hash_without_creds, hash_with_creds, - "URLs with no password, though with a username, should hash the same as URLs with different user credentials", - ); - - Ok(()) - } - - #[test] - fn canonical_url() -> Result<(), url::ParseError> { - // Two URLs should be considered equal regardless of the `.git` suffix. - assert_eq!( - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages")?, - ); - - // Two URLs should be considered equal regardless of the `.git` suffix. - assert_eq!( - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@2.0.0")?, - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages@2.0.0")?, - ); - - // Two URLs should be _not_ considered equal if they point to different repositories. - assert_ne!( - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, - CanonicalUrl::parse("git+https://github.com/pypa/sample-packages.git")?, - ); - - // Two URLs should _not_ be considered equal if they request different subdirectories. - assert_ne!( - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_a")?, - CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_b")?, - ); - - // Two URLs should _not_ be considered equal if they request different commit tags. - assert_ne!( - CanonicalUrl::parse( - "git+https://github.com/pypa/sample-namespace-packages.git@v1.0.0" - )?, - CanonicalUrl::parse( - "git+https://github.com/pypa/sample-namespace-packages.git@v2.0.0" - )?, - ); - - // Two URLs that cannot be a base should be considered equal. - assert_eq!( - CanonicalUrl::parse("git+https:://github.com/pypa/sample-namespace-packages.git")?, - CanonicalUrl::parse("git+https:://github.com/pypa/sample-namespace-packages.git")?, - ); - - Ok(()) - } - - #[test] - fn repository_url() -> Result<(), url::ParseError> { - // Two URLs should be considered equal regardless of the `.git` suffix. - assert_eq!( - RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, - RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages")?, - ); - - // Two URLs should be considered equal regardless of the `.git` suffix. - assert_eq!( - RepositoryUrl::parse( - "git+https://github.com/pypa/sample-namespace-packages.git@2.0.0" - )?, - RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages@2.0.0")?, - ); - - // Two URLs should be _not_ considered equal if they point to different repositories. - assert_ne!( - RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, - RepositoryUrl::parse("git+https://github.com/pypa/sample-packages.git")?, - ); - - // Two URLs should be considered equal if they map to the same repository, even if they - // request different subdirectories. - assert_eq!( - RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_a")?, - RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_b")?, - ); - - // Two URLs should be considered equal if they map to the same repository, even if they - // request different commit tags. - assert_eq!( - RepositoryUrl::parse( - "git+https://github.com/pypa/sample-namespace-packages.git@v1.0.0" - )?, - RepositoryUrl::parse( - "git+https://github.com/pypa/sample-namespace-packages.git@v2.0.0" - )?, - ); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-cache-key/src/canonical_url/tests.rs b/crates/uv-cache-key/src/canonical_url/tests.rs new file mode 100644 index 000000000..0f6d15788 --- /dev/null +++ b/crates/uv-cache-key/src/canonical_url/tests.rs @@ -0,0 +1,125 @@ +use super::*; + +#[test] +fn user_credential_does_not_affect_cache_key() -> Result<(), url::ParseError> { + let mut hasher = CacheKeyHasher::new(); + CanonicalUrl::parse("https://example.com/pypa/sample-namespace-packages.git@2.0.0")? + .cache_key(&mut hasher); + let hash_without_creds = hasher.finish(); + + let mut hasher = CacheKeyHasher::new(); + CanonicalUrl::parse("https://user:foo@example.com/pypa/sample-namespace-packages.git@2.0.0")? + .cache_key(&mut hasher); + let hash_with_creds = hasher.finish(); + assert_eq!( + hash_without_creds, hash_with_creds, + "URLs with no user credentials should hash the same as URLs with different user credentials", + ); + + let mut hasher = CacheKeyHasher::new(); + CanonicalUrl::parse("https://user:bar@example.com/pypa/sample-namespace-packages.git@2.0.0")? + .cache_key(&mut hasher); + let hash_with_creds = hasher.finish(); + assert_eq!( + hash_without_creds, hash_with_creds, + "URLs with different user credentials should hash the same", + ); + + let mut hasher = CacheKeyHasher::new(); + CanonicalUrl::parse("https://:bar@example.com/pypa/sample-namespace-packages.git@2.0.0")? + .cache_key(&mut hasher); + let hash_with_creds = hasher.finish(); + assert_eq!( + hash_without_creds, hash_with_creds, + "URLs with no username, though with a password, should hash the same as URLs with different user credentials", + ); + + let mut hasher = CacheKeyHasher::new(); + CanonicalUrl::parse("https://user:@example.com/pypa/sample-namespace-packages.git@2.0.0")? + .cache_key(&mut hasher); + let hash_with_creds = hasher.finish(); + assert_eq!( + hash_without_creds, hash_with_creds, + "URLs with no password, though with a username, should hash the same as URLs with different user credentials", + ); + + Ok(()) +} + +#[test] +fn canonical_url() -> Result<(), url::ParseError> { + // Two URLs should be considered equal regardless of the `.git` suffix. + assert_eq!( + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages")?, + ); + + // Two URLs should be considered equal regardless of the `.git` suffix. + assert_eq!( + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@2.0.0")?, + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages@2.0.0")?, + ); + + // Two URLs should be _not_ considered equal if they point to different repositories. + assert_ne!( + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, + CanonicalUrl::parse("git+https://github.com/pypa/sample-packages.git")?, + ); + + // Two URLs should _not_ be considered equal if they request different subdirectories. + assert_ne!( + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_a")?, + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_b")?, + ); + + // Two URLs should _not_ be considered equal if they request different commit tags. + assert_ne!( + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@v1.0.0")?, + CanonicalUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@v2.0.0")?, + ); + + // Two URLs that cannot be a base should be considered equal. + assert_eq!( + CanonicalUrl::parse("git+https:://github.com/pypa/sample-namespace-packages.git")?, + CanonicalUrl::parse("git+https:://github.com/pypa/sample-namespace-packages.git")?, + ); + + Ok(()) +} + +#[test] +fn repository_url() -> Result<(), url::ParseError> { + // Two URLs should be considered equal regardless of the `.git` suffix. + assert_eq!( + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages")?, + ); + + // Two URLs should be considered equal regardless of the `.git` suffix. + assert_eq!( + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@2.0.0")?, + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages@2.0.0")?, + ); + + // Two URLs should be _not_ considered equal if they point to different repositories. + assert_ne!( + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git")?, + RepositoryUrl::parse("git+https://github.com/pypa/sample-packages.git")?, + ); + + // Two URLs should be considered equal if they map to the same repository, even if they + // request different subdirectories. + assert_eq!( + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_a")?, + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git#subdirectory=pkg_resources/pkg_b")?, + ); + + // Two URLs should be considered equal if they map to the same repository, even if they + // request different commit tags. + assert_eq!( + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@v1.0.0")?, + RepositoryUrl::parse("git+https://github.com/pypa/sample-namespace-packages.git@v2.0.0")?, + ); + + Ok(()) +} diff --git a/crates/uv-cache/Cargo.toml b/crates/uv-cache/Cargo.toml index dab67387c..0a4923017 100644 --- a/crates/uv-cache/Cargo.toml +++ b/crates/uv-cache/Cargo.toml @@ -10,6 +10,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-cli/Cargo.toml b/crates/uv-cli/Cargo.toml index 1414e429d..846eec9a5 100644 --- a/crates/uv-cli/Cargo.toml +++ b/crates/uv-cli/Cargo.toml @@ -10,6 +10,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-cli/src/version.rs b/crates/uv-cli/src/version.rs index 87593f787..9eaa462b2 100644 --- a/crates/uv-cli/src/version.rs +++ b/crates/uv-cli/src/version.rs @@ -77,73 +77,4 @@ pub fn version() -> VersionInfo { } #[cfg(test)] -mod tests { - use insta::{assert_json_snapshot, assert_snapshot}; - - use super::{CommitInfo, VersionInfo}; - - #[test] - fn version_formatting() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: None, - }; - assert_snapshot!(version, @"0.0.0"); - } - - #[test] - fn version_formatting_with_commit_info() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), - last_tag: Some("v0.0.1".to_string()), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 0, - }), - }; - assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); - } - - #[test] - fn version_formatting_with_commits_since_last_tag() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), - last_tag: Some("v0.0.1".to_string()), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 24, - }), - }; - assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); - } - - #[test] - fn version_serializable() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), - last_tag: Some("v0.0.1".to_string()), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 0, - }), - }; - assert_json_snapshot!(version, @r###" - { - "version": "0.0.0", - "commit_info": { - "short_commit_hash": "53b0f5d92", - "commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7", - "commit_date": "2023-10-19", - "last_tag": "v0.0.1", - "commits_since_last_tag": 0 - } - } - "###); - } -} +mod tests; diff --git a/crates/uv-cli/src/version/tests.rs b/crates/uv-cli/src/version/tests.rs new file mode 100644 index 000000000..de54bd6e1 --- /dev/null +++ b/crates/uv-cli/src/version/tests.rs @@ -0,0 +1,68 @@ +use insta::{assert_json_snapshot, assert_snapshot}; + +use super::{CommitInfo, VersionInfo}; + +#[test] +fn version_formatting() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: None, + }; + assert_snapshot!(version, @"0.0.0"); +} + +#[test] +fn version_formatting_with_commit_info() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), + last_tag: Some("v0.0.1".to_string()), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 0, + }), + }; + assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); +} + +#[test] +fn version_formatting_with_commits_since_last_tag() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), + last_tag: Some("v0.0.1".to_string()), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 24, + }), + }; + assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); +} + +#[test] +fn version_serializable() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), + last_tag: Some("v0.0.1".to_string()), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 0, + }), + }; + assert_json_snapshot!(version, @r#" + { + "version": "0.0.0", + "commit_info": { + "short_commit_hash": "53b0f5d92", + "commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7", + "commit_date": "2023-10-19", + "last_tag": "v0.0.1", + "commits_since_last_tag": 0 + } + } + "#); +} diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 2147d604b..ded1f134b 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -3,6 +3,9 @@ name = "uv-client" version = "0.0.1" edition = "2021" +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 8eb2f5c84..838bf0f17 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -207,1000 +207,4 @@ pub enum Error { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_sha256() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: Some( - "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - ), - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_md5() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: Some( - "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - ), - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl#md5=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_base() { - let text = r#" - - - - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "index.python.org", - ), - ), - port: None, - path: "/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: Some( - "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - ), - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_escaped_fragment() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2+233fca715f49-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2+233fca715f49-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: Some( - "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - ), - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2+233fca715f49-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_encoded_fragment() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: Some( - "4095ada29e51070f7d199a0a5bdf5c8d8e238e03f0bf4dcc02571e78c9ae800d", - ), - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256%3D4095ada29e51070f7d199a0a5bdf5c8d8e238e03f0bf4dcc02571e78c9ae800d", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_quoted_filepath() { - let text = r#" - - - -

Links for jinja2

- cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "torchtext-0.17.0+cpu-cp39-cp39-win_amd64.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_missing_hash() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_missing_href() { - let text = r" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Missing href attribute on anchor link"); - } - - #[test] - fn parse_empty_href() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Missing href attribute on anchor link"); - } - - #[test] - fn parse_empty_fragment() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl#", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_query_string() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl?project=legacy", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_missing_hash_value() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Unexpected fragment (expected `#sha256=...` or similar) on URL: sha256"); - } - - #[test] - fn parse_unknown_hash() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, or `sha512`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`"); - } - - #[test] - fn parse_flat_index_html() { - let text = r#" - - - - - cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl
- cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl
- - - "#; - let base = Url::parse("https://storage.googleapis.com/jax-releases/jax_cuda_releases.html") - .unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "storage.googleapis.com", - ), - ), - port: None, - path: "/jax-releases/jax_cuda_releases.html", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl", - yanked: None, - }, - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl", - yanked: None, - }, - ], - } - "###); - } - - /// Test for AWS Code Artifact - /// - /// See: - #[test] - fn parse_code_artifact_index_html() { - let text = r#" - - - - Links for flask - - -

Links for flask

- Flask-0.1.tar.gz -
- Flask-0.10.1.tar.gz -
- flask-3.0.1.tar.gz -
- - - "#; - let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") - .unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "account.d.codeartifact.us-west-2.amazonaws.com", - ), - ), - port: None, - path: "/pypi/shared-packages-pypi/simple/flask/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Flask-0.1.tar.gz", - hashes: Hashes { - md5: None, - sha256: Some( - "9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", - ), - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", - yanked: None, - }, - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Flask-0.10.1.tar.gz", - hashes: Hashes { - md5: None, - sha256: Some( - "4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", - ), - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", - yanked: None, - }, - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "flask-3.0.1.tar.gz", - hashes: Hashes { - md5: None, - sha256: Some( - "6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", - ), - sha384: None, - sha512: None, - }, - requires_python: Some( - Ok( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "3.8", - }, - ], - ), - ), - ), - size: None, - upload_time: None, - url: "3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", - yanked: None, - }, - ], - } - "###); - } - - #[test] - fn parse_file_requires_python_trailing_comma() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- - - "#; - let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: None, - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: Some( - "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - ), - sha384: None, - sha512: None, - }, - requires_python: Some( - Ok( - VersionSpecifiers( - [ - VersionSpecifier { - operator: GreaterThanEqual, - version: "3.8", - }, - ], - ), - ), - ), - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", - yanked: None, - }, - ], - } - "###); - } - - /// Respect PEP 714 (see: ). - #[test] - fn parse_core_metadata() { - let text = r#" - - - -

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
- Jinja2-3.1.3-py3-none-any.whl
- Jinja2-3.1.4-py3-none-any.whl
- Jinja2-3.1.5-py3-none-any.whl
- Jinja2-3.1.6-py3-none-any.whl
- - - "#; - let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") - .unwrap(); - let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" - SimpleHtml { - base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "account.d.codeartifact.us-west-2.amazonaws.com", - ), - ), - port: None, - path: "/pypi/shared-packages-pypi/simple/flask/", - query: None, - fragment: None, - }, - ), - files: [ - File { - core_metadata: Some( - Bool( - true, - ), - ), - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.2-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.2-py3-none-any.whl", - yanked: None, - }, - File { - core_metadata: Some( - Bool( - true, - ), - ), - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.3-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.3-py3-none-any.whl", - yanked: None, - }, - File { - core_metadata: Some( - Bool( - false, - ), - ), - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.4-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.4-py3-none-any.whl", - yanked: None, - }, - File { - core_metadata: Some( - Bool( - false, - ), - ), - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.5-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.5-py3-none-any.whl", - yanked: None, - }, - File { - core_metadata: Some( - Bool( - true, - ), - ), - dist_info_metadata: None, - data_dist_info_metadata: None, - filename: "Jinja2-3.1.6-py3-none-any.whl", - hashes: Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: None, - }, - requires_python: None, - size: None, - upload_time: None, - url: "/whl/Jinja2-3.1.6-py3-none-any.whl", - yanked: None, - }, - ], - } - "###); - } -} +mod tests; diff --git a/crates/uv-client/src/html/tests.rs b/crates/uv-client/src/html/tests.rs new file mode 100644 index 000000000..c8ba90390 --- /dev/null +++ b/crates/uv-client/src/html/tests.rs @@ -0,0 +1,995 @@ +use super::*; + +#[test] +fn parse_sha256() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: Some( + "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + ), + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_md5() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: Some( + "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + ), + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl#md5=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_base() { + let text = r#" + + + + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "index.python.org", + ), + ), + port: None, + path: "/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: Some( + "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + ), + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_escaped_fragment() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2+233fca715f49-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2+233fca715f49-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: Some( + "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + ), + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2+233fca715f49-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_encoded_fragment() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: Some( + "4095ada29e51070f7d199a0a5bdf5c8d8e238e03f0bf4dcc02571e78c9ae800d", + ), + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256%3D4095ada29e51070f7d199a0a5bdf5c8d8e238e03f0bf4dcc02571e78c9ae800d", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_quoted_filepath() { + let text = r#" + + + +

Links for jinja2

+cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "torchtext-0.17.0+cpu-cp39-cp39-win_amd64.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_missing_hash() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_missing_href() { + let text = r" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap_err(); + insta::assert_snapshot!(result, @"Missing href attribute on anchor link"); +} + +#[test] +fn parse_empty_href() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap_err(); + insta::assert_snapshot!(result, @"Missing href attribute on anchor link"); +} + +#[test] +fn parse_empty_fragment() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl#", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_query_string() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl?project=legacy", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_missing_hash_value() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap_err(); + insta::assert_snapshot!(result, @"Unexpected fragment (expected `#sha256=...` or similar) on URL: sha256"); +} + +#[test] +fn parse_unknown_hash() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap_err(); + insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, or `sha512`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`"); +} + +#[test] +fn parse_flat_index_html() { + let text = r#" + + + + + cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl
+ cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl
+ + + "#; + let base = + Url::parse("https://storage.googleapis.com/jax-releases/jax_cuda_releases.html").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "storage.googleapis.com", + ), + ), + port: None, + path: "/jax-releases/jax_cuda_releases.html", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl", + yanked: None, + }, + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl", + yanked: None, + }, + ], + } + "###); +} + +/// Test for AWS Code Artifact +/// +/// See: +#[test] +fn parse_code_artifact_index_html() { + let text = r#" + + + + Links for flask + + +

Links for flask

+ Flask-0.1.tar.gz +
+ Flask-0.10.1.tar.gz +
+ flask-3.0.1.tar.gz +
+ + + "#; + let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") + .unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "account.d.codeartifact.us-west-2.amazonaws.com", + ), + ), + port: None, + path: "/pypi/shared-packages-pypi/simple/flask/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Flask-0.1.tar.gz", + hashes: Hashes { + md5: None, + sha256: Some( + "9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", + ), + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", + yanked: None, + }, + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Flask-0.10.1.tar.gz", + hashes: Hashes { + md5: None, + sha256: Some( + "4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", + ), + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", + yanked: None, + }, + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "flask-3.0.1.tar.gz", + hashes: Hashes { + md5: None, + sha256: Some( + "6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", + ), + sha384: None, + sha512: None, + }, + requires_python: Some( + Ok( + VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.8", + }, + ], + ), + ), + ), + size: None, + upload_time: None, + url: "3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", + yanked: None, + }, + ], + } + "###); +} + +#[test] +fn parse_file_requires_python_trailing_comma() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+ + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: None, + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: Some( + "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + ), + sha384: None, + sha512: None, + }, + requires_python: Some( + Ok( + VersionSpecifiers( + [ + VersionSpecifier { + operator: GreaterThanEqual, + version: "3.8", + }, + ], + ), + ), + ), + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", + yanked: None, + }, + ], + } + "###); +} + +/// Respect PEP 714 (see: ). +#[test] +fn parse_core_metadata() { + let text = r#" + + + +

Links for jinja2

+Jinja2-3.1.2-py3-none-any.whl
+Jinja2-3.1.3-py3-none-any.whl
+Jinja2-3.1.4-py3-none-any.whl
+Jinja2-3.1.5-py3-none-any.whl
+Jinja2-3.1.6-py3-none-any.whl
+ + + "#; + let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") + .unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "account.d.codeartifact.us-west-2.amazonaws.com", + ), + ), + port: None, + path: "/pypi/shared-packages-pypi/simple/flask/", + query: None, + fragment: None, + }, + ), + files: [ + File { + core_metadata: Some( + Bool( + true, + ), + ), + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl", + yanked: None, + }, + File { + core_metadata: Some( + Bool( + true, + ), + ), + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.3-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.3-py3-none-any.whl", + yanked: None, + }, + File { + core_metadata: Some( + Bool( + false, + ), + ), + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.4-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.4-py3-none-any.whl", + yanked: None, + }, + File { + core_metadata: Some( + Bool( + false, + ), + ), + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.5-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.5-py3-none-any.whl", + yanked: None, + }, + File { + core_metadata: Some( + Bool( + true, + ), + ), + dist_info_metadata: None, + data_dist_info_metadata: None, + filename: "Jinja2-3.1.6-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.6-py3-none-any.whl", + yanked: None, + }, + ], + } + "###); +} diff --git a/crates/uv-client/src/httpcache/control.rs b/crates/uv-client/src/httpcache/control.rs index 6860386bf..e728d6f08 100644 --- a/crates/uv-client/src/httpcache/control.rs +++ b/crates/uv-client/src/httpcache/control.rs @@ -453,326 +453,4 @@ impl CacheControlDirective { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn cache_control_token() { - let cc: CacheControl = CacheControlParser::new(["no-cache"]).collect(); - assert!(cc.no_cache); - assert!(!cc.must_revalidate); - } - - #[test] - fn cache_control_max_age() { - let cc: CacheControl = CacheControlParser::new(["max-age=60"]).collect(); - assert_eq!(Some(60), cc.max_age_seconds); - assert!(!cc.must_revalidate); - } - - // [RFC 9111 S5.2.1.1] says that client MUST NOT quote max-age, but we - // support parsing it that way anyway. - // - // [RFC 9111 S5.2.1.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1 - #[test] - fn cache_control_max_age_quoted() { - let cc: CacheControl = CacheControlParser::new([r#"max-age="60""#]).collect(); - assert_eq!(Some(60), cc.max_age_seconds); - assert!(!cc.must_revalidate); - } - - #[test] - fn cache_control_max_age_invalid() { - let cc: CacheControl = CacheControlParser::new(["max-age=6a0"]).collect(); - assert_eq!(None, cc.max_age_seconds); - assert!(cc.must_revalidate); - } - - #[test] - fn cache_control_immutable() { - let cc: CacheControl = CacheControlParser::new(["max-age=31536000, immutable"]).collect(); - assert_eq!(Some(31_536_000), cc.max_age_seconds); - assert!(cc.immutable); - assert!(!cc.must_revalidate); - } - - #[test] - fn cache_control_unrecognized() { - let cc: CacheControl = CacheControlParser::new(["lion,max-age=60,zebra"]).collect(); - assert_eq!(Some(60), cc.max_age_seconds); - } - - #[test] - fn cache_control_invalid_squashes_remainder() { - let cc: CacheControl = CacheControlParser::new(["no-cache,\x00,max-age=60"]).collect(); - // The invalid data doesn't impact things before it. - assert!(cc.no_cache); - // The invalid data precludes parsing anything after. - assert_eq!(None, cc.max_age_seconds); - // The invalid contents should force revalidation. - assert!(cc.must_revalidate); - } - - #[test] - fn cache_control_invalid_squashes_remainder_but_not_other_header_values() { - let cc: CacheControl = - CacheControlParser::new(["no-cache,\x00,max-age=60", "max-stale=30"]).collect(); - // The invalid data doesn't impact things before it. - assert!(cc.no_cache); - // The invalid data precludes parsing anything after - // in the same header value, but not in other - // header values. - assert_eq!(Some(30), cc.max_stale_seconds); - // The invalid contents should force revalidation. - assert!(cc.must_revalidate); - } - - #[test] - fn cache_control_parse_token() { - let directives = CacheControlParser::new(["no-cache"]).collect::>(); - assert_eq!( - directives, - vec![CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }] - ); - } - - #[test] - fn cache_control_parse_token_to_token_value() { - let directives = CacheControlParser::new(["max-age=60"]).collect::>(); - assert_eq!( - directives, - vec![CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }] - ); - } - - #[test] - fn cache_control_parse_token_to_quoted_string() { - let directives = - CacheControlParser::new([r#"private="cookie,x-something-else""#]).collect::>(); - assert_eq!( - directives, - vec![CacheControlDirective { - name: "private".to_string(), - value: b"cookie,x-something-else".to_vec(), - }] - ); - } - - #[test] - fn cache_control_parse_token_to_quoted_string_with_escape() { - let directives = - CacheControlParser::new([r#"private="something\"crazy""#]).collect::>(); - assert_eq!( - directives, - vec![CacheControlDirective { - name: "private".to_string(), - value: br#"something"crazy"#.to_vec(), - }] - ); - } - - #[test] - fn cache_control_parse_multiple_directives() { - let header = r#"max-age=60, no-cache, private="cookie", no-transform"#; - let directives = CacheControlParser::new([header]).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "private".to_string(), - value: b"cookie".to_vec(), - }, - CacheControlDirective { - name: "no-transform".to_string(), - value: vec![] - }, - ] - ); - } - - #[test] - fn cache_control_parse_multiple_directives_across_multiple_header_values() { - let headers = [ - r"max-age=60, no-cache", - r#"private="cookie""#, - r"no-transform", - ]; - let directives = CacheControlParser::new(headers).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "private".to_string(), - value: b"cookie".to_vec(), - }, - CacheControlDirective { - name: "no-transform".to_string(), - value: vec![] - }, - ] - ); - } - - #[test] - fn cache_control_parse_one_header_invalid() { - let headers = [ - r"max-age=60, no-cache", - r#", private="cookie""#, - r"no-transform", - ]; - let directives = CacheControlParser::new(headers).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "must-revalidate".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "no-transform".to_string(), - value: vec![] - }, - ] - ); - } - - #[test] - fn cache_control_parse_invalid_directive_drops_remainder() { - let header = r#"max-age=60, no-cache, ="cookie", no-transform"#; - let directives = CacheControlParser::new([header]).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "must-revalidate".to_string(), - value: vec![] - }, - ] - ); - } - - #[test] - fn cache_control_parse_name_normalized() { - let header = r"MAX-AGE=60"; - let directives = CacheControlParser::new([header]).collect::>(); - assert_eq!( - directives, - vec![CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - },] - ); - } - - // When a duplicate directive is found, we keep the first one - // and add in a `must-revalidate` directive to indicate that - // things are stale and the client should do a re-check. - #[test] - fn cache_control_parse_duplicate_directives() { - let header = r"max-age=60, no-cache, max-age=30"; - let directives = CacheControlParser::new([header]).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "must-revalidate".to_string(), - value: vec![] - }, - ] - ); - } - - #[test] - fn cache_control_parse_duplicate_directives_across_headers() { - let headers = [r"max-age=60, no-cache", r"max-age=30"]; - let directives = CacheControlParser::new(headers).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "must-revalidate".to_string(), - value: vec![] - }, - ] - ); - } - - // Tests that we don't emit must-revalidate multiple times - // even when something is duplicated multiple times. - #[test] - fn cache_control_parse_duplicate_redux() { - let header = r"max-age=60, no-cache, no-cache, max-age=30"; - let directives = CacheControlParser::new([header]).collect::>(); - assert_eq!( - directives, - vec![ - CacheControlDirective { - name: "max-age".to_string(), - value: b"60".to_vec(), - }, - CacheControlDirective { - name: "no-cache".to_string(), - value: vec![] - }, - CacheControlDirective { - name: "must-revalidate".to_string(), - value: vec![] - }, - ] - ); - } -} +mod tests; diff --git a/crates/uv-client/src/httpcache/control/tests.rs b/crates/uv-client/src/httpcache/control/tests.rs new file mode 100644 index 000000000..34cf770fa --- /dev/null +++ b/crates/uv-client/src/httpcache/control/tests.rs @@ -0,0 +1,320 @@ +use super::*; + +#[test] +fn cache_control_token() { + let cc: CacheControl = CacheControlParser::new(["no-cache"]).collect(); + assert!(cc.no_cache); + assert!(!cc.must_revalidate); +} + +#[test] +fn cache_control_max_age() { + let cc: CacheControl = CacheControlParser::new(["max-age=60"]).collect(); + assert_eq!(Some(60), cc.max_age_seconds); + assert!(!cc.must_revalidate); +} + +// [RFC 9111 S5.2.1.1] says that client MUST NOT quote max-age, but we +// support parsing it that way anyway. +// +// [RFC 9111 S5.2.1.1]: https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1 +#[test] +fn cache_control_max_age_quoted() { + let cc: CacheControl = CacheControlParser::new([r#"max-age="60""#]).collect(); + assert_eq!(Some(60), cc.max_age_seconds); + assert!(!cc.must_revalidate); +} + +#[test] +fn cache_control_max_age_invalid() { + let cc: CacheControl = CacheControlParser::new(["max-age=6a0"]).collect(); + assert_eq!(None, cc.max_age_seconds); + assert!(cc.must_revalidate); +} + +#[test] +fn cache_control_immutable() { + let cc: CacheControl = CacheControlParser::new(["max-age=31536000, immutable"]).collect(); + assert_eq!(Some(31_536_000), cc.max_age_seconds); + assert!(cc.immutable); + assert!(!cc.must_revalidate); +} + +#[test] +fn cache_control_unrecognized() { + let cc: CacheControl = CacheControlParser::new(["lion,max-age=60,zebra"]).collect(); + assert_eq!(Some(60), cc.max_age_seconds); +} + +#[test] +fn cache_control_invalid_squashes_remainder() { + let cc: CacheControl = CacheControlParser::new(["no-cache,\x00,max-age=60"]).collect(); + // The invalid data doesn't impact things before it. + assert!(cc.no_cache); + // The invalid data precludes parsing anything after. + assert_eq!(None, cc.max_age_seconds); + // The invalid contents should force revalidation. + assert!(cc.must_revalidate); +} + +#[test] +fn cache_control_invalid_squashes_remainder_but_not_other_header_values() { + let cc: CacheControl = + CacheControlParser::new(["no-cache,\x00,max-age=60", "max-stale=30"]).collect(); + // The invalid data doesn't impact things before it. + assert!(cc.no_cache); + // The invalid data precludes parsing anything after + // in the same header value, but not in other + // header values. + assert_eq!(Some(30), cc.max_stale_seconds); + // The invalid contents should force revalidation. + assert!(cc.must_revalidate); +} + +#[test] +fn cache_control_parse_token() { + let directives = CacheControlParser::new(["no-cache"]).collect::>(); + assert_eq!( + directives, + vec![CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }] + ); +} + +#[test] +fn cache_control_parse_token_to_token_value() { + let directives = CacheControlParser::new(["max-age=60"]).collect::>(); + assert_eq!( + directives, + vec![CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }] + ); +} + +#[test] +fn cache_control_parse_token_to_quoted_string() { + let directives = + CacheControlParser::new([r#"private="cookie,x-something-else""#]).collect::>(); + assert_eq!( + directives, + vec![CacheControlDirective { + name: "private".to_string(), + value: b"cookie,x-something-else".to_vec(), + }] + ); +} + +#[test] +fn cache_control_parse_token_to_quoted_string_with_escape() { + let directives = CacheControlParser::new([r#"private="something\"crazy""#]).collect::>(); + assert_eq!( + directives, + vec![CacheControlDirective { + name: "private".to_string(), + value: br#"something"crazy"#.to_vec(), + }] + ); +} + +#[test] +fn cache_control_parse_multiple_directives() { + let header = r#"max-age=60, no-cache, private="cookie", no-transform"#; + let directives = CacheControlParser::new([header]).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "private".to_string(), + value: b"cookie".to_vec(), + }, + CacheControlDirective { + name: "no-transform".to_string(), + value: vec![] + }, + ] + ); +} + +#[test] +fn cache_control_parse_multiple_directives_across_multiple_header_values() { + let headers = [ + r"max-age=60, no-cache", + r#"private="cookie""#, + r"no-transform", + ]; + let directives = CacheControlParser::new(headers).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "private".to_string(), + value: b"cookie".to_vec(), + }, + CacheControlDirective { + name: "no-transform".to_string(), + value: vec![] + }, + ] + ); +} + +#[test] +fn cache_control_parse_one_header_invalid() { + let headers = [ + r"max-age=60, no-cache", + r#", private="cookie""#, + r"no-transform", + ]; + let directives = CacheControlParser::new(headers).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "must-revalidate".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "no-transform".to_string(), + value: vec![] + }, + ] + ); +} + +#[test] +fn cache_control_parse_invalid_directive_drops_remainder() { + let header = r#"max-age=60, no-cache, ="cookie", no-transform"#; + let directives = CacheControlParser::new([header]).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "must-revalidate".to_string(), + value: vec![] + }, + ] + ); +} + +#[test] +fn cache_control_parse_name_normalized() { + let header = r"MAX-AGE=60"; + let directives = CacheControlParser::new([header]).collect::>(); + assert_eq!( + directives, + vec![CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + },] + ); +} + +// When a duplicate directive is found, we keep the first one +// and add in a `must-revalidate` directive to indicate that +// things are stale and the client should do a re-check. +#[test] +fn cache_control_parse_duplicate_directives() { + let header = r"max-age=60, no-cache, max-age=30"; + let directives = CacheControlParser::new([header]).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "must-revalidate".to_string(), + value: vec![] + }, + ] + ); +} + +#[test] +fn cache_control_parse_duplicate_directives_across_headers() { + let headers = [r"max-age=60, no-cache", r"max-age=30"]; + let directives = CacheControlParser::new(headers).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "must-revalidate".to_string(), + value: vec![] + }, + ] + ); +} + +// Tests that we don't emit must-revalidate multiple times +// even when something is duplicated multiple times. +#[test] +fn cache_control_parse_duplicate_redux() { + let header = r"max-age=60, no-cache, no-cache, max-age=30"; + let directives = CacheControlParser::new([header]).collect::>(); + assert_eq!( + directives, + vec![ + CacheControlDirective { + name: "max-age".to_string(), + value: b"60".to_vec(), + }, + CacheControlDirective { + name: "no-cache".to_string(), + value: vec![] + }, + CacheControlDirective { + name: "must-revalidate".to_string(), + value: vec![] + }, + ] + ); +} diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index f98ca7f8c..923fa8a9a 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -901,107 +901,4 @@ impl Connectivity { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use url::Url; - - use uv_normalize::PackageName; - use uv_pypi_types::{JoinRelativeError, SimpleJson}; - - use crate::{html::SimpleHtml, SimpleMetadata, SimpleMetadatum}; - - #[test] - fn ignore_failing_files() { - // 1.7.7 has an invalid requires-python field (double comma), 1.7.8 is valid - let response = r#" - { - "files": [ - { - "core-metadata": false, - "data-dist-info-metadata": false, - "filename": "pyflyby-1.7.7.tar.gz", - "hashes": { - "sha256": "0c4d953f405a7be1300b440dbdbc6917011a07d8401345a97e72cd410d5fb291" - }, - "requires-python": ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.2.*, !=3.3.*, !=3.4.*,, !=3.5.*, !=3.6.*, <4", - "size": 427200, - "upload-time": "2022-05-19T09:14:36.591835Z", - "url": "https://files.pythonhosted.org/packages/61/93/9fec62902d0b4fc2521333eba047bff4adbba41f1723a6382367f84ee522/pyflyby-1.7.7.tar.gz", - "yanked": false - }, - { - "core-metadata": false, - "data-dist-info-metadata": false, - "filename": "pyflyby-1.7.8.tar.gz", - "hashes": { - "sha256": "1ee37474f6da8f98653dbcc208793f50b7ace1d9066f49e2707750a5ba5d53c6" - }, - "requires-python": ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, <4", - "size": 424460, - "upload-time": "2022-08-04T10:42:02.190074Z", - "url": "https://files.pythonhosted.org/packages/ad/39/17180d9806a1c50197bc63b25d0f1266f745fc3b23f11439fccb3d6baa50/pyflyby-1.7.8.tar.gz", - "yanked": false - } - ] - } - "#; - let data: SimpleJson = serde_json::from_str(response).unwrap(); - let base = Url::parse("https://pypi.org/simple/pyflyby/").unwrap(); - let simple_metadata = SimpleMetadata::from_files( - data.files, - &PackageName::from_str("pyflyby").unwrap(), - &base, - ); - let versions: Vec = simple_metadata - .iter() - .map(|SimpleMetadatum { version, .. }| version.to_string()) - .collect(); - assert_eq!(versions, ["1.7.8".to_string()]); - } - - /// Test for AWS Code Artifact registry - /// - /// See: - #[test] - fn relative_urls_code_artifact() -> Result<(), JoinRelativeError> { - let text = r#" - - - - Links for flask - - -

Links for flask

- Flask-0.1.tar.gz -
- Flask-0.10.1.tar.gz -
- flask-3.0.1.tar.gz -
- - - "#; - - // Note the lack of a trailing `/` here is important for coverage of url-join behavior - let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask") - .unwrap(); - let SimpleHtml { base, files } = SimpleHtml::parse(text, &base).unwrap(); - - // Test parsing of the file urls - let urls = files - .iter() - .map(|file| uv_pypi_types::base_url_join_relative(base.as_url().as_str(), &file.url)) - .collect::, JoinRelativeError>>()?; - let urls = urls.iter().map(reqwest::Url::as_str).collect::>(); - insta::assert_debug_snapshot!(urls, @r###" - [ - "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", - "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", - "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", - ] - "###); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-client/src/registry_client/tests.rs b/crates/uv-client/src/registry_client/tests.rs new file mode 100644 index 000000000..6e31a5cf3 --- /dev/null +++ b/crates/uv-client/src/registry_client/tests.rs @@ -0,0 +1,102 @@ +use std::str::FromStr; + +use url::Url; + +use uv_normalize::PackageName; +use uv_pypi_types::{JoinRelativeError, SimpleJson}; + +use crate::{html::SimpleHtml, SimpleMetadata, SimpleMetadatum}; + +#[test] +fn ignore_failing_files() { + // 1.7.7 has an invalid requires-python field (double comma), 1.7.8 is valid + let response = r#" + { + "files": [ + { + "core-metadata": false, + "data-dist-info-metadata": false, + "filename": "pyflyby-1.7.7.tar.gz", + "hashes": { + "sha256": "0c4d953f405a7be1300b440dbdbc6917011a07d8401345a97e72cd410d5fb291" + }, + "requires-python": ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.2.*, !=3.3.*, !=3.4.*,, !=3.5.*, !=3.6.*, <4", + "size": 427200, + "upload-time": "2022-05-19T09:14:36.591835Z", + "url": "https://files.pythonhosted.org/packages/61/93/9fec62902d0b4fc2521333eba047bff4adbba41f1723a6382367f84ee522/pyflyby-1.7.7.tar.gz", + "yanked": false + }, + { + "core-metadata": false, + "data-dist-info-metadata": false, + "filename": "pyflyby-1.7.8.tar.gz", + "hashes": { + "sha256": "1ee37474f6da8f98653dbcc208793f50b7ace1d9066f49e2707750a5ba5d53c6" + }, + "requires-python": ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, <4", + "size": 424460, + "upload-time": "2022-08-04T10:42:02.190074Z", + "url": "https://files.pythonhosted.org/packages/ad/39/17180d9806a1c50197bc63b25d0f1266f745fc3b23f11439fccb3d6baa50/pyflyby-1.7.8.tar.gz", + "yanked": false + } + ] + } + "#; + let data: SimpleJson = serde_json::from_str(response).unwrap(); + let base = Url::parse("https://pypi.org/simple/pyflyby/").unwrap(); + let simple_metadata = SimpleMetadata::from_files( + data.files, + &PackageName::from_str("pyflyby").unwrap(), + &base, + ); + let versions: Vec = simple_metadata + .iter() + .map(|SimpleMetadatum { version, .. }| version.to_string()) + .collect(); + assert_eq!(versions, ["1.7.8".to_string()]); +} + +/// Test for AWS Code Artifact registry +/// +/// See: +#[test] +fn relative_urls_code_artifact() -> Result<(), JoinRelativeError> { + let text = r#" + + + + Links for flask + + +

Links for flask

+ Flask-0.1.tar.gz +
+ Flask-0.10.1.tar.gz +
+ flask-3.0.1.tar.gz +
+ + + "#; + + // Note the lack of a trailing `/` here is important for coverage of url-join behavior + let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask") + .unwrap(); + let SimpleHtml { base, files } = SimpleHtml::parse(text, &base).unwrap(); + + // Test parsing of the file urls + let urls = files + .iter() + .map(|file| uv_pypi_types::base_url_join_relative(base.as_url().as_str(), &file.url)) + .collect::, JoinRelativeError>>()?; + let urls = urls.iter().map(reqwest::Url::as_str).collect::>(); + insta::assert_debug_snapshot!(urls, @r###" + [ + "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", + "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", + "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", + ] + "###); + + Ok(()) +} diff --git a/crates/uv-client/tests/it/main.rs b/crates/uv-client/tests/it/main.rs new file mode 100644 index 000000000..c6f2b96e8 --- /dev/null +++ b/crates/uv-client/tests/it/main.rs @@ -0,0 +1,2 @@ +mod remote_metadata; +mod user_agent_version; diff --git a/crates/uv-client/tests/remote_metadata.rs b/crates/uv-client/tests/it/remote_metadata.rs similarity index 100% rename from crates/uv-client/tests/remote_metadata.rs rename to crates/uv-client/tests/it/remote_metadata.rs diff --git a/crates/uv-client/tests/user_agent_version.rs b/crates/uv-client/tests/it/user_agent_version.rs similarity index 100% rename from crates/uv-client/tests/user_agent_version.rs rename to crates/uv-client/tests/it/user_agent_version.rs diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 974c76165..a7f07eb20 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-configuration/src/build_options.rs b/crates/uv-configuration/src/build_options.rs index 1a62a1a12..cfdb807af 100644 --- a/crates/uv-configuration/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -354,66 +354,4 @@ pub enum IndexStrategy { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use anyhow::Error; - - use super::*; - - #[test] - fn no_build_from_args() -> Result<(), Error> { - assert_eq!( - NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], false), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], true), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], true), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], false), - NoBuild::None, - ); - assert_eq!( - NoBuild::from_pip_args( - vec![ - PackageNameSpecifier::from_str("foo")?, - PackageNameSpecifier::from_str("bar")? - ], - false - ), - NoBuild::Packages(vec![ - PackageName::from_str("foo")?, - PackageName::from_str("bar")? - ]), - ); - assert_eq!( - NoBuild::from_pip_args( - vec![ - PackageNameSpecifier::from_str("test")?, - PackageNameSpecifier::All - ], - false - ), - NoBuild::All, - ); - assert_eq!( - NoBuild::from_pip_args( - vec![ - PackageNameSpecifier::from_str("foo")?, - PackageNameSpecifier::from_str(":none:")?, - PackageNameSpecifier::from_str("bar")? - ], - false - ), - NoBuild::Packages(vec![PackageName::from_str("bar")?]), - ); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-configuration/src/build_options/tests.rs b/crates/uv-configuration/src/build_options/tests.rs new file mode 100644 index 000000000..4eabc928b --- /dev/null +++ b/crates/uv-configuration/src/build_options/tests.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use anyhow::Error; + +use super::*; + +#[test] +fn no_build_from_args() -> Result<(), Error> { + assert_eq!( + NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], false), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":all:")?], true), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], true), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_pip_args(vec![PackageNameSpecifier::from_str(":none:")?], false), + NoBuild::None, + ); + assert_eq!( + NoBuild::from_pip_args( + vec![ + PackageNameSpecifier::from_str("foo")?, + PackageNameSpecifier::from_str("bar")? + ], + false + ), + NoBuild::Packages(vec![ + PackageName::from_str("foo")?, + PackageName::from_str("bar")? + ]), + ); + assert_eq!( + NoBuild::from_pip_args( + vec![ + PackageNameSpecifier::from_str("test")?, + PackageNameSpecifier::All + ], + false + ), + NoBuild::All, + ); + assert_eq!( + NoBuild::from_pip_args( + vec![ + PackageNameSpecifier::from_str("foo")?, + PackageNameSpecifier::from_str(":none:")?, + PackageNameSpecifier::from_str("bar")? + ], + false + ), + NoBuild::Packages(vec![PackageName::from_str("bar")?]), + ); + + Ok(()) +} diff --git a/crates/uv-configuration/src/config_settings.rs b/crates/uv-configuration/src/config_settings.rs index 7d8907483..a25521e49 100644 --- a/crates/uv-configuration/src/config_settings.rs +++ b/crates/uv-configuration/src/config_settings.rs @@ -213,82 +213,4 @@ impl<'de> serde::Deserialize<'de> for ConfigSettings { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn collect_config_settings() { - let settings: ConfigSettings = vec![ - ConfigSettingEntry { - key: "key".to_string(), - value: "value".to_string(), - }, - ConfigSettingEntry { - key: "key".to_string(), - value: "value2".to_string(), - }, - ConfigSettingEntry { - key: "list".to_string(), - value: "value3".to_string(), - }, - ConfigSettingEntry { - key: "list".to_string(), - value: "value4".to_string(), - }, - ] - .into_iter() - .collect(); - assert_eq!( - settings.0.get("key"), - Some(&ConfigSettingValue::List(vec![ - "value".to_string(), - "value2".to_string() - ])) - ); - assert_eq!( - settings.0.get("list"), - Some(&ConfigSettingValue::List(vec![ - "value3".to_string(), - "value4".to_string() - ])) - ); - } - - #[test] - fn escape_for_python() { - let mut settings = ConfigSettings::default(); - settings.0.insert( - "key".to_string(), - ConfigSettingValue::String("value".to_string()), - ); - settings.0.insert( - "list".to_string(), - ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]), - ); - assert_eq!( - settings.escape_for_python(), - r#"{"key":"value","list":["value1","value2"]}"# - ); - - let mut settings = ConfigSettings::default(); - settings.0.insert( - "key".to_string(), - ConfigSettingValue::String("Hello, \"world!\"".to_string()), - ); - settings.0.insert( - "list".to_string(), - ConfigSettingValue::List(vec!["'value1'".to_string()]), - ); - assert_eq!( - settings.escape_for_python(), - r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"# - ); - - let mut settings = ConfigSettings::default(); - settings.0.insert( - "key".to_string(), - ConfigSettingValue::String("val\\1 {}value".to_string()), - ); - assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#); - } -} +mod tests; diff --git a/crates/uv-configuration/src/config_settings/tests.rs b/crates/uv-configuration/src/config_settings/tests.rs new file mode 100644 index 000000000..120a331ce --- /dev/null +++ b/crates/uv-configuration/src/config_settings/tests.rs @@ -0,0 +1,77 @@ +use super::*; + +#[test] +fn collect_config_settings() { + let settings: ConfigSettings = vec![ + ConfigSettingEntry { + key: "key".to_string(), + value: "value".to_string(), + }, + ConfigSettingEntry { + key: "key".to_string(), + value: "value2".to_string(), + }, + ConfigSettingEntry { + key: "list".to_string(), + value: "value3".to_string(), + }, + ConfigSettingEntry { + key: "list".to_string(), + value: "value4".to_string(), + }, + ] + .into_iter() + .collect(); + assert_eq!( + settings.0.get("key"), + Some(&ConfigSettingValue::List(vec![ + "value".to_string(), + "value2".to_string() + ])) + ); + assert_eq!( + settings.0.get("list"), + Some(&ConfigSettingValue::List(vec![ + "value3".to_string(), + "value4".to_string() + ])) + ); +} + +#[test] +fn escape_for_python() { + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("value".to_string()), + ); + settings.0.insert( + "list".to_string(), + ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]), + ); + assert_eq!( + settings.escape_for_python(), + r#"{"key":"value","list":["value1","value2"]}"# + ); + + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("Hello, \"world!\"".to_string()), + ); + settings.0.insert( + "list".to_string(), + ConfigSettingValue::List(vec!["'value1'".to_string()]), + ); + assert_eq!( + settings.escape_for_python(), + r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"# + ); + + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("val\\1 {}value".to_string()), + ); + assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#); +} diff --git a/crates/uv-configuration/src/trusted_host.rs b/crates/uv-configuration/src/trusted_host.rs index 4fa1bce67..697f7afbf 100644 --- a/crates/uv-configuration/src/trusted_host.rs +++ b/crates/uv-configuration/src/trusted_host.rs @@ -145,45 +145,4 @@ impl schemars::JsonSchema for TrustedHost { } #[cfg(test)] -mod tests { - #[test] - fn parse() { - assert_eq!( - "example.com".parse::().unwrap(), - super::TrustedHost { - scheme: None, - host: "example.com".to_string(), - port: None - } - ); - - assert_eq!( - "example.com:8080".parse::().unwrap(), - super::TrustedHost { - scheme: None, - host: "example.com".to_string(), - port: Some(8080) - } - ); - - assert_eq!( - "https://example.com".parse::().unwrap(), - super::TrustedHost { - scheme: Some("https".to_string()), - host: "example.com".to_string(), - port: None - } - ); - - assert_eq!( - "https://example.com/hello/world" - .parse::() - .unwrap(), - super::TrustedHost { - scheme: Some("https".to_string()), - host: "example.com".to_string(), - port: None - } - ); - } -} +mod tests; diff --git a/crates/uv-configuration/src/trusted_host/tests.rs b/crates/uv-configuration/src/trusted_host/tests.rs new file mode 100644 index 000000000..b24a254da --- /dev/null +++ b/crates/uv-configuration/src/trusted_host/tests.rs @@ -0,0 +1,40 @@ +#[test] +fn parse() { + assert_eq!( + "example.com".parse::().unwrap(), + super::TrustedHost { + scheme: None, + host: "example.com".to_string(), + port: None + } + ); + + assert_eq!( + "example.com:8080".parse::().unwrap(), + super::TrustedHost { + scheme: None, + host: "example.com".to_string(), + port: Some(8080) + } + ); + + assert_eq!( + "https://example.com".parse::().unwrap(), + super::TrustedHost { + scheme: Some("https".to_string()), + host: "example.com".to_string(), + port: None + } + ); + + assert_eq!( + "https://example.com/hello/world" + .parse::() + .unwrap(), + super::TrustedHost { + scheme: Some("https".to_string()), + host: "example.com".to_string(), + port: None + } + ); +} diff --git a/crates/uv-console/Cargo.toml b/crates/uv-console/Cargo.toml index ef0303f71..597105ee9 100644 --- a/crates/uv-console/Cargo.toml +++ b/crates/uv-console/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" edition = "2021" description = "Utilities for interacting with the terminal" +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-dev/src/generate_cli_reference.rs b/crates/uv-dev/src/generate_cli_reference.rs index 0d743ecd9..f036053ce 100644 --- a/crates/uv-dev/src/generate_cli_reference.rs +++ b/crates/uv-dev/src/generate_cli_reference.rs @@ -324,22 +324,4 @@ fn emit_possible_options(opt: &clap::Arg, output: &mut String) { } #[cfg(test)] -mod tests { - use std::env; - - use anyhow::Result; - - use crate::generate_all::Mode; - - use super::{main, Args}; - - #[test] - fn test_generate_cli_reference() -> Result<()> { - let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") { - Mode::Write - } else { - Mode::Check - }; - main(&Args { mode }) - } -} +mod tests; diff --git a/crates/uv-dev/src/generate_cli_reference/tests.rs b/crates/uv-dev/src/generate_cli_reference/tests.rs new file mode 100644 index 000000000..0fd1f917d --- /dev/null +++ b/crates/uv-dev/src/generate_cli_reference/tests.rs @@ -0,0 +1,17 @@ +use std::env; + +use anyhow::Result; + +use crate::generate_all::Mode; + +use super::{main, Args}; + +#[test] +fn test_generate_cli_reference() -> Result<()> { + let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) +} diff --git a/crates/uv-dev/src/generate_json_schema.rs b/crates/uv-dev/src/generate_json_schema.rs index 7eda12cf8..2fcf5d5d8 100644 --- a/crates/uv-dev/src/generate_json_schema.rs +++ b/crates/uv-dev/src/generate_json_schema.rs @@ -81,22 +81,4 @@ pub(crate) fn main(args: &Args) -> Result<()> { } #[cfg(test)] -mod tests { - use std::env; - - use anyhow::Result; - - use crate::generate_all::Mode; - - use super::{main, Args}; - - #[test] - fn test_generate_json_schema() -> Result<()> { - let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") { - Mode::Write - } else { - Mode::Check - }; - main(&Args { mode }) - } -} +mod tests; diff --git a/crates/uv-dev/src/generate_json_schema/tests.rs b/crates/uv-dev/src/generate_json_schema/tests.rs new file mode 100644 index 000000000..8b23efa5a --- /dev/null +++ b/crates/uv-dev/src/generate_json_schema/tests.rs @@ -0,0 +1,17 @@ +use std::env; + +use anyhow::Result; + +use crate::generate_all::Mode; + +use super::{main, Args}; + +#[test] +fn test_generate_json_schema() -> Result<()> { + let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) +} diff --git a/crates/uv-dev/src/generate_options_reference.rs b/crates/uv-dev/src/generate_options_reference.rs index 4884de9e1..0e066d9af 100644 --- a/crates/uv-dev/src/generate_options_reference.rs +++ b/crates/uv-dev/src/generate_options_reference.rs @@ -350,22 +350,4 @@ impl Visit for CollectOptionsVisitor { } #[cfg(test)] -mod tests { - use std::env; - - use anyhow::Result; - - use crate::generate_all::Mode; - - use super::{main, Args}; - - #[test] - fn test_generate_options_reference() -> Result<()> { - let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") { - Mode::Write - } else { - Mode::Check - }; - main(&Args { mode }) - } -} +mod tests; diff --git a/crates/uv-dev/src/generate_options_reference/tests.rs b/crates/uv-dev/src/generate_options_reference/tests.rs new file mode 100644 index 000000000..aff7a6157 --- /dev/null +++ b/crates/uv-dev/src/generate_options_reference/tests.rs @@ -0,0 +1,17 @@ +use std::env; + +use anyhow::Result; + +use crate::generate_all::Mode; + +use super::{main, Args}; + +#[test] +fn test_generate_options_reference() -> Result<()> { + let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) +} diff --git a/crates/uv-dispatch/Cargo.toml b/crates/uv-dispatch/Cargo.toml index 638caa7aa..b61a7534b 100644 --- a/crates/uv-dispatch/Cargo.toml +++ b/crates/uv-dispatch/Cargo.toml @@ -10,6 +10,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-distribution-filename/Cargo.toml b/crates/uv-distribution-filename/Cargo.toml index 5411b75cb..b8484b97e 100644 --- a/crates/uv-distribution-filename/Cargo.toml +++ b/crates/uv-distribution-filename/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-distribution-filename/src/egg.rs b/crates/uv-distribution-filename/src/egg.rs index f891169a3..84bbf3b64 100644 --- a/crates/uv-distribution-filename/src/egg.rs +++ b/crates/uv-distribution-filename/src/egg.rs @@ -80,38 +80,4 @@ impl FromStr for EggInfoFilename { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn egg_info_filename() { - let filename = "zstandard-0.22.0-py3.12-darwin.egg-info"; - let parsed = EggInfoFilename::from_str(filename).unwrap(); - assert_eq!(parsed.name.as_ref(), "zstandard"); - assert_eq!( - parsed.version.map(|v| v.to_string()), - Some("0.22.0".to_string()) - ); - - let filename = "zstandard-0.22.0-py3.12.egg-info"; - let parsed = EggInfoFilename::from_str(filename).unwrap(); - assert_eq!(parsed.name.as_ref(), "zstandard"); - assert_eq!( - parsed.version.map(|v| v.to_string()), - Some("0.22.0".to_string()) - ); - - let filename = "zstandard-0.22.0.egg-info"; - let parsed = EggInfoFilename::from_str(filename).unwrap(); - assert_eq!(parsed.name.as_ref(), "zstandard"); - assert_eq!( - parsed.version.map(|v| v.to_string()), - Some("0.22.0".to_string()) - ); - - let filename = "zstandard.egg-info"; - let parsed = EggInfoFilename::from_str(filename).unwrap(); - assert_eq!(parsed.name.as_ref(), "zstandard"); - assert!(parsed.version.is_none()); - } -} +mod tests; diff --git a/crates/uv-distribution-filename/src/egg/tests.rs b/crates/uv-distribution-filename/src/egg/tests.rs new file mode 100644 index 000000000..47c4dd56b --- /dev/null +++ b/crates/uv-distribution-filename/src/egg/tests.rs @@ -0,0 +1,33 @@ +use super::*; + +#[test] +fn egg_info_filename() { + let filename = "zstandard-0.22.0-py3.12-darwin.egg-info"; + let parsed = EggInfoFilename::from_str(filename).unwrap(); + assert_eq!(parsed.name.as_ref(), "zstandard"); + assert_eq!( + parsed.version.map(|v| v.to_string()), + Some("0.22.0".to_string()) + ); + + let filename = "zstandard-0.22.0-py3.12.egg-info"; + let parsed = EggInfoFilename::from_str(filename).unwrap(); + assert_eq!(parsed.name.as_ref(), "zstandard"); + assert_eq!( + parsed.version.map(|v| v.to_string()), + Some("0.22.0".to_string()) + ); + + let filename = "zstandard-0.22.0.egg-info"; + let parsed = EggInfoFilename::from_str(filename).unwrap(); + assert_eq!(parsed.name.as_ref(), "zstandard"); + assert_eq!( + parsed.version.map(|v| v.to_string()), + Some("0.22.0".to_string()) + ); + + let filename = "zstandard.egg-info"; + let parsed = EggInfoFilename::from_str(filename).unwrap(); + assert_eq!(parsed.name.as_ref(), "zstandard"); + assert!(parsed.version.is_none()); +} diff --git a/crates/uv-distribution-filename/src/source_dist.rs b/crates/uv-distribution-filename/src/source_dist.rs index a3920a32d..2c1c26740 100644 --- a/crates/uv-distribution-filename/src/source_dist.rs +++ b/crates/uv-distribution-filename/src/source_dist.rs @@ -170,58 +170,4 @@ enum SourceDistFilenameErrorKind { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use uv_normalize::PackageName; - - use crate::{SourceDistExtension, SourceDistFilename}; - - /// Only test already normalized names since the parsing is lossy - /// - /// - /// - #[test] - fn roundtrip() { - for normalized in [ - "foo_lib-1.2.3.zip", - "foo_lib-1.2.3a3.zip", - "foo_lib-1.2.3.tar.gz", - "foo_lib-1.2.3.tar.bz2", - "foo_lib-1.2.3.tar.zst", - ] { - let ext = SourceDistExtension::from_path(normalized).unwrap(); - assert_eq!( - SourceDistFilename::parse( - normalized, - ext, - &PackageName::from_str("foo_lib").unwrap() - ) - .unwrap() - .to_string(), - normalized - ); - } - } - - #[test] - fn errors() { - for invalid in ["b-1.2.3.zip", "a-1.2.3-gamma.3.zip"] { - let ext = SourceDistExtension::from_path(invalid).unwrap(); - assert!( - SourceDistFilename::parse(invalid, ext, &PackageName::from_str("a").unwrap()) - .is_err() - ); - } - } - - #[test] - fn name_too_long() { - assert!(SourceDistFilename::parse( - "foo.zip", - SourceDistExtension::Zip, - &PackageName::from_str("foo-lib").unwrap() - ) - .is_err()); - } -} +mod tests; diff --git a/crates/uv-distribution-filename/src/source_dist/tests.rs b/crates/uv-distribution-filename/src/source_dist/tests.rs new file mode 100644 index 000000000..f81f089f1 --- /dev/null +++ b/crates/uv-distribution-filename/src/source_dist/tests.rs @@ -0,0 +1,48 @@ +use std::str::FromStr; + +use uv_normalize::PackageName; + +use crate::{SourceDistExtension, SourceDistFilename}; + +/// Only test already normalized names since the parsing is lossy +/// +/// +/// +#[test] +fn roundtrip() { + for normalized in [ + "foo_lib-1.2.3.zip", + "foo_lib-1.2.3a3.zip", + "foo_lib-1.2.3.tar.gz", + "foo_lib-1.2.3.tar.bz2", + "foo_lib-1.2.3.tar.zst", + ] { + let ext = SourceDistExtension::from_path(normalized).unwrap(); + assert_eq!( + SourceDistFilename::parse(normalized, ext, &PackageName::from_str("foo_lib").unwrap()) + .unwrap() + .to_string(), + normalized + ); + } +} + +#[test] +fn errors() { + for invalid in ["b-1.2.3.zip", "a-1.2.3-gamma.3.zip"] { + let ext = SourceDistExtension::from_path(invalid).unwrap(); + assert!( + SourceDistFilename::parse(invalid, ext, &PackageName::from_str("a").unwrap()).is_err() + ); + } +} + +#[test] +fn name_too_long() { + assert!(SourceDistFilename::parse( + "foo.zip", + SourceDistExtension::Zip, + &PackageName::from_str("foo-lib").unwrap() + ) + .is_err()); +} diff --git a/crates/uv-distribution-filename/src/wheel.rs b/crates/uv-distribution-filename/src/wheel.rs index a91c3e141..09b675b11 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -234,101 +234,4 @@ pub enum WheelFilenameError { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn err_not_whl_extension() { - let err = WheelFilename::from_str("foo.rs").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo.rs" is invalid: Must end with .whl"###); - } - - #[test] - fn err_1_part_empty() { - let err = WheelFilename::from_str(".whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename ".whl" is invalid: Must have a version"###); - } - - #[test] - fn err_1_part_no_version() { - let err = WheelFilename::from_str("foo.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo.whl" is invalid: Must have a version"###); - } - - #[test] - fn err_2_part_no_pythontag() { - let err = WheelFilename::from_str("foo-version.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo-version.whl" is invalid: Must have a Python tag"###); - } - - #[test] - fn err_3_part_no_abitag() { - let err = WheelFilename::from_str("foo-version-python.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo-version-python.whl" is invalid: Must have an ABI tag"###); - } - - #[test] - fn err_4_part_no_platformtag() { - let err = WheelFilename::from_str("foo-version-python-abi.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo-version-python-abi.whl" is invalid: Must have a platform tag"###); - } - - #[test] - fn err_too_many_parts() { - let err = - WheelFilename::from_str("foo-1.2.3-build-python-abi-platform-oops.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-build-python-abi-platform-oops.whl" is invalid: Must have 5 or 6 components, but has more"###); - } - - #[test] - fn err_invalid_package_name() { - let err = WheelFilename::from_str("f!oo-1.2.3-python-abi-platform.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "f!oo-1.2.3-python-abi-platform.whl" has an invalid package name"###); - } - - #[test] - fn err_invalid_version() { - let err = WheelFilename::from_str("foo-x.y.z-python-abi-platform.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo-x.y.z-python-abi-platform.whl" has an invalid version: expected version to start with a number, but no leading ASCII digits were found"###); - } - - #[test] - fn err_invalid_build_tag() { - let err = WheelFilename::from_str("foo-1.2.3-tag-python-abi-platform.whl").unwrap_err(); - insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-tag-python-abi-platform.whl" has an invalid build tag: must start with a digit"###); - } - - #[test] - fn ok_single_tags() { - insta::assert_debug_snapshot!(WheelFilename::from_str("foo-1.2.3-foo-bar-baz.whl")); - } - - #[test] - fn ok_multiple_tags() { - insta::assert_debug_snapshot!(WheelFilename::from_str( - "foo-1.2.3-ab.cd.ef-gh-ij.kl.mn.op.qr.st.whl" - )); - } - - #[test] - fn ok_build_tag() { - insta::assert_debug_snapshot!(WheelFilename::from_str( - "foo-1.2.3-202206090410-python-abi-platform.whl" - )); - } - - #[test] - fn from_and_to_string() { - let wheel_names = &[ - "django_allauth-0.51.0-py3-none-any.whl", - "osm2geojson-0.2.4-py3-none-any.whl", - "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - ]; - for wheel_name in wheel_names { - assert_eq!( - WheelFilename::from_str(wheel_name).unwrap().to_string(), - *wheel_name - ); - } - } -} +mod tests; diff --git a/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap b/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap new file mode 100644 index 000000000..ee6b96c71 --- /dev/null +++ b/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap @@ -0,0 +1,27 @@ +--- +source: crates/uv-distribution-filename/src/wheel/tests.rs +expression: "WheelFilename::from_str(\"foo-1.2.3-202206090410-python-abi-platform.whl\")" +--- +Ok( + WheelFilename { + name: PackageName( + "foo", + ), + version: "1.2.3", + build_tag: Some( + BuildTag( + 202206090410, + None, + ), + ), + python_tag: [ + "python", + ], + abi_tag: [ + "abi", + ], + platform_tag: [ + "platform", + ], + }, +) diff --git a/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap b/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap new file mode 100644 index 000000000..811974f8b --- /dev/null +++ b/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap @@ -0,0 +1,29 @@ +--- +source: crates/uv-distribution-filename/src/wheel/tests.rs +expression: "WheelFilename::from_str(\"foo-1.2.3-ab.cd.ef-gh-ij.kl.mn.op.qr.st.whl\")" +--- +Ok( + WheelFilename { + name: PackageName( + "foo", + ), + version: "1.2.3", + build_tag: None, + python_tag: [ + "ab", + "cd", + "ef", + ], + abi_tag: [ + "gh", + ], + platform_tag: [ + "ij", + "kl", + "mn", + "op", + "qr", + "st", + ], + }, +) diff --git a/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap b/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap new file mode 100644 index 000000000..ddd909a31 --- /dev/null +++ b/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap @@ -0,0 +1,22 @@ +--- +source: crates/uv-distribution-filename/src/wheel/tests.rs +expression: "WheelFilename::from_str(\"foo-1.2.3-foo-bar-baz.whl\")" +--- +Ok( + WheelFilename { + name: PackageName( + "foo", + ), + version: "1.2.3", + build_tag: None, + python_tag: [ + "foo", + ], + abi_tag: [ + "bar", + ], + platform_tag: [ + "baz", + ], + }, +) diff --git a/crates/uv-distribution-filename/src/wheel/tests.rs b/crates/uv-distribution-filename/src/wheel/tests.rs new file mode 100644 index 000000000..59c94e4a3 --- /dev/null +++ b/crates/uv-distribution-filename/src/wheel/tests.rs @@ -0,0 +1,95 @@ +use super::*; + +#[test] +fn err_not_whl_extension() { + let err = WheelFilename::from_str("foo.rs").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo.rs" is invalid: Must end with .whl"###); +} + +#[test] +fn err_1_part_empty() { + let err = WheelFilename::from_str(".whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename ".whl" is invalid: Must have a version"###); +} + +#[test] +fn err_1_part_no_version() { + let err = WheelFilename::from_str("foo.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo.whl" is invalid: Must have a version"###); +} + +#[test] +fn err_2_part_no_pythontag() { + let err = WheelFilename::from_str("foo-version.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo-version.whl" is invalid: Must have a Python tag"###); +} + +#[test] +fn err_3_part_no_abitag() { + let err = WheelFilename::from_str("foo-version-python.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo-version-python.whl" is invalid: Must have an ABI tag"###); +} + +#[test] +fn err_4_part_no_platformtag() { + let err = WheelFilename::from_str("foo-version-python-abi.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo-version-python-abi.whl" is invalid: Must have a platform tag"###); +} + +#[test] +fn err_too_many_parts() { + let err = WheelFilename::from_str("foo-1.2.3-build-python-abi-platform-oops.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-build-python-abi-platform-oops.whl" is invalid: Must have 5 or 6 components, but has more"###); +} + +#[test] +fn err_invalid_package_name() { + let err = WheelFilename::from_str("f!oo-1.2.3-python-abi-platform.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "f!oo-1.2.3-python-abi-platform.whl" has an invalid package name"###); +} + +#[test] +fn err_invalid_version() { + let err = WheelFilename::from_str("foo-x.y.z-python-abi-platform.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo-x.y.z-python-abi-platform.whl" has an invalid version: expected version to start with a number, but no leading ASCII digits were found"###); +} + +#[test] +fn err_invalid_build_tag() { + let err = WheelFilename::from_str("foo-1.2.3-tag-python-abi-platform.whl").unwrap_err(); + insta::assert_snapshot!(err, @r###"The wheel filename "foo-1.2.3-tag-python-abi-platform.whl" has an invalid build tag: must start with a digit"###); +} + +#[test] +fn ok_single_tags() { + insta::assert_debug_snapshot!(WheelFilename::from_str("foo-1.2.3-foo-bar-baz.whl")); +} + +#[test] +fn ok_multiple_tags() { + insta::assert_debug_snapshot!(WheelFilename::from_str( + "foo-1.2.3-ab.cd.ef-gh-ij.kl.mn.op.qr.st.whl" + )); +} + +#[test] +fn ok_build_tag() { + insta::assert_debug_snapshot!(WheelFilename::from_str( + "foo-1.2.3-202206090410-python-abi-platform.whl" + )); +} + +#[test] +fn from_and_to_string() { + let wheel_names = &[ + "django_allauth-0.51.0-py3-none-any.whl", + "osm2geojson-0.2.4-py3-none-any.whl", + "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + ]; + for wheel_name in wheel_names { + assert_eq!( + WheelFilename::from_str(wheel_name).unwrap().to_string(), + *wheel_name + ); + } +} diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index 6752ec66a..f1e284f88 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 631066b51..eebbc1dd5 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index 00f084f21..1ebc4edc6 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 88f53659b..1f5627f6f 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 4e5e8049e..54941df43 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -397,108 +397,4 @@ impl<'de> serde::de::Deserialize<'de> for PortablePathBuf { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_normalize_url() { - if cfg!(windows) { - assert_eq!( - normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"), - "C:\\Users\\ferris\\wheel-0.42.0.tar.gz" - ); - } else { - assert_eq!( - normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"), - "/C:/Users/ferris/wheel-0.42.0.tar.gz" - ); - } - - if cfg!(windows) { - assert_eq!( - normalize_url_path("./ferris/wheel-0.42.0.tar.gz"), - ".\\ferris\\wheel-0.42.0.tar.gz" - ); - } else { - assert_eq!( - normalize_url_path("./ferris/wheel-0.42.0.tar.gz"), - "./ferris/wheel-0.42.0.tar.gz" - ); - } - - if cfg!(windows) { - assert_eq!( - normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"), - ".\\wheel cache\\wheel-0.42.0.tar.gz" - ); - } else { - assert_eq!( - normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"), - "./wheel cache/wheel-0.42.0.tar.gz" - ); - } - } - - #[test] - fn test_normalize_path() { - let path = Path::new("/a/b/../c/./d"); - let normalized = normalize_absolute_path(path).unwrap(); - assert_eq!(normalized, Path::new("/a/c/d")); - - let path = Path::new("/a/../c/./d"); - let normalized = normalize_absolute_path(path).unwrap(); - assert_eq!(normalized, Path::new("/c/d")); - - // This should be an error. - let path = Path::new("/a/../../c/./d"); - let err = normalize_absolute_path(path).unwrap_err(); - assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); - } - - #[test] - fn test_relative_to() { - assert_eq!( - relative_to( - Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"), - Path::new("/home/ferris/carcinization/lib/python/site-packages"), - ) - .unwrap(), - Path::new("foo/__init__.py") - ); - assert_eq!( - relative_to( - Path::new("/home/ferris/carcinization/lib/marker.txt"), - Path::new("/home/ferris/carcinization/lib/python/site-packages"), - ) - .unwrap(), - Path::new("../../marker.txt") - ); - assert_eq!( - relative_to( - Path::new("/home/ferris/carcinization/bin/foo_launcher"), - Path::new("/home/ferris/carcinization/lib/python/site-packages"), - ) - .unwrap(), - Path::new("../../../bin/foo_launcher") - ); - } - - #[test] - fn test_normalize_relative() { - let cases = [ - ( - "../../workspace-git-path-dep-test/packages/c/../../packages/d", - "../../workspace-git-path-dep-test/packages/d", - ), - ( - "workspace-git-path-dep-test/packages/c/../../packages/d", - "workspace-git-path-dep-test/packages/d", - ), - ("./a/../../b", "../b"), - ("/usr/../../foo", "/../foo"), - ]; - for (input, expected) in cases { - assert_eq!(normalize_path(Path::new(input)), Path::new(expected)); - } - } -} +mod tests; diff --git a/crates/uv-fs/src/path/tests.rs b/crates/uv-fs/src/path/tests.rs new file mode 100644 index 000000000..6d5f619df --- /dev/null +++ b/crates/uv-fs/src/path/tests.rs @@ -0,0 +1,103 @@ +use super::*; + +#[test] +fn test_normalize_url() { + if cfg!(windows) { + assert_eq!( + normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"), + "C:\\Users\\ferris\\wheel-0.42.0.tar.gz" + ); + } else { + assert_eq!( + normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"), + "/C:/Users/ferris/wheel-0.42.0.tar.gz" + ); + } + + if cfg!(windows) { + assert_eq!( + normalize_url_path("./ferris/wheel-0.42.0.tar.gz"), + ".\\ferris\\wheel-0.42.0.tar.gz" + ); + } else { + assert_eq!( + normalize_url_path("./ferris/wheel-0.42.0.tar.gz"), + "./ferris/wheel-0.42.0.tar.gz" + ); + } + + if cfg!(windows) { + assert_eq!( + normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"), + ".\\wheel cache\\wheel-0.42.0.tar.gz" + ); + } else { + assert_eq!( + normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"), + "./wheel cache/wheel-0.42.0.tar.gz" + ); + } +} + +#[test] +fn test_normalize_path() { + let path = Path::new("/a/b/../c/./d"); + let normalized = normalize_absolute_path(path).unwrap(); + assert_eq!(normalized, Path::new("/a/c/d")); + + let path = Path::new("/a/../c/./d"); + let normalized = normalize_absolute_path(path).unwrap(); + assert_eq!(normalized, Path::new("/c/d")); + + // This should be an error. + let path = Path::new("/a/../../c/./d"); + let err = normalize_absolute_path(path).unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); +} + +#[test] +fn test_relative_to() { + assert_eq!( + relative_to( + Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"), + Path::new("/home/ferris/carcinization/lib/python/site-packages"), + ) + .unwrap(), + Path::new("foo/__init__.py") + ); + assert_eq!( + relative_to( + Path::new("/home/ferris/carcinization/lib/marker.txt"), + Path::new("/home/ferris/carcinization/lib/python/site-packages"), + ) + .unwrap(), + Path::new("../../marker.txt") + ); + assert_eq!( + relative_to( + Path::new("/home/ferris/carcinization/bin/foo_launcher"), + Path::new("/home/ferris/carcinization/lib/python/site-packages"), + ) + .unwrap(), + Path::new("../../../bin/foo_launcher") + ); +} + +#[test] +fn test_normalize_relative() { + let cases = [ + ( + "../../workspace-git-path-dep-test/packages/c/../../packages/d", + "../../workspace-git-path-dep-test/packages/d", + ), + ( + "workspace-git-path-dep-test/packages/c/../../packages/d", + "workspace-git-path-dep-test/packages/d", + ), + ("./a/../../b", "../b"), + ("/usr/../../foo", "/../foo"), + ]; + for (input, expected) in cases { + assert_eq!(normalize_path(Path::new(input)), Path::new(expected)); + } +} diff --git a/crates/uv-git/Cargo.toml b/crates/uv-git/Cargo.toml index c311cf794..5098cf9d2 100644 --- a/crates/uv-git/Cargo.toml +++ b/crates/uv-git/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-git/src/sha.rs b/crates/uv-git/src/sha.rs index 5ae7d3ec8..11a5c91ae 100644 --- a/crates/uv-git/src/sha.rs +++ b/crates/uv-git/src/sha.rs @@ -112,19 +112,4 @@ impl Display for GitOid { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::{GitOid, OidParseError}; - - #[test] - fn git_oid() { - GitOid::from_str("4a23745badf5bf5ef7928f1e346e9986bd696d82").unwrap(); - - assert_eq!(GitOid::from_str(""), Err(OidParseError::Empty)); - assert_eq!( - GitOid::from_str(&str::repeat("a", 41)), - Err(OidParseError::TooLong) - ); - } -} +mod tests; diff --git a/crates/uv-git/src/sha/tests.rs b/crates/uv-git/src/sha/tests.rs new file mode 100644 index 000000000..cac5e187d --- /dev/null +++ b/crates/uv-git/src/sha/tests.rs @@ -0,0 +1,14 @@ +use std::str::FromStr; + +use super::{GitOid, OidParseError}; + +#[test] +fn git_oid() { + GitOid::from_str("4a23745badf5bf5ef7928f1e346e9986bd696d82").unwrap(); + + assert_eq!(GitOid::from_str(""), Err(OidParseError::Empty)); + assert_eq!( + GitOid::from_str(&str::repeat("a", 41)), + Err(OidParseError::TooLong) + ); +} diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index 349a12b53..97f5cf57a 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -17,6 +17,7 @@ license = { workspace = true } workspace = true [lib] +doctest = false name = "uv_install_wheel" [dependencies] diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index 502bd88f2..369c0b334 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-macros/Cargo.toml b/crates/uv-macros/Cargo.toml index 3a3b6f971..38e4be48c 100644 --- a/crates/uv-macros/Cargo.toml +++ b/crates/uv-macros/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [lib] proc-macro = true +doctest = false [lints] workspace = true diff --git a/crates/uv-metadata/Cargo.toml b/crates/uv-metadata/Cargo.toml index c782a8df5..ddbf5b5aa 100644 --- a/crates/uv-metadata/Cargo.toml +++ b/crates/uv-metadata/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true authors.workspace = true license.workspace = true +[lib] +doctest = false + [dependencies] uv-distribution-filename = { workspace = true } uv-normalize = { workspace = true } diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index 0d1bdec42..8f4db6a15 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" edition = "2021" description = "Normalization for distribution, package and extra names." +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-normalize/src/dist_info_name.rs b/crates/uv-normalize/src/dist_info_name.rs index 0340cc677..f885589e7 100644 --- a/crates/uv-normalize/src/dist_info_name.rs +++ b/crates/uv-normalize/src/dist_info_name.rs @@ -86,23 +86,4 @@ impl AsRef for DistInfoName<'_> { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalize() { - let inputs = [ - "friendly-bard", - "Friendly-Bard", - "FRIENDLY-BARD", - "friendly.bard", - "friendly_bard", - "friendly--bard", - "friendly-.bard", - "FrIeNdLy-._.-bArD", - ]; - for input in inputs { - assert_eq!(DistInfoName::normalize(input), "friendly-bard"); - } - } -} +mod tests; diff --git a/crates/uv-normalize/src/dist_info_name/tests.rs b/crates/uv-normalize/src/dist_info_name/tests.rs new file mode 100644 index 000000000..156b0887a --- /dev/null +++ b/crates/uv-normalize/src/dist_info_name/tests.rs @@ -0,0 +1,18 @@ +use super::*; + +#[test] +fn normalize() { + let inputs = [ + "friendly-bard", + "Friendly-Bard", + "FRIENDLY-BARD", + "friendly.bard", + "friendly_bard", + "friendly--bard", + "friendly-.bard", + "FrIeNdLy-._.-bArD", + ]; + for input in inputs { + assert_eq!(DistInfoName::normalize(input), "friendly-bard"); + } +} diff --git a/crates/uv-normalize/src/lib.rs b/crates/uv-normalize/src/lib.rs index 0360a5a1a..29c6480a8 100644 --- a/crates/uv-normalize/src/lib.rs +++ b/crates/uv-normalize/src/lib.rs @@ -120,79 +120,4 @@ impl Display for InvalidNameError { impl Error for InvalidNameError {} #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalize() { - let inputs = [ - "friendly-bard", - "Friendly-Bard", - "FRIENDLY-BARD", - "friendly.bard", - "friendly_bard", - "friendly--bard", - "friendly-.bard", - "FrIeNdLy-._.-bArD", - ]; - for input in inputs { - assert_eq!(validate_and_normalize_ref(input).unwrap(), "friendly-bard"); - assert_eq!( - validate_and_normalize_owned(input.to_string()).unwrap(), - "friendly-bard" - ); - } - } - - #[test] - fn check() { - let inputs = ["friendly-bard", "friendlybard"]; - for input in inputs { - assert!(is_normalized(input).unwrap(), "{input:?}"); - } - - let inputs = [ - "friendly.bard", - "friendly.BARD", - "friendly_bard", - "friendly--bard", - "friendly-.bard", - "FrIeNdLy-._.-bArD", - ]; - for input in inputs { - assert!(!is_normalized(input).unwrap(), "{input:?}"); - } - } - - #[test] - fn unchanged() { - // Unchanged - let unchanged = ["friendly-bard", "1okay", "okay2"]; - for input in unchanged { - assert_eq!(validate_and_normalize_ref(input).unwrap(), input); - assert_eq!( - validate_and_normalize_owned(input.to_string()).unwrap(), - input - ); - assert!(is_normalized(input).unwrap()); - } - } - - #[test] - fn failures() { - let failures = [ - " starts-with-space", - "-starts-with-dash", - "ends-with-dash-", - "ends-with-space ", - "includes!invalid-char", - "space in middle", - "alpha-α", - ]; - for input in failures { - assert!(validate_and_normalize_ref(input).is_err()); - assert!(validate_and_normalize_owned(input.to_string()).is_err()); - assert!(is_normalized(input).is_err()); - } - } -} +mod tests; diff --git a/crates/uv-normalize/src/tests.rs b/crates/uv-normalize/src/tests.rs new file mode 100644 index 000000000..96368fcc7 --- /dev/null +++ b/crates/uv-normalize/src/tests.rs @@ -0,0 +1,74 @@ +use super::*; + +#[test] +fn normalize() { + let inputs = [ + "friendly-bard", + "Friendly-Bard", + "FRIENDLY-BARD", + "friendly.bard", + "friendly_bard", + "friendly--bard", + "friendly-.bard", + "FrIeNdLy-._.-bArD", + ]; + for input in inputs { + assert_eq!(validate_and_normalize_ref(input).unwrap(), "friendly-bard"); + assert_eq!( + validate_and_normalize_owned(input.to_string()).unwrap(), + "friendly-bard" + ); + } +} + +#[test] +fn check() { + let inputs = ["friendly-bard", "friendlybard"]; + for input in inputs { + assert!(is_normalized(input).unwrap(), "{input:?}"); + } + + let inputs = [ + "friendly.bard", + "friendly.BARD", + "friendly_bard", + "friendly--bard", + "friendly-.bard", + "FrIeNdLy-._.-bArD", + ]; + for input in inputs { + assert!(!is_normalized(input).unwrap(), "{input:?}"); + } +} + +#[test] +fn unchanged() { + // Unchanged + let unchanged = ["friendly-bard", "1okay", "okay2"]; + for input in unchanged { + assert_eq!(validate_and_normalize_ref(input).unwrap(), input); + assert_eq!( + validate_and_normalize_owned(input.to_string()).unwrap(), + input + ); + assert!(is_normalized(input).unwrap()); + } +} + +#[test] +fn failures() { + let failures = [ + " starts-with-space", + "-starts-with-dash", + "ends-with-dash-", + "ends-with-space ", + "includes!invalid-char", + "space in middle", + "alpha-α", + ]; + for input in failures { + assert!(validate_and_normalize_ref(input).is_err()); + assert!(validate_and_normalize_owned(input.to_string()).is_err()); + assert!(is_normalized(input).is_err()); + } +} diff --git a/crates/uv-once-map/Cargo.toml b/crates/uv-once-map/Cargo.toml index 03b877bf7..4ed767cd3 100644 --- a/crates/uv-once-map/Cargo.toml +++ b/crates/uv-once-map/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-options-metadata/Cargo.toml b/crates/uv-options-metadata/Cargo.toml index 24fc367c0..a159ce16d 100644 --- a/crates/uv-options-metadata/Cargo.toml +++ b/crates/uv-options-metadata/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-options-metadata/src/lib.rs b/crates/uv-options-metadata/src/lib.rs index 8f7e9ec62..fad3b9921 100644 --- a/crates/uv-options-metadata/src/lib.rs +++ b/crates/uv-options-metadata/src/lib.rs @@ -100,76 +100,6 @@ impl OptionSet { /// Returns `true` if this set has an option that resolves to `name`. /// /// The name can be separated by `.` to find a nested option. - /// - /// ## Examples - /// - /// ### Test for the existence of a child option - /// - /// ```rust - /// # use uv_options_metadata::{OptionField, OptionsMetadata, Visit}; - /// - /// struct WithOptions; - /// - /// impl OptionsMetadata for WithOptions { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// possible_values: None - /// }); - /// } - /// } - /// - /// assert!(WithOptions::metadata().has("ignore-git-ignore")); - /// assert!(!WithOptions::metadata().has("does-not-exist")); - /// ``` - /// ### Test for the existence of a nested option - /// - /// ```rust - /// # use uv_options_metadata::{OptionField, OptionsMetadata, Visit}; - /// - /// struct Root; - /// - /// impl OptionsMetadata for Root { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// possible_values: None - /// }); - /// - /// visit.record_set("format", Nested::metadata()); - /// } - /// } - /// - /// struct Nested; - /// - /// impl OptionsMetadata for Nested { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("hard-tabs", OptionField { - /// doc: "Use hard tabs for indentation and spaces for alignment.", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// possible_values: None - /// }); - /// } - /// } - /// - /// assert!(Root::metadata().has("format.hard-tabs")); - /// assert!(!Root::metadata().has("format.spaces")); - /// assert!(!Root::metadata().has("lint.hard-tabs")); - /// ``` pub fn has(&self, name: &str) -> bool { self.find(name).is_some() } @@ -177,81 +107,6 @@ impl OptionSet { /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise. /// /// The name can be separated by `.` to find a nested option. - /// - /// ## Examples - /// - /// ### Find a child option - /// - /// ```rust - /// # use uv_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit}; - /// - /// struct WithOptions; - /// - /// static IGNORE_GIT_IGNORE: OptionField = OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// possible_values: None - /// }; - /// - /// impl OptionsMetadata for WithOptions { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone()); - /// } - /// } - /// - /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore"), Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))); - /// assert_eq!(WithOptions::metadata().find("does-not-exist"), None); - /// ``` - /// ### Find a nested option - /// - /// ```rust - /// # use uv_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit}; - /// - /// static HARD_TABS: OptionField = OptionField { - /// doc: "Use hard tabs for indentation and spaces for alignment.", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// possible_values: None - /// }; - /// - /// struct Root; - /// - /// impl OptionsMetadata for Root { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("ignore-git-ignore", OptionField { - /// doc: "Whether Ruff should respect the gitignore file", - /// default: "false", - /// value_type: "bool", - /// example: "", - /// scope: None, - /// deprecated: None, - /// possible_values: None - /// }); - /// - /// visit.record_set("format", Nested::metadata()); - /// } - /// } - /// - /// struct Nested; - /// - /// impl OptionsMetadata for Nested { - /// fn record(visit: &mut dyn Visit) { - /// visit.record_field("hard-tabs", HARD_TABS.clone()); - /// } - /// } - /// - /// assert_eq!(Root::metadata().find("format.hard-tabs"), Some(OptionEntry::Field(HARD_TABS.clone()))); - /// assert_eq!(Root::metadata().find("format"), Some(OptionEntry::Set(Nested::metadata()))); - /// assert_eq!(Root::metadata().find("format.spaces"), None); - /// assert_eq!(Root::metadata().find("lint.hard-tabs"), None); - /// ``` pub fn find(&self, name: &str) -> Option { struct FindOptionVisitor<'a> { option: Option, @@ -459,3 +314,6 @@ impl Display for PossibleValue { Ok(()) } } + +#[cfg(test)] +mod tests; diff --git a/crates/uv-options-metadata/src/tests.rs b/crates/uv-options-metadata/src/tests.rs new file mode 100644 index 000000000..20374f0af --- /dev/null +++ b/crates/uv-options-metadata/src/tests.rs @@ -0,0 +1,153 @@ +use super::*; + +#[test] +fn test_has_child_option() { + struct WithOptions; + + impl OptionsMetadata for WithOptions { + fn record(visit: &mut dyn Visit) { + visit.record_field( + "ignore-git-ignore", + OptionField { + doc: "Whether Ruff should respect the gitignore file", + default: "false", + value_type: "bool", + example: "", + scope: None, + deprecated: None, + possible_values: None, + }, + ); + } + } + + assert!(WithOptions::metadata().has("ignore-git-ignore")); + assert!(!WithOptions::metadata().has("does-not-exist")); +} + +#[test] +fn test_has_nested_option() { + struct Root; + + impl OptionsMetadata for Root { + fn record(visit: &mut dyn Visit) { + visit.record_field( + "ignore-git-ignore", + OptionField { + doc: "Whether Ruff should respect the gitignore file", + default: "false", + value_type: "bool", + example: "", + scope: None, + deprecated: None, + possible_values: None, + }, + ); + + visit.record_set("format", Nested::metadata()); + } + } + + struct Nested; + + impl OptionsMetadata for Nested { + fn record(visit: &mut dyn Visit) { + visit.record_field( + "hard-tabs", + OptionField { + doc: "Use hard tabs for indentation and spaces for alignment.", + default: "false", + value_type: "bool", + example: "", + scope: None, + deprecated: None, + possible_values: None, + }, + ); + } + } + + assert!(Root::metadata().has("format.hard-tabs")); + assert!(!Root::metadata().has("format.spaces")); + assert!(!Root::metadata().has("lint.hard-tabs")); +} + +#[test] +fn test_find_child_option() { + struct WithOptions; + + static IGNORE_GIT_IGNORE: OptionField = OptionField { + doc: "Whether Ruff should respect the gitignore file", + default: "false", + value_type: "bool", + example: "", + scope: None, + deprecated: None, + possible_values: None, + }; + + impl OptionsMetadata for WithOptions { + fn record(visit: &mut dyn Visit) { + visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone()); + } + } + + assert_eq!( + WithOptions::metadata().find("ignore-git-ignore"), + Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone())) + ); + assert_eq!(WithOptions::metadata().find("does-not-exist"), None); +} + +#[test] +fn test_find_nested_option() { + static HARD_TABS: OptionField = OptionField { + doc: "Use hard tabs for indentation and spaces for alignment.", + default: "false", + value_type: "bool", + example: "", + scope: None, + deprecated: None, + possible_values: None, + }; + + struct Root; + + impl OptionsMetadata for Root { + fn record(visit: &mut dyn Visit) { + visit.record_field( + "ignore-git-ignore", + OptionField { + doc: "Whether Ruff should respect the gitignore file", + default: "false", + value_type: "bool", + example: "", + scope: None, + deprecated: None, + possible_values: None, + }, + ); + + visit.record_set("format", Nested::metadata()); + } + } + + struct Nested; + + impl OptionsMetadata for Nested { + fn record(visit: &mut dyn Visit) { + visit.record_field("hard-tabs", HARD_TABS.clone()); + } + } + + assert_eq!( + Root::metadata().find("format.hard-tabs"), + Some(OptionEntry::Field(HARD_TABS.clone())) + ); + assert_eq!( + Root::metadata().find("format"), + Some(OptionEntry::Set(Nested::metadata())) + ); + assert_eq!(Root::metadata().find("format.spaces"), None); + assert_eq!(Root::metadata().find("lint.hard-tabs"), None); +} diff --git a/crates/uv-pep440/Cargo.toml b/crates/uv-pep440/Cargo.toml index 47dd3b26d..66a51e848 100644 --- a/crates/uv-pep440/Cargo.toml +++ b/crates/uv-pep440/Cargo.toml @@ -14,6 +14,7 @@ authors = { workspace = true } [lib] name = "uv_pep440" crate-type = ["rlib", "cdylib"] +doctest = false [lints] workspace = true @@ -27,6 +28,7 @@ unscanny = { workspace = true } [dev-dependencies] indoc = { version = "2.0.5" } +tracing = { workspace = true } [features] # Match the API of the published crate, for compatibility. diff --git a/crates/uv-pep440/src/lib.rs b/crates/uv-pep440/src/lib.rs index e04b52f69..ca848d382 100644 --- a/crates/uv-pep440/src/lib.rs +++ b/crates/uv-pep440/src/lib.rs @@ -1,17 +1,6 @@ //! A library for python version numbers and specifiers, implementing //! [PEP 440](https://peps.python.org/pep-0440) //! -//! ```rust -//! use std::str::FromStr; -//! use uv_pep440::{VersionSpecifiers, Version, VersionSpecifier}; -//! -//! let version = Version::from_str("1.19").unwrap(); -//! let version_specifier = VersionSpecifier::from_str("== 1.*").unwrap(); -//! assert!(version_specifier.contains(&version)); -//! let version_specifiers = VersionSpecifiers::from_str(">=1.16, <2.0").unwrap(); -//! assert!(version_specifiers.contains(&version)); -//! ``` -//! //! PEP 440 has a lot of unintuitive features, including: //! //! * An epoch that you can prefix the version which, e.g. `1!1.2.3`. Lower epoch always means lower @@ -47,3 +36,6 @@ pub use { mod version; mod version_specifier; + +#[cfg(test)] +mod tests; diff --git a/crates/uv-pep440/src/tests.rs b/crates/uv-pep440/src/tests.rs new file mode 100644 index 000000000..4495f0db7 --- /dev/null +++ b/crates/uv-pep440/src/tests.rs @@ -0,0 +1,11 @@ +use super::{Version, VersionSpecifier, VersionSpecifiers}; +use std::str::FromStr; + +#[test] +fn test_version() { + let version = Version::from_str("1.19").unwrap(); + let version_specifier = VersionSpecifier::from_str("== 1.*").unwrap(); + assert!(version_specifier.contains(&version)); + let version_specifiers = VersionSpecifiers::from_str(">=1.16, <2.0").unwrap(); + assert!(version_specifiers.contains(&version)); +} diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 7c9f32540..839f90bea 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -2398,1350 +2398,4 @@ pub static MIN_VERSION: LazyLock = LazyLock::new(|| Version::from_str("0a0.dev0").unwrap()); #[cfg(test)] -mod tests { - use std::str::FromStr; - - use crate::VersionSpecifier; - - use super::*; - - /// - #[test] - fn test_packaging_versions() { - let versions = [ - // Implicit epoch of 0 - ("1.0.dev456", Version::new([1, 0]).with_dev(Some(456))), - ( - "1.0a1", - Version::new([1, 0]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1, - })), - ), - ( - "1.0a2.dev456", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 2, - })) - .with_dev(Some(456)), - ), - ( - "1.0a12.dev456", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 12, - })) - .with_dev(Some(456)), - ), - ( - "1.0a12", - Version::new([1, 0]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 12, - })), - ), - ( - "1.0b1.dev456", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 1, - })) - .with_dev(Some(456)), - ), - ( - "1.0b2", - Version::new([1, 0]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })), - ), - ( - "1.0b2.post345.dev456", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })) - .with_dev(Some(456)) - .with_post(Some(345)), - ), - ( - "1.0b2.post345", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })) - .with_post(Some(345)), - ), - ( - "1.0b2-346", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })) - .with_post(Some(346)), - ), - ( - "1.0c1.dev456", - Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1, - })) - .with_dev(Some(456)), - ), - ( - "1.0c1", - Version::new([1, 0]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1, - })), - ), - ( - "1.0rc2", - Version::new([1, 0]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 2, - })), - ), - ( - "1.0c3", - Version::new([1, 0]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 3, - })), - ), - ("1.0", Version::new([1, 0])), - ( - "1.0.post456.dev34", - Version::new([1, 0]).with_post(Some(456)).with_dev(Some(34)), - ), - ("1.0.post456", Version::new([1, 0]).with_post(Some(456))), - ("1.1.dev1", Version::new([1, 1]).with_dev(Some(1))), - ( - "1.2+123abc", - Version::new([1, 2]).with_local(vec![LocalSegment::String("123abc".to_string())]), - ), - ( - "1.2+123abc456", - Version::new([1, 2]) - .with_local(vec![LocalSegment::String("123abc456".to_string())]), - ), - ( - "1.2+abc", - Version::new([1, 2]).with_local(vec![LocalSegment::String("abc".to_string())]), - ), - ( - "1.2+abc123", - Version::new([1, 2]).with_local(vec![LocalSegment::String("abc123".to_string())]), - ), - ( - "1.2+abc123def", - Version::new([1, 2]) - .with_local(vec![LocalSegment::String("abc123def".to_string())]), - ), - ( - "1.2+1234.abc", - Version::new([1, 2]).with_local(vec![ - LocalSegment::Number(1234), - LocalSegment::String("abc".to_string()), - ]), - ), - ( - "1.2+123456", - Version::new([1, 2]).with_local(vec![LocalSegment::Number(123_456)]), - ), - ( - "1.2.r32+123456", - Version::new([1, 2]) - .with_post(Some(32)) - .with_local(vec![LocalSegment::Number(123_456)]), - ), - ( - "1.2.rev33+123456", - Version::new([1, 2]) - .with_post(Some(33)) - .with_local(vec![LocalSegment::Number(123_456)]), - ), - // Explicit epoch of 1 - ( - "1!1.0.dev456", - Version::new([1, 0]).with_epoch(1).with_dev(Some(456)), - ), - ( - "1!1.0a1", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1, - })), - ), - ( - "1!1.0a2.dev456", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 2, - })) - .with_dev(Some(456)), - ), - ( - "1!1.0a12.dev456", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 12, - })) - .with_dev(Some(456)), - ), - ( - "1!1.0a12", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 12, - })), - ), - ( - "1!1.0b1.dev456", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 1, - })) - .with_dev(Some(456)), - ), - ( - "1!1.0b2", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })), - ), - ( - "1!1.0b2.post345.dev456", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })) - .with_post(Some(345)) - .with_dev(Some(456)), - ), - ( - "1!1.0b2.post345", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })) - .with_post(Some(345)), - ), - ( - "1!1.0b2-346", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 2, - })) - .with_post(Some(346)), - ), - ( - "1!1.0c1.dev456", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1, - })) - .with_dev(Some(456)), - ), - ( - "1!1.0c1", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1, - })), - ), - ( - "1!1.0rc2", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 2, - })), - ), - ( - "1!1.0c3", - Version::new([1, 0]) - .with_epoch(1) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 3, - })), - ), - ("1!1.0", Version::new([1, 0]).with_epoch(1)), - ( - "1!1.0.post456.dev34", - Version::new([1, 0]) - .with_epoch(1) - .with_post(Some(456)) - .with_dev(Some(34)), - ), - ( - "1!1.0.post456", - Version::new([1, 0]).with_epoch(1).with_post(Some(456)), - ), - ( - "1!1.1.dev1", - Version::new([1, 1]).with_epoch(1).with_dev(Some(1)), - ), - ( - "1!1.2+123abc", - Version::new([1, 2]) - .with_epoch(1) - .with_local(vec![LocalSegment::String("123abc".to_string())]), - ), - ( - "1!1.2+123abc456", - Version::new([1, 2]) - .with_epoch(1) - .with_local(vec![LocalSegment::String("123abc456".to_string())]), - ), - ( - "1!1.2+abc", - Version::new([1, 2]) - .with_epoch(1) - .with_local(vec![LocalSegment::String("abc".to_string())]), - ), - ( - "1!1.2+abc123", - Version::new([1, 2]) - .with_epoch(1) - .with_local(vec![LocalSegment::String("abc123".to_string())]), - ), - ( - "1!1.2+abc123def", - Version::new([1, 2]) - .with_epoch(1) - .with_local(vec![LocalSegment::String("abc123def".to_string())]), - ), - ( - "1!1.2+1234.abc", - Version::new([1, 2]).with_epoch(1).with_local(vec![ - LocalSegment::Number(1234), - LocalSegment::String("abc".to_string()), - ]), - ), - ( - "1!1.2+123456", - Version::new([1, 2]) - .with_epoch(1) - .with_local(vec![LocalSegment::Number(123_456)]), - ), - ( - "1!1.2.r32+123456", - Version::new([1, 2]) - .with_epoch(1) - .with_post(Some(32)) - .with_local(vec![LocalSegment::Number(123_456)]), - ), - ( - "1!1.2.rev33+123456", - Version::new([1, 2]) - .with_epoch(1) - .with_post(Some(33)) - .with_local(vec![LocalSegment::Number(123_456)]), - ), - ( - "98765!1.2.rev33+123456", - Version::new([1, 2]) - .with_epoch(98765) - .with_post(Some(33)) - .with_local(vec![LocalSegment::Number(123_456)]), - ), - ]; - for (string, structured) in versions { - match Version::from_str(string) { - Err(err) => { - unreachable!( - "expected {string:?} to parse as {structured:?}, but got {err:?}", - structured = structured.as_bloated_debug(), - ) - } - Ok(v) => assert!( - v == structured, - "for {string:?}, expected {structured:?} but got {v:?}", - structured = structured.as_bloated_debug(), - v = v.as_bloated_debug(), - ), - } - let spec = format!("=={string}"); - match VersionSpecifier::from_str(&spec) { - Err(err) => { - unreachable!( - "expected version in {spec:?} to parse as {structured:?}, but got {err:?}", - structured = structured.as_bloated_debug(), - ) - } - Ok(v) => assert!( - v.version() == &structured, - "for {string:?}, expected {structured:?} but got {v:?}", - structured = structured.as_bloated_debug(), - v = v.version.as_bloated_debug(), - ), - } - } - } - - /// - #[test] - fn test_packaging_failures() { - let versions = [ - // Versions with invalid local versions - "1.0+a+", - "1.0++", - "1.0+_foobar", - "1.0+foo&asd", - "1.0+1+1", - // Nonsensical versions should also be invalid - "french toast", - "==french toast", - ]; - for version in versions { - assert!(Version::from_str(version).is_err()); - assert!(VersionSpecifier::from_str(&format!("=={version}")).is_err()); - } - } - - #[test] - fn test_equality_and_normalization() { - let versions = [ - // Various development release incarnations - ("1.0dev", "1.0.dev0"), - ("1.0.dev", "1.0.dev0"), - ("1.0dev1", "1.0.dev1"), - ("1.0dev", "1.0.dev0"), - ("1.0-dev", "1.0.dev0"), - ("1.0-dev1", "1.0.dev1"), - ("1.0DEV", "1.0.dev0"), - ("1.0.DEV", "1.0.dev0"), - ("1.0DEV1", "1.0.dev1"), - ("1.0DEV", "1.0.dev0"), - ("1.0.DEV1", "1.0.dev1"), - ("1.0-DEV", "1.0.dev0"), - ("1.0-DEV1", "1.0.dev1"), - // Various alpha incarnations - ("1.0a", "1.0a0"), - ("1.0.a", "1.0a0"), - ("1.0.a1", "1.0a1"), - ("1.0-a", "1.0a0"), - ("1.0-a1", "1.0a1"), - ("1.0alpha", "1.0a0"), - ("1.0.alpha", "1.0a0"), - ("1.0.alpha1", "1.0a1"), - ("1.0-alpha", "1.0a0"), - ("1.0-alpha1", "1.0a1"), - ("1.0A", "1.0a0"), - ("1.0.A", "1.0a0"), - ("1.0.A1", "1.0a1"), - ("1.0-A", "1.0a0"), - ("1.0-A1", "1.0a1"), - ("1.0ALPHA", "1.0a0"), - ("1.0.ALPHA", "1.0a0"), - ("1.0.ALPHA1", "1.0a1"), - ("1.0-ALPHA", "1.0a0"), - ("1.0-ALPHA1", "1.0a1"), - // Various beta incarnations - ("1.0b", "1.0b0"), - ("1.0.b", "1.0b0"), - ("1.0.b1", "1.0b1"), - ("1.0-b", "1.0b0"), - ("1.0-b1", "1.0b1"), - ("1.0beta", "1.0b0"), - ("1.0.beta", "1.0b0"), - ("1.0.beta1", "1.0b1"), - ("1.0-beta", "1.0b0"), - ("1.0-beta1", "1.0b1"), - ("1.0B", "1.0b0"), - ("1.0.B", "1.0b0"), - ("1.0.B1", "1.0b1"), - ("1.0-B", "1.0b0"), - ("1.0-B1", "1.0b1"), - ("1.0BETA", "1.0b0"), - ("1.0.BETA", "1.0b0"), - ("1.0.BETA1", "1.0b1"), - ("1.0-BETA", "1.0b0"), - ("1.0-BETA1", "1.0b1"), - // Various release candidate incarnations - ("1.0c", "1.0rc0"), - ("1.0.c", "1.0rc0"), - ("1.0.c1", "1.0rc1"), - ("1.0-c", "1.0rc0"), - ("1.0-c1", "1.0rc1"), - ("1.0rc", "1.0rc0"), - ("1.0.rc", "1.0rc0"), - ("1.0.rc1", "1.0rc1"), - ("1.0-rc", "1.0rc0"), - ("1.0-rc1", "1.0rc1"), - ("1.0C", "1.0rc0"), - ("1.0.C", "1.0rc0"), - ("1.0.C1", "1.0rc1"), - ("1.0-C", "1.0rc0"), - ("1.0-C1", "1.0rc1"), - ("1.0RC", "1.0rc0"), - ("1.0.RC", "1.0rc0"), - ("1.0.RC1", "1.0rc1"), - ("1.0-RC", "1.0rc0"), - ("1.0-RC1", "1.0rc1"), - // Various post release incarnations - ("1.0post", "1.0.post0"), - ("1.0.post", "1.0.post0"), - ("1.0post1", "1.0.post1"), - ("1.0post", "1.0.post0"), - ("1.0-post", "1.0.post0"), - ("1.0-post1", "1.0.post1"), - ("1.0POST", "1.0.post0"), - ("1.0.POST", "1.0.post0"), - ("1.0POST1", "1.0.post1"), - ("1.0POST", "1.0.post0"), - ("1.0r", "1.0.post0"), - ("1.0rev", "1.0.post0"), - ("1.0.POST1", "1.0.post1"), - ("1.0.r1", "1.0.post1"), - ("1.0.rev1", "1.0.post1"), - ("1.0-POST", "1.0.post0"), - ("1.0-POST1", "1.0.post1"), - ("1.0-5", "1.0.post5"), - ("1.0-r5", "1.0.post5"), - ("1.0-rev5", "1.0.post5"), - // Local version case insensitivity - ("1.0+AbC", "1.0+abc"), - // Integer Normalization - ("1.01", "1.1"), - ("1.0a05", "1.0a5"), - ("1.0b07", "1.0b7"), - ("1.0c056", "1.0rc56"), - ("1.0rc09", "1.0rc9"), - ("1.0.post000", "1.0.post0"), - ("1.1.dev09000", "1.1.dev9000"), - ("00!1.2", "1.2"), - ("0100!0.0", "100!0.0"), - // Various other normalizations - ("v1.0", "1.0"), - (" v1.0\t\n", "1.0"), - ]; - for (version_str, normalized_str) in versions { - let version = Version::from_str(version_str).unwrap(); - let normalized = Version::from_str(normalized_str).unwrap(); - // Just test version parsing again - assert_eq!(version, normalized, "{version_str} {normalized_str}"); - // Test version normalization - assert_eq!( - version.to_string(), - normalized.to_string(), - "{version_str} {normalized_str}" - ); - } - } - - /// - #[test] - fn test_equality_and_normalization2() { - let versions = [ - ("1.0.dev456", "1.0.dev456"), - ("1.0a1", "1.0a1"), - ("1.0a2.dev456", "1.0a2.dev456"), - ("1.0a12.dev456", "1.0a12.dev456"), - ("1.0a12", "1.0a12"), - ("1.0b1.dev456", "1.0b1.dev456"), - ("1.0b2", "1.0b2"), - ("1.0b2.post345.dev456", "1.0b2.post345.dev456"), - ("1.0b2.post345", "1.0b2.post345"), - ("1.0rc1.dev456", "1.0rc1.dev456"), - ("1.0rc1", "1.0rc1"), - ("1.0", "1.0"), - ("1.0.post456.dev34", "1.0.post456.dev34"), - ("1.0.post456", "1.0.post456"), - ("1.0.1", "1.0.1"), - ("0!1.0.2", "1.0.2"), - ("1.0.3+7", "1.0.3+7"), - ("0!1.0.4+8.0", "1.0.4+8.0"), - ("1.0.5+9.5", "1.0.5+9.5"), - ("1.2+1234.abc", "1.2+1234.abc"), - ("1.2+123456", "1.2+123456"), - ("1.2+123abc", "1.2+123abc"), - ("1.2+123abc456", "1.2+123abc456"), - ("1.2+abc", "1.2+abc"), - ("1.2+abc123", "1.2+abc123"), - ("1.2+abc123def", "1.2+abc123def"), - ("1.1.dev1", "1.1.dev1"), - ("7!1.0.dev456", "7!1.0.dev456"), - ("7!1.0a1", "7!1.0a1"), - ("7!1.0a2.dev456", "7!1.0a2.dev456"), - ("7!1.0a12.dev456", "7!1.0a12.dev456"), - ("7!1.0a12", "7!1.0a12"), - ("7!1.0b1.dev456", "7!1.0b1.dev456"), - ("7!1.0b2", "7!1.0b2"), - ("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"), - ("7!1.0b2.post345", "7!1.0b2.post345"), - ("7!1.0rc1.dev456", "7!1.0rc1.dev456"), - ("7!1.0rc1", "7!1.0rc1"), - ("7!1.0", "7!1.0"), - ("7!1.0.post456.dev34", "7!1.0.post456.dev34"), - ("7!1.0.post456", "7!1.0.post456"), - ("7!1.0.1", "7!1.0.1"), - ("7!1.0.2", "7!1.0.2"), - ("7!1.0.3+7", "7!1.0.3+7"), - ("7!1.0.4+8.0", "7!1.0.4+8.0"), - ("7!1.0.5+9.5", "7!1.0.5+9.5"), - ("7!1.1.dev1", "7!1.1.dev1"), - ]; - for (version_str, normalized_str) in versions { - let version = Version::from_str(version_str).unwrap(); - let normalized = Version::from_str(normalized_str).unwrap(); - assert_eq!(version, normalized, "{version_str} {normalized_str}"); - // Test version normalization - assert_eq!( - version.to_string(), - normalized_str, - "{version_str} {normalized_str}" - ); - // Since we're already at it - assert_eq!( - version.to_string(), - normalized.to_string(), - "{version_str} {normalized_str}" - ); - } - } - - #[test] - fn test_star_fixed_version() { - let result = Version::from_str("0.9.1.*"); - assert_eq!(result.unwrap_err(), ErrorKind::Wildcard.into()); - } - - #[test] - fn test_invalid_word() { - let result = Version::from_str("blergh"); - assert_eq!(result.unwrap_err(), ErrorKind::NoLeadingNumber.into()); - } - - #[test] - fn test_from_version_star() { - let p = |s: &str| -> Result { s.parse() }; - assert!(!p("1.2.3").unwrap().is_wildcard()); - assert!(p("1.2.3.*").unwrap().is_wildcard()); - assert_eq!( - p("1.2.*.4.*").unwrap_err(), - PatternErrorKind::WildcardNotTrailing.into(), - ); - assert_eq!( - p("1.0-dev1.*").unwrap_err(), - ErrorKind::UnexpectedEnd { - version: "1.0-dev1".to_string(), - remaining: ".*".to_string() - } - .into(), - ); - assert_eq!( - p("1.0a1.*").unwrap_err(), - ErrorKind::UnexpectedEnd { - version: "1.0a1".to_string(), - remaining: ".*".to_string() - } - .into(), - ); - assert_eq!( - p("1.0.post1.*").unwrap_err(), - ErrorKind::UnexpectedEnd { - version: "1.0.post1".to_string(), - remaining: ".*".to_string() - } - .into(), - ); - assert_eq!( - p("1.0+lolwat.*").unwrap_err(), - ErrorKind::LocalEmpty { precursor: '.' }.into(), - ); - } - - // Tests the valid cases of our version parser. These were written - // in tandem with the parser. - // - // They are meant to be additional (but in some cases likely redundant) - // with some of the above tests. - #[test] - fn parse_version_valid() { - let p = |s: &str| match Parser::new(s.as_bytes()).parse() { - Ok(v) => v, - Err(err) => unreachable!("expected valid version, but got error: {err:?}"), - }; - - // release-only tests - assert_eq!(p("5"), Version::new([5])); - assert_eq!(p("5.6"), Version::new([5, 6])); - assert_eq!(p("5.6.7"), Version::new([5, 6, 7])); - assert_eq!(p("512.623.734"), Version::new([512, 623, 734])); - assert_eq!(p("1.2.3.4"), Version::new([1, 2, 3, 4])); - assert_eq!(p("1.2.3.4.5"), Version::new([1, 2, 3, 4, 5])); - - // epoch tests - assert_eq!(p("4!5"), Version::new([5]).with_epoch(4)); - assert_eq!(p("4!5.6"), Version::new([5, 6]).with_epoch(4)); - - // pre-release tests - assert_eq!( - p("5a1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1 - })) - ); - assert_eq!( - p("5alpha1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1 - })) - ); - assert_eq!( - p("5b1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 1 - })) - ); - assert_eq!( - p("5beta1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Beta, - number: 1 - })) - ); - assert_eq!( - p("5rc1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1 - })) - ); - assert_eq!( - p("5c1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1 - })) - ); - assert_eq!( - p("5preview1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1 - })) - ); - assert_eq!( - p("5pre1"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1 - })) - ); - assert_eq!( - p("5.6.7pre1"), - Version::new([5, 6, 7]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Rc, - number: 1 - })) - ); - assert_eq!( - p("5alpha789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5.alpha789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5-alpha789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5_alpha789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5alpha.789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5alpha-789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5alpha_789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5ALPHA789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5aLpHa789"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 789 - })) - ); - assert_eq!( - p("5alpha"), - Version::new([5]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 0 - })) - ); - - // post-release tests - assert_eq!(p("5post2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5rev2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5r2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5.post2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5-post2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5_post2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5.post.2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5.post-2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5.post_2"), Version::new([5]).with_post(Some(2))); - assert_eq!( - p("5.6.7.post_2"), - Version::new([5, 6, 7]).with_post(Some(2)) - ); - assert_eq!(p("5-2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5.6.7-2"), Version::new([5, 6, 7]).with_post(Some(2))); - assert_eq!(p("5POST2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5PoSt2"), Version::new([5]).with_post(Some(2))); - assert_eq!(p("5post"), Version::new([5]).with_post(Some(0))); - - // dev-release tests - assert_eq!(p("5dev2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5.dev2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5-dev2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5_dev2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5.dev.2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5.dev-2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5.dev_2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5.6.7.dev_2"), Version::new([5, 6, 7]).with_dev(Some(2))); - assert_eq!(p("5DEV2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5dEv2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5DeV2"), Version::new([5]).with_dev(Some(2))); - assert_eq!(p("5dev"), Version::new([5]).with_dev(Some(0))); - - // local tests - assert_eq!( - p("5+2"), - Version::new([5]).with_local(vec![LocalSegment::Number(2)]) - ); - assert_eq!( - p("5+a"), - Version::new([5]).with_local(vec![LocalSegment::String("a".to_string())]) - ); - assert_eq!( - p("5+abc.123"), - Version::new([5]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - ]) - ); - assert_eq!( - p("5+123.abc"), - Version::new([5]).with_local(vec![ - LocalSegment::Number(123), - LocalSegment::String("abc".to_string()), - ]) - ); - assert_eq!( - p("5+18446744073709551615.abc"), - Version::new([5]).with_local(vec![ - LocalSegment::Number(18_446_744_073_709_551_615), - LocalSegment::String("abc".to_string()), - ]) - ); - assert_eq!( - p("5+18446744073709551616.abc"), - Version::new([5]).with_local(vec![ - LocalSegment::String("18446744073709551616".to_string()), - LocalSegment::String("abc".to_string()), - ]) - ); - assert_eq!( - p("5+ABC.123"), - Version::new([5]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - ]) - ); - assert_eq!( - p("5+ABC-123.4_5_xyz-MNO"), - Version::new([5]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - LocalSegment::Number(4), - LocalSegment::Number(5), - LocalSegment::String("xyz".to_string()), - LocalSegment::String("mno".to_string()), - ]) - ); - assert_eq!( - p("5.6.7+abc-00123"), - Version::new([5, 6, 7]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - ]) - ); - assert_eq!( - p("5.6.7+abc-foo00123"), - Version::new([5, 6, 7]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::String("foo00123".to_string()), - ]) - ); - assert_eq!( - p("5.6.7+abc-00123a"), - Version::new([5, 6, 7]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::String("00123a".to_string()), - ]) - ); - - // {pre-release, post-release} tests - assert_eq!( - p("5a2post3"), - Version::new([5]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 2 - })) - .with_post(Some(3)) - ); - assert_eq!( - p("5.a-2_post-3"), - Version::new([5]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 2 - })) - .with_post(Some(3)) - ); - assert_eq!( - p("5a2-3"), - Version::new([5]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 2 - })) - .with_post(Some(3)) - ); - - // Ignoring a no-op 'v' prefix. - assert_eq!(p("v5"), Version::new([5])); - assert_eq!(p("V5"), Version::new([5])); - assert_eq!(p("v5.6.7"), Version::new([5, 6, 7])); - - // Ignoring leading and trailing whitespace. - assert_eq!(p(" v5 "), Version::new([5])); - assert_eq!(p(" 5 "), Version::new([5])); - assert_eq!( - p(" 5.6.7+abc.123.xyz "), - Version::new([5, 6, 7]).with_local(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - LocalSegment::String("xyz".to_string()) - ]) - ); - assert_eq!(p(" \n5\n \t"), Version::new([5])); - - // min tests - assert!(Parser::new("1.min0".as_bytes()).parse().is_err()); - } - - // Tests the error cases of our version parser. - // - // I wrote these with the intent to cover every possible error - // case. - // - // They are meant to be additional (but in some cases likely redundant) - // with some of the above tests. - #[test] - fn parse_version_invalid() { - let p = |s: &str| match Parser::new(s.as_bytes()).parse() { - Err(err) => err, - Ok(v) => unreachable!( - "expected version parser error, but got: {v:?}", - v = v.as_bloated_debug() - ), - }; - - assert_eq!(p(""), ErrorKind::NoLeadingNumber.into()); - assert_eq!(p("a"), ErrorKind::NoLeadingNumber.into()); - assert_eq!(p("v 5"), ErrorKind::NoLeadingNumber.into()); - assert_eq!(p("V 5"), ErrorKind::NoLeadingNumber.into()); - assert_eq!(p("x 5"), ErrorKind::NoLeadingNumber.into()); - assert_eq!( - p("18446744073709551616"), - ErrorKind::NumberTooBig { - bytes: b"18446744073709551616".to_vec() - } - .into() - ); - assert_eq!(p("5!"), ErrorKind::NoLeadingReleaseNumber.into()); - assert_eq!( - p("5.6./"), - ErrorKind::UnexpectedEnd { - version: "5.6".to_string(), - remaining: "./".to_string() - } - .into() - ); - assert_eq!( - p("5.6.-alpha2"), - ErrorKind::UnexpectedEnd { - version: "5.6".to_string(), - remaining: ".-alpha2".to_string() - } - .into() - ); - assert_eq!( - p("1.2.3a18446744073709551616"), - ErrorKind::NumberTooBig { - bytes: b"18446744073709551616".to_vec() - } - .into() - ); - assert_eq!(p("5+"), ErrorKind::LocalEmpty { precursor: '+' }.into()); - assert_eq!(p("5+ "), ErrorKind::LocalEmpty { precursor: '+' }.into()); - assert_eq!(p("5+abc."), ErrorKind::LocalEmpty { precursor: '.' }.into()); - assert_eq!(p("5+abc-"), ErrorKind::LocalEmpty { precursor: '-' }.into()); - assert_eq!(p("5+abc_"), ErrorKind::LocalEmpty { precursor: '_' }.into()); - assert_eq!( - p("5+abc. "), - ErrorKind::LocalEmpty { precursor: '.' }.into() - ); - assert_eq!( - p("5.6-"), - ErrorKind::UnexpectedEnd { - version: "5.6".to_string(), - remaining: "-".to_string() - } - .into() - ); - } - - #[test] - fn parse_version_pattern_valid() { - let p = |s: &str| match Parser::new(s.as_bytes()).parse_pattern() { - Ok(v) => v, - Err(err) => unreachable!("expected valid version, but got error: {err:?}"), - }; - - assert_eq!(p("5.*"), VersionPattern::wildcard(Version::new([5]))); - assert_eq!(p("5.6.*"), VersionPattern::wildcard(Version::new([5, 6]))); - assert_eq!( - p("2!5.6.*"), - VersionPattern::wildcard(Version::new([5, 6]).with_epoch(2)) - ); - } - - #[test] - fn parse_version_pattern_invalid() { - let p = |s: &str| match Parser::new(s.as_bytes()).parse_pattern() { - Err(err) => err, - Ok(vpat) => unreachable!("expected version pattern parser error, but got: {vpat:?}"), - }; - - assert_eq!(p("*"), ErrorKind::NoLeadingNumber.into()); - assert_eq!(p("2!*"), ErrorKind::NoLeadingReleaseNumber.into()); - } - - // Tests that the ordering between versions is correct. - // - // The ordering example used here was taken from PEP 440: - // https://packaging.python.org/en/latest/specifications/version-specifiers/#summary-of-permitted-suffixes-and-relative-ordering - #[test] - fn ordering() { - let versions = &[ - "1.dev0", - "1.0.dev456", - "1.0a1", - "1.0a2.dev456", - "1.0a12.dev456", - "1.0a12", - "1.0b1.dev456", - "1.0b2", - "1.0b2.post345.dev456", - "1.0b2.post345", - "1.0rc1.dev456", - "1.0rc1", - "1.0", - "1.0+abc.5", - "1.0+abc.7", - "1.0+5", - "1.0.post456.dev34", - "1.0.post456", - "1.0.15", - "1.1.dev1", - ]; - for (i, v1) in versions.iter().enumerate() { - for v2 in &versions[i + 1..] { - let less = v1.parse::().unwrap(); - let greater = v2.parse::().unwrap(); - assert_eq!( - less.cmp(&greater), - Ordering::Less, - "less: {:?}\ngreater: {:?}", - less.as_bloated_debug(), - greater.as_bloated_debug() - ); - } - } - } - - #[test] - fn min_version() { - // Ensure that the `.min` suffix precedes all other suffixes. - let less = Version::new([1, 0]).with_min(Some(0)); - - let versions = &[ - "1.dev0", - "1.0.dev456", - "1.0a1", - "1.0a2.dev456", - "1.0a12.dev456", - "1.0a12", - "1.0b1.dev456", - "1.0b2", - "1.0b2.post345.dev456", - "1.0b2.post345", - "1.0rc1.dev456", - "1.0rc1", - "1.0", - "1.0+abc.5", - "1.0+abc.7", - "1.0+5", - "1.0.post456.dev34", - "1.0.post456", - "1.0.15", - "1.1.dev1", - ]; - - for greater in versions { - let greater = greater.parse::().unwrap(); - assert_eq!( - less.cmp(&greater), - Ordering::Less, - "less: {:?}\ngreater: {:?}", - less.as_bloated_debug(), - greater.as_bloated_debug() - ); - } - } - - #[test] - fn max_version() { - // Ensure that the `.max` suffix succeeds all other suffixes. - let greater = Version::new([1, 0]).with_max(Some(0)); - - let versions = &[ - "1.dev0", - "1.0.dev456", - "1.0a1", - "1.0a2.dev456", - "1.0a12.dev456", - "1.0a12", - "1.0b1.dev456", - "1.0b2", - "1.0b2.post345.dev456", - "1.0b2.post345", - "1.0rc1.dev456", - "1.0rc1", - "1.0", - "1.0+abc.5", - "1.0+abc.7", - "1.0+5", - "1.0.post456.dev34", - "1.0.post456", - "1.0", - ]; - - for less in versions { - let less = less.parse::().unwrap(); - assert_eq!( - less.cmp(&greater), - Ordering::Less, - "less: {:?}\ngreater: {:?}", - less.as_bloated_debug(), - greater.as_bloated_debug() - ); - } - - // Ensure that the `.max` suffix plays nicely with pre-release versions. - let greater = Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1, - })) - .with_max(Some(0)); - - let versions = &["1.0a1", "1.0a1+local", "1.0a1.post1"]; - - for less in versions { - let less = less.parse::().unwrap(); - assert_eq!( - less.cmp(&greater), - Ordering::Less, - "less: {:?}\ngreater: {:?}", - less.as_bloated_debug(), - greater.as_bloated_debug() - ); - } - - // Ensure that the `.max` suffix plays nicely with pre-release versions. - let less = Version::new([1, 0]) - .with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1, - })) - .with_max(Some(0)); - - let versions = &["1.0b1", "1.0b1+local", "1.0b1.post1", "1.0"]; - - for greater in versions { - let greater = greater.parse::().unwrap(); - assert_eq!( - less.cmp(&greater), - Ordering::Less, - "less: {:?}\ngreater: {:?}", - less.as_bloated_debug(), - greater.as_bloated_debug() - ); - } - } - - // Tests our bespoke u64 decimal integer parser. - #[test] - fn parse_number_u64() { - let p = |s: &str| parse_u64(s.as_bytes()); - assert_eq!(p("0"), Ok(0)); - assert_eq!(p("00"), Ok(0)); - assert_eq!(p("1"), Ok(1)); - assert_eq!(p("01"), Ok(1)); - assert_eq!(p("9"), Ok(9)); - assert_eq!(p("10"), Ok(10)); - assert_eq!(p("18446744073709551615"), Ok(18_446_744_073_709_551_615)); - assert_eq!(p("018446744073709551615"), Ok(18_446_744_073_709_551_615)); - assert_eq!( - p("000000018446744073709551615"), - Ok(18_446_744_073_709_551_615) - ); - - assert_eq!(p("10a"), Err(ErrorKind::InvalidDigit { got: b'a' }.into())); - assert_eq!(p("10["), Err(ErrorKind::InvalidDigit { got: b'[' }.into())); - assert_eq!(p("10/"), Err(ErrorKind::InvalidDigit { got: b'/' }.into())); - assert_eq!( - p("18446744073709551616"), - Err(ErrorKind::NumberTooBig { - bytes: b"18446744073709551616".to_vec() - } - .into()) - ); - assert_eq!( - p("18446744073799551615abc"), - Err(ErrorKind::NumberTooBig { - bytes: b"18446744073799551615abc".to_vec() - } - .into()) - ); - assert_eq!( - parse_u64(b"18446744073799551615\xFF"), - Err(ErrorKind::NumberTooBig { - bytes: b"18446744073799551615\xFF".to_vec() - } - .into()) - ); - } - - /// Wraps a `Version` and provides a more "bloated" debug but standard - /// representation. - /// - /// We don't do this by default because it takes up a ton of space, and - /// just printing out the display version of the version is quite a bit - /// simpler. - /// - /// Nevertheless, when *testing* version parsing, you really want to - /// be able to peek at all of its constituent parts. So we use this in - /// assertion failure messages. - struct VersionBloatedDebug<'a>(&'a Version); - - impl<'a> std::fmt::Debug for VersionBloatedDebug<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Version") - .field("epoch", &self.0.epoch()) - .field("release", &self.0.release()) - .field("pre", &self.0.pre()) - .field("post", &self.0.post()) - .field("dev", &self.0.dev()) - .field("local", &self.0.local()) - .field("min", &self.0.min()) - .field("max", &self.0.max()) - .finish() - } - } - - impl Version { - pub(crate) fn as_bloated_debug(&self) -> impl std::fmt::Debug + '_ { - VersionBloatedDebug(self) - } - } -} +mod tests; diff --git a/crates/uv-pep440/src/version/tests.rs b/crates/uv-pep440/src/version/tests.rs new file mode 100644 index 000000000..54aa2e51f --- /dev/null +++ b/crates/uv-pep440/src/version/tests.rs @@ -0,0 +1,1343 @@ +use std::str::FromStr; + +use crate::VersionSpecifier; + +use super::*; + +/// +#[test] +fn test_packaging_versions() { + let versions = [ + // Implicit epoch of 0 + ("1.0.dev456", Version::new([1, 0]).with_dev(Some(456))), + ( + "1.0a1", + Version::new([1, 0]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1, + })), + ), + ( + "1.0a2.dev456", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 2, + })) + .with_dev(Some(456)), + ), + ( + "1.0a12.dev456", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 12, + })) + .with_dev(Some(456)), + ), + ( + "1.0a12", + Version::new([1, 0]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 12, + })), + ), + ( + "1.0b1.dev456", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 1, + })) + .with_dev(Some(456)), + ), + ( + "1.0b2", + Version::new([1, 0]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })), + ), + ( + "1.0b2.post345.dev456", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })) + .with_dev(Some(456)) + .with_post(Some(345)), + ), + ( + "1.0b2.post345", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })) + .with_post(Some(345)), + ), + ( + "1.0b2-346", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })) + .with_post(Some(346)), + ), + ( + "1.0c1.dev456", + Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1, + })) + .with_dev(Some(456)), + ), + ( + "1.0c1", + Version::new([1, 0]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1, + })), + ), + ( + "1.0rc2", + Version::new([1, 0]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 2, + })), + ), + ( + "1.0c3", + Version::new([1, 0]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 3, + })), + ), + ("1.0", Version::new([1, 0])), + ( + "1.0.post456.dev34", + Version::new([1, 0]).with_post(Some(456)).with_dev(Some(34)), + ), + ("1.0.post456", Version::new([1, 0]).with_post(Some(456))), + ("1.1.dev1", Version::new([1, 1]).with_dev(Some(1))), + ( + "1.2+123abc", + Version::new([1, 2]).with_local(vec![LocalSegment::String("123abc".to_string())]), + ), + ( + "1.2+123abc456", + Version::new([1, 2]).with_local(vec![LocalSegment::String("123abc456".to_string())]), + ), + ( + "1.2+abc", + Version::new([1, 2]).with_local(vec![LocalSegment::String("abc".to_string())]), + ), + ( + "1.2+abc123", + Version::new([1, 2]).with_local(vec![LocalSegment::String("abc123".to_string())]), + ), + ( + "1.2+abc123def", + Version::new([1, 2]).with_local(vec![LocalSegment::String("abc123def".to_string())]), + ), + ( + "1.2+1234.abc", + Version::new([1, 2]).with_local(vec![ + LocalSegment::Number(1234), + LocalSegment::String("abc".to_string()), + ]), + ), + ( + "1.2+123456", + Version::new([1, 2]).with_local(vec![LocalSegment::Number(123_456)]), + ), + ( + "1.2.r32+123456", + Version::new([1, 2]) + .with_post(Some(32)) + .with_local(vec![LocalSegment::Number(123_456)]), + ), + ( + "1.2.rev33+123456", + Version::new([1, 2]) + .with_post(Some(33)) + .with_local(vec![LocalSegment::Number(123_456)]), + ), + // Explicit epoch of 1 + ( + "1!1.0.dev456", + Version::new([1, 0]).with_epoch(1).with_dev(Some(456)), + ), + ( + "1!1.0a1", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1, + })), + ), + ( + "1!1.0a2.dev456", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 2, + })) + .with_dev(Some(456)), + ), + ( + "1!1.0a12.dev456", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 12, + })) + .with_dev(Some(456)), + ), + ( + "1!1.0a12", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 12, + })), + ), + ( + "1!1.0b1.dev456", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 1, + })) + .with_dev(Some(456)), + ), + ( + "1!1.0b2", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })), + ), + ( + "1!1.0b2.post345.dev456", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })) + .with_post(Some(345)) + .with_dev(Some(456)), + ), + ( + "1!1.0b2.post345", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })) + .with_post(Some(345)), + ), + ( + "1!1.0b2-346", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 2, + })) + .with_post(Some(346)), + ), + ( + "1!1.0c1.dev456", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1, + })) + .with_dev(Some(456)), + ), + ( + "1!1.0c1", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1, + })), + ), + ( + "1!1.0rc2", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 2, + })), + ), + ( + "1!1.0c3", + Version::new([1, 0]) + .with_epoch(1) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 3, + })), + ), + ("1!1.0", Version::new([1, 0]).with_epoch(1)), + ( + "1!1.0.post456.dev34", + Version::new([1, 0]) + .with_epoch(1) + .with_post(Some(456)) + .with_dev(Some(34)), + ), + ( + "1!1.0.post456", + Version::new([1, 0]).with_epoch(1).with_post(Some(456)), + ), + ( + "1!1.1.dev1", + Version::new([1, 1]).with_epoch(1).with_dev(Some(1)), + ), + ( + "1!1.2+123abc", + Version::new([1, 2]) + .with_epoch(1) + .with_local(vec![LocalSegment::String("123abc".to_string())]), + ), + ( + "1!1.2+123abc456", + Version::new([1, 2]) + .with_epoch(1) + .with_local(vec![LocalSegment::String("123abc456".to_string())]), + ), + ( + "1!1.2+abc", + Version::new([1, 2]) + .with_epoch(1) + .with_local(vec![LocalSegment::String("abc".to_string())]), + ), + ( + "1!1.2+abc123", + Version::new([1, 2]) + .with_epoch(1) + .with_local(vec![LocalSegment::String("abc123".to_string())]), + ), + ( + "1!1.2+abc123def", + Version::new([1, 2]) + .with_epoch(1) + .with_local(vec![LocalSegment::String("abc123def".to_string())]), + ), + ( + "1!1.2+1234.abc", + Version::new([1, 2]).with_epoch(1).with_local(vec![ + LocalSegment::Number(1234), + LocalSegment::String("abc".to_string()), + ]), + ), + ( + "1!1.2+123456", + Version::new([1, 2]) + .with_epoch(1) + .with_local(vec![LocalSegment::Number(123_456)]), + ), + ( + "1!1.2.r32+123456", + Version::new([1, 2]) + .with_epoch(1) + .with_post(Some(32)) + .with_local(vec![LocalSegment::Number(123_456)]), + ), + ( + "1!1.2.rev33+123456", + Version::new([1, 2]) + .with_epoch(1) + .with_post(Some(33)) + .with_local(vec![LocalSegment::Number(123_456)]), + ), + ( + "98765!1.2.rev33+123456", + Version::new([1, 2]) + .with_epoch(98765) + .with_post(Some(33)) + .with_local(vec![LocalSegment::Number(123_456)]), + ), + ]; + for (string, structured) in versions { + match Version::from_str(string) { + Err(err) => { + unreachable!( + "expected {string:?} to parse as {structured:?}, but got {err:?}", + structured = structured.as_bloated_debug(), + ) + } + Ok(v) => assert!( + v == structured, + "for {string:?}, expected {structured:?} but got {v:?}", + structured = structured.as_bloated_debug(), + v = v.as_bloated_debug(), + ), + } + let spec = format!("=={string}"); + match VersionSpecifier::from_str(&spec) { + Err(err) => { + unreachable!( + "expected version in {spec:?} to parse as {structured:?}, but got {err:?}", + structured = structured.as_bloated_debug(), + ) + } + Ok(v) => assert!( + v.version() == &structured, + "for {string:?}, expected {structured:?} but got {v:?}", + structured = structured.as_bloated_debug(), + v = v.version.as_bloated_debug(), + ), + } + } +} + +/// +#[test] +fn test_packaging_failures() { + let versions = [ + // Versions with invalid local versions + "1.0+a+", + "1.0++", + "1.0+_foobar", + "1.0+foo&asd", + "1.0+1+1", + // Nonsensical versions should also be invalid + "french toast", + "==french toast", + ]; + for version in versions { + assert!(Version::from_str(version).is_err()); + assert!(VersionSpecifier::from_str(&format!("=={version}")).is_err()); + } +} + +#[test] +fn test_equality_and_normalization() { + let versions = [ + // Various development release incarnations + ("1.0dev", "1.0.dev0"), + ("1.0.dev", "1.0.dev0"), + ("1.0dev1", "1.0.dev1"), + ("1.0dev", "1.0.dev0"), + ("1.0-dev", "1.0.dev0"), + ("1.0-dev1", "1.0.dev1"), + ("1.0DEV", "1.0.dev0"), + ("1.0.DEV", "1.0.dev0"), + ("1.0DEV1", "1.0.dev1"), + ("1.0DEV", "1.0.dev0"), + ("1.0.DEV1", "1.0.dev1"), + ("1.0-DEV", "1.0.dev0"), + ("1.0-DEV1", "1.0.dev1"), + // Various alpha incarnations + ("1.0a", "1.0a0"), + ("1.0.a", "1.0a0"), + ("1.0.a1", "1.0a1"), + ("1.0-a", "1.0a0"), + ("1.0-a1", "1.0a1"), + ("1.0alpha", "1.0a0"), + ("1.0.alpha", "1.0a0"), + ("1.0.alpha1", "1.0a1"), + ("1.0-alpha", "1.0a0"), + ("1.0-alpha1", "1.0a1"), + ("1.0A", "1.0a0"), + ("1.0.A", "1.0a0"), + ("1.0.A1", "1.0a1"), + ("1.0-A", "1.0a0"), + ("1.0-A1", "1.0a1"), + ("1.0ALPHA", "1.0a0"), + ("1.0.ALPHA", "1.0a0"), + ("1.0.ALPHA1", "1.0a1"), + ("1.0-ALPHA", "1.0a0"), + ("1.0-ALPHA1", "1.0a1"), + // Various beta incarnations + ("1.0b", "1.0b0"), + ("1.0.b", "1.0b0"), + ("1.0.b1", "1.0b1"), + ("1.0-b", "1.0b0"), + ("1.0-b1", "1.0b1"), + ("1.0beta", "1.0b0"), + ("1.0.beta", "1.0b0"), + ("1.0.beta1", "1.0b1"), + ("1.0-beta", "1.0b0"), + ("1.0-beta1", "1.0b1"), + ("1.0B", "1.0b0"), + ("1.0.B", "1.0b0"), + ("1.0.B1", "1.0b1"), + ("1.0-B", "1.0b0"), + ("1.0-B1", "1.0b1"), + ("1.0BETA", "1.0b0"), + ("1.0.BETA", "1.0b0"), + ("1.0.BETA1", "1.0b1"), + ("1.0-BETA", "1.0b0"), + ("1.0-BETA1", "1.0b1"), + // Various release candidate incarnations + ("1.0c", "1.0rc0"), + ("1.0.c", "1.0rc0"), + ("1.0.c1", "1.0rc1"), + ("1.0-c", "1.0rc0"), + ("1.0-c1", "1.0rc1"), + ("1.0rc", "1.0rc0"), + ("1.0.rc", "1.0rc0"), + ("1.0.rc1", "1.0rc1"), + ("1.0-rc", "1.0rc0"), + ("1.0-rc1", "1.0rc1"), + ("1.0C", "1.0rc0"), + ("1.0.C", "1.0rc0"), + ("1.0.C1", "1.0rc1"), + ("1.0-C", "1.0rc0"), + ("1.0-C1", "1.0rc1"), + ("1.0RC", "1.0rc0"), + ("1.0.RC", "1.0rc0"), + ("1.0.RC1", "1.0rc1"), + ("1.0-RC", "1.0rc0"), + ("1.0-RC1", "1.0rc1"), + // Various post release incarnations + ("1.0post", "1.0.post0"), + ("1.0.post", "1.0.post0"), + ("1.0post1", "1.0.post1"), + ("1.0post", "1.0.post0"), + ("1.0-post", "1.0.post0"), + ("1.0-post1", "1.0.post1"), + ("1.0POST", "1.0.post0"), + ("1.0.POST", "1.0.post0"), + ("1.0POST1", "1.0.post1"), + ("1.0POST", "1.0.post0"), + ("1.0r", "1.0.post0"), + ("1.0rev", "1.0.post0"), + ("1.0.POST1", "1.0.post1"), + ("1.0.r1", "1.0.post1"), + ("1.0.rev1", "1.0.post1"), + ("1.0-POST", "1.0.post0"), + ("1.0-POST1", "1.0.post1"), + ("1.0-5", "1.0.post5"), + ("1.0-r5", "1.0.post5"), + ("1.0-rev5", "1.0.post5"), + // Local version case insensitivity + ("1.0+AbC", "1.0+abc"), + // Integer Normalization + ("1.01", "1.1"), + ("1.0a05", "1.0a5"), + ("1.0b07", "1.0b7"), + ("1.0c056", "1.0rc56"), + ("1.0rc09", "1.0rc9"), + ("1.0.post000", "1.0.post0"), + ("1.1.dev09000", "1.1.dev9000"), + ("00!1.2", "1.2"), + ("0100!0.0", "100!0.0"), + // Various other normalizations + ("v1.0", "1.0"), + (" v1.0\t\n", "1.0"), + ]; + for (version_str, normalized_str) in versions { + let version = Version::from_str(version_str).unwrap(); + let normalized = Version::from_str(normalized_str).unwrap(); + // Just test version parsing again + assert_eq!(version, normalized, "{version_str} {normalized_str}"); + // Test version normalization + assert_eq!( + version.to_string(), + normalized.to_string(), + "{version_str} {normalized_str}" + ); + } +} + +/// +#[test] +fn test_equality_and_normalization2() { + let versions = [ + ("1.0.dev456", "1.0.dev456"), + ("1.0a1", "1.0a1"), + ("1.0a2.dev456", "1.0a2.dev456"), + ("1.0a12.dev456", "1.0a12.dev456"), + ("1.0a12", "1.0a12"), + ("1.0b1.dev456", "1.0b1.dev456"), + ("1.0b2", "1.0b2"), + ("1.0b2.post345.dev456", "1.0b2.post345.dev456"), + ("1.0b2.post345", "1.0b2.post345"), + ("1.0rc1.dev456", "1.0rc1.dev456"), + ("1.0rc1", "1.0rc1"), + ("1.0", "1.0"), + ("1.0.post456.dev34", "1.0.post456.dev34"), + ("1.0.post456", "1.0.post456"), + ("1.0.1", "1.0.1"), + ("0!1.0.2", "1.0.2"), + ("1.0.3+7", "1.0.3+7"), + ("0!1.0.4+8.0", "1.0.4+8.0"), + ("1.0.5+9.5", "1.0.5+9.5"), + ("1.2+1234.abc", "1.2+1234.abc"), + ("1.2+123456", "1.2+123456"), + ("1.2+123abc", "1.2+123abc"), + ("1.2+123abc456", "1.2+123abc456"), + ("1.2+abc", "1.2+abc"), + ("1.2+abc123", "1.2+abc123"), + ("1.2+abc123def", "1.2+abc123def"), + ("1.1.dev1", "1.1.dev1"), + ("7!1.0.dev456", "7!1.0.dev456"), + ("7!1.0a1", "7!1.0a1"), + ("7!1.0a2.dev456", "7!1.0a2.dev456"), + ("7!1.0a12.dev456", "7!1.0a12.dev456"), + ("7!1.0a12", "7!1.0a12"), + ("7!1.0b1.dev456", "7!1.0b1.dev456"), + ("7!1.0b2", "7!1.0b2"), + ("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"), + ("7!1.0b2.post345", "7!1.0b2.post345"), + ("7!1.0rc1.dev456", "7!1.0rc1.dev456"), + ("7!1.0rc1", "7!1.0rc1"), + ("7!1.0", "7!1.0"), + ("7!1.0.post456.dev34", "7!1.0.post456.dev34"), + ("7!1.0.post456", "7!1.0.post456"), + ("7!1.0.1", "7!1.0.1"), + ("7!1.0.2", "7!1.0.2"), + ("7!1.0.3+7", "7!1.0.3+7"), + ("7!1.0.4+8.0", "7!1.0.4+8.0"), + ("7!1.0.5+9.5", "7!1.0.5+9.5"), + ("7!1.1.dev1", "7!1.1.dev1"), + ]; + for (version_str, normalized_str) in versions { + let version = Version::from_str(version_str).unwrap(); + let normalized = Version::from_str(normalized_str).unwrap(); + assert_eq!(version, normalized, "{version_str} {normalized_str}"); + // Test version normalization + assert_eq!( + version.to_string(), + normalized_str, + "{version_str} {normalized_str}" + ); + // Since we're already at it + assert_eq!( + version.to_string(), + normalized.to_string(), + "{version_str} {normalized_str}" + ); + } +} + +#[test] +fn test_star_fixed_version() { + let result = Version::from_str("0.9.1.*"); + assert_eq!(result.unwrap_err(), ErrorKind::Wildcard.into()); +} + +#[test] +fn test_invalid_word() { + let result = Version::from_str("blergh"); + assert_eq!(result.unwrap_err(), ErrorKind::NoLeadingNumber.into()); +} + +#[test] +fn test_from_version_star() { + let p = |s: &str| -> Result { s.parse() }; + assert!(!p("1.2.3").unwrap().is_wildcard()); + assert!(p("1.2.3.*").unwrap().is_wildcard()); + assert_eq!( + p("1.2.*.4.*").unwrap_err(), + PatternErrorKind::WildcardNotTrailing.into(), + ); + assert_eq!( + p("1.0-dev1.*").unwrap_err(), + ErrorKind::UnexpectedEnd { + version: "1.0-dev1".to_string(), + remaining: ".*".to_string() + } + .into(), + ); + assert_eq!( + p("1.0a1.*").unwrap_err(), + ErrorKind::UnexpectedEnd { + version: "1.0a1".to_string(), + remaining: ".*".to_string() + } + .into(), + ); + assert_eq!( + p("1.0.post1.*").unwrap_err(), + ErrorKind::UnexpectedEnd { + version: "1.0.post1".to_string(), + remaining: ".*".to_string() + } + .into(), + ); + assert_eq!( + p("1.0+lolwat.*").unwrap_err(), + ErrorKind::LocalEmpty { precursor: '.' }.into(), + ); +} + +// Tests the valid cases of our version parser. These were written +// in tandem with the parser. +// +// They are meant to be additional (but in some cases likely redundant) +// with some of the above tests. +#[test] +fn parse_version_valid() { + let p = |s: &str| match Parser::new(s.as_bytes()).parse() { + Ok(v) => v, + Err(err) => unreachable!("expected valid version, but got error: {err:?}"), + }; + + // release-only tests + assert_eq!(p("5"), Version::new([5])); + assert_eq!(p("5.6"), Version::new([5, 6])); + assert_eq!(p("5.6.7"), Version::new([5, 6, 7])); + assert_eq!(p("512.623.734"), Version::new([512, 623, 734])); + assert_eq!(p("1.2.3.4"), Version::new([1, 2, 3, 4])); + assert_eq!(p("1.2.3.4.5"), Version::new([1, 2, 3, 4, 5])); + + // epoch tests + assert_eq!(p("4!5"), Version::new([5]).with_epoch(4)); + assert_eq!(p("4!5.6"), Version::new([5, 6]).with_epoch(4)); + + // pre-release tests + assert_eq!( + p("5a1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1 + })) + ); + assert_eq!( + p("5alpha1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1 + })) + ); + assert_eq!( + p("5b1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 1 + })) + ); + assert_eq!( + p("5beta1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Beta, + number: 1 + })) + ); + assert_eq!( + p("5rc1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1 + })) + ); + assert_eq!( + p("5c1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1 + })) + ); + assert_eq!( + p("5preview1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1 + })) + ); + assert_eq!( + p("5pre1"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1 + })) + ); + assert_eq!( + p("5.6.7pre1"), + Version::new([5, 6, 7]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Rc, + number: 1 + })) + ); + assert_eq!( + p("5alpha789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5.alpha789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5-alpha789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5_alpha789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5alpha.789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5alpha-789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5alpha_789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5ALPHA789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5aLpHa789"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 789 + })) + ); + assert_eq!( + p("5alpha"), + Version::new([5]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 0 + })) + ); + + // post-release tests + assert_eq!(p("5post2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5rev2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5r2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5.post2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5-post2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5_post2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5.post.2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5.post-2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5.post_2"), Version::new([5]).with_post(Some(2))); + assert_eq!( + p("5.6.7.post_2"), + Version::new([5, 6, 7]).with_post(Some(2)) + ); + assert_eq!(p("5-2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5.6.7-2"), Version::new([5, 6, 7]).with_post(Some(2))); + assert_eq!(p("5POST2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5PoSt2"), Version::new([5]).with_post(Some(2))); + assert_eq!(p("5post"), Version::new([5]).with_post(Some(0))); + + // dev-release tests + assert_eq!(p("5dev2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5.dev2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5-dev2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5_dev2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5.dev.2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5.dev-2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5.dev_2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5.6.7.dev_2"), Version::new([5, 6, 7]).with_dev(Some(2))); + assert_eq!(p("5DEV2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5dEv2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5DeV2"), Version::new([5]).with_dev(Some(2))); + assert_eq!(p("5dev"), Version::new([5]).with_dev(Some(0))); + + // local tests + assert_eq!( + p("5+2"), + Version::new([5]).with_local(vec![LocalSegment::Number(2)]) + ); + assert_eq!( + p("5+a"), + Version::new([5]).with_local(vec![LocalSegment::String("a".to_string())]) + ); + assert_eq!( + p("5+abc.123"), + Version::new([5]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + ]) + ); + assert_eq!( + p("5+123.abc"), + Version::new([5]).with_local(vec![ + LocalSegment::Number(123), + LocalSegment::String("abc".to_string()), + ]) + ); + assert_eq!( + p("5+18446744073709551615.abc"), + Version::new([5]).with_local(vec![ + LocalSegment::Number(18_446_744_073_709_551_615), + LocalSegment::String("abc".to_string()), + ]) + ); + assert_eq!( + p("5+18446744073709551616.abc"), + Version::new([5]).with_local(vec![ + LocalSegment::String("18446744073709551616".to_string()), + LocalSegment::String("abc".to_string()), + ]) + ); + assert_eq!( + p("5+ABC.123"), + Version::new([5]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + ]) + ); + assert_eq!( + p("5+ABC-123.4_5_xyz-MNO"), + Version::new([5]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + LocalSegment::Number(4), + LocalSegment::Number(5), + LocalSegment::String("xyz".to_string()), + LocalSegment::String("mno".to_string()), + ]) + ); + assert_eq!( + p("5.6.7+abc-00123"), + Version::new([5, 6, 7]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + ]) + ); + assert_eq!( + p("5.6.7+abc-foo00123"), + Version::new([5, 6, 7]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::String("foo00123".to_string()), + ]) + ); + assert_eq!( + p("5.6.7+abc-00123a"), + Version::new([5, 6, 7]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::String("00123a".to_string()), + ]) + ); + + // {pre-release, post-release} tests + assert_eq!( + p("5a2post3"), + Version::new([5]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 2 + })) + .with_post(Some(3)) + ); + assert_eq!( + p("5.a-2_post-3"), + Version::new([5]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 2 + })) + .with_post(Some(3)) + ); + assert_eq!( + p("5a2-3"), + Version::new([5]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 2 + })) + .with_post(Some(3)) + ); + + // Ignoring a no-op 'v' prefix. + assert_eq!(p("v5"), Version::new([5])); + assert_eq!(p("V5"), Version::new([5])); + assert_eq!(p("v5.6.7"), Version::new([5, 6, 7])); + + // Ignoring leading and trailing whitespace. + assert_eq!(p(" v5 "), Version::new([5])); + assert_eq!(p(" 5 "), Version::new([5])); + assert_eq!( + p(" 5.6.7+abc.123.xyz "), + Version::new([5, 6, 7]).with_local(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + LocalSegment::String("xyz".to_string()) + ]) + ); + assert_eq!(p(" \n5\n \t"), Version::new([5])); + + // min tests + assert!(Parser::new("1.min0".as_bytes()).parse().is_err()); +} + +// Tests the error cases of our version parser. +// +// I wrote these with the intent to cover every possible error +// case. +// +// They are meant to be additional (but in some cases likely redundant) +// with some of the above tests. +#[test] +fn parse_version_invalid() { + let p = |s: &str| match Parser::new(s.as_bytes()).parse() { + Err(err) => err, + Ok(v) => unreachable!( + "expected version parser error, but got: {v:?}", + v = v.as_bloated_debug() + ), + }; + + assert_eq!(p(""), ErrorKind::NoLeadingNumber.into()); + assert_eq!(p("a"), ErrorKind::NoLeadingNumber.into()); + assert_eq!(p("v 5"), ErrorKind::NoLeadingNumber.into()); + assert_eq!(p("V 5"), ErrorKind::NoLeadingNumber.into()); + assert_eq!(p("x 5"), ErrorKind::NoLeadingNumber.into()); + assert_eq!( + p("18446744073709551616"), + ErrorKind::NumberTooBig { + bytes: b"18446744073709551616".to_vec() + } + .into() + ); + assert_eq!(p("5!"), ErrorKind::NoLeadingReleaseNumber.into()); + assert_eq!( + p("5.6./"), + ErrorKind::UnexpectedEnd { + version: "5.6".to_string(), + remaining: "./".to_string() + } + .into() + ); + assert_eq!( + p("5.6.-alpha2"), + ErrorKind::UnexpectedEnd { + version: "5.6".to_string(), + remaining: ".-alpha2".to_string() + } + .into() + ); + assert_eq!( + p("1.2.3a18446744073709551616"), + ErrorKind::NumberTooBig { + bytes: b"18446744073709551616".to_vec() + } + .into() + ); + assert_eq!(p("5+"), ErrorKind::LocalEmpty { precursor: '+' }.into()); + assert_eq!(p("5+ "), ErrorKind::LocalEmpty { precursor: '+' }.into()); + assert_eq!(p("5+abc."), ErrorKind::LocalEmpty { precursor: '.' }.into()); + assert_eq!(p("5+abc-"), ErrorKind::LocalEmpty { precursor: '-' }.into()); + assert_eq!(p("5+abc_"), ErrorKind::LocalEmpty { precursor: '_' }.into()); + assert_eq!( + p("5+abc. "), + ErrorKind::LocalEmpty { precursor: '.' }.into() + ); + assert_eq!( + p("5.6-"), + ErrorKind::UnexpectedEnd { + version: "5.6".to_string(), + remaining: "-".to_string() + } + .into() + ); +} + +#[test] +fn parse_version_pattern_valid() { + let p = |s: &str| match Parser::new(s.as_bytes()).parse_pattern() { + Ok(v) => v, + Err(err) => unreachable!("expected valid version, but got error: {err:?}"), + }; + + assert_eq!(p("5.*"), VersionPattern::wildcard(Version::new([5]))); + assert_eq!(p("5.6.*"), VersionPattern::wildcard(Version::new([5, 6]))); + assert_eq!( + p("2!5.6.*"), + VersionPattern::wildcard(Version::new([5, 6]).with_epoch(2)) + ); +} + +#[test] +fn parse_version_pattern_invalid() { + let p = |s: &str| match Parser::new(s.as_bytes()).parse_pattern() { + Err(err) => err, + Ok(vpat) => unreachable!("expected version pattern parser error, but got: {vpat:?}"), + }; + + assert_eq!(p("*"), ErrorKind::NoLeadingNumber.into()); + assert_eq!(p("2!*"), ErrorKind::NoLeadingReleaseNumber.into()); +} + +// Tests that the ordering between versions is correct. +// +// The ordering example used here was taken from PEP 440: +// https://packaging.python.org/en/latest/specifications/version-specifiers/#summary-of-permitted-suffixes-and-relative-ordering +#[test] +fn ordering() { + let versions = &[ + "1.dev0", + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345", + "1.0rc1.dev456", + "1.0rc1", + "1.0", + "1.0+abc.5", + "1.0+abc.7", + "1.0+5", + "1.0.post456.dev34", + "1.0.post456", + "1.0.15", + "1.1.dev1", + ]; + for (i, v1) in versions.iter().enumerate() { + for v2 in &versions[i + 1..] { + let less = v1.parse::().unwrap(); + let greater = v2.parse::().unwrap(); + assert_eq!( + less.cmp(&greater), + Ordering::Less, + "less: {:?}\ngreater: {:?}", + less.as_bloated_debug(), + greater.as_bloated_debug() + ); + } + } +} + +#[test] +fn min_version() { + // Ensure that the `.min` suffix precedes all other suffixes. + let less = Version::new([1, 0]).with_min(Some(0)); + + let versions = &[ + "1.dev0", + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345", + "1.0rc1.dev456", + "1.0rc1", + "1.0", + "1.0+abc.5", + "1.0+abc.7", + "1.0+5", + "1.0.post456.dev34", + "1.0.post456", + "1.0.15", + "1.1.dev1", + ]; + + for greater in versions { + let greater = greater.parse::().unwrap(); + assert_eq!( + less.cmp(&greater), + Ordering::Less, + "less: {:?}\ngreater: {:?}", + less.as_bloated_debug(), + greater.as_bloated_debug() + ); + } +} + +#[test] +fn max_version() { + // Ensure that the `.max` suffix succeeds all other suffixes. + let greater = Version::new([1, 0]).with_max(Some(0)); + + let versions = &[ + "1.dev0", + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345", + "1.0rc1.dev456", + "1.0rc1", + "1.0", + "1.0+abc.5", + "1.0+abc.7", + "1.0+5", + "1.0.post456.dev34", + "1.0.post456", + "1.0", + ]; + + for less in versions { + let less = less.parse::().unwrap(); + assert_eq!( + less.cmp(&greater), + Ordering::Less, + "less: {:?}\ngreater: {:?}", + less.as_bloated_debug(), + greater.as_bloated_debug() + ); + } + + // Ensure that the `.max` suffix plays nicely with pre-release versions. + let greater = Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1, + })) + .with_max(Some(0)); + + let versions = &["1.0a1", "1.0a1+local", "1.0a1.post1"]; + + for less in versions { + let less = less.parse::().unwrap(); + assert_eq!( + less.cmp(&greater), + Ordering::Less, + "less: {:?}\ngreater: {:?}", + less.as_bloated_debug(), + greater.as_bloated_debug() + ); + } + + // Ensure that the `.max` suffix plays nicely with pre-release versions. + let less = Version::new([1, 0]) + .with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1, + })) + .with_max(Some(0)); + + let versions = &["1.0b1", "1.0b1+local", "1.0b1.post1", "1.0"]; + + for greater in versions { + let greater = greater.parse::().unwrap(); + assert_eq!( + less.cmp(&greater), + Ordering::Less, + "less: {:?}\ngreater: {:?}", + less.as_bloated_debug(), + greater.as_bloated_debug() + ); + } +} + +// Tests our bespoke u64 decimal integer parser. +#[test] +fn parse_number_u64() { + let p = |s: &str| parse_u64(s.as_bytes()); + assert_eq!(p("0"), Ok(0)); + assert_eq!(p("00"), Ok(0)); + assert_eq!(p("1"), Ok(1)); + assert_eq!(p("01"), Ok(1)); + assert_eq!(p("9"), Ok(9)); + assert_eq!(p("10"), Ok(10)); + assert_eq!(p("18446744073709551615"), Ok(18_446_744_073_709_551_615)); + assert_eq!(p("018446744073709551615"), Ok(18_446_744_073_709_551_615)); + assert_eq!( + p("000000018446744073709551615"), + Ok(18_446_744_073_709_551_615) + ); + + assert_eq!(p("10a"), Err(ErrorKind::InvalidDigit { got: b'a' }.into())); + assert_eq!(p("10["), Err(ErrorKind::InvalidDigit { got: b'[' }.into())); + assert_eq!(p("10/"), Err(ErrorKind::InvalidDigit { got: b'/' }.into())); + assert_eq!( + p("18446744073709551616"), + Err(ErrorKind::NumberTooBig { + bytes: b"18446744073709551616".to_vec() + } + .into()) + ); + assert_eq!( + p("18446744073799551615abc"), + Err(ErrorKind::NumberTooBig { + bytes: b"18446744073799551615abc".to_vec() + } + .into()) + ); + assert_eq!( + parse_u64(b"18446744073799551615\xFF"), + Err(ErrorKind::NumberTooBig { + bytes: b"18446744073799551615\xFF".to_vec() + } + .into()) + ); +} + +/// Wraps a `Version` and provides a more "bloated" debug but standard +/// representation. +/// +/// We don't do this by default because it takes up a ton of space, and +/// just printing out the display version of the version is quite a bit +/// simpler. +/// +/// Nevertheless, when *testing* version parsing, you really want to +/// be able to peek at all of its constituent parts. So we use this in +/// assertion failure messages. +struct VersionBloatedDebug<'a>(&'a Version); + +impl<'a> std::fmt::Debug for VersionBloatedDebug<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Version") + .field("epoch", &self.0.epoch()) + .field("release", &self.0.release()) + .field("pre", &self.0.pre()) + .field("post", &self.0.post()) + .field("dev", &self.0.dev()) + .field("local", &self.0.local()) + .field("min", &self.0.min()) + .field("max", &self.0.max()) + .finish() + } +} + +impl Version { + pub(crate) fn as_bloated_debug(&self) -> impl std::fmt::Debug + '_ { + VersionBloatedDebug(self) + } +} diff --git a/crates/uv-pep440/src/version_specifier.rs b/crates/uv-pep440/src/version_specifier.rs index dfa123228..3af4f252f 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -802,959 +802,4 @@ pub(crate) fn parse_version_specifiers( } #[cfg(test)] -mod tests { - use std::{cmp::Ordering, str::FromStr}; - - use indoc::indoc; - - use crate::LocalSegment; - - use super::*; - - /// - #[test] - fn test_equal() { - let version = Version::from_str("1.1.post1").unwrap(); - - assert!(!VersionSpecifier::from_str("== 1.1") - .unwrap() - .contains(&version)); - assert!(VersionSpecifier::from_str("== 1.1.post1") - .unwrap() - .contains(&version)); - assert!(VersionSpecifier::from_str("== 1.1.*") - .unwrap() - .contains(&version)); - } - - const VERSIONS_ALL: &[&str] = &[ - // Implicit epoch of 0 - "1.0.dev456", - "1.0a1", - "1.0a2.dev456", - "1.0a12.dev456", - "1.0a12", - "1.0b1.dev456", - "1.0b2", - "1.0b2.post345.dev456", - "1.0b2.post345", - "1.0b2-346", - "1.0c1.dev456", - "1.0c1", - "1.0rc2", - "1.0c3", - "1.0", - "1.0.post456.dev34", - "1.0.post456", - "1.1.dev1", - "1.2+123abc", - "1.2+123abc456", - "1.2+abc", - "1.2+abc123", - "1.2+abc123def", - "1.2+1234.abc", - "1.2+123456", - "1.2.r32+123456", - "1.2.rev33+123456", - // Explicit epoch of 1 - "1!1.0.dev456", - "1!1.0a1", - "1!1.0a2.dev456", - "1!1.0a12.dev456", - "1!1.0a12", - "1!1.0b1.dev456", - "1!1.0b2", - "1!1.0b2.post345.dev456", - "1!1.0b2.post345", - "1!1.0b2-346", - "1!1.0c1.dev456", - "1!1.0c1", - "1!1.0rc2", - "1!1.0c3", - "1!1.0", - "1!1.0.post456.dev34", - "1!1.0.post456", - "1!1.1.dev1", - "1!1.2+123abc", - "1!1.2+123abc456", - "1!1.2+abc", - "1!1.2+abc123", - "1!1.2+abc123def", - "1!1.2+1234.abc", - "1!1.2+123456", - "1!1.2.r32+123456", - "1!1.2.rev33+123456", - ]; - - /// - /// - /// - /// These tests are a lot shorter than the pypa/packaging version since we implement all - /// comparisons through one method - #[test] - fn test_operators_true() { - let versions: Vec = VERSIONS_ALL - .iter() - .map(|version| Version::from_str(version).unwrap()) - .collect(); - - // Below we'll generate every possible combination of VERSIONS_ALL that - // should be true for the given operator - let operations = [ - // Verify that the less than (<) operator works correctly - versions - .iter() - .enumerate() - .flat_map(|(i, x)| { - versions[i + 1..] - .iter() - .map(move |y| (x, y, Ordering::Less)) - }) - .collect::>(), - // Verify that the equal (==) operator works correctly - versions - .iter() - .map(move |x| (x, x, Ordering::Equal)) - .collect::>(), - // Verify that the greater than (>) operator works correctly - versions - .iter() - .enumerate() - .flat_map(|(i, x)| versions[..i].iter().map(move |y| (x, y, Ordering::Greater))) - .collect::>(), - ] - .into_iter() - .flatten(); - - for (a, b, ordering) in operations { - assert_eq!(a.cmp(b), ordering, "{a} {ordering:?} {b}"); - } - } - - const VERSIONS_0: &[&str] = &[ - "1.0.dev456", - "1.0a1", - "1.0a2.dev456", - "1.0a12.dev456", - "1.0a12", - "1.0b1.dev456", - "1.0b2", - "1.0b2.post345.dev456", - "1.0b2.post345", - "1.0b2-346", - "1.0c1.dev456", - "1.0c1", - "1.0rc2", - "1.0c3", - "1.0", - "1.0.post456.dev34", - "1.0.post456", - "1.1.dev1", - "1.2+123abc", - "1.2+123abc456", - "1.2+abc", - "1.2+abc123", - "1.2+abc123def", - "1.2+1234.abc", - "1.2+123456", - "1.2.r32+123456", - "1.2.rev33+123456", - ]; - - const SPECIFIERS_OTHER: &[&str] = &[ - "== 1.*", "== 1.0.*", "== 1.1.*", "== 1.2.*", "== 2.*", "~= 1.0", "~= 1.0b1", "~= 1.1", - "~= 1.2", "~= 2.0", - ]; - - const EXPECTED_OTHER: &[[bool; 10]] = &[ - [ - true, true, false, false, false, false, false, false, false, false, - ], - [ - true, true, false, false, false, false, false, false, false, false, - ], - [ - true, true, false, false, false, false, false, false, false, false, - ], - [ - true, true, false, false, false, false, false, false, false, false, - ], - [ - true, true, false, false, false, false, false, false, false, false, - ], - [ - true, true, false, false, false, false, false, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, false, true, false, false, false, - ], - [ - true, true, false, false, false, true, true, false, false, false, - ], - [ - true, true, false, false, false, true, true, false, false, false, - ], - [ - true, true, false, false, false, true, true, false, false, false, - ], - [ - true, false, true, false, false, true, true, false, false, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - [ - true, false, false, true, false, true, true, true, true, false, - ], - ]; - - /// Test for tilde equal (~=) and star equal (== x.y.*) recorded from pypa/packaging - /// - /// Well, except for - #[test] - fn test_operators_other() { - let versions = VERSIONS_0 - .iter() - .map(|version| Version::from_str(version).unwrap()); - let specifiers: Vec<_> = SPECIFIERS_OTHER - .iter() - .map(|specifier| VersionSpecifier::from_str(specifier).unwrap()) - .collect(); - - for (version, expected) in versions.zip(EXPECTED_OTHER) { - let actual = specifiers - .iter() - .map(|specifier| specifier.contains(&version)); - for ((actual, expected), _specifier) in actual.zip(expected).zip(SPECIFIERS_OTHER) { - assert_eq!(actual, *expected); - } - } - } - - #[test] - fn test_arbitrary_equality() { - assert!(VersionSpecifier::from_str("=== 1.2a1") - .unwrap() - .contains(&Version::from_str("1.2a1").unwrap())); - assert!(!VersionSpecifier::from_str("=== 1.2a1") - .unwrap() - .contains(&Version::from_str("1.2a1+local").unwrap())); - } - - #[test] - fn test_specifiers_true() { - let pairs = [ - // Test the equality operation - ("2.0", "==2"), - ("2.0", "==2.0"), - ("2.0", "==2.0.0"), - ("2.0+deadbeef", "==2"), - ("2.0+deadbeef", "==2.0"), - ("2.0+deadbeef", "==2.0.0"), - ("2.0+deadbeef", "==2+deadbeef"), - ("2.0+deadbeef", "==2.0+deadbeef"), - ("2.0+deadbeef", "==2.0.0+deadbeef"), - ("2.0+deadbeef.0", "==2.0.0+deadbeef.00"), - // Test the equality operation with a prefix - ("2.dev1", "==2.*"), - ("2a1", "==2.*"), - ("2a1.post1", "==2.*"), - ("2b1", "==2.*"), - ("2b1.dev1", "==2.*"), - ("2c1", "==2.*"), - ("2c1.post1.dev1", "==2.*"), - ("2c1.post1.dev1", "==2.0.*"), - ("2rc1", "==2.*"), - ("2rc1", "==2.0.*"), - ("2", "==2.*"), - ("2", "==2.0.*"), - ("2", "==0!2.*"), - ("0!2", "==2.*"), - ("2.0", "==2.*"), - ("2.0.0", "==2.*"), - ("2.1+local.version", "==2.1.*"), - // Test the in-equality operation - ("2.1", "!=2"), - ("2.1", "!=2.0"), - ("2.0.1", "!=2"), - ("2.0.1", "!=2.0"), - ("2.0.1", "!=2.0.0"), - ("2.0", "!=2.0+deadbeef"), - // Test the in-equality operation with a prefix - ("2.0", "!=3.*"), - ("2.1", "!=2.0.*"), - // Test the greater than equal operation - ("2.0", ">=2"), - ("2.0", ">=2.0"), - ("2.0", ">=2.0.0"), - ("2.0.post1", ">=2"), - ("2.0.post1.dev1", ">=2"), - ("3", ">=2"), - // Test the less than equal operation - ("2.0", "<=2"), - ("2.0", "<=2.0"), - ("2.0", "<=2.0.0"), - ("2.0.dev1", "<=2"), - ("2.0a1", "<=2"), - ("2.0a1.dev1", "<=2"), - ("2.0b1", "<=2"), - ("2.0b1.post1", "<=2"), - ("2.0c1", "<=2"), - ("2.0c1.post1.dev1", "<=2"), - ("2.0rc1", "<=2"), - ("1", "<=2"), - // Test the greater than operation - ("3", ">2"), - ("2.1", ">2.0"), - ("2.0.1", ">2"), - ("2.1.post1", ">2"), - ("2.1+local.version", ">2"), - // Test the less than operation - ("1", "<2"), - ("2.0", "<2.1"), - ("2.0.dev0", "<2.1"), - // Test the compatibility operation - ("1", "~=1.0"), - ("1.0.1", "~=1.0"), - ("1.1", "~=1.0"), - ("1.9999999", "~=1.0"), - ("1.1", "~=1.0a1"), - ("2022.01.01", "~=2022.01.01"), - // Test that epochs are handled sanely - ("2!1.0", "~=2!1.0"), - ("2!1.0", "==2!1.*"), - ("2!1.0", "==2!1.0"), - ("2!1.0", "!=1.0"), - ("1.0", "!=2!1.0"), - ("1.0", "<=2!0.1"), - ("2!1.0", ">=2.0"), - ("1.0", "<2!0.1"), - ("2!1.0", ">2.0"), - // Test some normalization rules - ("2.0.5", ">2.0dev"), - ]; - - for (s_version, s_spec) in pairs { - let version = s_version.parse::().unwrap(); - let spec = s_spec.parse::().unwrap(); - assert!( - spec.contains(&version), - "{s_version} {s_spec}\nversion repr: {:?}\nspec version repr: {:?}", - version.as_bloated_debug(), - spec.version.as_bloated_debug(), - ); - } - } - - #[test] - fn test_specifier_false() { - let pairs = [ - // Test the equality operation - ("2.1", "==2"), - ("2.1", "==2.0"), - ("2.1", "==2.0.0"), - ("2.0", "==2.0+deadbeef"), - // Test the equality operation with a prefix - ("2.0", "==3.*"), - ("2.1", "==2.0.*"), - // Test the in-equality operation - ("2.0", "!=2"), - ("2.0", "!=2.0"), - ("2.0", "!=2.0.0"), - ("2.0+deadbeef", "!=2"), - ("2.0+deadbeef", "!=2.0"), - ("2.0+deadbeef", "!=2.0.0"), - ("2.0+deadbeef", "!=2+deadbeef"), - ("2.0+deadbeef", "!=2.0+deadbeef"), - ("2.0+deadbeef", "!=2.0.0+deadbeef"), - ("2.0+deadbeef.0", "!=2.0.0+deadbeef.00"), - // Test the in-equality operation with a prefix - ("2.dev1", "!=2.*"), - ("2a1", "!=2.*"), - ("2a1.post1", "!=2.*"), - ("2b1", "!=2.*"), - ("2b1.dev1", "!=2.*"), - ("2c1", "!=2.*"), - ("2c1.post1.dev1", "!=2.*"), - ("2c1.post1.dev1", "!=2.0.*"), - ("2rc1", "!=2.*"), - ("2rc1", "!=2.0.*"), - ("2", "!=2.*"), - ("2", "!=2.0.*"), - ("2.0", "!=2.*"), - ("2.0.0", "!=2.*"), - // Test the greater than equal operation - ("2.0.dev1", ">=2"), - ("2.0a1", ">=2"), - ("2.0a1.dev1", ">=2"), - ("2.0b1", ">=2"), - ("2.0b1.post1", ">=2"), - ("2.0c1", ">=2"), - ("2.0c1.post1.dev1", ">=2"), - ("2.0rc1", ">=2"), - ("1", ">=2"), - // Test the less than equal operation - ("2.0.post1", "<=2"), - ("2.0.post1.dev1", "<=2"), - ("3", "<=2"), - // Test the greater than operation - ("1", ">2"), - ("2.0.dev1", ">2"), - ("2.0a1", ">2"), - ("2.0a1.post1", ">2"), - ("2.0b1", ">2"), - ("2.0b1.dev1", ">2"), - ("2.0c1", ">2"), - ("2.0c1.post1.dev1", ">2"), - ("2.0rc1", ">2"), - ("2.0", ">2"), - ("2.0.post1", ">2"), - ("2.0.post1.dev1", ">2"), - ("2.0+local.version", ">2"), - // Test the less than operation - ("2.0.dev1", "<2"), - ("2.0a1", "<2"), - ("2.0a1.post1", "<2"), - ("2.0b1", "<2"), - ("2.0b2.dev1", "<2"), - ("2.0c1", "<2"), - ("2.0c1.post1.dev1", "<2"), - ("2.0rc1", "<2"), - ("2.0", "<2"), - ("2.post1", "<2"), - ("2.post1.dev1", "<2"), - ("3", "<2"), - // Test the compatibility operation - ("2.0", "~=1.0"), - ("1.1.0", "~=1.0.0"), - ("1.1.post1", "~=1.0.0"), - // Test that epochs are handled sanely - ("1.0", "~=2!1.0"), - ("2!1.0", "~=1.0"), - ("2!1.0", "==1.0"), - ("1.0", "==2!1.0"), - ("2!1.0", "==1.*"), - ("1.0", "==2!1.*"), - ("2!1.0", "!=2!1.0"), - ]; - for (version, specifier) in pairs { - assert!( - !VersionSpecifier::from_str(specifier) - .unwrap() - .contains(&Version::from_str(version).unwrap()), - "{version} {specifier}" - ); - } - } - - #[test] - fn test_parse_version_specifiers() { - let result = VersionSpecifiers::from_str("~= 0.9, >= 1.0, != 1.3.4.*, < 2.0").unwrap(); - assert_eq!( - result.0, - [ - VersionSpecifier { - operator: Operator::TildeEqual, - version: Version::new([0, 9]), - }, - VersionSpecifier { - operator: Operator::GreaterThanEqual, - version: Version::new([1, 0]), - }, - VersionSpecifier { - operator: Operator::NotEqualStar, - version: Version::new([1, 3, 4]), - }, - VersionSpecifier { - operator: Operator::LessThan, - version: Version::new([2, 0]), - } - ] - ); - } - - #[test] - fn test_parse_error() { - let result = VersionSpecifiers::from_str("~= 0.9, %‍= 1.0, != 1.3.4.*"); - assert_eq!( - result.unwrap_err().to_string(), - indoc! {r" - Failed to parse version: Unexpected end of version specifier, expected operator: - ~= 0.9, %‍= 1.0, != 1.3.4.* - ^^^^^^^ - "} - ); - } - - #[test] - fn test_non_star_after_star() { - let result = VersionSpecifiers::from_str("== 0.9.*.1"); - assert_eq!( - result.unwrap_err().inner.err, - ParseErrorKind::InvalidVersion(version::PatternErrorKind::WildcardNotTrailing.into()) - .into(), - ); - } - - #[test] - fn test_star_wrong_operator() { - let result = VersionSpecifiers::from_str(">= 0.9.1.*"); - assert_eq!( - result.unwrap_err().inner.err, - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorWithStar { - operator: Operator::GreaterThanEqual, - } - .into() - ) - .into(), - ); - } - - #[test] - fn test_invalid_word() { - let result = VersionSpecifiers::from_str("blergh"); - assert_eq!( - result.unwrap_err().inner.err, - ParseErrorKind::MissingOperator.into(), - ); - } - - /// - #[test] - fn test_invalid_specifier() { - let specifiers = [ - // Operator-less specifier - ("2.0", ParseErrorKind::MissingOperator.into()), - // Invalid operator - ( - "=>2.0", - ParseErrorKind::InvalidOperator(OperatorParseError { - got: "=>".to_string(), - }) - .into(), - ), - // Version-less specifier - ("==", ParseErrorKind::MissingVersion.into()), - // Local segment on operators which don't support them - ( - "~=1.0+5", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorLocalCombo { - operator: Operator::TildeEqual, - version: Version::new([1, 0]).with_local(vec![LocalSegment::Number(5)]), - } - .into(), - ) - .into(), - ), - ( - ">=1.0+deadbeef", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorLocalCombo { - operator: Operator::GreaterThanEqual, - version: Version::new([1, 0]) - .with_local(vec![LocalSegment::String("deadbeef".to_string())]), - } - .into(), - ) - .into(), - ), - ( - "<=1.0+abc123", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorLocalCombo { - operator: Operator::LessThanEqual, - version: Version::new([1, 0]) - .with_local(vec![LocalSegment::String("abc123".to_string())]), - } - .into(), - ) - .into(), - ), - ( - ">1.0+watwat", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorLocalCombo { - operator: Operator::GreaterThan, - version: Version::new([1, 0]) - .with_local(vec![LocalSegment::String("watwat".to_string())]), - } - .into(), - ) - .into(), - ), - ( - "<1.0+1.0", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorLocalCombo { - operator: Operator::LessThan, - version: Version::new([1, 0]) - .with_local(vec![LocalSegment::Number(1), LocalSegment::Number(0)]), - } - .into(), - ) - .into(), - ), - // Prefix matching on operators which don't support them - ( - "~=1.0.*", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorWithStar { - operator: Operator::TildeEqual, - } - .into(), - ) - .into(), - ), - ( - ">=1.0.*", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorWithStar { - operator: Operator::GreaterThanEqual, - } - .into(), - ) - .into(), - ), - ( - "<=1.0.*", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorWithStar { - operator: Operator::LessThanEqual, - } - .into(), - ) - .into(), - ), - ( - ">1.0.*", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorWithStar { - operator: Operator::GreaterThan, - } - .into(), - ) - .into(), - ), - ( - "<1.0.*", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorWithStar { - operator: Operator::LessThan, - } - .into(), - ) - .into(), - ), - // Combination of local and prefix matching on operators which do - // support one or the other - ( - "==1.0.*+5", - ParseErrorKind::InvalidVersion( - version::PatternErrorKind::WildcardNotTrailing.into(), - ) - .into(), - ), - ( - "!=1.0.*+deadbeef", - ParseErrorKind::InvalidVersion( - version::PatternErrorKind::WildcardNotTrailing.into(), - ) - .into(), - ), - // Prefix matching cannot be used with a pre-release, post-release, - // dev or local version - ( - "==2.0a1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "2.0a1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "!=2.0a1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "2.0a1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "==2.0.post1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "2.0.post1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "!=2.0.post1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "2.0.post1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "==2.0.dev1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "2.0.dev1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "!=2.0.dev1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "2.0.dev1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "==1.0+5.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::LocalEmpty { precursor: '.' }.into(), - ) - .into(), - ), - ( - "!=1.0+deadbeef.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::LocalEmpty { precursor: '.' }.into(), - ) - .into(), - ), - // Prefix matching must appear at the end - ( - "==1.0.*.5", - ParseErrorKind::InvalidVersion( - version::PatternErrorKind::WildcardNotTrailing.into(), - ) - .into(), - ), - // Compatible operator requires 2 digits in the release operator - ( - "~=1", - ParseErrorKind::InvalidSpecifier(BuildErrorKind::CompatibleRelease.into()).into(), - ), - // Cannot use a prefix matching after a .devN version - ( - "==1.0.dev1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "1.0.dev1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ( - "!=1.0.dev1.*", - ParseErrorKind::InvalidVersion( - version::ErrorKind::UnexpectedEnd { - version: "1.0.dev1".to_string(), - remaining: ".*".to_string(), - } - .into(), - ) - .into(), - ), - ]; - for (specifier, error) in specifiers { - assert_eq!(VersionSpecifier::from_str(specifier).unwrap_err(), error); - } - } - - #[test] - fn test_display_start() { - assert_eq!( - VersionSpecifier::from_str("== 1.1.*") - .unwrap() - .to_string(), - "==1.1.*" - ); - assert_eq!( - VersionSpecifier::from_str("!= 1.1.*") - .unwrap() - .to_string(), - "!=1.1.*" - ); - } - - #[test] - fn test_version_specifiers_str() { - assert_eq!( - VersionSpecifiers::from_str(">= 3.7").unwrap().to_string(), - ">=3.7" - ); - assert_eq!( - VersionSpecifiers::from_str(">=3.7, < 4.0, != 3.9.0") - .unwrap() - .to_string(), - ">=3.7, !=3.9.0, <4.0" - ); - } - - /// These occur in the simple api, e.g. - /// - #[test] - fn test_version_specifiers_empty() { - assert_eq!(VersionSpecifiers::from_str("").unwrap().to_string(), ""); - } - - /// All non-ASCII version specifiers are invalid, but the user can still - /// attempt to parse a non-ASCII string as a version specifier. This - /// ensures no panics occur and that the error reported has correct info. - #[test] - fn non_ascii_version_specifier() { - let s = "💩"; - let err = s.parse::().unwrap_err(); - assert_eq!(err.inner.start, 0); - assert_eq!(err.inner.end, 4); - - // The first test here is plain ASCII and it gives the - // expected result: the error starts at codepoint 12, - // which is the start of `>5.%`. - let s = ">=3.7, <4.0,>5.%"; - let err = s.parse::().unwrap_err(); - assert_eq!(err.inner.start, 12); - assert_eq!(err.inner.end, 16); - // In this case, we replace a single ASCII codepoint - // with U+3000 IDEOGRAPHIC SPACE. Its *visual* width is - // 2 despite it being a single codepoint. This causes - // the offsets in the error reporting logic to become - // incorrect. - // - // ... it did. This bug was fixed by switching to byte - // offsets. - let s = ">=3.7,\u{3000}<4.0,>5.%"; - let err = s.parse::().unwrap_err(); - assert_eq!(err.inner.start, 14); - assert_eq!(err.inner.end, 18); - } - - /// Tests the human readable error messages generated from an invalid - /// sequence of version specifiers. - #[test] - fn error_message_version_specifiers_parse_error() { - let specs = ">=1.2.3, 5.4.3, >=3.4.5"; - let err = VersionSpecifierParseError { - kind: Box::new(ParseErrorKind::MissingOperator), - }; - let inner = Box::new(VersionSpecifiersParseErrorInner { - err, - line: specs.to_string(), - start: 8, - end: 14, - }); - let err = VersionSpecifiersParseError { inner }; - assert_eq!(err, VersionSpecifiers::from_str(specs).unwrap_err()); - assert_eq!( - err.to_string(), - "\ -Failed to parse version: Unexpected end of version specifier, expected operator: ->=1.2.3, 5.4.3, >=3.4.5 - ^^^^^^ -" - ); - } - - /// Tests the human readable error messages generated when building an - /// invalid version specifier. - #[test] - fn error_message_version_specifier_build_error() { - let err = VersionSpecifierBuildError { - kind: Box::new(BuildErrorKind::CompatibleRelease), - }; - let op = Operator::TildeEqual; - let v = Version::new([5]); - let vpat = VersionPattern::verbatim(v); - assert_eq!(err, VersionSpecifier::from_pattern(op, vpat).unwrap_err()); - assert_eq!( - err.to_string(), - "The ~= operator requires at least two segments in the release version" - ); - } - - /// Tests the human readable error messages generated from parsing invalid - /// version specifier. - #[test] - fn error_message_version_specifier_parse_error() { - let err = VersionSpecifierParseError { - kind: Box::new(ParseErrorKind::InvalidSpecifier( - VersionSpecifierBuildError { - kind: Box::new(BuildErrorKind::CompatibleRelease), - }, - )), - }; - assert_eq!(err, VersionSpecifier::from_str("~=5").unwrap_err()); - assert_eq!( - err.to_string(), - "The ~= operator requires at least two segments in the release version" - ); - } -} +mod tests; diff --git a/crates/uv-pep440/src/version_specifier/tests.rs b/crates/uv-pep440/src/version_specifier/tests.rs new file mode 100644 index 000000000..f853aab28 --- /dev/null +++ b/crates/uv-pep440/src/version_specifier/tests.rs @@ -0,0 +1,948 @@ +use std::{cmp::Ordering, str::FromStr}; + +use indoc::indoc; + +use crate::LocalSegment; + +use super::*; + +/// +#[test] +fn test_equal() { + let version = Version::from_str("1.1.post1").unwrap(); + + assert!(!VersionSpecifier::from_str("== 1.1") + .unwrap() + .contains(&version)); + assert!(VersionSpecifier::from_str("== 1.1.post1") + .unwrap() + .contains(&version)); + assert!(VersionSpecifier::from_str("== 1.1.*") + .unwrap() + .contains(&version)); +} + +const VERSIONS_ALL: &[&str] = &[ + // Implicit epoch of 0 + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345", + "1.0b2-346", + "1.0c1.dev456", + "1.0c1", + "1.0rc2", + "1.0c3", + "1.0", + "1.0.post456.dev34", + "1.0.post456", + "1.1.dev1", + "1.2+123abc", + "1.2+123abc456", + "1.2+abc", + "1.2+abc123", + "1.2+abc123def", + "1.2+1234.abc", + "1.2+123456", + "1.2.r32+123456", + "1.2.rev33+123456", + // Explicit epoch of 1 + "1!1.0.dev456", + "1!1.0a1", + "1!1.0a2.dev456", + "1!1.0a12.dev456", + "1!1.0a12", + "1!1.0b1.dev456", + "1!1.0b2", + "1!1.0b2.post345.dev456", + "1!1.0b2.post345", + "1!1.0b2-346", + "1!1.0c1.dev456", + "1!1.0c1", + "1!1.0rc2", + "1!1.0c3", + "1!1.0", + "1!1.0.post456.dev34", + "1!1.0.post456", + "1!1.1.dev1", + "1!1.2+123abc", + "1!1.2+123abc456", + "1!1.2+abc", + "1!1.2+abc123", + "1!1.2+abc123def", + "1!1.2+1234.abc", + "1!1.2+123456", + "1!1.2.r32+123456", + "1!1.2.rev33+123456", +]; + +/// +/// +/// +/// These tests are a lot shorter than the pypa/packaging version since we implement all +/// comparisons through one method +#[test] +fn test_operators_true() { + let versions: Vec = VERSIONS_ALL + .iter() + .map(|version| Version::from_str(version).unwrap()) + .collect(); + + // Below we'll generate every possible combination of VERSIONS_ALL that + // should be true for the given operator + let operations = [ + // Verify that the less than (<) operator works correctly + versions + .iter() + .enumerate() + .flat_map(|(i, x)| { + versions[i + 1..] + .iter() + .map(move |y| (x, y, Ordering::Less)) + }) + .collect::>(), + // Verify that the equal (==) operator works correctly + versions + .iter() + .map(move |x| (x, x, Ordering::Equal)) + .collect::>(), + // Verify that the greater than (>) operator works correctly + versions + .iter() + .enumerate() + .flat_map(|(i, x)| versions[..i].iter().map(move |y| (x, y, Ordering::Greater))) + .collect::>(), + ] + .into_iter() + .flatten(); + + for (a, b, ordering) in operations { + assert_eq!(a.cmp(b), ordering, "{a} {ordering:?} {b}"); + } +} + +const VERSIONS_0: &[&str] = &[ + "1.0.dev456", + "1.0a1", + "1.0a2.dev456", + "1.0a12.dev456", + "1.0a12", + "1.0b1.dev456", + "1.0b2", + "1.0b2.post345.dev456", + "1.0b2.post345", + "1.0b2-346", + "1.0c1.dev456", + "1.0c1", + "1.0rc2", + "1.0c3", + "1.0", + "1.0.post456.dev34", + "1.0.post456", + "1.1.dev1", + "1.2+123abc", + "1.2+123abc456", + "1.2+abc", + "1.2+abc123", + "1.2+abc123def", + "1.2+1234.abc", + "1.2+123456", + "1.2.r32+123456", + "1.2.rev33+123456", +]; + +const SPECIFIERS_OTHER: &[&str] = &[ + "== 1.*", "== 1.0.*", "== 1.1.*", "== 1.2.*", "== 2.*", "~= 1.0", "~= 1.0b1", "~= 1.1", + "~= 1.2", "~= 2.0", +]; + +const EXPECTED_OTHER: &[[bool; 10]] = &[ + [ + true, true, false, false, false, false, false, false, false, false, + ], + [ + true, true, false, false, false, false, false, false, false, false, + ], + [ + true, true, false, false, false, false, false, false, false, false, + ], + [ + true, true, false, false, false, false, false, false, false, false, + ], + [ + true, true, false, false, false, false, false, false, false, false, + ], + [ + true, true, false, false, false, false, false, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, false, true, false, false, false, + ], + [ + true, true, false, false, false, true, true, false, false, false, + ], + [ + true, true, false, false, false, true, true, false, false, false, + ], + [ + true, true, false, false, false, true, true, false, false, false, + ], + [ + true, false, true, false, false, true, true, false, false, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], + [ + true, false, false, true, false, true, true, true, true, false, + ], +]; + +/// Test for tilde equal (~=) and star equal (== x.y.*) recorded from pypa/packaging +/// +/// Well, except for +#[test] +fn test_operators_other() { + let versions = VERSIONS_0 + .iter() + .map(|version| Version::from_str(version).unwrap()); + let specifiers: Vec<_> = SPECIFIERS_OTHER + .iter() + .map(|specifier| VersionSpecifier::from_str(specifier).unwrap()) + .collect(); + + for (version, expected) in versions.zip(EXPECTED_OTHER) { + let actual = specifiers + .iter() + .map(|specifier| specifier.contains(&version)); + for ((actual, expected), _specifier) in actual.zip(expected).zip(SPECIFIERS_OTHER) { + assert_eq!(actual, *expected); + } + } +} + +#[test] +fn test_arbitrary_equality() { + assert!(VersionSpecifier::from_str("=== 1.2a1") + .unwrap() + .contains(&Version::from_str("1.2a1").unwrap())); + assert!(!VersionSpecifier::from_str("=== 1.2a1") + .unwrap() + .contains(&Version::from_str("1.2a1+local").unwrap())); +} + +#[test] +fn test_specifiers_true() { + let pairs = [ + // Test the equality operation + ("2.0", "==2"), + ("2.0", "==2.0"), + ("2.0", "==2.0.0"), + ("2.0+deadbeef", "==2"), + ("2.0+deadbeef", "==2.0"), + ("2.0+deadbeef", "==2.0.0"), + ("2.0+deadbeef", "==2+deadbeef"), + ("2.0+deadbeef", "==2.0+deadbeef"), + ("2.0+deadbeef", "==2.0.0+deadbeef"), + ("2.0+deadbeef.0", "==2.0.0+deadbeef.00"), + // Test the equality operation with a prefix + ("2.dev1", "==2.*"), + ("2a1", "==2.*"), + ("2a1.post1", "==2.*"), + ("2b1", "==2.*"), + ("2b1.dev1", "==2.*"), + ("2c1", "==2.*"), + ("2c1.post1.dev1", "==2.*"), + ("2c1.post1.dev1", "==2.0.*"), + ("2rc1", "==2.*"), + ("2rc1", "==2.0.*"), + ("2", "==2.*"), + ("2", "==2.0.*"), + ("2", "==0!2.*"), + ("0!2", "==2.*"), + ("2.0", "==2.*"), + ("2.0.0", "==2.*"), + ("2.1+local.version", "==2.1.*"), + // Test the in-equality operation + ("2.1", "!=2"), + ("2.1", "!=2.0"), + ("2.0.1", "!=2"), + ("2.0.1", "!=2.0"), + ("2.0.1", "!=2.0.0"), + ("2.0", "!=2.0+deadbeef"), + // Test the in-equality operation with a prefix + ("2.0", "!=3.*"), + ("2.1", "!=2.0.*"), + // Test the greater than equal operation + ("2.0", ">=2"), + ("2.0", ">=2.0"), + ("2.0", ">=2.0.0"), + ("2.0.post1", ">=2"), + ("2.0.post1.dev1", ">=2"), + ("3", ">=2"), + // Test the less than equal operation + ("2.0", "<=2"), + ("2.0", "<=2.0"), + ("2.0", "<=2.0.0"), + ("2.0.dev1", "<=2"), + ("2.0a1", "<=2"), + ("2.0a1.dev1", "<=2"), + ("2.0b1", "<=2"), + ("2.0b1.post1", "<=2"), + ("2.0c1", "<=2"), + ("2.0c1.post1.dev1", "<=2"), + ("2.0rc1", "<=2"), + ("1", "<=2"), + // Test the greater than operation + ("3", ">2"), + ("2.1", ">2.0"), + ("2.0.1", ">2"), + ("2.1.post1", ">2"), + ("2.1+local.version", ">2"), + // Test the less than operation + ("1", "<2"), + ("2.0", "<2.1"), + ("2.0.dev0", "<2.1"), + // Test the compatibility operation + ("1", "~=1.0"), + ("1.0.1", "~=1.0"), + ("1.1", "~=1.0"), + ("1.9999999", "~=1.0"), + ("1.1", "~=1.0a1"), + ("2022.01.01", "~=2022.01.01"), + // Test that epochs are handled sanely + ("2!1.0", "~=2!1.0"), + ("2!1.0", "==2!1.*"), + ("2!1.0", "==2!1.0"), + ("2!1.0", "!=1.0"), + ("1.0", "!=2!1.0"), + ("1.0", "<=2!0.1"), + ("2!1.0", ">=2.0"), + ("1.0", "<2!0.1"), + ("2!1.0", ">2.0"), + // Test some normalization rules + ("2.0.5", ">2.0dev"), + ]; + + for (s_version, s_spec) in pairs { + let version = s_version.parse::().unwrap(); + let spec = s_spec.parse::().unwrap(); + assert!( + spec.contains(&version), + "{s_version} {s_spec}\nversion repr: {:?}\nspec version repr: {:?}", + version.as_bloated_debug(), + spec.version.as_bloated_debug(), + ); + } +} + +#[test] +fn test_specifier_false() { + let pairs = [ + // Test the equality operation + ("2.1", "==2"), + ("2.1", "==2.0"), + ("2.1", "==2.0.0"), + ("2.0", "==2.0+deadbeef"), + // Test the equality operation with a prefix + ("2.0", "==3.*"), + ("2.1", "==2.0.*"), + // Test the in-equality operation + ("2.0", "!=2"), + ("2.0", "!=2.0"), + ("2.0", "!=2.0.0"), + ("2.0+deadbeef", "!=2"), + ("2.0+deadbeef", "!=2.0"), + ("2.0+deadbeef", "!=2.0.0"), + ("2.0+deadbeef", "!=2+deadbeef"), + ("2.0+deadbeef", "!=2.0+deadbeef"), + ("2.0+deadbeef", "!=2.0.0+deadbeef"), + ("2.0+deadbeef.0", "!=2.0.0+deadbeef.00"), + // Test the in-equality operation with a prefix + ("2.dev1", "!=2.*"), + ("2a1", "!=2.*"), + ("2a1.post1", "!=2.*"), + ("2b1", "!=2.*"), + ("2b1.dev1", "!=2.*"), + ("2c1", "!=2.*"), + ("2c1.post1.dev1", "!=2.*"), + ("2c1.post1.dev1", "!=2.0.*"), + ("2rc1", "!=2.*"), + ("2rc1", "!=2.0.*"), + ("2", "!=2.*"), + ("2", "!=2.0.*"), + ("2.0", "!=2.*"), + ("2.0.0", "!=2.*"), + // Test the greater than equal operation + ("2.0.dev1", ">=2"), + ("2.0a1", ">=2"), + ("2.0a1.dev1", ">=2"), + ("2.0b1", ">=2"), + ("2.0b1.post1", ">=2"), + ("2.0c1", ">=2"), + ("2.0c1.post1.dev1", ">=2"), + ("2.0rc1", ">=2"), + ("1", ">=2"), + // Test the less than equal operation + ("2.0.post1", "<=2"), + ("2.0.post1.dev1", "<=2"), + ("3", "<=2"), + // Test the greater than operation + ("1", ">2"), + ("2.0.dev1", ">2"), + ("2.0a1", ">2"), + ("2.0a1.post1", ">2"), + ("2.0b1", ">2"), + ("2.0b1.dev1", ">2"), + ("2.0c1", ">2"), + ("2.0c1.post1.dev1", ">2"), + ("2.0rc1", ">2"), + ("2.0", ">2"), + ("2.0.post1", ">2"), + ("2.0.post1.dev1", ">2"), + ("2.0+local.version", ">2"), + // Test the less than operation + ("2.0.dev1", "<2"), + ("2.0a1", "<2"), + ("2.0a1.post1", "<2"), + ("2.0b1", "<2"), + ("2.0b2.dev1", "<2"), + ("2.0c1", "<2"), + ("2.0c1.post1.dev1", "<2"), + ("2.0rc1", "<2"), + ("2.0", "<2"), + ("2.post1", "<2"), + ("2.post1.dev1", "<2"), + ("3", "<2"), + // Test the compatibility operation + ("2.0", "~=1.0"), + ("1.1.0", "~=1.0.0"), + ("1.1.post1", "~=1.0.0"), + // Test that epochs are handled sanely + ("1.0", "~=2!1.0"), + ("2!1.0", "~=1.0"), + ("2!1.0", "==1.0"), + ("1.0", "==2!1.0"), + ("2!1.0", "==1.*"), + ("1.0", "==2!1.*"), + ("2!1.0", "!=2!1.0"), + ]; + for (version, specifier) in pairs { + assert!( + !VersionSpecifier::from_str(specifier) + .unwrap() + .contains(&Version::from_str(version).unwrap()), + "{version} {specifier}" + ); + } +} + +#[test] +fn test_parse_version_specifiers() { + let result = VersionSpecifiers::from_str("~= 0.9, >= 1.0, != 1.3.4.*, < 2.0").unwrap(); + assert_eq!( + result.0, + [ + VersionSpecifier { + operator: Operator::TildeEqual, + version: Version::new([0, 9]), + }, + VersionSpecifier { + operator: Operator::GreaterThanEqual, + version: Version::new([1, 0]), + }, + VersionSpecifier { + operator: Operator::NotEqualStar, + version: Version::new([1, 3, 4]), + }, + VersionSpecifier { + operator: Operator::LessThan, + version: Version::new([2, 0]), + } + ] + ); +} + +#[test] +fn test_parse_error() { + let result = VersionSpecifiers::from_str("~= 0.9, %‍= 1.0, != 1.3.4.*"); + assert_eq!( + result.unwrap_err().to_string(), + indoc! {r" + Failed to parse version: Unexpected end of version specifier, expected operator: + ~= 0.9, %‍= 1.0, != 1.3.4.* + ^^^^^^^ + "} + ); +} + +#[test] +fn test_non_star_after_star() { + let result = VersionSpecifiers::from_str("== 0.9.*.1"); + assert_eq!( + result.unwrap_err().inner.err, + ParseErrorKind::InvalidVersion(version::PatternErrorKind::WildcardNotTrailing.into()) + .into(), + ); +} + +#[test] +fn test_star_wrong_operator() { + let result = VersionSpecifiers::from_str(">= 0.9.1.*"); + assert_eq!( + result.unwrap_err().inner.err, + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorWithStar { + operator: Operator::GreaterThanEqual, + } + .into() + ) + .into(), + ); +} + +#[test] +fn test_invalid_word() { + let result = VersionSpecifiers::from_str("blergh"); + assert_eq!( + result.unwrap_err().inner.err, + ParseErrorKind::MissingOperator.into(), + ); +} + +/// +#[test] +fn test_invalid_specifier() { + let specifiers = [ + // Operator-less specifier + ("2.0", ParseErrorKind::MissingOperator.into()), + // Invalid operator + ( + "=>2.0", + ParseErrorKind::InvalidOperator(OperatorParseError { + got: "=>".to_string(), + }) + .into(), + ), + // Version-less specifier + ("==", ParseErrorKind::MissingVersion.into()), + // Local segment on operators which don't support them + ( + "~=1.0+5", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorLocalCombo { + operator: Operator::TildeEqual, + version: Version::new([1, 0]).with_local(vec![LocalSegment::Number(5)]), + } + .into(), + ) + .into(), + ), + ( + ">=1.0+deadbeef", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorLocalCombo { + operator: Operator::GreaterThanEqual, + version: Version::new([1, 0]) + .with_local(vec![LocalSegment::String("deadbeef".to_string())]), + } + .into(), + ) + .into(), + ), + ( + "<=1.0+abc123", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorLocalCombo { + operator: Operator::LessThanEqual, + version: Version::new([1, 0]) + .with_local(vec![LocalSegment::String("abc123".to_string())]), + } + .into(), + ) + .into(), + ), + ( + ">1.0+watwat", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorLocalCombo { + operator: Operator::GreaterThan, + version: Version::new([1, 0]) + .with_local(vec![LocalSegment::String("watwat".to_string())]), + } + .into(), + ) + .into(), + ), + ( + "<1.0+1.0", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorLocalCombo { + operator: Operator::LessThan, + version: Version::new([1, 0]) + .with_local(vec![LocalSegment::Number(1), LocalSegment::Number(0)]), + } + .into(), + ) + .into(), + ), + // Prefix matching on operators which don't support them + ( + "~=1.0.*", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorWithStar { + operator: Operator::TildeEqual, + } + .into(), + ) + .into(), + ), + ( + ">=1.0.*", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorWithStar { + operator: Operator::GreaterThanEqual, + } + .into(), + ) + .into(), + ), + ( + "<=1.0.*", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorWithStar { + operator: Operator::LessThanEqual, + } + .into(), + ) + .into(), + ), + ( + ">1.0.*", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorWithStar { + operator: Operator::GreaterThan, + } + .into(), + ) + .into(), + ), + ( + "<1.0.*", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorWithStar { + operator: Operator::LessThan, + } + .into(), + ) + .into(), + ), + // Combination of local and prefix matching on operators which do + // support one or the other + ( + "==1.0.*+5", + ParseErrorKind::InvalidVersion(version::PatternErrorKind::WildcardNotTrailing.into()) + .into(), + ), + ( + "!=1.0.*+deadbeef", + ParseErrorKind::InvalidVersion(version::PatternErrorKind::WildcardNotTrailing.into()) + .into(), + ), + // Prefix matching cannot be used with a pre-release, post-release, + // dev or local version + ( + "==2.0a1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "2.0a1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "!=2.0a1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "2.0a1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "==2.0.post1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "2.0.post1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "!=2.0.post1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "2.0.post1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "==2.0.dev1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "2.0.dev1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "!=2.0.dev1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "2.0.dev1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "==1.0+5.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::LocalEmpty { precursor: '.' }.into(), + ) + .into(), + ), + ( + "!=1.0+deadbeef.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::LocalEmpty { precursor: '.' }.into(), + ) + .into(), + ), + // Prefix matching must appear at the end + ( + "==1.0.*.5", + ParseErrorKind::InvalidVersion(version::PatternErrorKind::WildcardNotTrailing.into()) + .into(), + ), + // Compatible operator requires 2 digits in the release operator + ( + "~=1", + ParseErrorKind::InvalidSpecifier(BuildErrorKind::CompatibleRelease.into()).into(), + ), + // Cannot use a prefix matching after a .devN version + ( + "==1.0.dev1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "1.0.dev1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ( + "!=1.0.dev1.*", + ParseErrorKind::InvalidVersion( + version::ErrorKind::UnexpectedEnd { + version: "1.0.dev1".to_string(), + remaining: ".*".to_string(), + } + .into(), + ) + .into(), + ), + ]; + for (specifier, error) in specifiers { + assert_eq!(VersionSpecifier::from_str(specifier).unwrap_err(), error); + } +} + +#[test] +fn test_display_start() { + assert_eq!( + VersionSpecifier::from_str("== 1.1.*") + .unwrap() + .to_string(), + "==1.1.*" + ); + assert_eq!( + VersionSpecifier::from_str("!= 1.1.*") + .unwrap() + .to_string(), + "!=1.1.*" + ); +} + +#[test] +fn test_version_specifiers_str() { + assert_eq!( + VersionSpecifiers::from_str(">= 3.7").unwrap().to_string(), + ">=3.7" + ); + assert_eq!( + VersionSpecifiers::from_str(">=3.7, < 4.0, != 3.9.0") + .unwrap() + .to_string(), + ">=3.7, !=3.9.0, <4.0" + ); +} + +/// These occur in the simple api, e.g. +/// +#[test] +fn test_version_specifiers_empty() { + assert_eq!(VersionSpecifiers::from_str("").unwrap().to_string(), ""); +} + +/// All non-ASCII version specifiers are invalid, but the user can still +/// attempt to parse a non-ASCII string as a version specifier. This +/// ensures no panics occur and that the error reported has correct info. +#[test] +fn non_ascii_version_specifier() { + let s = "💩"; + let err = s.parse::().unwrap_err(); + assert_eq!(err.inner.start, 0); + assert_eq!(err.inner.end, 4); + + // The first test here is plain ASCII and it gives the + // expected result: the error starts at codepoint 12, + // which is the start of `>5.%`. + let s = ">=3.7, <4.0,>5.%"; + let err = s.parse::().unwrap_err(); + assert_eq!(err.inner.start, 12); + assert_eq!(err.inner.end, 16); + // In this case, we replace a single ASCII codepoint + // with U+3000 IDEOGRAPHIC SPACE. Its *visual* width is + // 2 despite it being a single codepoint. This causes + // the offsets in the error reporting logic to become + // incorrect. + // + // ... it did. This bug was fixed by switching to byte + // offsets. + let s = ">=3.7,\u{3000}<4.0,>5.%"; + let err = s.parse::().unwrap_err(); + assert_eq!(err.inner.start, 14); + assert_eq!(err.inner.end, 18); +} + +/// Tests the human readable error messages generated from an invalid +/// sequence of version specifiers. +#[test] +fn error_message_version_specifiers_parse_error() { + let specs = ">=1.2.3, 5.4.3, >=3.4.5"; + let err = VersionSpecifierParseError { + kind: Box::new(ParseErrorKind::MissingOperator), + }; + let inner = Box::new(VersionSpecifiersParseErrorInner { + err, + line: specs.to_string(), + start: 8, + end: 14, + }); + let err = VersionSpecifiersParseError { inner }; + assert_eq!(err, VersionSpecifiers::from_str(specs).unwrap_err()); + assert_eq!( + err.to_string(), + "\ +Failed to parse version: Unexpected end of version specifier, expected operator: +>=1.2.3, 5.4.3, >=3.4.5 + ^^^^^^ +" + ); +} + +/// Tests the human readable error messages generated when building an +/// invalid version specifier. +#[test] +fn error_message_version_specifier_build_error() { + let err = VersionSpecifierBuildError { + kind: Box::new(BuildErrorKind::CompatibleRelease), + }; + let op = Operator::TildeEqual; + let v = Version::new([5]); + let vpat = VersionPattern::verbatim(v); + assert_eq!(err, VersionSpecifier::from_pattern(op, vpat).unwrap_err()); + assert_eq!( + err.to_string(), + "The ~= operator requires at least two segments in the release version" + ); +} + +/// Tests the human readable error messages generated from parsing invalid +/// version specifier. +#[test] +fn error_message_version_specifier_parse_error() { + let err = VersionSpecifierParseError { + kind: Box::new(ParseErrorKind::InvalidSpecifier( + VersionSpecifierBuildError { + kind: Box::new(BuildErrorKind::CompatibleRelease), + }, + )), + }; + assert_eq!(err, VersionSpecifier::from_str("~=5").unwrap_err()); + assert_eq!( + err.to_string(), + "The ~= operator requires at least two segments in the release version" + ); +} diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index e2f93592c..a5d5dbc71 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -15,6 +15,7 @@ authors = { workspace = true } [lib] name = "uv_pep508" crate-type = ["cdylib", "rlib"] +doctest = false [lints] workspace = true diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index af3c335be..e2f432bbc 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -997,787 +997,5 @@ fn parse_pep508_requirement( }) } -/// Half of these tests are copied from #[cfg(test)] -mod tests { - use std::env; - use std::str::FromStr; - - use insta::assert_snapshot; - use url::Url; - - use uv_normalize::{ExtraName, InvalidNameError, PackageName}; - use uv_pep440::{Operator, Version, VersionPattern, VersionSpecifier}; - - use crate::cursor::Cursor; - use crate::marker::{parse, MarkerExpression, MarkerTree, MarkerValueVersion}; - use crate::{ - MarkerOperator, MarkerValueString, Requirement, TracingReporter, VerbatimUrl, VersionOrUrl, - }; - - fn parse_pep508_err(input: &str) -> String { - Requirement::::from_str(input) - .unwrap_err() - .to_string() - } - - #[cfg(feature = "non-pep508-extensions")] - fn parse_unnamed_err(input: &str) -> String { - crate::UnnamedRequirement::::from_str(input) - .unwrap_err() - .to_string() - } - - #[cfg(windows)] - #[test] - fn test_preprocess_url_windows() { - use std::path::PathBuf; - - let actual = crate::parse_url::( - &mut Cursor::new("file:///C:/Users/ferris/wheel-0.42.0.tar.gz"), - None, - ) - .unwrap() - .to_file_path(); - let expected = PathBuf::from(r"C:\Users\ferris\wheel-0.42.0.tar.gz"); - assert_eq!(actual, Ok(expected)); - } - - #[test] - fn error_empty() { - assert_snapshot!( - parse_pep508_err(""), - @r" - Empty field is not allowed for PEP508 - - ^" - ); - } - - #[test] - fn error_start() { - assert_snapshot!( - parse_pep508_err("_name"), - @" - Expected package name starting with an alphanumeric character, found `_` - _name - ^" - ); - } - - #[test] - fn error_end() { - assert_snapshot!( - parse_pep508_err("name_"), - @" - Package name must end with an alphanumeric character, not '_' - name_ - ^" - ); - } - - #[test] - fn basic_examples() { - let input = r"requests[security,tests]==2.8.*,>=2.8.1 ; python_full_version < '2.7'"; - let requests = Requirement::::from_str(input).unwrap(); - assert_eq!(input, requests.to_string()); - let expected = Requirement { - name: PackageName::from_str("requests").unwrap(), - extras: vec![ - ExtraName::from_str("security").unwrap(), - ExtraName::from_str("tests").unwrap(), - ], - version_or_url: Some(VersionOrUrl::VersionSpecifier( - [ - VersionSpecifier::from_pattern( - Operator::Equal, - VersionPattern::wildcard(Version::new([2, 8])), - ) - .unwrap(), - VersionSpecifier::from_pattern( - Operator::GreaterThanEqual, - VersionPattern::verbatim(Version::new([2, 8, 1])), - ) - .unwrap(), - ] - .into_iter() - .collect(), - )), - marker: MarkerTree::expression(MarkerExpression::Version { - key: MarkerValueVersion::PythonFullVersion, - specifier: VersionSpecifier::from_pattern( - uv_pep440::Operator::LessThan, - "2.7".parse().unwrap(), - ) - .unwrap(), - }), - origin: None, - }; - assert_eq!(requests, expected); - } - - #[test] - fn parenthesized_single() { - let numpy = Requirement::::from_str("numpy ( >=1.19 )").unwrap(); - assert_eq!(numpy.name.as_ref(), "numpy"); - } - - #[test] - fn parenthesized_double() { - let numpy = Requirement::::from_str("numpy ( >=1.19, <2.0 )").unwrap(); - assert_eq!(numpy.name.as_ref(), "numpy"); - } - - #[test] - fn versions_single() { - let numpy = Requirement::::from_str("numpy >=1.19 ").unwrap(); - assert_eq!(numpy.name.as_ref(), "numpy"); - } - - #[test] - fn versions_double() { - let numpy = Requirement::::from_str("numpy >=1.19, <2.0 ").unwrap(); - assert_eq!(numpy.name.as_ref(), "numpy"); - } - - #[test] - #[cfg(feature = "non-pep508-extensions")] - fn direct_url_no_extras() { - let numpy = crate::UnnamedRequirement::::from_str("https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl").unwrap(); - assert_eq!(numpy.url.to_string(), "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl"); - assert_eq!(numpy.extras, vec![]); - } - - #[test] - #[cfg(all(unix, feature = "non-pep508-extensions"))] - fn direct_url_extras() { - let numpy = crate::UnnamedRequirement::::from_str( - "/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]", - ) - .unwrap(); - assert_eq!( - numpy.url.to_string(), - "file:///path/to/numpy-1.26.4-cp312-cp312-win32.whl" - ); - assert_eq!(numpy.extras, vec![ExtraName::from_str("dev").unwrap()]); - } - - #[test] - #[cfg(all(windows, feature = "non-pep508-extensions"))] - fn direct_url_extras() { - let numpy = crate::UnnamedRequirement::::from_str( - "C:\\path\\to\\numpy-1.26.4-cp312-cp312-win32.whl[dev]", - ) - .unwrap(); - assert_eq!( - numpy.url.to_string(), - "file:///C:/path/to/numpy-1.26.4-cp312-cp312-win32.whl" - ); - assert_eq!(numpy.extras, vec![ExtraName::from_str("dev").unwrap()]); - } - - #[test] - fn error_extras_eof1() { - assert_snapshot!( - parse_pep508_err("black["), - @" - Missing closing bracket (expected ']', found end of dependency specification) - black[ - ^" - ); - } - - #[test] - fn error_extras_eof2() { - assert_snapshot!( - parse_pep508_err("black[d"), - @" - Missing closing bracket (expected ']', found end of dependency specification) - black[d - ^" - ); - } - - #[test] - fn error_extras_eof3() { - assert_snapshot!( - parse_pep508_err("black[d,"), - @" - Missing closing bracket (expected ']', found end of dependency specification) - black[d, - ^" - ); - } - - #[test] - fn error_extras_illegal_start1() { - assert_snapshot!( - parse_pep508_err("black[ö]"), - @" - Expected an alphanumeric character starting the extra name, found `ö` - black[ö] - ^" - ); - } - - #[test] - fn error_extras_illegal_start2() { - assert_snapshot!( - parse_pep508_err("black[_d]"), - @" - Expected an alphanumeric character starting the extra name, found `_` - black[_d] - ^" - ); - } - - #[test] - fn error_extras_illegal_start3() { - assert_snapshot!( - parse_pep508_err("black[,]"), - @" - Expected either alphanumerical character (starting the extra name) or `]` (ending the extras section), found `,` - black[,] - ^" - ); - } - - #[test] - fn error_extras_illegal_character() { - assert_snapshot!( - parse_pep508_err("black[jüpyter]"), - @" - Invalid character in extras name, expected an alphanumeric character, `-`, `_`, `.`, `,` or `]`, found `ü` - black[jüpyter] - ^" - ); - } - - #[test] - fn error_extras1() { - let numpy = Requirement::::from_str("black[d]").unwrap(); - assert_eq!(numpy.extras, vec![ExtraName::from_str("d").unwrap()]); - } - - #[test] - fn error_extras2() { - let numpy = Requirement::::from_str("black[d,jupyter]").unwrap(); - assert_eq!( - numpy.extras, - vec![ - ExtraName::from_str("d").unwrap(), - ExtraName::from_str("jupyter").unwrap(), - ] - ); - } - - #[test] - fn empty_extras() { - let black = Requirement::::from_str("black[]").unwrap(); - assert_eq!(black.extras, vec![]); - } - - #[test] - fn empty_extras_with_spaces() { - let black = Requirement::::from_str("black[ ]").unwrap(); - assert_eq!(black.extras, vec![]); - } - - #[test] - fn error_extra_with_trailing_comma() { - assert_snapshot!( - parse_pep508_err("black[d,]"), - @" - Expected an alphanumeric character starting the extra name, found `]` - black[d,] - ^" - ); - } - - #[test] - fn error_parenthesized_pep440() { - assert_snapshot!( - parse_pep508_err("numpy ( ><1.19 )"), - @" - no such comparison operator \"><\", must be one of ~= == != <= >= < > === - numpy ( ><1.19 ) - ^^^^^^^" - ); - } - - #[test] - fn error_parenthesized_parenthesis() { - assert_snapshot!( - parse_pep508_err("numpy ( >=1.19"), - @" - Missing closing parenthesis (expected ')', found end of dependency specification) - numpy ( >=1.19 - ^" - ); - } - - #[test] - fn error_whats_that() { - assert_snapshot!( - parse_pep508_err("numpy % 1.16"), - @" - Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `%` - numpy % 1.16 - ^" - ); - } - - #[test] - fn url() { - let pip_url = - Requirement::from_str("pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686") - .unwrap(); - let url = "https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686"; - let expected = Requirement { - name: PackageName::from_str("pip").unwrap(), - extras: vec![], - marker: MarkerTree::TRUE, - version_or_url: Some(VersionOrUrl::Url(Url::parse(url).unwrap())), - origin: None, - }; - assert_eq!(pip_url, expected); - } - - #[test] - fn test_marker_parsing() { - let marker = r#"python_version == "2.7" and (sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython'))"#; - let actual = parse::parse_markers_cursor::( - &mut Cursor::new(marker), - &mut TracingReporter, - ) - .unwrap() - .unwrap(); - - let mut a = MarkerTree::expression(MarkerExpression::Version { - key: MarkerValueVersion::PythonVersion, - specifier: VersionSpecifier::from_pattern( - uv_pep440::Operator::Equal, - "2.7".parse().unwrap(), - ) - .unwrap(), - }); - let mut b = MarkerTree::expression(MarkerExpression::String { - key: MarkerValueString::SysPlatform, - operator: MarkerOperator::Equal, - value: "win32".to_string(), - }); - let mut c = MarkerTree::expression(MarkerExpression::String { - key: MarkerValueString::OsName, - operator: MarkerOperator::Equal, - value: "linux".to_string(), - }); - let d = MarkerTree::expression(MarkerExpression::String { - key: MarkerValueString::ImplementationName, - operator: MarkerOperator::Equal, - value: "cpython".to_string(), - }); - - c.and(d); - b.or(c); - a.and(b); - - assert_eq!(a, actual); - } - - #[test] - fn name_and_marker() { - Requirement::::from_str(r#"numpy; sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython')"#).unwrap(); - } - - #[test] - fn error_marker_incomplete1() { - assert_snapshot!( - parse_pep508_err(r"numpy; sys_platform"), - @" - Expected a valid marker operator (such as `>=` or `not in`), found `` - numpy; sys_platform - ^ - " - ); - } - - #[test] - fn error_marker_incomplete2() { - assert_snapshot!( - parse_pep508_err(r"numpy; sys_platform =="), - @r" - Expected marker value, found end of dependency specification - numpy; sys_platform == - ^" - ); - } - - #[test] - fn error_marker_incomplete3() { - assert_snapshot!( - parse_pep508_err(r#"numpy; sys_platform == "win32" or"#), - @r#" - Expected marker value, found end of dependency specification - numpy; sys_platform == "win32" or - ^"# - ); - } - - #[test] - fn error_marker_incomplete4() { - assert_snapshot!( - parse_pep508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux""#), - @r#" - Expected ')', found end of dependency specification - numpy; sys_platform == "win32" or (os_name == "linux" - ^"# - ); - } - - #[test] - fn error_marker_incomplete5() { - assert_snapshot!( - parse_pep508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux" and"#), - @r#" - Expected marker value, found end of dependency specification - numpy; sys_platform == "win32" or (os_name == "linux" and - ^"# - ); - } - - #[test] - fn error_pep440() { - assert_snapshot!( - parse_pep508_err(r"numpy >=1.1.*"), - @r" - Operator >= cannot be used with a wildcard version specifier - numpy >=1.1.* - ^^^^^^^" - ); - } - - #[test] - fn error_no_name() { - assert_snapshot!( - parse_pep508_err(r"==0.0"), - @r" - Expected package name starting with an alphanumeric character, found `=` - ==0.0 - ^ - " - ); - } - - #[test] - fn error_unnamedunnamed_url() { - assert_snapshot!( - parse_pep508_err(r"git+https://github.com/pallets/flask.git"), - @" - URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`). - git+https://github.com/pallets/flask.git - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" - ); - } - - #[test] - fn error_unnamed_file_path() { - assert_snapshot!( - parse_pep508_err(r"/path/to/flask.tar.gz"), - @r###" - URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`). - /path/to/flask.tar.gz - ^^^^^^^^^^^^^^^^^^^^^ - "### - ); - } - - #[test] - fn error_no_comma_between_extras() { - assert_snapshot!( - parse_pep508_err(r"name[bar baz]"), - @" - Expected either `,` (separating extras) or `]` (ending the extras section), found `b` - name[bar baz] - ^" - ); - } - - #[test] - fn error_extra_comma_after_extras() { - assert_snapshot!( - parse_pep508_err(r"name[bar, baz,]"), - @" - Expected an alphanumeric character starting the extra name, found `]` - name[bar, baz,] - ^" - ); - } - - #[test] - fn error_extras_not_closed() { - assert_snapshot!( - parse_pep508_err(r"name[bar, baz >= 1.0"), - @" - Expected either `,` (separating extras) or `]` (ending the extras section), found `>` - name[bar, baz >= 1.0 - ^" - ); - } - - #[test] - fn error_no_space_after_url() { - assert_snapshot!( - parse_pep508_err(r"name @ https://example.com/; extra == 'example'"), - @" - Missing space before ';', the end of the URL is ambiguous - name @ https://example.com/; extra == 'example' - ^" - ); - } - - #[test] - fn error_name_at_nothing() { - assert_snapshot!( - parse_pep508_err(r"name @"), - @" - Expected URL - name @ - ^" - ); - } - - #[test] - fn test_error_invalid_marker_key() { - assert_snapshot!( - parse_pep508_err(r"name; invalid_name"), - @" - Expected a quoted string or a valid marker name, found `invalid_name` - name; invalid_name - ^^^^^^^^^^^^ - " - ); - } - - #[test] - fn error_markers_invalid_order() { - assert_snapshot!( - parse_pep508_err("name; '3.7' <= invalid_name"), - @" - Expected a quoted string or a valid marker name, found `invalid_name` - name; '3.7' <= invalid_name - ^^^^^^^^^^^^ - " - ); - } - - #[test] - fn error_markers_notin() { - assert_snapshot!( - parse_pep508_err("name; '3.7' notin python_version"), - @" - Expected a valid marker operator (such as `>=` or `not in`), found `notin` - name; '3.7' notin python_version - ^^^^^" - ); - } - - #[test] - fn error_missing_quote() { - assert_snapshot!( - parse_pep508_err("name; python_version == 3.10"), - @" - Expected a quoted string or a valid marker name, found `3.10` - name; python_version == 3.10 - ^^^^ - " - ); - } - - #[test] - fn error_markers_inpython_version() { - assert_snapshot!( - parse_pep508_err("name; '3.6'inpython_version"), - @" - Expected a valid marker operator (such as `>=` or `not in`), found `inpython_version` - name; '3.6'inpython_version - ^^^^^^^^^^^^^^^^" - ); - } - - #[test] - fn error_markers_not_python_version() { - assert_snapshot!( - parse_pep508_err("name; '3.7' not python_version"), - @" - Expected `i`, found `p` - name; '3.7' not python_version - ^" - ); - } - - #[test] - fn error_markers_invalid_operator() { - assert_snapshot!( - parse_pep508_err("name; '3.7' ~ python_version"), - @" - Expected a valid marker operator (such as `>=` or `not in`), found `~` - name; '3.7' ~ python_version - ^" - ); - } - - #[test] - fn error_invalid_prerelease() { - assert_snapshot!( - parse_pep508_err("name==1.0.org1"), - @r###" - after parsing `1.0`, found `.org1`, which is not part of a valid version - name==1.0.org1 - ^^^^^^^^^^ - "### - ); - } - - #[test] - fn error_no_version_value() { - assert_snapshot!( - parse_pep508_err("name=="), - @" - Unexpected end of version specifier, expected version - name== - ^^" - ); - } - - #[test] - fn error_no_version_operator() { - assert_snapshot!( - parse_pep508_err("name 1.0"), - @" - Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `1` - name 1.0 - ^" - ); - } - - #[test] - fn error_random_char() { - assert_snapshot!( - parse_pep508_err("name >= 1.0 #"), - @" - Trailing `#` is not allowed - name >= 1.0 # - ^^^^^^^^" - ); - } - - #[test] - #[cfg(feature = "non-pep508-extensions")] - fn error_invalid_extra_unnamed_url() { - assert_snapshot!( - parse_unnamed_err("/foo-3.0.0-py3-none-any.whl[d,]"), - @r###" - Expected an alphanumeric character starting the extra name, found `]` - /foo-3.0.0-py3-none-any.whl[d,] - ^ - "### - ); - } - - /// Check that the relative path support feature toggle works. - #[test] - fn non_pep508_paths() { - let requirements = &[ - "foo @ file://./foo", - "foo @ file://foo-3.0.0-py3-none-any.whl", - "foo @ file:foo-3.0.0-py3-none-any.whl", - "foo @ ./foo-3.0.0-py3-none-any.whl", - ]; - let cwd = env::current_dir().unwrap(); - - for requirement in requirements { - assert_eq!( - Requirement::::parse(requirement, &cwd).is_ok(), - cfg!(feature = "non-pep508-extensions"), - "{}: {:?}", - requirement, - Requirement::::parse(requirement, &cwd) - ); - } - } - - #[test] - fn no_space_after_operator() { - let requirement = Requirement::::from_str("pytest;python_version<='4.0'").unwrap(); - assert_eq!( - requirement.to_string(), - "pytest ; python_full_version < '4.1'" - ); - - let requirement = Requirement::::from_str("pytest;'4.0'>=python_version").unwrap(); - assert_eq!( - requirement.to_string(), - "pytest ; python_full_version < '4.1'" - ); - } - - #[test] - fn path_with_fragment() { - let requirements = if cfg!(windows) { - &[ - "wheel @ file:///C:/Users/ferris/wheel-0.42.0.whl#hash=somehash", - "wheel @ C:/Users/ferris/wheel-0.42.0.whl#hash=somehash", - ] - } else { - &[ - "wheel @ file:///Users/ferris/wheel-0.42.0.whl#hash=somehash", - "wheel @ /Users/ferris/wheel-0.42.0.whl#hash=somehash", - ] - }; - - for requirement in requirements { - // Extract the URL. - let Some(VersionOrUrl::Url(url)) = Requirement::::from_str(requirement) - .unwrap() - .version_or_url - else { - unreachable!("Expected a URL") - }; - - // Assert that the fragment and path have been separated correctly. - assert_eq!(url.fragment(), Some("hash=somehash")); - assert!( - url.path().ends_with("/Users/ferris/wheel-0.42.0.whl"), - "Expected the path to end with `/Users/ferris/wheel-0.42.0.whl`, found `{}`", - url.path() - ); - } - } - - #[test] - fn add_extra_marker() -> Result<(), InvalidNameError> { - let requirement = Requirement::::from_str("pytest").unwrap(); - let expected = Requirement::::from_str("pytest; extra == 'dotenv'").unwrap(); - let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); - assert_eq!(actual, expected); - - let requirement = Requirement::::from_str("pytest; '4.0' >= python_version").unwrap(); - let expected = - Requirement::from_str("pytest; '4.0' >= python_version and extra == 'dotenv'").unwrap(); - let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); - assert_eq!(actual, expected); - - let requirement = Requirement::::from_str( - "pytest; '4.0' >= python_version or sys_platform == 'win32'", - ) - .unwrap(); - let expected = Requirement::from_str( - "pytest; ('4.0' >= python_version or sys_platform == 'win32') and extra == 'dotenv'", - ) - .unwrap(); - let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); - assert_eq!(actual, expected); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-pep508/src/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index 8cb73669a..f44ec54c9 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -1236,89 +1236,4 @@ impl fmt::Debug for NodeId { } #[cfg(test)] -mod tests { - use super::{NodeId, INTERNER}; - use crate::MarkerExpression; - - fn expr(s: &str) -> NodeId { - INTERNER - .lock() - .expression(MarkerExpression::from_str(s).unwrap().unwrap()) - } - - #[test] - fn basic() { - let m = || INTERNER.lock(); - let extra_foo = expr("extra == 'foo'"); - assert!(!extra_foo.is_false()); - - let os_foo = expr("os_name == 'foo'"); - let extra_and_os_foo = m().or(extra_foo, os_foo); - assert!(!extra_and_os_foo.is_false()); - assert!(!m().and(extra_foo, os_foo).is_false()); - - let trivially_true = m().or(extra_and_os_foo, extra_and_os_foo.not()); - assert!(!trivially_true.is_false()); - assert!(trivially_true.is_true()); - - let trivially_false = m().and(extra_foo, extra_foo.not()); - assert!(trivially_false.is_false()); - - let e = m().or(trivially_false, os_foo); - assert!(!e.is_false()); - - let extra_not_foo = expr("extra != 'foo'"); - assert!(m().and(extra_foo, extra_not_foo).is_false()); - assert!(m().or(extra_foo, extra_not_foo).is_true()); - - let os_geq_bar = expr("os_name >= 'bar'"); - assert!(!os_geq_bar.is_false()); - - let os_le_bar = expr("os_name < 'bar'"); - assert!(m().and(os_geq_bar, os_le_bar).is_false()); - assert!(m().or(os_geq_bar, os_le_bar).is_true()); - - let os_leq_bar = expr("os_name <= 'bar'"); - assert!(!m().and(os_geq_bar, os_leq_bar).is_false()); - assert!(m().or(os_geq_bar, os_leq_bar).is_true()); - } - - #[test] - fn version() { - let m = || INTERNER.lock(); - let eq_3 = expr("python_version == '3'"); - let neq_3 = expr("python_version != '3'"); - let geq_3 = expr("python_version >= '3'"); - let leq_3 = expr("python_version <= '3'"); - - let eq_2 = expr("python_version == '2'"); - let eq_1 = expr("python_version == '1'"); - assert!(m().and(eq_2, eq_1).is_false()); - - assert_eq!(eq_3.not(), neq_3); - assert_eq!(eq_3, neq_3.not()); - - assert!(m().and(eq_3, neq_3).is_false()); - assert!(m().or(eq_3, neq_3).is_true()); - - assert_eq!(m().and(eq_3, geq_3), eq_3); - assert_eq!(m().and(eq_3, leq_3), eq_3); - - assert_eq!(m().and(geq_3, leq_3), eq_3); - - assert!(!m().and(geq_3, leq_3).is_false()); - assert!(m().or(geq_3, leq_3).is_true()); - } - - #[test] - fn simplify() { - let m = || INTERNER.lock(); - let x86 = expr("platform_machine == 'x86_64'"); - let not_x86 = expr("platform_machine != 'x86_64'"); - let windows = expr("platform_machine == 'Windows'"); - - let a = m().and(x86, windows); - let b = m().and(not_x86, windows); - assert_eq!(m().or(a, b), windows); - } -} +mod tests; diff --git a/crates/uv-pep508/src/marker/algebra/tests.rs b/crates/uv-pep508/src/marker/algebra/tests.rs new file mode 100644 index 000000000..00559d4cd --- /dev/null +++ b/crates/uv-pep508/src/marker/algebra/tests.rs @@ -0,0 +1,84 @@ +use super::{NodeId, INTERNER}; +use crate::MarkerExpression; + +fn expr(s: &str) -> NodeId { + INTERNER + .lock() + .expression(MarkerExpression::from_str(s).unwrap().unwrap()) +} + +#[test] +fn basic() { + let m = || INTERNER.lock(); + let extra_foo = expr("extra == 'foo'"); + assert!(!extra_foo.is_false()); + + let os_foo = expr("os_name == 'foo'"); + let extra_and_os_foo = m().or(extra_foo, os_foo); + assert!(!extra_and_os_foo.is_false()); + assert!(!m().and(extra_foo, os_foo).is_false()); + + let trivially_true = m().or(extra_and_os_foo, extra_and_os_foo.not()); + assert!(!trivially_true.is_false()); + assert!(trivially_true.is_true()); + + let trivially_false = m().and(extra_foo, extra_foo.not()); + assert!(trivially_false.is_false()); + + let e = m().or(trivially_false, os_foo); + assert!(!e.is_false()); + + let extra_not_foo = expr("extra != 'foo'"); + assert!(m().and(extra_foo, extra_not_foo).is_false()); + assert!(m().or(extra_foo, extra_not_foo).is_true()); + + let os_geq_bar = expr("os_name >= 'bar'"); + assert!(!os_geq_bar.is_false()); + + let os_le_bar = expr("os_name < 'bar'"); + assert!(m().and(os_geq_bar, os_le_bar).is_false()); + assert!(m().or(os_geq_bar, os_le_bar).is_true()); + + let os_leq_bar = expr("os_name <= 'bar'"); + assert!(!m().and(os_geq_bar, os_leq_bar).is_false()); + assert!(m().or(os_geq_bar, os_leq_bar).is_true()); +} + +#[test] +fn version() { + let m = || INTERNER.lock(); + let eq_3 = expr("python_version == '3'"); + let neq_3 = expr("python_version != '3'"); + let geq_3 = expr("python_version >= '3'"); + let leq_3 = expr("python_version <= '3'"); + + let eq_2 = expr("python_version == '2'"); + let eq_1 = expr("python_version == '1'"); + assert!(m().and(eq_2, eq_1).is_false()); + + assert_eq!(eq_3.not(), neq_3); + assert_eq!(eq_3, neq_3.not()); + + assert!(m().and(eq_3, neq_3).is_false()); + assert!(m().or(eq_3, neq_3).is_true()); + + assert_eq!(m().and(eq_3, geq_3), eq_3); + assert_eq!(m().and(eq_3, leq_3), eq_3); + + assert_eq!(m().and(geq_3, leq_3), eq_3); + + assert!(!m().and(geq_3, leq_3).is_false()); + assert!(m().or(geq_3, leq_3).is_true()); +} + +#[test] +fn simplify() { + let m = || INTERNER.lock(); + let x86 = expr("platform_machine == 'x86_64'"); + let not_x86 = expr("platform_machine != 'x86_64'"); + let windows = expr("platform_machine == 'Windows'"); + + let a = m().and(x86, windows); + let b = m().and(not_x86, windows); + assert_eq!(m().or(a, b), windows); +} diff --git a/crates/uv-pep508/src/tests.rs b/crates/uv-pep508/src/tests.rs new file mode 100644 index 000000000..661fedb18 --- /dev/null +++ b/crates/uv-pep508/src/tests.rs @@ -0,0 +1,801 @@ +//! Half of these tests are copied from + +use std::env; +use std::str::FromStr; + +use insta::assert_snapshot; +use url::Url; + +use uv_normalize::{ExtraName, InvalidNameError, PackageName}; +use uv_pep440::{Operator, Version, VersionPattern, VersionSpecifier}; + +use crate::cursor::Cursor; +use crate::marker::{parse, MarkerExpression, MarkerTree, MarkerValueVersion}; +use crate::{ + MarkerOperator, MarkerValueString, Requirement, TracingReporter, VerbatimUrl, VersionOrUrl, +}; + +fn parse_pep508_err(input: &str) -> String { + Requirement::::from_str(input) + .unwrap_err() + .to_string() +} + +#[cfg(feature = "non-pep508-extensions")] +fn parse_unnamed_err(input: &str) -> String { + crate::UnnamedRequirement::::from_str(input) + .unwrap_err() + .to_string() +} + +#[cfg(windows)] +#[test] +fn test_preprocess_url_windows() { + use std::path::PathBuf; + + let actual = crate::parse_url::( + &mut Cursor::new("file:///C:/Users/ferris/wheel-0.42.0.tar.gz"), + None, + ) + .unwrap() + .to_file_path(); + let expected = PathBuf::from(r"C:\Users\ferris\wheel-0.42.0.tar.gz"); + assert_eq!(actual, Ok(expected)); +} + +#[test] +fn error_empty() { + assert_snapshot!( + parse_pep508_err(""), + @r" + Empty field is not allowed for PEP508 + + ^" + ); +} + +#[test] +fn error_start() { + assert_snapshot!( + parse_pep508_err("_name"), + @" + Expected package name starting with an alphanumeric character, found `_` + _name + ^" + ); +} + +#[test] +fn error_end() { + assert_snapshot!( + parse_pep508_err("name_"), + @" + Package name must end with an alphanumeric character, not '_' + name_ + ^" + ); +} + +#[test] +fn basic_examples() { + let input = r"requests[security,tests]==2.8.*,>=2.8.1 ; python_full_version < '2.7'"; + let requests = Requirement::::from_str(input).unwrap(); + assert_eq!(input, requests.to_string()); + let expected = Requirement { + name: PackageName::from_str("requests").unwrap(), + extras: vec![ + ExtraName::from_str("security").unwrap(), + ExtraName::from_str("tests").unwrap(), + ], + version_or_url: Some(VersionOrUrl::VersionSpecifier( + [ + VersionSpecifier::from_pattern( + Operator::Equal, + VersionPattern::wildcard(Version::new([2, 8])), + ) + .unwrap(), + VersionSpecifier::from_pattern( + Operator::GreaterThanEqual, + VersionPattern::verbatim(Version::new([2, 8, 1])), + ) + .unwrap(), + ] + .into_iter() + .collect(), + )), + marker: MarkerTree::expression(MarkerExpression::Version { + key: MarkerValueVersion::PythonFullVersion, + specifier: VersionSpecifier::from_pattern( + uv_pep440::Operator::LessThan, + "2.7".parse().unwrap(), + ) + .unwrap(), + }), + origin: None, + }; + assert_eq!(requests, expected); +} + +#[test] +fn parenthesized_single() { + let numpy = Requirement::::from_str("numpy ( >=1.19 )").unwrap(); + assert_eq!(numpy.name.as_ref(), "numpy"); +} + +#[test] +fn parenthesized_double() { + let numpy = Requirement::::from_str("numpy ( >=1.19, <2.0 )").unwrap(); + assert_eq!(numpy.name.as_ref(), "numpy"); +} + +#[test] +fn versions_single() { + let numpy = Requirement::::from_str("numpy >=1.19 ").unwrap(); + assert_eq!(numpy.name.as_ref(), "numpy"); +} + +#[test] +fn versions_double() { + let numpy = Requirement::::from_str("numpy >=1.19, <2.0 ").unwrap(); + assert_eq!(numpy.name.as_ref(), "numpy"); +} + +#[test] +#[cfg(feature = "non-pep508-extensions")] +fn direct_url_no_extras() { + let numpy = crate::UnnamedRequirement::::from_str("https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl").unwrap(); + assert_eq!(numpy.url.to_string(), "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl"); + assert_eq!(numpy.extras, vec![]); +} + +#[test] +#[cfg(all(unix, feature = "non-pep508-extensions"))] +fn direct_url_extras() { + let numpy = crate::UnnamedRequirement::::from_str( + "/path/to/numpy-1.26.4-cp312-cp312-win32.whl[dev]", + ) + .unwrap(); + assert_eq!( + numpy.url.to_string(), + "file:///path/to/numpy-1.26.4-cp312-cp312-win32.whl" + ); + assert_eq!(numpy.extras, vec![ExtraName::from_str("dev").unwrap()]); +} + +#[test] +#[cfg(all(windows, feature = "non-pep508-extensions"))] +fn direct_url_extras() { + let numpy = crate::UnnamedRequirement::::from_str( + "C:\\path\\to\\numpy-1.26.4-cp312-cp312-win32.whl[dev]", + ) + .unwrap(); + assert_eq!( + numpy.url.to_string(), + "file:///C:/path/to/numpy-1.26.4-cp312-cp312-win32.whl" + ); + assert_eq!(numpy.extras, vec![ExtraName::from_str("dev").unwrap()]); +} + +#[test] +fn error_extras_eof1() { + assert_snapshot!( + parse_pep508_err("black["), + @r#" + Missing closing bracket (expected ']', found end of dependency specification) + black[ + ^ + "# + ); +} + +#[test] +fn error_extras_eof2() { + assert_snapshot!( + parse_pep508_err("black[d"), + @r#" + Missing closing bracket (expected ']', found end of dependency specification) + black[d + ^ + "# + ); +} + +#[test] +fn error_extras_eof3() { + assert_snapshot!( + parse_pep508_err("black[d,"), + @r#" + Missing closing bracket (expected ']', found end of dependency specification) + black[d, + ^ + "# + ); +} + +#[test] +fn error_extras_illegal_start1() { + assert_snapshot!( + parse_pep508_err("black[ö]"), + @r#" + Expected an alphanumeric character starting the extra name, found `ö` + black[ö] + ^ + "# + ); +} + +#[test] +fn error_extras_illegal_start2() { + assert_snapshot!( + parse_pep508_err("black[_d]"), + @r#" + Expected an alphanumeric character starting the extra name, found `_` + black[_d] + ^ + "# + ); +} + +#[test] +fn error_extras_illegal_start3() { + assert_snapshot!( + parse_pep508_err("black[,]"), + @r#" + Expected either alphanumerical character (starting the extra name) or `]` (ending the extras section), found `,` + black[,] + ^ + "# + ); +} + +#[test] +fn error_extras_illegal_character() { + assert_snapshot!( + parse_pep508_err("black[jüpyter]"), + @r#" + Invalid character in extras name, expected an alphanumeric character, `-`, `_`, `.`, `,` or `]`, found `ü` + black[jüpyter] + ^ + "# + ); +} + +#[test] +fn error_extras1() { + let numpy = Requirement::::from_str("black[d]").unwrap(); + assert_eq!(numpy.extras, vec![ExtraName::from_str("d").unwrap()]); +} + +#[test] +fn error_extras2() { + let numpy = Requirement::::from_str("black[d,jupyter]").unwrap(); + assert_eq!( + numpy.extras, + vec![ + ExtraName::from_str("d").unwrap(), + ExtraName::from_str("jupyter").unwrap(), + ] + ); +} + +#[test] +fn empty_extras() { + let black = Requirement::::from_str("black[]").unwrap(); + assert_eq!(black.extras, vec![]); +} + +#[test] +fn empty_extras_with_spaces() { + let black = Requirement::::from_str("black[ ]").unwrap(); + assert_eq!(black.extras, vec![]); +} + +#[test] +fn error_extra_with_trailing_comma() { + assert_snapshot!( + parse_pep508_err("black[d,]"), + @" + Expected an alphanumeric character starting the extra name, found `]` + black[d,] + ^" + ); +} + +#[test] +fn error_parenthesized_pep440() { + assert_snapshot!( + parse_pep508_err("numpy ( ><1.19 )"), + @" + no such comparison operator \"><\", must be one of ~= == != <= >= < > === + numpy ( ><1.19 ) + ^^^^^^^" + ); +} + +#[test] +fn error_parenthesized_parenthesis() { + assert_snapshot!( + parse_pep508_err("numpy ( >=1.19"), + @r#" + Missing closing parenthesis (expected ')', found end of dependency specification) + numpy ( >=1.19 + ^ + "# + ); +} + +#[test] +fn error_whats_that() { + assert_snapshot!( + parse_pep508_err("numpy % 1.16"), + @r#" + Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `%` + numpy % 1.16 + ^ + "# + ); +} + +#[test] +fn url() { + let pip_url = + Requirement::from_str("pip @ https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686") + .unwrap(); + let url = "https://github.com/pypa/pip/archive/1.3.1.zip#sha1=da9234ee9982d4bbb3c72346a6de940a148ea686"; + let expected = Requirement { + name: PackageName::from_str("pip").unwrap(), + extras: vec![], + marker: MarkerTree::TRUE, + version_or_url: Some(VersionOrUrl::Url(Url::parse(url).unwrap())), + origin: None, + }; + assert_eq!(pip_url, expected); +} + +#[test] +fn test_marker_parsing() { + let marker = r#"python_version == "2.7" and (sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython'))"#; + let actual = + parse::parse_markers_cursor::(&mut Cursor::new(marker), &mut TracingReporter) + .unwrap() + .unwrap(); + + let mut a = MarkerTree::expression(MarkerExpression::Version { + key: MarkerValueVersion::PythonVersion, + specifier: VersionSpecifier::from_pattern( + uv_pep440::Operator::Equal, + "2.7".parse().unwrap(), + ) + .unwrap(), + }); + let mut b = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::SysPlatform, + operator: MarkerOperator::Equal, + value: "win32".to_string(), + }); + let mut c = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::OsName, + operator: MarkerOperator::Equal, + value: "linux".to_string(), + }); + let d = MarkerTree::expression(MarkerExpression::String { + key: MarkerValueString::ImplementationName, + operator: MarkerOperator::Equal, + value: "cpython".to_string(), + }); + + c.and(d); + b.or(c); + a.and(b); + + assert_eq!(a, actual); +} + +#[test] +fn name_and_marker() { + Requirement::::from_str(r#"numpy; sys_platform == "win32" or (os_name == "linux" and implementation_name == 'cpython')"#).unwrap(); +} + +#[test] +fn error_marker_incomplete1() { + assert_snapshot!( + parse_pep508_err(r"numpy; sys_platform"), + @r#" + Expected a valid marker operator (such as `>=` or `not in`), found `` + numpy; sys_platform + ^ + "# + ); +} + +#[test] +fn error_marker_incomplete2() { + assert_snapshot!( + parse_pep508_err(r"numpy; sys_platform =="), + @r#" + Expected marker value, found end of dependency specification + numpy; sys_platform == + ^ + "# + ); +} + +#[test] +fn error_marker_incomplete3() { + assert_snapshot!( + parse_pep508_err(r#"numpy; sys_platform == "win32" or"#), + @r#" + Expected marker value, found end of dependency specification + numpy; sys_platform == "win32" or + ^ + "# + ); +} + +#[test] +fn error_marker_incomplete4() { + assert_snapshot!( + parse_pep508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux""#), + @r#" + Expected ')', found end of dependency specification + numpy; sys_platform == "win32" or (os_name == "linux" + ^ + "# + ); +} + +#[test] +fn error_marker_incomplete5() { + assert_snapshot!( + parse_pep508_err(r#"numpy; sys_platform == "win32" or (os_name == "linux" and"#), + @r#" + Expected marker value, found end of dependency specification + numpy; sys_platform == "win32" or (os_name == "linux" and + ^ + "# + ); +} + +#[test] +fn error_pep440() { + assert_snapshot!( + parse_pep508_err(r"numpy >=1.1.*"), + @r#" + Operator >= cannot be used with a wildcard version specifier + numpy >=1.1.* + ^^^^^^^ + "# + ); +} + +#[test] +fn error_no_name() { + assert_snapshot!( + parse_pep508_err(r"==0.0"), + @r" + Expected package name starting with an alphanumeric character, found `=` + ==0.0 + ^ + " + ); +} + +#[test] +fn error_unnamedunnamed_url() { + assert_snapshot!( + parse_pep508_err(r"git+https://github.com/pallets/flask.git"), + @" + URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`). + git+https://github.com/pallets/flask.git + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + ); +} + +#[test] +fn error_unnamed_file_path() { + assert_snapshot!( + parse_pep508_err(r"/path/to/flask.tar.gz"), + @r###" + URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ /path/to/file`). + /path/to/flask.tar.gz + ^^^^^^^^^^^^^^^^^^^^^ + "### + ); +} + +#[test] +fn error_no_comma_between_extras() { + assert_snapshot!( + parse_pep508_err(r"name[bar baz]"), + @r#" + Expected either `,` (separating extras) or `]` (ending the extras section), found `b` + name[bar baz] + ^ + "# + ); +} + +#[test] +fn error_extra_comma_after_extras() { + assert_snapshot!( + parse_pep508_err(r"name[bar, baz,]"), + @r#" + Expected an alphanumeric character starting the extra name, found `]` + name[bar, baz,] + ^ + "# + ); +} + +#[test] +fn error_extras_not_closed() { + assert_snapshot!( + parse_pep508_err(r"name[bar, baz >= 1.0"), + @r#" + Expected either `,` (separating extras) or `]` (ending the extras section), found `>` + name[bar, baz >= 1.0 + ^ + "# + ); +} + +#[test] +fn error_no_space_after_url() { + assert_snapshot!( + parse_pep508_err(r"name @ https://example.com/; extra == 'example'"), + @r#" + Missing space before ';', the end of the URL is ambiguous + name @ https://example.com/; extra == 'example' + ^ + "# + ); +} + +#[test] +fn error_name_at_nothing() { + assert_snapshot!( + parse_pep508_err(r"name @"), + @r#" + Expected URL + name @ + ^ + "# + ); +} + +#[test] +fn test_error_invalid_marker_key() { + assert_snapshot!( + parse_pep508_err(r"name; invalid_name"), + @r#" + Expected a quoted string or a valid marker name, found `invalid_name` + name; invalid_name + ^^^^^^^^^^^^ + "# + ); +} + +#[test] +fn error_markers_invalid_order() { + assert_snapshot!( + parse_pep508_err("name; '3.7' <= invalid_name"), + @r#" + Expected a quoted string or a valid marker name, found `invalid_name` + name; '3.7' <= invalid_name + ^^^^^^^^^^^^ + "# + ); +} + +#[test] +fn error_markers_notin() { + assert_snapshot!( + parse_pep508_err("name; '3.7' notin python_version"), + @" + Expected a valid marker operator (such as `>=` or `not in`), found `notin` + name; '3.7' notin python_version + ^^^^^" + ); +} + +#[test] +fn error_missing_quote() { + assert_snapshot!( + parse_pep508_err("name; python_version == 3.10"), + @" + Expected a quoted string or a valid marker name, found `3.10` + name; python_version == 3.10 + ^^^^ + " + ); +} + +#[test] +fn error_markers_inpython_version() { + assert_snapshot!( + parse_pep508_err("name; '3.6'inpython_version"), + @r#" + Expected a valid marker operator (such as `>=` or `not in`), found `inpython_version` + name; '3.6'inpython_version + ^^^^^^^^^^^^^^^^ + "# + ); +} + +#[test] +fn error_markers_not_python_version() { + assert_snapshot!( + parse_pep508_err("name; '3.7' not python_version"), + @" + Expected `i`, found `p` + name; '3.7' not python_version + ^" + ); +} + +#[test] +fn error_markers_invalid_operator() { + assert_snapshot!( + parse_pep508_err("name; '3.7' ~ python_version"), + @" + Expected a valid marker operator (such as `>=` or `not in`), found `~` + name; '3.7' ~ python_version + ^" + ); +} + +#[test] +fn error_invalid_prerelease() { + assert_snapshot!( + parse_pep508_err("name==1.0.org1"), + @r###" + after parsing `1.0`, found `.org1`, which is not part of a valid version + name==1.0.org1 + ^^^^^^^^^^ + "### + ); +} + +#[test] +fn error_no_version_value() { + assert_snapshot!( + parse_pep508_err("name=="), + @" + Unexpected end of version specifier, expected version + name== + ^^" + ); +} + +#[test] +fn error_no_version_operator() { + assert_snapshot!( + parse_pep508_err("name 1.0"), + @r#" + Expected one of `@`, `(`, `<`, `=`, `>`, `~`, `!`, `;`, found `1` + name 1.0 + ^ + "# + ); +} + +#[test] +fn error_random_char() { + assert_snapshot!( + parse_pep508_err("name >= 1.0 #"), + @r##" + Trailing `#` is not allowed + name >= 1.0 # + ^^^^^^^^ + "## + ); +} + +#[test] +#[cfg(feature = "non-pep508-extensions")] +fn error_invalid_extra_unnamed_url() { + assert_snapshot!( + parse_unnamed_err("/foo-3.0.0-py3-none-any.whl[d,]"), + @r#" + Expected an alphanumeric character starting the extra name, found `]` + /foo-3.0.0-py3-none-any.whl[d,] + ^ + "# + ); +} + +/// Check that the relative path support feature toggle works. +#[test] +fn non_pep508_paths() { + let requirements = &[ + "foo @ file://./foo", + "foo @ file://foo-3.0.0-py3-none-any.whl", + "foo @ file:foo-3.0.0-py3-none-any.whl", + "foo @ ./foo-3.0.0-py3-none-any.whl", + ]; + let cwd = env::current_dir().unwrap(); + + for requirement in requirements { + assert_eq!( + Requirement::::parse(requirement, &cwd).is_ok(), + cfg!(feature = "non-pep508-extensions"), + "{}: {:?}", + requirement, + Requirement::::parse(requirement, &cwd) + ); + } +} + +#[test] +fn no_space_after_operator() { + let requirement = Requirement::::from_str("pytest;python_version<='4.0'").unwrap(); + assert_eq!( + requirement.to_string(), + "pytest ; python_full_version < '4.1'" + ); + + let requirement = Requirement::::from_str("pytest;'4.0'>=python_version").unwrap(); + assert_eq!( + requirement.to_string(), + "pytest ; python_full_version < '4.1'" + ); +} + +#[test] +fn path_with_fragment() { + let requirements = if cfg!(windows) { + &[ + "wheel @ file:///C:/Users/ferris/wheel-0.42.0.whl#hash=somehash", + "wheel @ C:/Users/ferris/wheel-0.42.0.whl#hash=somehash", + ] + } else { + &[ + "wheel @ file:///Users/ferris/wheel-0.42.0.whl#hash=somehash", + "wheel @ /Users/ferris/wheel-0.42.0.whl#hash=somehash", + ] + }; + + for requirement in requirements { + // Extract the URL. + let Some(VersionOrUrl::Url(url)) = Requirement::::from_str(requirement) + .unwrap() + .version_or_url + else { + unreachable!("Expected a URL") + }; + + // Assert that the fragment and path have been separated correctly. + assert_eq!(url.fragment(), Some("hash=somehash")); + assert!( + url.path().ends_with("/Users/ferris/wheel-0.42.0.whl"), + "Expected the path to end with `/Users/ferris/wheel-0.42.0.whl`, found `{}`", + url.path() + ); + } +} + +#[test] +fn add_extra_marker() -> Result<(), InvalidNameError> { + let requirement = Requirement::::from_str("pytest").unwrap(); + let expected = Requirement::::from_str("pytest; extra == 'dotenv'").unwrap(); + let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); + assert_eq!(actual, expected); + + let requirement = Requirement::::from_str("pytest; '4.0' >= python_version").unwrap(); + let expected = + Requirement::from_str("pytest; '4.0' >= python_version and extra == 'dotenv'").unwrap(); + let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); + assert_eq!(actual, expected); + + let requirement = + Requirement::::from_str("pytest; '4.0' >= python_version or sys_platform == 'win32'") + .unwrap(); + let expected = Requirement::from_str( + "pytest; ('4.0' >= python_version or sys_platform == 'win32') and extra == 'dotenv'", + ) + .unwrap(); + let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?); + assert_eq!(actual, expected); + + Ok(()) +} diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index 9445b13cf..e3224c99e 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -516,61 +516,4 @@ impl std::fmt::Display for Scheme { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn scheme() { - assert_eq!( - split_scheme("file:///home/ferris/project/scripts"), - Some(("file", "///home/ferris/project/scripts")) - ); - assert_eq!( - split_scheme("file:home/ferris/project/scripts"), - Some(("file", "home/ferris/project/scripts")) - ); - assert_eq!( - split_scheme("https://example.com"), - Some(("https", "//example.com")) - ); - assert_eq!(split_scheme("https:"), Some(("https", ""))); - } - - #[test] - fn fragment() { - assert_eq!( - split_fragment(Path::new( - "file:///home/ferris/project/scripts#hash=somehash" - )), - ( - Cow::Owned(PathBuf::from("file:///home/ferris/project/scripts")), - Some("hash=somehash") - ) - ); - assert_eq!( - split_fragment(Path::new("file:home/ferris/project/scripts#hash=somehash")), - ( - Cow::Owned(PathBuf::from("file:home/ferris/project/scripts")), - Some("hash=somehash") - ) - ); - assert_eq!( - split_fragment(Path::new("/home/ferris/project/scripts#hash=somehash")), - ( - Cow::Owned(PathBuf::from("/home/ferris/project/scripts")), - Some("hash=somehash") - ) - ); - assert_eq!( - split_fragment(Path::new("file:///home/ferris/project/scripts")), - ( - Cow::Borrowed(Path::new("file:///home/ferris/project/scripts")), - None - ) - ); - assert_eq!( - split_fragment(Path::new("")), - (Cow::Borrowed(Path::new("")), None) - ); - } -} +mod tests; diff --git a/crates/uv-pep508/src/verbatim_url/tests.rs b/crates/uv-pep508/src/verbatim_url/tests.rs new file mode 100644 index 000000000..5706d0394 --- /dev/null +++ b/crates/uv-pep508/src/verbatim_url/tests.rs @@ -0,0 +1,56 @@ +use super::*; + +#[test] +fn scheme() { + assert_eq!( + split_scheme("file:///home/ferris/project/scripts"), + Some(("file", "///home/ferris/project/scripts")) + ); + assert_eq!( + split_scheme("file:home/ferris/project/scripts"), + Some(("file", "home/ferris/project/scripts")) + ); + assert_eq!( + split_scheme("https://example.com"), + Some(("https", "//example.com")) + ); + assert_eq!(split_scheme("https:"), Some(("https", ""))); +} + +#[test] +fn fragment() { + assert_eq!( + split_fragment(Path::new( + "file:///home/ferris/project/scripts#hash=somehash" + )), + ( + Cow::Owned(PathBuf::from("file:///home/ferris/project/scripts")), + Some("hash=somehash") + ) + ); + assert_eq!( + split_fragment(Path::new("file:home/ferris/project/scripts#hash=somehash")), + ( + Cow::Owned(PathBuf::from("file:home/ferris/project/scripts")), + Some("hash=somehash") + ) + ); + assert_eq!( + split_fragment(Path::new("/home/ferris/project/scripts#hash=somehash")), + ( + Cow::Owned(PathBuf::from("/home/ferris/project/scripts")), + Some("hash=somehash") + ) + ); + assert_eq!( + split_fragment(Path::new("file:///home/ferris/project/scripts")), + ( + Cow::Borrowed(Path::new("file:///home/ferris/project/scripts")), + None + ) + ); + assert_eq!( + split_fragment(Path::new("")), + (Cow::Borrowed(Path::new("")), None) + ); +} diff --git a/crates/uv-performance-flate2-backend/Cargo.toml b/crates/uv-performance-flate2-backend/Cargo.toml index 5dba55cc8..df9e4190b 100644 --- a/crates/uv-performance-flate2-backend/Cargo.toml +++ b/crates/uv-performance-flate2-backend/Cargo.toml @@ -3,6 +3,9 @@ name = "uv-performance-flate2-backend" version = "0.1.0" publish = false +[lib] +doctest = false + [target.'cfg(not(any(target_arch = "s390x", target_arch = "powerpc64")))'.dependencies] flate2 = { version = "1.0.28", default-features = false, features = ["zlib-ng"] } diff --git a/crates/uv-performance-memory-allocator/Cargo.toml b/crates/uv-performance-memory-allocator/Cargo.toml index 50d2a149a..173072875 100644 --- a/crates/uv-performance-memory-allocator/Cargo.toml +++ b/crates/uv-performance-memory-allocator/Cargo.toml @@ -3,6 +3,9 @@ name = "uv-performance-memory-allocator" version = "0.1.0" publish = false +[lib] +doctest = false + [dependencies] [target.'cfg(all(target_os = "windows"))'.dependencies] diff --git a/crates/uv-platform-tags/Cargo.toml b/crates/uv-platform-tags/Cargo.toml index 0879e62ae..add9e4856 100644 --- a/crates/uv-platform-tags/Cargo.toml +++ b/crates/uv-platform-tags/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 67ab80357..ae4f702d5 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -574,1535 +574,4 @@ fn get_mac_binary_formats(arch: Arch) -> Vec { } #[cfg(test)] -mod tests { - use insta::{assert_debug_snapshot, assert_snapshot}; - - use super::*; - - /// Check platform tag ordering. - /// The list is displayed in decreasing priority. - /// - /// A reference list can be generated with: - /// ```text - /// $ python -c "from packaging import tags; [print(tag) for tag in tags.platform_tags()]"` - /// ```` - #[test] - fn test_platform_tags_manylinux() { - let tags = compatible_tags(&Platform::new( - Os::Manylinux { - major: 2, - minor: 20, - }, - Arch::X86_64, - )) - .unwrap(); - assert_debug_snapshot!( - tags, - @r###" - [ - "manylinux_2_20_x86_64", - "manylinux_2_19_x86_64", - "manylinux_2_18_x86_64", - "manylinux_2_17_x86_64", - "manylinux2014_x86_64", - "manylinux_2_16_x86_64", - "manylinux_2_15_x86_64", - "manylinux_2_14_x86_64", - "manylinux_2_13_x86_64", - "manylinux_2_12_x86_64", - "manylinux2010_x86_64", - "manylinux_2_11_x86_64", - "manylinux_2_10_x86_64", - "manylinux_2_9_x86_64", - "manylinux_2_8_x86_64", - "manylinux_2_7_x86_64", - "manylinux_2_6_x86_64", - "manylinux_2_5_x86_64", - "manylinux1_x86_64", - "linux_x86_64", - ] - "### - ); - } - - #[test] - fn test_platform_tags_macos() { - let tags = compatible_tags(&Platform::new( - Os::Macos { - major: 21, - minor: 6, - }, - Arch::X86_64, - )) - .unwrap(); - assert_debug_snapshot!( - tags, - @r###" - [ - "macosx_21_0_x86_64", - "macosx_21_0_intel", - "macosx_21_0_fat64", - "macosx_21_0_fat32", - "macosx_21_0_universal2", - "macosx_21_0_universal", - "macosx_20_0_x86_64", - "macosx_20_0_intel", - "macosx_20_0_fat64", - "macosx_20_0_fat32", - "macosx_20_0_universal2", - "macosx_20_0_universal", - "macosx_19_0_x86_64", - "macosx_19_0_intel", - "macosx_19_0_fat64", - "macosx_19_0_fat32", - "macosx_19_0_universal2", - "macosx_19_0_universal", - "macosx_18_0_x86_64", - "macosx_18_0_intel", - "macosx_18_0_fat64", - "macosx_18_0_fat32", - "macosx_18_0_universal2", - "macosx_18_0_universal", - "macosx_17_0_x86_64", - "macosx_17_0_intel", - "macosx_17_0_fat64", - "macosx_17_0_fat32", - "macosx_17_0_universal2", - "macosx_17_0_universal", - "macosx_16_0_x86_64", - "macosx_16_0_intel", - "macosx_16_0_fat64", - "macosx_16_0_fat32", - "macosx_16_0_universal2", - "macosx_16_0_universal", - "macosx_15_0_x86_64", - "macosx_15_0_intel", - "macosx_15_0_fat64", - "macosx_15_0_fat32", - "macosx_15_0_universal2", - "macosx_15_0_universal", - "macosx_14_0_x86_64", - "macosx_14_0_intel", - "macosx_14_0_fat64", - "macosx_14_0_fat32", - "macosx_14_0_universal2", - "macosx_14_0_universal", - "macosx_13_0_x86_64", - "macosx_13_0_intel", - "macosx_13_0_fat64", - "macosx_13_0_fat32", - "macosx_13_0_universal2", - "macosx_13_0_universal", - "macosx_12_0_x86_64", - "macosx_12_0_intel", - "macosx_12_0_fat64", - "macosx_12_0_fat32", - "macosx_12_0_universal2", - "macosx_12_0_universal", - "macosx_11_0_x86_64", - "macosx_11_0_intel", - "macosx_11_0_fat64", - "macosx_11_0_fat32", - "macosx_11_0_universal2", - "macosx_11_0_universal", - "macosx_10_16_x86_64", - "macosx_10_16_intel", - "macosx_10_16_fat64", - "macosx_10_16_fat32", - "macosx_10_16_universal2", - "macosx_10_16_universal", - "macosx_10_15_x86_64", - "macosx_10_15_intel", - "macosx_10_15_fat64", - "macosx_10_15_fat32", - "macosx_10_15_universal2", - "macosx_10_15_universal", - "macosx_10_14_x86_64", - "macosx_10_14_intel", - "macosx_10_14_fat64", - "macosx_10_14_fat32", - "macosx_10_14_universal2", - "macosx_10_14_universal", - "macosx_10_13_x86_64", - "macosx_10_13_intel", - "macosx_10_13_fat64", - "macosx_10_13_fat32", - "macosx_10_13_universal2", - "macosx_10_13_universal", - "macosx_10_12_x86_64", - "macosx_10_12_intel", - "macosx_10_12_fat64", - "macosx_10_12_fat32", - "macosx_10_12_universal2", - "macosx_10_12_universal", - "macosx_10_11_x86_64", - "macosx_10_11_intel", - "macosx_10_11_fat64", - "macosx_10_11_fat32", - "macosx_10_11_universal2", - "macosx_10_11_universal", - "macosx_10_10_x86_64", - "macosx_10_10_intel", - "macosx_10_10_fat64", - "macosx_10_10_fat32", - "macosx_10_10_universal2", - "macosx_10_10_universal", - "macosx_10_9_x86_64", - "macosx_10_9_intel", - "macosx_10_9_fat64", - "macosx_10_9_fat32", - "macosx_10_9_universal2", - "macosx_10_9_universal", - "macosx_10_8_x86_64", - "macosx_10_8_intel", - "macosx_10_8_fat64", - "macosx_10_8_fat32", - "macosx_10_8_universal2", - "macosx_10_8_universal", - "macosx_10_7_x86_64", - "macosx_10_7_intel", - "macosx_10_7_fat64", - "macosx_10_7_fat32", - "macosx_10_7_universal2", - "macosx_10_7_universal", - "macosx_10_6_x86_64", - "macosx_10_6_intel", - "macosx_10_6_fat64", - "macosx_10_6_fat32", - "macosx_10_6_universal2", - "macosx_10_6_universal", - "macosx_10_5_x86_64", - "macosx_10_5_intel", - "macosx_10_5_fat64", - "macosx_10_5_fat32", - "macosx_10_5_universal2", - "macosx_10_5_universal", - "macosx_10_4_x86_64", - "macosx_10_4_intel", - "macosx_10_4_fat64", - "macosx_10_4_fat32", - "macosx_10_4_universal2", - "macosx_10_4_universal", - ] - "### - ); - - let tags = compatible_tags(&Platform::new( - Os::Macos { - major: 14, - minor: 0, - }, - Arch::X86_64, - )) - .unwrap(); - assert_debug_snapshot!( - tags, - @r###" - [ - "macosx_14_0_x86_64", - "macosx_14_0_intel", - "macosx_14_0_fat64", - "macosx_14_0_fat32", - "macosx_14_0_universal2", - "macosx_14_0_universal", - "macosx_13_0_x86_64", - "macosx_13_0_intel", - "macosx_13_0_fat64", - "macosx_13_0_fat32", - "macosx_13_0_universal2", - "macosx_13_0_universal", - "macosx_12_0_x86_64", - "macosx_12_0_intel", - "macosx_12_0_fat64", - "macosx_12_0_fat32", - "macosx_12_0_universal2", - "macosx_12_0_universal", - "macosx_11_0_x86_64", - "macosx_11_0_intel", - "macosx_11_0_fat64", - "macosx_11_0_fat32", - "macosx_11_0_universal2", - "macosx_11_0_universal", - "macosx_10_16_x86_64", - "macosx_10_16_intel", - "macosx_10_16_fat64", - "macosx_10_16_fat32", - "macosx_10_16_universal2", - "macosx_10_16_universal", - "macosx_10_15_x86_64", - "macosx_10_15_intel", - "macosx_10_15_fat64", - "macosx_10_15_fat32", - "macosx_10_15_universal2", - "macosx_10_15_universal", - "macosx_10_14_x86_64", - "macosx_10_14_intel", - "macosx_10_14_fat64", - "macosx_10_14_fat32", - "macosx_10_14_universal2", - "macosx_10_14_universal", - "macosx_10_13_x86_64", - "macosx_10_13_intel", - "macosx_10_13_fat64", - "macosx_10_13_fat32", - "macosx_10_13_universal2", - "macosx_10_13_universal", - "macosx_10_12_x86_64", - "macosx_10_12_intel", - "macosx_10_12_fat64", - "macosx_10_12_fat32", - "macosx_10_12_universal2", - "macosx_10_12_universal", - "macosx_10_11_x86_64", - "macosx_10_11_intel", - "macosx_10_11_fat64", - "macosx_10_11_fat32", - "macosx_10_11_universal2", - "macosx_10_11_universal", - "macosx_10_10_x86_64", - "macosx_10_10_intel", - "macosx_10_10_fat64", - "macosx_10_10_fat32", - "macosx_10_10_universal2", - "macosx_10_10_universal", - "macosx_10_9_x86_64", - "macosx_10_9_intel", - "macosx_10_9_fat64", - "macosx_10_9_fat32", - "macosx_10_9_universal2", - "macosx_10_9_universal", - "macosx_10_8_x86_64", - "macosx_10_8_intel", - "macosx_10_8_fat64", - "macosx_10_8_fat32", - "macosx_10_8_universal2", - "macosx_10_8_universal", - "macosx_10_7_x86_64", - "macosx_10_7_intel", - "macosx_10_7_fat64", - "macosx_10_7_fat32", - "macosx_10_7_universal2", - "macosx_10_7_universal", - "macosx_10_6_x86_64", - "macosx_10_6_intel", - "macosx_10_6_fat64", - "macosx_10_6_fat32", - "macosx_10_6_universal2", - "macosx_10_6_universal", - "macosx_10_5_x86_64", - "macosx_10_5_intel", - "macosx_10_5_fat64", - "macosx_10_5_fat32", - "macosx_10_5_universal2", - "macosx_10_5_universal", - "macosx_10_4_x86_64", - "macosx_10_4_intel", - "macosx_10_4_fat64", - "macosx_10_4_fat32", - "macosx_10_4_universal2", - "macosx_10_4_universal", - ] - "### - ); - - let tags = compatible_tags(&Platform::new( - Os::Macos { - major: 10, - minor: 6, - }, - Arch::X86_64, - )) - .unwrap(); - assert_debug_snapshot!( - tags, - @r###" - [ - "macosx_10_6_x86_64", - "macosx_10_6_intel", - "macosx_10_6_fat64", - "macosx_10_6_fat32", - "macosx_10_6_universal2", - "macosx_10_6_universal", - "macosx_10_5_x86_64", - "macosx_10_5_intel", - "macosx_10_5_fat64", - "macosx_10_5_fat32", - "macosx_10_5_universal2", - "macosx_10_5_universal", - "macosx_10_4_x86_64", - "macosx_10_4_intel", - "macosx_10_4_fat64", - "macosx_10_4_fat32", - "macosx_10_4_universal2", - "macosx_10_4_universal", - ] - "### - ); - } - - /// Ensure the tags returned do not include the `manylinux` tags - /// when `manylinux_incompatible` is set to `false`. - #[test] - fn test_manylinux_incompatible() { - let tags = Tags::from_env( - &Platform::new( - Os::Manylinux { - major: 2, - minor: 28, - }, - Arch::X86_64, - ), - (3, 9), - "cpython", - (3, 9), - false, - false, - ) - .unwrap(); - assert_snapshot!( - tags, - @r###" - cp39-cp39-linux_x86_64 - cp39-abi3-linux_x86_64 - cp39-none-linux_x86_64 - cp38-abi3-linux_x86_64 - cp37-abi3-linux_x86_64 - cp36-abi3-linux_x86_64 - cp35-abi3-linux_x86_64 - cp34-abi3-linux_x86_64 - cp33-abi3-linux_x86_64 - cp32-abi3-linux_x86_64 - py39-none-linux_x86_64 - py3-none-linux_x86_64 - py38-none-linux_x86_64 - py37-none-linux_x86_64 - py36-none-linux_x86_64 - py35-none-linux_x86_64 - py34-none-linux_x86_64 - py33-none-linux_x86_64 - py32-none-linux_x86_64 - py31-none-linux_x86_64 - py30-none-linux_x86_64 - cp39-none-any - py39-none-any - py3-none-any - py38-none-any - py37-none-any - py36-none-any - py35-none-any - py34-none-any - py33-none-any - py32-none-any - py31-none-any - py30-none-any - "###); - } - - /// Check full tag ordering. - /// The list is displayed in decreasing priority. - /// - /// A reference list can be generated with: - /// ```text - /// $ python -c "from packaging import tags; [print(tag) for tag in tags.sys_tags()]"` - /// ``` - #[test] - fn test_system_tags_manylinux() { - let tags = Tags::from_env( - &Platform::new( - Os::Manylinux { - major: 2, - minor: 28, - }, - Arch::X86_64, - ), - (3, 9), - "cpython", - (3, 9), - true, - false, - ) - .unwrap(); - assert_snapshot!( - tags, - @r###" - cp39-cp39-manylinux_2_28_x86_64 - cp39-cp39-manylinux_2_27_x86_64 - cp39-cp39-manylinux_2_26_x86_64 - cp39-cp39-manylinux_2_25_x86_64 - cp39-cp39-manylinux_2_24_x86_64 - cp39-cp39-manylinux_2_23_x86_64 - cp39-cp39-manylinux_2_22_x86_64 - cp39-cp39-manylinux_2_21_x86_64 - cp39-cp39-manylinux_2_20_x86_64 - cp39-cp39-manylinux_2_19_x86_64 - cp39-cp39-manylinux_2_18_x86_64 - cp39-cp39-manylinux_2_17_x86_64 - cp39-cp39-manylinux2014_x86_64 - cp39-cp39-manylinux_2_16_x86_64 - cp39-cp39-manylinux_2_15_x86_64 - cp39-cp39-manylinux_2_14_x86_64 - cp39-cp39-manylinux_2_13_x86_64 - cp39-cp39-manylinux_2_12_x86_64 - cp39-cp39-manylinux2010_x86_64 - cp39-cp39-manylinux_2_11_x86_64 - cp39-cp39-manylinux_2_10_x86_64 - cp39-cp39-manylinux_2_9_x86_64 - cp39-cp39-manylinux_2_8_x86_64 - cp39-cp39-manylinux_2_7_x86_64 - cp39-cp39-manylinux_2_6_x86_64 - cp39-cp39-manylinux_2_5_x86_64 - cp39-cp39-manylinux1_x86_64 - cp39-cp39-linux_x86_64 - cp39-abi3-manylinux_2_28_x86_64 - cp39-abi3-manylinux_2_27_x86_64 - cp39-abi3-manylinux_2_26_x86_64 - cp39-abi3-manylinux_2_25_x86_64 - cp39-abi3-manylinux_2_24_x86_64 - cp39-abi3-manylinux_2_23_x86_64 - cp39-abi3-manylinux_2_22_x86_64 - cp39-abi3-manylinux_2_21_x86_64 - cp39-abi3-manylinux_2_20_x86_64 - cp39-abi3-manylinux_2_19_x86_64 - cp39-abi3-manylinux_2_18_x86_64 - cp39-abi3-manylinux_2_17_x86_64 - cp39-abi3-manylinux2014_x86_64 - cp39-abi3-manylinux_2_16_x86_64 - cp39-abi3-manylinux_2_15_x86_64 - cp39-abi3-manylinux_2_14_x86_64 - cp39-abi3-manylinux_2_13_x86_64 - cp39-abi3-manylinux_2_12_x86_64 - cp39-abi3-manylinux2010_x86_64 - cp39-abi3-manylinux_2_11_x86_64 - cp39-abi3-manylinux_2_10_x86_64 - cp39-abi3-manylinux_2_9_x86_64 - cp39-abi3-manylinux_2_8_x86_64 - cp39-abi3-manylinux_2_7_x86_64 - cp39-abi3-manylinux_2_6_x86_64 - cp39-abi3-manylinux_2_5_x86_64 - cp39-abi3-manylinux1_x86_64 - cp39-abi3-linux_x86_64 - cp39-none-manylinux_2_28_x86_64 - cp39-none-manylinux_2_27_x86_64 - cp39-none-manylinux_2_26_x86_64 - cp39-none-manylinux_2_25_x86_64 - cp39-none-manylinux_2_24_x86_64 - cp39-none-manylinux_2_23_x86_64 - cp39-none-manylinux_2_22_x86_64 - cp39-none-manylinux_2_21_x86_64 - cp39-none-manylinux_2_20_x86_64 - cp39-none-manylinux_2_19_x86_64 - cp39-none-manylinux_2_18_x86_64 - cp39-none-manylinux_2_17_x86_64 - cp39-none-manylinux2014_x86_64 - cp39-none-manylinux_2_16_x86_64 - cp39-none-manylinux_2_15_x86_64 - cp39-none-manylinux_2_14_x86_64 - cp39-none-manylinux_2_13_x86_64 - cp39-none-manylinux_2_12_x86_64 - cp39-none-manylinux2010_x86_64 - cp39-none-manylinux_2_11_x86_64 - cp39-none-manylinux_2_10_x86_64 - cp39-none-manylinux_2_9_x86_64 - cp39-none-manylinux_2_8_x86_64 - cp39-none-manylinux_2_7_x86_64 - cp39-none-manylinux_2_6_x86_64 - cp39-none-manylinux_2_5_x86_64 - cp39-none-manylinux1_x86_64 - cp39-none-linux_x86_64 - cp38-abi3-manylinux_2_28_x86_64 - cp38-abi3-manylinux_2_27_x86_64 - cp38-abi3-manylinux_2_26_x86_64 - cp38-abi3-manylinux_2_25_x86_64 - cp38-abi3-manylinux_2_24_x86_64 - cp38-abi3-manylinux_2_23_x86_64 - cp38-abi3-manylinux_2_22_x86_64 - cp38-abi3-manylinux_2_21_x86_64 - cp38-abi3-manylinux_2_20_x86_64 - cp38-abi3-manylinux_2_19_x86_64 - cp38-abi3-manylinux_2_18_x86_64 - cp38-abi3-manylinux_2_17_x86_64 - cp38-abi3-manylinux2014_x86_64 - cp38-abi3-manylinux_2_16_x86_64 - cp38-abi3-manylinux_2_15_x86_64 - cp38-abi3-manylinux_2_14_x86_64 - cp38-abi3-manylinux_2_13_x86_64 - cp38-abi3-manylinux_2_12_x86_64 - cp38-abi3-manylinux2010_x86_64 - cp38-abi3-manylinux_2_11_x86_64 - cp38-abi3-manylinux_2_10_x86_64 - cp38-abi3-manylinux_2_9_x86_64 - cp38-abi3-manylinux_2_8_x86_64 - cp38-abi3-manylinux_2_7_x86_64 - cp38-abi3-manylinux_2_6_x86_64 - cp38-abi3-manylinux_2_5_x86_64 - cp38-abi3-manylinux1_x86_64 - cp38-abi3-linux_x86_64 - cp37-abi3-manylinux_2_28_x86_64 - cp37-abi3-manylinux_2_27_x86_64 - cp37-abi3-manylinux_2_26_x86_64 - cp37-abi3-manylinux_2_25_x86_64 - cp37-abi3-manylinux_2_24_x86_64 - cp37-abi3-manylinux_2_23_x86_64 - cp37-abi3-manylinux_2_22_x86_64 - cp37-abi3-manylinux_2_21_x86_64 - cp37-abi3-manylinux_2_20_x86_64 - cp37-abi3-manylinux_2_19_x86_64 - cp37-abi3-manylinux_2_18_x86_64 - cp37-abi3-manylinux_2_17_x86_64 - cp37-abi3-manylinux2014_x86_64 - cp37-abi3-manylinux_2_16_x86_64 - cp37-abi3-manylinux_2_15_x86_64 - cp37-abi3-manylinux_2_14_x86_64 - cp37-abi3-manylinux_2_13_x86_64 - cp37-abi3-manylinux_2_12_x86_64 - cp37-abi3-manylinux2010_x86_64 - cp37-abi3-manylinux_2_11_x86_64 - cp37-abi3-manylinux_2_10_x86_64 - cp37-abi3-manylinux_2_9_x86_64 - cp37-abi3-manylinux_2_8_x86_64 - cp37-abi3-manylinux_2_7_x86_64 - cp37-abi3-manylinux_2_6_x86_64 - cp37-abi3-manylinux_2_5_x86_64 - cp37-abi3-manylinux1_x86_64 - cp37-abi3-linux_x86_64 - cp36-abi3-manylinux_2_28_x86_64 - cp36-abi3-manylinux_2_27_x86_64 - cp36-abi3-manylinux_2_26_x86_64 - cp36-abi3-manylinux_2_25_x86_64 - cp36-abi3-manylinux_2_24_x86_64 - cp36-abi3-manylinux_2_23_x86_64 - cp36-abi3-manylinux_2_22_x86_64 - cp36-abi3-manylinux_2_21_x86_64 - cp36-abi3-manylinux_2_20_x86_64 - cp36-abi3-manylinux_2_19_x86_64 - cp36-abi3-manylinux_2_18_x86_64 - cp36-abi3-manylinux_2_17_x86_64 - cp36-abi3-manylinux2014_x86_64 - cp36-abi3-manylinux_2_16_x86_64 - cp36-abi3-manylinux_2_15_x86_64 - cp36-abi3-manylinux_2_14_x86_64 - cp36-abi3-manylinux_2_13_x86_64 - cp36-abi3-manylinux_2_12_x86_64 - cp36-abi3-manylinux2010_x86_64 - cp36-abi3-manylinux_2_11_x86_64 - cp36-abi3-manylinux_2_10_x86_64 - cp36-abi3-manylinux_2_9_x86_64 - cp36-abi3-manylinux_2_8_x86_64 - cp36-abi3-manylinux_2_7_x86_64 - cp36-abi3-manylinux_2_6_x86_64 - cp36-abi3-manylinux_2_5_x86_64 - cp36-abi3-manylinux1_x86_64 - cp36-abi3-linux_x86_64 - cp35-abi3-manylinux_2_28_x86_64 - cp35-abi3-manylinux_2_27_x86_64 - cp35-abi3-manylinux_2_26_x86_64 - cp35-abi3-manylinux_2_25_x86_64 - cp35-abi3-manylinux_2_24_x86_64 - cp35-abi3-manylinux_2_23_x86_64 - cp35-abi3-manylinux_2_22_x86_64 - cp35-abi3-manylinux_2_21_x86_64 - cp35-abi3-manylinux_2_20_x86_64 - cp35-abi3-manylinux_2_19_x86_64 - cp35-abi3-manylinux_2_18_x86_64 - cp35-abi3-manylinux_2_17_x86_64 - cp35-abi3-manylinux2014_x86_64 - cp35-abi3-manylinux_2_16_x86_64 - cp35-abi3-manylinux_2_15_x86_64 - cp35-abi3-manylinux_2_14_x86_64 - cp35-abi3-manylinux_2_13_x86_64 - cp35-abi3-manylinux_2_12_x86_64 - cp35-abi3-manylinux2010_x86_64 - cp35-abi3-manylinux_2_11_x86_64 - cp35-abi3-manylinux_2_10_x86_64 - cp35-abi3-manylinux_2_9_x86_64 - cp35-abi3-manylinux_2_8_x86_64 - cp35-abi3-manylinux_2_7_x86_64 - cp35-abi3-manylinux_2_6_x86_64 - cp35-abi3-manylinux_2_5_x86_64 - cp35-abi3-manylinux1_x86_64 - cp35-abi3-linux_x86_64 - cp34-abi3-manylinux_2_28_x86_64 - cp34-abi3-manylinux_2_27_x86_64 - cp34-abi3-manylinux_2_26_x86_64 - cp34-abi3-manylinux_2_25_x86_64 - cp34-abi3-manylinux_2_24_x86_64 - cp34-abi3-manylinux_2_23_x86_64 - cp34-abi3-manylinux_2_22_x86_64 - cp34-abi3-manylinux_2_21_x86_64 - cp34-abi3-manylinux_2_20_x86_64 - cp34-abi3-manylinux_2_19_x86_64 - cp34-abi3-manylinux_2_18_x86_64 - cp34-abi3-manylinux_2_17_x86_64 - cp34-abi3-manylinux2014_x86_64 - cp34-abi3-manylinux_2_16_x86_64 - cp34-abi3-manylinux_2_15_x86_64 - cp34-abi3-manylinux_2_14_x86_64 - cp34-abi3-manylinux_2_13_x86_64 - cp34-abi3-manylinux_2_12_x86_64 - cp34-abi3-manylinux2010_x86_64 - cp34-abi3-manylinux_2_11_x86_64 - cp34-abi3-manylinux_2_10_x86_64 - cp34-abi3-manylinux_2_9_x86_64 - cp34-abi3-manylinux_2_8_x86_64 - cp34-abi3-manylinux_2_7_x86_64 - cp34-abi3-manylinux_2_6_x86_64 - cp34-abi3-manylinux_2_5_x86_64 - cp34-abi3-manylinux1_x86_64 - cp34-abi3-linux_x86_64 - cp33-abi3-manylinux_2_28_x86_64 - cp33-abi3-manylinux_2_27_x86_64 - cp33-abi3-manylinux_2_26_x86_64 - cp33-abi3-manylinux_2_25_x86_64 - cp33-abi3-manylinux_2_24_x86_64 - cp33-abi3-manylinux_2_23_x86_64 - cp33-abi3-manylinux_2_22_x86_64 - cp33-abi3-manylinux_2_21_x86_64 - cp33-abi3-manylinux_2_20_x86_64 - cp33-abi3-manylinux_2_19_x86_64 - cp33-abi3-manylinux_2_18_x86_64 - cp33-abi3-manylinux_2_17_x86_64 - cp33-abi3-manylinux2014_x86_64 - cp33-abi3-manylinux_2_16_x86_64 - cp33-abi3-manylinux_2_15_x86_64 - cp33-abi3-manylinux_2_14_x86_64 - cp33-abi3-manylinux_2_13_x86_64 - cp33-abi3-manylinux_2_12_x86_64 - cp33-abi3-manylinux2010_x86_64 - cp33-abi3-manylinux_2_11_x86_64 - cp33-abi3-manylinux_2_10_x86_64 - cp33-abi3-manylinux_2_9_x86_64 - cp33-abi3-manylinux_2_8_x86_64 - cp33-abi3-manylinux_2_7_x86_64 - cp33-abi3-manylinux_2_6_x86_64 - cp33-abi3-manylinux_2_5_x86_64 - cp33-abi3-manylinux1_x86_64 - cp33-abi3-linux_x86_64 - cp32-abi3-manylinux_2_28_x86_64 - cp32-abi3-manylinux_2_27_x86_64 - cp32-abi3-manylinux_2_26_x86_64 - cp32-abi3-manylinux_2_25_x86_64 - cp32-abi3-manylinux_2_24_x86_64 - cp32-abi3-manylinux_2_23_x86_64 - cp32-abi3-manylinux_2_22_x86_64 - cp32-abi3-manylinux_2_21_x86_64 - cp32-abi3-manylinux_2_20_x86_64 - cp32-abi3-manylinux_2_19_x86_64 - cp32-abi3-manylinux_2_18_x86_64 - cp32-abi3-manylinux_2_17_x86_64 - cp32-abi3-manylinux2014_x86_64 - cp32-abi3-manylinux_2_16_x86_64 - cp32-abi3-manylinux_2_15_x86_64 - cp32-abi3-manylinux_2_14_x86_64 - cp32-abi3-manylinux_2_13_x86_64 - cp32-abi3-manylinux_2_12_x86_64 - cp32-abi3-manylinux2010_x86_64 - cp32-abi3-manylinux_2_11_x86_64 - cp32-abi3-manylinux_2_10_x86_64 - cp32-abi3-manylinux_2_9_x86_64 - cp32-abi3-manylinux_2_8_x86_64 - cp32-abi3-manylinux_2_7_x86_64 - cp32-abi3-manylinux_2_6_x86_64 - cp32-abi3-manylinux_2_5_x86_64 - cp32-abi3-manylinux1_x86_64 - cp32-abi3-linux_x86_64 - py39-none-manylinux_2_28_x86_64 - py39-none-manylinux_2_27_x86_64 - py39-none-manylinux_2_26_x86_64 - py39-none-manylinux_2_25_x86_64 - py39-none-manylinux_2_24_x86_64 - py39-none-manylinux_2_23_x86_64 - py39-none-manylinux_2_22_x86_64 - py39-none-manylinux_2_21_x86_64 - py39-none-manylinux_2_20_x86_64 - py39-none-manylinux_2_19_x86_64 - py39-none-manylinux_2_18_x86_64 - py39-none-manylinux_2_17_x86_64 - py39-none-manylinux2014_x86_64 - py39-none-manylinux_2_16_x86_64 - py39-none-manylinux_2_15_x86_64 - py39-none-manylinux_2_14_x86_64 - py39-none-manylinux_2_13_x86_64 - py39-none-manylinux_2_12_x86_64 - py39-none-manylinux2010_x86_64 - py39-none-manylinux_2_11_x86_64 - py39-none-manylinux_2_10_x86_64 - py39-none-manylinux_2_9_x86_64 - py39-none-manylinux_2_8_x86_64 - py39-none-manylinux_2_7_x86_64 - py39-none-manylinux_2_6_x86_64 - py39-none-manylinux_2_5_x86_64 - py39-none-manylinux1_x86_64 - py39-none-linux_x86_64 - py3-none-manylinux_2_28_x86_64 - py3-none-manylinux_2_27_x86_64 - py3-none-manylinux_2_26_x86_64 - py3-none-manylinux_2_25_x86_64 - py3-none-manylinux_2_24_x86_64 - py3-none-manylinux_2_23_x86_64 - py3-none-manylinux_2_22_x86_64 - py3-none-manylinux_2_21_x86_64 - py3-none-manylinux_2_20_x86_64 - py3-none-manylinux_2_19_x86_64 - py3-none-manylinux_2_18_x86_64 - py3-none-manylinux_2_17_x86_64 - py3-none-manylinux2014_x86_64 - py3-none-manylinux_2_16_x86_64 - py3-none-manylinux_2_15_x86_64 - py3-none-manylinux_2_14_x86_64 - py3-none-manylinux_2_13_x86_64 - py3-none-manylinux_2_12_x86_64 - py3-none-manylinux2010_x86_64 - py3-none-manylinux_2_11_x86_64 - py3-none-manylinux_2_10_x86_64 - py3-none-manylinux_2_9_x86_64 - py3-none-manylinux_2_8_x86_64 - py3-none-manylinux_2_7_x86_64 - py3-none-manylinux_2_6_x86_64 - py3-none-manylinux_2_5_x86_64 - py3-none-manylinux1_x86_64 - py3-none-linux_x86_64 - py38-none-manylinux_2_28_x86_64 - py38-none-manylinux_2_27_x86_64 - py38-none-manylinux_2_26_x86_64 - py38-none-manylinux_2_25_x86_64 - py38-none-manylinux_2_24_x86_64 - py38-none-manylinux_2_23_x86_64 - py38-none-manylinux_2_22_x86_64 - py38-none-manylinux_2_21_x86_64 - py38-none-manylinux_2_20_x86_64 - py38-none-manylinux_2_19_x86_64 - py38-none-manylinux_2_18_x86_64 - py38-none-manylinux_2_17_x86_64 - py38-none-manylinux2014_x86_64 - py38-none-manylinux_2_16_x86_64 - py38-none-manylinux_2_15_x86_64 - py38-none-manylinux_2_14_x86_64 - py38-none-manylinux_2_13_x86_64 - py38-none-manylinux_2_12_x86_64 - py38-none-manylinux2010_x86_64 - py38-none-manylinux_2_11_x86_64 - py38-none-manylinux_2_10_x86_64 - py38-none-manylinux_2_9_x86_64 - py38-none-manylinux_2_8_x86_64 - py38-none-manylinux_2_7_x86_64 - py38-none-manylinux_2_6_x86_64 - py38-none-manylinux_2_5_x86_64 - py38-none-manylinux1_x86_64 - py38-none-linux_x86_64 - py37-none-manylinux_2_28_x86_64 - py37-none-manylinux_2_27_x86_64 - py37-none-manylinux_2_26_x86_64 - py37-none-manylinux_2_25_x86_64 - py37-none-manylinux_2_24_x86_64 - py37-none-manylinux_2_23_x86_64 - py37-none-manylinux_2_22_x86_64 - py37-none-manylinux_2_21_x86_64 - py37-none-manylinux_2_20_x86_64 - py37-none-manylinux_2_19_x86_64 - py37-none-manylinux_2_18_x86_64 - py37-none-manylinux_2_17_x86_64 - py37-none-manylinux2014_x86_64 - py37-none-manylinux_2_16_x86_64 - py37-none-manylinux_2_15_x86_64 - py37-none-manylinux_2_14_x86_64 - py37-none-manylinux_2_13_x86_64 - py37-none-manylinux_2_12_x86_64 - py37-none-manylinux2010_x86_64 - py37-none-manylinux_2_11_x86_64 - py37-none-manylinux_2_10_x86_64 - py37-none-manylinux_2_9_x86_64 - py37-none-manylinux_2_8_x86_64 - py37-none-manylinux_2_7_x86_64 - py37-none-manylinux_2_6_x86_64 - py37-none-manylinux_2_5_x86_64 - py37-none-manylinux1_x86_64 - py37-none-linux_x86_64 - py36-none-manylinux_2_28_x86_64 - py36-none-manylinux_2_27_x86_64 - py36-none-manylinux_2_26_x86_64 - py36-none-manylinux_2_25_x86_64 - py36-none-manylinux_2_24_x86_64 - py36-none-manylinux_2_23_x86_64 - py36-none-manylinux_2_22_x86_64 - py36-none-manylinux_2_21_x86_64 - py36-none-manylinux_2_20_x86_64 - py36-none-manylinux_2_19_x86_64 - py36-none-manylinux_2_18_x86_64 - py36-none-manylinux_2_17_x86_64 - py36-none-manylinux2014_x86_64 - py36-none-manylinux_2_16_x86_64 - py36-none-manylinux_2_15_x86_64 - py36-none-manylinux_2_14_x86_64 - py36-none-manylinux_2_13_x86_64 - py36-none-manylinux_2_12_x86_64 - py36-none-manylinux2010_x86_64 - py36-none-manylinux_2_11_x86_64 - py36-none-manylinux_2_10_x86_64 - py36-none-manylinux_2_9_x86_64 - py36-none-manylinux_2_8_x86_64 - py36-none-manylinux_2_7_x86_64 - py36-none-manylinux_2_6_x86_64 - py36-none-manylinux_2_5_x86_64 - py36-none-manylinux1_x86_64 - py36-none-linux_x86_64 - py35-none-manylinux_2_28_x86_64 - py35-none-manylinux_2_27_x86_64 - py35-none-manylinux_2_26_x86_64 - py35-none-manylinux_2_25_x86_64 - py35-none-manylinux_2_24_x86_64 - py35-none-manylinux_2_23_x86_64 - py35-none-manylinux_2_22_x86_64 - py35-none-manylinux_2_21_x86_64 - py35-none-manylinux_2_20_x86_64 - py35-none-manylinux_2_19_x86_64 - py35-none-manylinux_2_18_x86_64 - py35-none-manylinux_2_17_x86_64 - py35-none-manylinux2014_x86_64 - py35-none-manylinux_2_16_x86_64 - py35-none-manylinux_2_15_x86_64 - py35-none-manylinux_2_14_x86_64 - py35-none-manylinux_2_13_x86_64 - py35-none-manylinux_2_12_x86_64 - py35-none-manylinux2010_x86_64 - py35-none-manylinux_2_11_x86_64 - py35-none-manylinux_2_10_x86_64 - py35-none-manylinux_2_9_x86_64 - py35-none-manylinux_2_8_x86_64 - py35-none-manylinux_2_7_x86_64 - py35-none-manylinux_2_6_x86_64 - py35-none-manylinux_2_5_x86_64 - py35-none-manylinux1_x86_64 - py35-none-linux_x86_64 - py34-none-manylinux_2_28_x86_64 - py34-none-manylinux_2_27_x86_64 - py34-none-manylinux_2_26_x86_64 - py34-none-manylinux_2_25_x86_64 - py34-none-manylinux_2_24_x86_64 - py34-none-manylinux_2_23_x86_64 - py34-none-manylinux_2_22_x86_64 - py34-none-manylinux_2_21_x86_64 - py34-none-manylinux_2_20_x86_64 - py34-none-manylinux_2_19_x86_64 - py34-none-manylinux_2_18_x86_64 - py34-none-manylinux_2_17_x86_64 - py34-none-manylinux2014_x86_64 - py34-none-manylinux_2_16_x86_64 - py34-none-manylinux_2_15_x86_64 - py34-none-manylinux_2_14_x86_64 - py34-none-manylinux_2_13_x86_64 - py34-none-manylinux_2_12_x86_64 - py34-none-manylinux2010_x86_64 - py34-none-manylinux_2_11_x86_64 - py34-none-manylinux_2_10_x86_64 - py34-none-manylinux_2_9_x86_64 - py34-none-manylinux_2_8_x86_64 - py34-none-manylinux_2_7_x86_64 - py34-none-manylinux_2_6_x86_64 - py34-none-manylinux_2_5_x86_64 - py34-none-manylinux1_x86_64 - py34-none-linux_x86_64 - py33-none-manylinux_2_28_x86_64 - py33-none-manylinux_2_27_x86_64 - py33-none-manylinux_2_26_x86_64 - py33-none-manylinux_2_25_x86_64 - py33-none-manylinux_2_24_x86_64 - py33-none-manylinux_2_23_x86_64 - py33-none-manylinux_2_22_x86_64 - py33-none-manylinux_2_21_x86_64 - py33-none-manylinux_2_20_x86_64 - py33-none-manylinux_2_19_x86_64 - py33-none-manylinux_2_18_x86_64 - py33-none-manylinux_2_17_x86_64 - py33-none-manylinux2014_x86_64 - py33-none-manylinux_2_16_x86_64 - py33-none-manylinux_2_15_x86_64 - py33-none-manylinux_2_14_x86_64 - py33-none-manylinux_2_13_x86_64 - py33-none-manylinux_2_12_x86_64 - py33-none-manylinux2010_x86_64 - py33-none-manylinux_2_11_x86_64 - py33-none-manylinux_2_10_x86_64 - py33-none-manylinux_2_9_x86_64 - py33-none-manylinux_2_8_x86_64 - py33-none-manylinux_2_7_x86_64 - py33-none-manylinux_2_6_x86_64 - py33-none-manylinux_2_5_x86_64 - py33-none-manylinux1_x86_64 - py33-none-linux_x86_64 - py32-none-manylinux_2_28_x86_64 - py32-none-manylinux_2_27_x86_64 - py32-none-manylinux_2_26_x86_64 - py32-none-manylinux_2_25_x86_64 - py32-none-manylinux_2_24_x86_64 - py32-none-manylinux_2_23_x86_64 - py32-none-manylinux_2_22_x86_64 - py32-none-manylinux_2_21_x86_64 - py32-none-manylinux_2_20_x86_64 - py32-none-manylinux_2_19_x86_64 - py32-none-manylinux_2_18_x86_64 - py32-none-manylinux_2_17_x86_64 - py32-none-manylinux2014_x86_64 - py32-none-manylinux_2_16_x86_64 - py32-none-manylinux_2_15_x86_64 - py32-none-manylinux_2_14_x86_64 - py32-none-manylinux_2_13_x86_64 - py32-none-manylinux_2_12_x86_64 - py32-none-manylinux2010_x86_64 - py32-none-manylinux_2_11_x86_64 - py32-none-manylinux_2_10_x86_64 - py32-none-manylinux_2_9_x86_64 - py32-none-manylinux_2_8_x86_64 - py32-none-manylinux_2_7_x86_64 - py32-none-manylinux_2_6_x86_64 - py32-none-manylinux_2_5_x86_64 - py32-none-manylinux1_x86_64 - py32-none-linux_x86_64 - py31-none-manylinux_2_28_x86_64 - py31-none-manylinux_2_27_x86_64 - py31-none-manylinux_2_26_x86_64 - py31-none-manylinux_2_25_x86_64 - py31-none-manylinux_2_24_x86_64 - py31-none-manylinux_2_23_x86_64 - py31-none-manylinux_2_22_x86_64 - py31-none-manylinux_2_21_x86_64 - py31-none-manylinux_2_20_x86_64 - py31-none-manylinux_2_19_x86_64 - py31-none-manylinux_2_18_x86_64 - py31-none-manylinux_2_17_x86_64 - py31-none-manylinux2014_x86_64 - py31-none-manylinux_2_16_x86_64 - py31-none-manylinux_2_15_x86_64 - py31-none-manylinux_2_14_x86_64 - py31-none-manylinux_2_13_x86_64 - py31-none-manylinux_2_12_x86_64 - py31-none-manylinux2010_x86_64 - py31-none-manylinux_2_11_x86_64 - py31-none-manylinux_2_10_x86_64 - py31-none-manylinux_2_9_x86_64 - py31-none-manylinux_2_8_x86_64 - py31-none-manylinux_2_7_x86_64 - py31-none-manylinux_2_6_x86_64 - py31-none-manylinux_2_5_x86_64 - py31-none-manylinux1_x86_64 - py31-none-linux_x86_64 - py30-none-manylinux_2_28_x86_64 - py30-none-manylinux_2_27_x86_64 - py30-none-manylinux_2_26_x86_64 - py30-none-manylinux_2_25_x86_64 - py30-none-manylinux_2_24_x86_64 - py30-none-manylinux_2_23_x86_64 - py30-none-manylinux_2_22_x86_64 - py30-none-manylinux_2_21_x86_64 - py30-none-manylinux_2_20_x86_64 - py30-none-manylinux_2_19_x86_64 - py30-none-manylinux_2_18_x86_64 - py30-none-manylinux_2_17_x86_64 - py30-none-manylinux2014_x86_64 - py30-none-manylinux_2_16_x86_64 - py30-none-manylinux_2_15_x86_64 - py30-none-manylinux_2_14_x86_64 - py30-none-manylinux_2_13_x86_64 - py30-none-manylinux_2_12_x86_64 - py30-none-manylinux2010_x86_64 - py30-none-manylinux_2_11_x86_64 - py30-none-manylinux_2_10_x86_64 - py30-none-manylinux_2_9_x86_64 - py30-none-manylinux_2_8_x86_64 - py30-none-manylinux_2_7_x86_64 - py30-none-manylinux_2_6_x86_64 - py30-none-manylinux_2_5_x86_64 - py30-none-manylinux1_x86_64 - py30-none-linux_x86_64 - cp39-none-any - py39-none-any - py3-none-any - py38-none-any - py37-none-any - py36-none-any - py35-none-any - py34-none-any - py33-none-any - py32-none-any - py31-none-any - py30-none-any - "### - ); - } - - #[test] - fn test_system_tags_macos() { - let tags = Tags::from_env( - &Platform::new( - Os::Macos { - major: 14, - minor: 0, - }, - Arch::Aarch64, - ), - (3, 9), - "cpython", - (3, 9), - false, - false, - ) - .unwrap(); - assert_snapshot!( - tags, - @r###" - cp39-cp39-macosx_14_0_arm64 - cp39-cp39-macosx_14_0_universal2 - cp39-cp39-macosx_13_0_arm64 - cp39-cp39-macosx_13_0_universal2 - cp39-cp39-macosx_12_0_arm64 - cp39-cp39-macosx_12_0_universal2 - cp39-cp39-macosx_11_0_arm64 - cp39-cp39-macosx_11_0_universal2 - cp39-cp39-macosx_10_16_universal2 - cp39-cp39-macosx_10_15_universal2 - cp39-cp39-macosx_10_14_universal2 - cp39-cp39-macosx_10_13_universal2 - cp39-cp39-macosx_10_12_universal2 - cp39-cp39-macosx_10_11_universal2 - cp39-cp39-macosx_10_10_universal2 - cp39-cp39-macosx_10_9_universal2 - cp39-cp39-macosx_10_8_universal2 - cp39-cp39-macosx_10_7_universal2 - cp39-cp39-macosx_10_6_universal2 - cp39-cp39-macosx_10_5_universal2 - cp39-cp39-macosx_10_4_universal2 - cp39-abi3-macosx_14_0_arm64 - cp39-abi3-macosx_14_0_universal2 - cp39-abi3-macosx_13_0_arm64 - cp39-abi3-macosx_13_0_universal2 - cp39-abi3-macosx_12_0_arm64 - cp39-abi3-macosx_12_0_universal2 - cp39-abi3-macosx_11_0_arm64 - cp39-abi3-macosx_11_0_universal2 - cp39-abi3-macosx_10_16_universal2 - cp39-abi3-macosx_10_15_universal2 - cp39-abi3-macosx_10_14_universal2 - cp39-abi3-macosx_10_13_universal2 - cp39-abi3-macosx_10_12_universal2 - cp39-abi3-macosx_10_11_universal2 - cp39-abi3-macosx_10_10_universal2 - cp39-abi3-macosx_10_9_universal2 - cp39-abi3-macosx_10_8_universal2 - cp39-abi3-macosx_10_7_universal2 - cp39-abi3-macosx_10_6_universal2 - cp39-abi3-macosx_10_5_universal2 - cp39-abi3-macosx_10_4_universal2 - cp39-none-macosx_14_0_arm64 - cp39-none-macosx_14_0_universal2 - cp39-none-macosx_13_0_arm64 - cp39-none-macosx_13_0_universal2 - cp39-none-macosx_12_0_arm64 - cp39-none-macosx_12_0_universal2 - cp39-none-macosx_11_0_arm64 - cp39-none-macosx_11_0_universal2 - cp39-none-macosx_10_16_universal2 - cp39-none-macosx_10_15_universal2 - cp39-none-macosx_10_14_universal2 - cp39-none-macosx_10_13_universal2 - cp39-none-macosx_10_12_universal2 - cp39-none-macosx_10_11_universal2 - cp39-none-macosx_10_10_universal2 - cp39-none-macosx_10_9_universal2 - cp39-none-macosx_10_8_universal2 - cp39-none-macosx_10_7_universal2 - cp39-none-macosx_10_6_universal2 - cp39-none-macosx_10_5_universal2 - cp39-none-macosx_10_4_universal2 - cp38-abi3-macosx_14_0_arm64 - cp38-abi3-macosx_14_0_universal2 - cp38-abi3-macosx_13_0_arm64 - cp38-abi3-macosx_13_0_universal2 - cp38-abi3-macosx_12_0_arm64 - cp38-abi3-macosx_12_0_universal2 - cp38-abi3-macosx_11_0_arm64 - cp38-abi3-macosx_11_0_universal2 - cp38-abi3-macosx_10_16_universal2 - cp38-abi3-macosx_10_15_universal2 - cp38-abi3-macosx_10_14_universal2 - cp38-abi3-macosx_10_13_universal2 - cp38-abi3-macosx_10_12_universal2 - cp38-abi3-macosx_10_11_universal2 - cp38-abi3-macosx_10_10_universal2 - cp38-abi3-macosx_10_9_universal2 - cp38-abi3-macosx_10_8_universal2 - cp38-abi3-macosx_10_7_universal2 - cp38-abi3-macosx_10_6_universal2 - cp38-abi3-macosx_10_5_universal2 - cp38-abi3-macosx_10_4_universal2 - cp37-abi3-macosx_14_0_arm64 - cp37-abi3-macosx_14_0_universal2 - cp37-abi3-macosx_13_0_arm64 - cp37-abi3-macosx_13_0_universal2 - cp37-abi3-macosx_12_0_arm64 - cp37-abi3-macosx_12_0_universal2 - cp37-abi3-macosx_11_0_arm64 - cp37-abi3-macosx_11_0_universal2 - cp37-abi3-macosx_10_16_universal2 - cp37-abi3-macosx_10_15_universal2 - cp37-abi3-macosx_10_14_universal2 - cp37-abi3-macosx_10_13_universal2 - cp37-abi3-macosx_10_12_universal2 - cp37-abi3-macosx_10_11_universal2 - cp37-abi3-macosx_10_10_universal2 - cp37-abi3-macosx_10_9_universal2 - cp37-abi3-macosx_10_8_universal2 - cp37-abi3-macosx_10_7_universal2 - cp37-abi3-macosx_10_6_universal2 - cp37-abi3-macosx_10_5_universal2 - cp37-abi3-macosx_10_4_universal2 - cp36-abi3-macosx_14_0_arm64 - cp36-abi3-macosx_14_0_universal2 - cp36-abi3-macosx_13_0_arm64 - cp36-abi3-macosx_13_0_universal2 - cp36-abi3-macosx_12_0_arm64 - cp36-abi3-macosx_12_0_universal2 - cp36-abi3-macosx_11_0_arm64 - cp36-abi3-macosx_11_0_universal2 - cp36-abi3-macosx_10_16_universal2 - cp36-abi3-macosx_10_15_universal2 - cp36-abi3-macosx_10_14_universal2 - cp36-abi3-macosx_10_13_universal2 - cp36-abi3-macosx_10_12_universal2 - cp36-abi3-macosx_10_11_universal2 - cp36-abi3-macosx_10_10_universal2 - cp36-abi3-macosx_10_9_universal2 - cp36-abi3-macosx_10_8_universal2 - cp36-abi3-macosx_10_7_universal2 - cp36-abi3-macosx_10_6_universal2 - cp36-abi3-macosx_10_5_universal2 - cp36-abi3-macosx_10_4_universal2 - cp35-abi3-macosx_14_0_arm64 - cp35-abi3-macosx_14_0_universal2 - cp35-abi3-macosx_13_0_arm64 - cp35-abi3-macosx_13_0_universal2 - cp35-abi3-macosx_12_0_arm64 - cp35-abi3-macosx_12_0_universal2 - cp35-abi3-macosx_11_0_arm64 - cp35-abi3-macosx_11_0_universal2 - cp35-abi3-macosx_10_16_universal2 - cp35-abi3-macosx_10_15_universal2 - cp35-abi3-macosx_10_14_universal2 - cp35-abi3-macosx_10_13_universal2 - cp35-abi3-macosx_10_12_universal2 - cp35-abi3-macosx_10_11_universal2 - cp35-abi3-macosx_10_10_universal2 - cp35-abi3-macosx_10_9_universal2 - cp35-abi3-macosx_10_8_universal2 - cp35-abi3-macosx_10_7_universal2 - cp35-abi3-macosx_10_6_universal2 - cp35-abi3-macosx_10_5_universal2 - cp35-abi3-macosx_10_4_universal2 - cp34-abi3-macosx_14_0_arm64 - cp34-abi3-macosx_14_0_universal2 - cp34-abi3-macosx_13_0_arm64 - cp34-abi3-macosx_13_0_universal2 - cp34-abi3-macosx_12_0_arm64 - cp34-abi3-macosx_12_0_universal2 - cp34-abi3-macosx_11_0_arm64 - cp34-abi3-macosx_11_0_universal2 - cp34-abi3-macosx_10_16_universal2 - cp34-abi3-macosx_10_15_universal2 - cp34-abi3-macosx_10_14_universal2 - cp34-abi3-macosx_10_13_universal2 - cp34-abi3-macosx_10_12_universal2 - cp34-abi3-macosx_10_11_universal2 - cp34-abi3-macosx_10_10_universal2 - cp34-abi3-macosx_10_9_universal2 - cp34-abi3-macosx_10_8_universal2 - cp34-abi3-macosx_10_7_universal2 - cp34-abi3-macosx_10_6_universal2 - cp34-abi3-macosx_10_5_universal2 - cp34-abi3-macosx_10_4_universal2 - cp33-abi3-macosx_14_0_arm64 - cp33-abi3-macosx_14_0_universal2 - cp33-abi3-macosx_13_0_arm64 - cp33-abi3-macosx_13_0_universal2 - cp33-abi3-macosx_12_0_arm64 - cp33-abi3-macosx_12_0_universal2 - cp33-abi3-macosx_11_0_arm64 - cp33-abi3-macosx_11_0_universal2 - cp33-abi3-macosx_10_16_universal2 - cp33-abi3-macosx_10_15_universal2 - cp33-abi3-macosx_10_14_universal2 - cp33-abi3-macosx_10_13_universal2 - cp33-abi3-macosx_10_12_universal2 - cp33-abi3-macosx_10_11_universal2 - cp33-abi3-macosx_10_10_universal2 - cp33-abi3-macosx_10_9_universal2 - cp33-abi3-macosx_10_8_universal2 - cp33-abi3-macosx_10_7_universal2 - cp33-abi3-macosx_10_6_universal2 - cp33-abi3-macosx_10_5_universal2 - cp33-abi3-macosx_10_4_universal2 - cp32-abi3-macosx_14_0_arm64 - cp32-abi3-macosx_14_0_universal2 - cp32-abi3-macosx_13_0_arm64 - cp32-abi3-macosx_13_0_universal2 - cp32-abi3-macosx_12_0_arm64 - cp32-abi3-macosx_12_0_universal2 - cp32-abi3-macosx_11_0_arm64 - cp32-abi3-macosx_11_0_universal2 - cp32-abi3-macosx_10_16_universal2 - cp32-abi3-macosx_10_15_universal2 - cp32-abi3-macosx_10_14_universal2 - cp32-abi3-macosx_10_13_universal2 - cp32-abi3-macosx_10_12_universal2 - cp32-abi3-macosx_10_11_universal2 - cp32-abi3-macosx_10_10_universal2 - cp32-abi3-macosx_10_9_universal2 - cp32-abi3-macosx_10_8_universal2 - cp32-abi3-macosx_10_7_universal2 - cp32-abi3-macosx_10_6_universal2 - cp32-abi3-macosx_10_5_universal2 - cp32-abi3-macosx_10_4_universal2 - py39-none-macosx_14_0_arm64 - py39-none-macosx_14_0_universal2 - py39-none-macosx_13_0_arm64 - py39-none-macosx_13_0_universal2 - py39-none-macosx_12_0_arm64 - py39-none-macosx_12_0_universal2 - py39-none-macosx_11_0_arm64 - py39-none-macosx_11_0_universal2 - py39-none-macosx_10_16_universal2 - py39-none-macosx_10_15_universal2 - py39-none-macosx_10_14_universal2 - py39-none-macosx_10_13_universal2 - py39-none-macosx_10_12_universal2 - py39-none-macosx_10_11_universal2 - py39-none-macosx_10_10_universal2 - py39-none-macosx_10_9_universal2 - py39-none-macosx_10_8_universal2 - py39-none-macosx_10_7_universal2 - py39-none-macosx_10_6_universal2 - py39-none-macosx_10_5_universal2 - py39-none-macosx_10_4_universal2 - py3-none-macosx_14_0_arm64 - py3-none-macosx_14_0_universal2 - py3-none-macosx_13_0_arm64 - py3-none-macosx_13_0_universal2 - py3-none-macosx_12_0_arm64 - py3-none-macosx_12_0_universal2 - py3-none-macosx_11_0_arm64 - py3-none-macosx_11_0_universal2 - py3-none-macosx_10_16_universal2 - py3-none-macosx_10_15_universal2 - py3-none-macosx_10_14_universal2 - py3-none-macosx_10_13_universal2 - py3-none-macosx_10_12_universal2 - py3-none-macosx_10_11_universal2 - py3-none-macosx_10_10_universal2 - py3-none-macosx_10_9_universal2 - py3-none-macosx_10_8_universal2 - py3-none-macosx_10_7_universal2 - py3-none-macosx_10_6_universal2 - py3-none-macosx_10_5_universal2 - py3-none-macosx_10_4_universal2 - py38-none-macosx_14_0_arm64 - py38-none-macosx_14_0_universal2 - py38-none-macosx_13_0_arm64 - py38-none-macosx_13_0_universal2 - py38-none-macosx_12_0_arm64 - py38-none-macosx_12_0_universal2 - py38-none-macosx_11_0_arm64 - py38-none-macosx_11_0_universal2 - py38-none-macosx_10_16_universal2 - py38-none-macosx_10_15_universal2 - py38-none-macosx_10_14_universal2 - py38-none-macosx_10_13_universal2 - py38-none-macosx_10_12_universal2 - py38-none-macosx_10_11_universal2 - py38-none-macosx_10_10_universal2 - py38-none-macosx_10_9_universal2 - py38-none-macosx_10_8_universal2 - py38-none-macosx_10_7_universal2 - py38-none-macosx_10_6_universal2 - py38-none-macosx_10_5_universal2 - py38-none-macosx_10_4_universal2 - py37-none-macosx_14_0_arm64 - py37-none-macosx_14_0_universal2 - py37-none-macosx_13_0_arm64 - py37-none-macosx_13_0_universal2 - py37-none-macosx_12_0_arm64 - py37-none-macosx_12_0_universal2 - py37-none-macosx_11_0_arm64 - py37-none-macosx_11_0_universal2 - py37-none-macosx_10_16_universal2 - py37-none-macosx_10_15_universal2 - py37-none-macosx_10_14_universal2 - py37-none-macosx_10_13_universal2 - py37-none-macosx_10_12_universal2 - py37-none-macosx_10_11_universal2 - py37-none-macosx_10_10_universal2 - py37-none-macosx_10_9_universal2 - py37-none-macosx_10_8_universal2 - py37-none-macosx_10_7_universal2 - py37-none-macosx_10_6_universal2 - py37-none-macosx_10_5_universal2 - py37-none-macosx_10_4_universal2 - py36-none-macosx_14_0_arm64 - py36-none-macosx_14_0_universal2 - py36-none-macosx_13_0_arm64 - py36-none-macosx_13_0_universal2 - py36-none-macosx_12_0_arm64 - py36-none-macosx_12_0_universal2 - py36-none-macosx_11_0_arm64 - py36-none-macosx_11_0_universal2 - py36-none-macosx_10_16_universal2 - py36-none-macosx_10_15_universal2 - py36-none-macosx_10_14_universal2 - py36-none-macosx_10_13_universal2 - py36-none-macosx_10_12_universal2 - py36-none-macosx_10_11_universal2 - py36-none-macosx_10_10_universal2 - py36-none-macosx_10_9_universal2 - py36-none-macosx_10_8_universal2 - py36-none-macosx_10_7_universal2 - py36-none-macosx_10_6_universal2 - py36-none-macosx_10_5_universal2 - py36-none-macosx_10_4_universal2 - py35-none-macosx_14_0_arm64 - py35-none-macosx_14_0_universal2 - py35-none-macosx_13_0_arm64 - py35-none-macosx_13_0_universal2 - py35-none-macosx_12_0_arm64 - py35-none-macosx_12_0_universal2 - py35-none-macosx_11_0_arm64 - py35-none-macosx_11_0_universal2 - py35-none-macosx_10_16_universal2 - py35-none-macosx_10_15_universal2 - py35-none-macosx_10_14_universal2 - py35-none-macosx_10_13_universal2 - py35-none-macosx_10_12_universal2 - py35-none-macosx_10_11_universal2 - py35-none-macosx_10_10_universal2 - py35-none-macosx_10_9_universal2 - py35-none-macosx_10_8_universal2 - py35-none-macosx_10_7_universal2 - py35-none-macosx_10_6_universal2 - py35-none-macosx_10_5_universal2 - py35-none-macosx_10_4_universal2 - py34-none-macosx_14_0_arm64 - py34-none-macosx_14_0_universal2 - py34-none-macosx_13_0_arm64 - py34-none-macosx_13_0_universal2 - py34-none-macosx_12_0_arm64 - py34-none-macosx_12_0_universal2 - py34-none-macosx_11_0_arm64 - py34-none-macosx_11_0_universal2 - py34-none-macosx_10_16_universal2 - py34-none-macosx_10_15_universal2 - py34-none-macosx_10_14_universal2 - py34-none-macosx_10_13_universal2 - py34-none-macosx_10_12_universal2 - py34-none-macosx_10_11_universal2 - py34-none-macosx_10_10_universal2 - py34-none-macosx_10_9_universal2 - py34-none-macosx_10_8_universal2 - py34-none-macosx_10_7_universal2 - py34-none-macosx_10_6_universal2 - py34-none-macosx_10_5_universal2 - py34-none-macosx_10_4_universal2 - py33-none-macosx_14_0_arm64 - py33-none-macosx_14_0_universal2 - py33-none-macosx_13_0_arm64 - py33-none-macosx_13_0_universal2 - py33-none-macosx_12_0_arm64 - py33-none-macosx_12_0_universal2 - py33-none-macosx_11_0_arm64 - py33-none-macosx_11_0_universal2 - py33-none-macosx_10_16_universal2 - py33-none-macosx_10_15_universal2 - py33-none-macosx_10_14_universal2 - py33-none-macosx_10_13_universal2 - py33-none-macosx_10_12_universal2 - py33-none-macosx_10_11_universal2 - py33-none-macosx_10_10_universal2 - py33-none-macosx_10_9_universal2 - py33-none-macosx_10_8_universal2 - py33-none-macosx_10_7_universal2 - py33-none-macosx_10_6_universal2 - py33-none-macosx_10_5_universal2 - py33-none-macosx_10_4_universal2 - py32-none-macosx_14_0_arm64 - py32-none-macosx_14_0_universal2 - py32-none-macosx_13_0_arm64 - py32-none-macosx_13_0_universal2 - py32-none-macosx_12_0_arm64 - py32-none-macosx_12_0_universal2 - py32-none-macosx_11_0_arm64 - py32-none-macosx_11_0_universal2 - py32-none-macosx_10_16_universal2 - py32-none-macosx_10_15_universal2 - py32-none-macosx_10_14_universal2 - py32-none-macosx_10_13_universal2 - py32-none-macosx_10_12_universal2 - py32-none-macosx_10_11_universal2 - py32-none-macosx_10_10_universal2 - py32-none-macosx_10_9_universal2 - py32-none-macosx_10_8_universal2 - py32-none-macosx_10_7_universal2 - py32-none-macosx_10_6_universal2 - py32-none-macosx_10_5_universal2 - py32-none-macosx_10_4_universal2 - py31-none-macosx_14_0_arm64 - py31-none-macosx_14_0_universal2 - py31-none-macosx_13_0_arm64 - py31-none-macosx_13_0_universal2 - py31-none-macosx_12_0_arm64 - py31-none-macosx_12_0_universal2 - py31-none-macosx_11_0_arm64 - py31-none-macosx_11_0_universal2 - py31-none-macosx_10_16_universal2 - py31-none-macosx_10_15_universal2 - py31-none-macosx_10_14_universal2 - py31-none-macosx_10_13_universal2 - py31-none-macosx_10_12_universal2 - py31-none-macosx_10_11_universal2 - py31-none-macosx_10_10_universal2 - py31-none-macosx_10_9_universal2 - py31-none-macosx_10_8_universal2 - py31-none-macosx_10_7_universal2 - py31-none-macosx_10_6_universal2 - py31-none-macosx_10_5_universal2 - py31-none-macosx_10_4_universal2 - py30-none-macosx_14_0_arm64 - py30-none-macosx_14_0_universal2 - py30-none-macosx_13_0_arm64 - py30-none-macosx_13_0_universal2 - py30-none-macosx_12_0_arm64 - py30-none-macosx_12_0_universal2 - py30-none-macosx_11_0_arm64 - py30-none-macosx_11_0_universal2 - py30-none-macosx_10_16_universal2 - py30-none-macosx_10_15_universal2 - py30-none-macosx_10_14_universal2 - py30-none-macosx_10_13_universal2 - py30-none-macosx_10_12_universal2 - py30-none-macosx_10_11_universal2 - py30-none-macosx_10_10_universal2 - py30-none-macosx_10_9_universal2 - py30-none-macosx_10_8_universal2 - py30-none-macosx_10_7_universal2 - py30-none-macosx_10_6_universal2 - py30-none-macosx_10_5_universal2 - py30-none-macosx_10_4_universal2 - cp39-none-any - py39-none-any - py3-none-any - py38-none-any - py37-none-any - py36-none-any - py35-none-any - py34-none-any - py33-none-any - py32-none-any - py31-none-any - py30-none-any - "### - ); - } -} +mod tests; diff --git a/crates/uv-platform-tags/src/tags/tests.rs b/crates/uv-platform-tags/src/tags/tests.rs new file mode 100644 index 000000000..fca79b948 --- /dev/null +++ b/crates/uv-platform-tags/src/tags/tests.rs @@ -0,0 +1,1530 @@ +use insta::{assert_debug_snapshot, assert_snapshot}; + +use super::*; + +/// Check platform tag ordering. +/// The list is displayed in decreasing priority. +/// +/// A reference list can be generated with: +/// ```text +/// $ python -c "from packaging import tags; [print(tag) for tag in tags.platform_tags()]"` +/// ```` +#[test] +fn test_platform_tags_manylinux() { + let tags = compatible_tags(&Platform::new( + Os::Manylinux { + major: 2, + minor: 20, + }, + Arch::X86_64, + )) + .unwrap(); + assert_debug_snapshot!( + tags, + @r###" + [ + "manylinux_2_20_x86_64", + "manylinux_2_19_x86_64", + "manylinux_2_18_x86_64", + "manylinux_2_17_x86_64", + "manylinux2014_x86_64", + "manylinux_2_16_x86_64", + "manylinux_2_15_x86_64", + "manylinux_2_14_x86_64", + "manylinux_2_13_x86_64", + "manylinux_2_12_x86_64", + "manylinux2010_x86_64", + "manylinux_2_11_x86_64", + "manylinux_2_10_x86_64", + "manylinux_2_9_x86_64", + "manylinux_2_8_x86_64", + "manylinux_2_7_x86_64", + "manylinux_2_6_x86_64", + "manylinux_2_5_x86_64", + "manylinux1_x86_64", + "linux_x86_64", + ] + "### + ); +} + +#[test] +fn test_platform_tags_macos() { + let tags = compatible_tags(&Platform::new( + Os::Macos { + major: 21, + minor: 6, + }, + Arch::X86_64, + )) + .unwrap(); + assert_debug_snapshot!( + tags, + @r###" + [ + "macosx_21_0_x86_64", + "macosx_21_0_intel", + "macosx_21_0_fat64", + "macosx_21_0_fat32", + "macosx_21_0_universal2", + "macosx_21_0_universal", + "macosx_20_0_x86_64", + "macosx_20_0_intel", + "macosx_20_0_fat64", + "macosx_20_0_fat32", + "macosx_20_0_universal2", + "macosx_20_0_universal", + "macosx_19_0_x86_64", + "macosx_19_0_intel", + "macosx_19_0_fat64", + "macosx_19_0_fat32", + "macosx_19_0_universal2", + "macosx_19_0_universal", + "macosx_18_0_x86_64", + "macosx_18_0_intel", + "macosx_18_0_fat64", + "macosx_18_0_fat32", + "macosx_18_0_universal2", + "macosx_18_0_universal", + "macosx_17_0_x86_64", + "macosx_17_0_intel", + "macosx_17_0_fat64", + "macosx_17_0_fat32", + "macosx_17_0_universal2", + "macosx_17_0_universal", + "macosx_16_0_x86_64", + "macosx_16_0_intel", + "macosx_16_0_fat64", + "macosx_16_0_fat32", + "macosx_16_0_universal2", + "macosx_16_0_universal", + "macosx_15_0_x86_64", + "macosx_15_0_intel", + "macosx_15_0_fat64", + "macosx_15_0_fat32", + "macosx_15_0_universal2", + "macosx_15_0_universal", + "macosx_14_0_x86_64", + "macosx_14_0_intel", + "macosx_14_0_fat64", + "macosx_14_0_fat32", + "macosx_14_0_universal2", + "macosx_14_0_universal", + "macosx_13_0_x86_64", + "macosx_13_0_intel", + "macosx_13_0_fat64", + "macosx_13_0_fat32", + "macosx_13_0_universal2", + "macosx_13_0_universal", + "macosx_12_0_x86_64", + "macosx_12_0_intel", + "macosx_12_0_fat64", + "macosx_12_0_fat32", + "macosx_12_0_universal2", + "macosx_12_0_universal", + "macosx_11_0_x86_64", + "macosx_11_0_intel", + "macosx_11_0_fat64", + "macosx_11_0_fat32", + "macosx_11_0_universal2", + "macosx_11_0_universal", + "macosx_10_16_x86_64", + "macosx_10_16_intel", + "macosx_10_16_fat64", + "macosx_10_16_fat32", + "macosx_10_16_universal2", + "macosx_10_16_universal", + "macosx_10_15_x86_64", + "macosx_10_15_intel", + "macosx_10_15_fat64", + "macosx_10_15_fat32", + "macosx_10_15_universal2", + "macosx_10_15_universal", + "macosx_10_14_x86_64", + "macosx_10_14_intel", + "macosx_10_14_fat64", + "macosx_10_14_fat32", + "macosx_10_14_universal2", + "macosx_10_14_universal", + "macosx_10_13_x86_64", + "macosx_10_13_intel", + "macosx_10_13_fat64", + "macosx_10_13_fat32", + "macosx_10_13_universal2", + "macosx_10_13_universal", + "macosx_10_12_x86_64", + "macosx_10_12_intel", + "macosx_10_12_fat64", + "macosx_10_12_fat32", + "macosx_10_12_universal2", + "macosx_10_12_universal", + "macosx_10_11_x86_64", + "macosx_10_11_intel", + "macosx_10_11_fat64", + "macosx_10_11_fat32", + "macosx_10_11_universal2", + "macosx_10_11_universal", + "macosx_10_10_x86_64", + "macosx_10_10_intel", + "macosx_10_10_fat64", + "macosx_10_10_fat32", + "macosx_10_10_universal2", + "macosx_10_10_universal", + "macosx_10_9_x86_64", + "macosx_10_9_intel", + "macosx_10_9_fat64", + "macosx_10_9_fat32", + "macosx_10_9_universal2", + "macosx_10_9_universal", + "macosx_10_8_x86_64", + "macosx_10_8_intel", + "macosx_10_8_fat64", + "macosx_10_8_fat32", + "macosx_10_8_universal2", + "macosx_10_8_universal", + "macosx_10_7_x86_64", + "macosx_10_7_intel", + "macosx_10_7_fat64", + "macosx_10_7_fat32", + "macosx_10_7_universal2", + "macosx_10_7_universal", + "macosx_10_6_x86_64", + "macosx_10_6_intel", + "macosx_10_6_fat64", + "macosx_10_6_fat32", + "macosx_10_6_universal2", + "macosx_10_6_universal", + "macosx_10_5_x86_64", + "macosx_10_5_intel", + "macosx_10_5_fat64", + "macosx_10_5_fat32", + "macosx_10_5_universal2", + "macosx_10_5_universal", + "macosx_10_4_x86_64", + "macosx_10_4_intel", + "macosx_10_4_fat64", + "macosx_10_4_fat32", + "macosx_10_4_universal2", + "macosx_10_4_universal", + ] + "### + ); + + let tags = compatible_tags(&Platform::new( + Os::Macos { + major: 14, + minor: 0, + }, + Arch::X86_64, + )) + .unwrap(); + assert_debug_snapshot!( + tags, + @r###" + [ + "macosx_14_0_x86_64", + "macosx_14_0_intel", + "macosx_14_0_fat64", + "macosx_14_0_fat32", + "macosx_14_0_universal2", + "macosx_14_0_universal", + "macosx_13_0_x86_64", + "macosx_13_0_intel", + "macosx_13_0_fat64", + "macosx_13_0_fat32", + "macosx_13_0_universal2", + "macosx_13_0_universal", + "macosx_12_0_x86_64", + "macosx_12_0_intel", + "macosx_12_0_fat64", + "macosx_12_0_fat32", + "macosx_12_0_universal2", + "macosx_12_0_universal", + "macosx_11_0_x86_64", + "macosx_11_0_intel", + "macosx_11_0_fat64", + "macosx_11_0_fat32", + "macosx_11_0_universal2", + "macosx_11_0_universal", + "macosx_10_16_x86_64", + "macosx_10_16_intel", + "macosx_10_16_fat64", + "macosx_10_16_fat32", + "macosx_10_16_universal2", + "macosx_10_16_universal", + "macosx_10_15_x86_64", + "macosx_10_15_intel", + "macosx_10_15_fat64", + "macosx_10_15_fat32", + "macosx_10_15_universal2", + "macosx_10_15_universal", + "macosx_10_14_x86_64", + "macosx_10_14_intel", + "macosx_10_14_fat64", + "macosx_10_14_fat32", + "macosx_10_14_universal2", + "macosx_10_14_universal", + "macosx_10_13_x86_64", + "macosx_10_13_intel", + "macosx_10_13_fat64", + "macosx_10_13_fat32", + "macosx_10_13_universal2", + "macosx_10_13_universal", + "macosx_10_12_x86_64", + "macosx_10_12_intel", + "macosx_10_12_fat64", + "macosx_10_12_fat32", + "macosx_10_12_universal2", + "macosx_10_12_universal", + "macosx_10_11_x86_64", + "macosx_10_11_intel", + "macosx_10_11_fat64", + "macosx_10_11_fat32", + "macosx_10_11_universal2", + "macosx_10_11_universal", + "macosx_10_10_x86_64", + "macosx_10_10_intel", + "macosx_10_10_fat64", + "macosx_10_10_fat32", + "macosx_10_10_universal2", + "macosx_10_10_universal", + "macosx_10_9_x86_64", + "macosx_10_9_intel", + "macosx_10_9_fat64", + "macosx_10_9_fat32", + "macosx_10_9_universal2", + "macosx_10_9_universal", + "macosx_10_8_x86_64", + "macosx_10_8_intel", + "macosx_10_8_fat64", + "macosx_10_8_fat32", + "macosx_10_8_universal2", + "macosx_10_8_universal", + "macosx_10_7_x86_64", + "macosx_10_7_intel", + "macosx_10_7_fat64", + "macosx_10_7_fat32", + "macosx_10_7_universal2", + "macosx_10_7_universal", + "macosx_10_6_x86_64", + "macosx_10_6_intel", + "macosx_10_6_fat64", + "macosx_10_6_fat32", + "macosx_10_6_universal2", + "macosx_10_6_universal", + "macosx_10_5_x86_64", + "macosx_10_5_intel", + "macosx_10_5_fat64", + "macosx_10_5_fat32", + "macosx_10_5_universal2", + "macosx_10_5_universal", + "macosx_10_4_x86_64", + "macosx_10_4_intel", + "macosx_10_4_fat64", + "macosx_10_4_fat32", + "macosx_10_4_universal2", + "macosx_10_4_universal", + ] + "### + ); + + let tags = compatible_tags(&Platform::new( + Os::Macos { + major: 10, + minor: 6, + }, + Arch::X86_64, + )) + .unwrap(); + assert_debug_snapshot!( + tags, + @r###" + [ + "macosx_10_6_x86_64", + "macosx_10_6_intel", + "macosx_10_6_fat64", + "macosx_10_6_fat32", + "macosx_10_6_universal2", + "macosx_10_6_universal", + "macosx_10_5_x86_64", + "macosx_10_5_intel", + "macosx_10_5_fat64", + "macosx_10_5_fat32", + "macosx_10_5_universal2", + "macosx_10_5_universal", + "macosx_10_4_x86_64", + "macosx_10_4_intel", + "macosx_10_4_fat64", + "macosx_10_4_fat32", + "macosx_10_4_universal2", + "macosx_10_4_universal", + ] + "### + ); +} + +/// Ensure the tags returned do not include the `manylinux` tags +/// when `manylinux_incompatible` is set to `false`. +#[test] +fn test_manylinux_incompatible() { + let tags = Tags::from_env( + &Platform::new( + Os::Manylinux { + major: 2, + minor: 28, + }, + Arch::X86_64, + ), + (3, 9), + "cpython", + (3, 9), + false, + false, + ) + .unwrap(); + assert_snapshot!( + tags, + @r###" + cp39-cp39-linux_x86_64 + cp39-abi3-linux_x86_64 + cp39-none-linux_x86_64 + cp38-abi3-linux_x86_64 + cp37-abi3-linux_x86_64 + cp36-abi3-linux_x86_64 + cp35-abi3-linux_x86_64 + cp34-abi3-linux_x86_64 + cp33-abi3-linux_x86_64 + cp32-abi3-linux_x86_64 + py39-none-linux_x86_64 + py3-none-linux_x86_64 + py38-none-linux_x86_64 + py37-none-linux_x86_64 + py36-none-linux_x86_64 + py35-none-linux_x86_64 + py34-none-linux_x86_64 + py33-none-linux_x86_64 + py32-none-linux_x86_64 + py31-none-linux_x86_64 + py30-none-linux_x86_64 + cp39-none-any + py39-none-any + py3-none-any + py38-none-any + py37-none-any + py36-none-any + py35-none-any + py34-none-any + py33-none-any + py32-none-any + py31-none-any + py30-none-any + "###); +} + +/// Check full tag ordering. +/// The list is displayed in decreasing priority. +/// +/// A reference list can be generated with: +/// ```text +/// $ python -c "from packaging import tags; [print(tag) for tag in tags.sys_tags()]"` +/// ``` +#[test] +fn test_system_tags_manylinux() { + let tags = Tags::from_env( + &Platform::new( + Os::Manylinux { + major: 2, + minor: 28, + }, + Arch::X86_64, + ), + (3, 9), + "cpython", + (3, 9), + true, + false, + ) + .unwrap(); + assert_snapshot!( + tags, + @r###" + cp39-cp39-manylinux_2_28_x86_64 + cp39-cp39-manylinux_2_27_x86_64 + cp39-cp39-manylinux_2_26_x86_64 + cp39-cp39-manylinux_2_25_x86_64 + cp39-cp39-manylinux_2_24_x86_64 + cp39-cp39-manylinux_2_23_x86_64 + cp39-cp39-manylinux_2_22_x86_64 + cp39-cp39-manylinux_2_21_x86_64 + cp39-cp39-manylinux_2_20_x86_64 + cp39-cp39-manylinux_2_19_x86_64 + cp39-cp39-manylinux_2_18_x86_64 + cp39-cp39-manylinux_2_17_x86_64 + cp39-cp39-manylinux2014_x86_64 + cp39-cp39-manylinux_2_16_x86_64 + cp39-cp39-manylinux_2_15_x86_64 + cp39-cp39-manylinux_2_14_x86_64 + cp39-cp39-manylinux_2_13_x86_64 + cp39-cp39-manylinux_2_12_x86_64 + cp39-cp39-manylinux2010_x86_64 + cp39-cp39-manylinux_2_11_x86_64 + cp39-cp39-manylinux_2_10_x86_64 + cp39-cp39-manylinux_2_9_x86_64 + cp39-cp39-manylinux_2_8_x86_64 + cp39-cp39-manylinux_2_7_x86_64 + cp39-cp39-manylinux_2_6_x86_64 + cp39-cp39-manylinux_2_5_x86_64 + cp39-cp39-manylinux1_x86_64 + cp39-cp39-linux_x86_64 + cp39-abi3-manylinux_2_28_x86_64 + cp39-abi3-manylinux_2_27_x86_64 + cp39-abi3-manylinux_2_26_x86_64 + cp39-abi3-manylinux_2_25_x86_64 + cp39-abi3-manylinux_2_24_x86_64 + cp39-abi3-manylinux_2_23_x86_64 + cp39-abi3-manylinux_2_22_x86_64 + cp39-abi3-manylinux_2_21_x86_64 + cp39-abi3-manylinux_2_20_x86_64 + cp39-abi3-manylinux_2_19_x86_64 + cp39-abi3-manylinux_2_18_x86_64 + cp39-abi3-manylinux_2_17_x86_64 + cp39-abi3-manylinux2014_x86_64 + cp39-abi3-manylinux_2_16_x86_64 + cp39-abi3-manylinux_2_15_x86_64 + cp39-abi3-manylinux_2_14_x86_64 + cp39-abi3-manylinux_2_13_x86_64 + cp39-abi3-manylinux_2_12_x86_64 + cp39-abi3-manylinux2010_x86_64 + cp39-abi3-manylinux_2_11_x86_64 + cp39-abi3-manylinux_2_10_x86_64 + cp39-abi3-manylinux_2_9_x86_64 + cp39-abi3-manylinux_2_8_x86_64 + cp39-abi3-manylinux_2_7_x86_64 + cp39-abi3-manylinux_2_6_x86_64 + cp39-abi3-manylinux_2_5_x86_64 + cp39-abi3-manylinux1_x86_64 + cp39-abi3-linux_x86_64 + cp39-none-manylinux_2_28_x86_64 + cp39-none-manylinux_2_27_x86_64 + cp39-none-manylinux_2_26_x86_64 + cp39-none-manylinux_2_25_x86_64 + cp39-none-manylinux_2_24_x86_64 + cp39-none-manylinux_2_23_x86_64 + cp39-none-manylinux_2_22_x86_64 + cp39-none-manylinux_2_21_x86_64 + cp39-none-manylinux_2_20_x86_64 + cp39-none-manylinux_2_19_x86_64 + cp39-none-manylinux_2_18_x86_64 + cp39-none-manylinux_2_17_x86_64 + cp39-none-manylinux2014_x86_64 + cp39-none-manylinux_2_16_x86_64 + cp39-none-manylinux_2_15_x86_64 + cp39-none-manylinux_2_14_x86_64 + cp39-none-manylinux_2_13_x86_64 + cp39-none-manylinux_2_12_x86_64 + cp39-none-manylinux2010_x86_64 + cp39-none-manylinux_2_11_x86_64 + cp39-none-manylinux_2_10_x86_64 + cp39-none-manylinux_2_9_x86_64 + cp39-none-manylinux_2_8_x86_64 + cp39-none-manylinux_2_7_x86_64 + cp39-none-manylinux_2_6_x86_64 + cp39-none-manylinux_2_5_x86_64 + cp39-none-manylinux1_x86_64 + cp39-none-linux_x86_64 + cp38-abi3-manylinux_2_28_x86_64 + cp38-abi3-manylinux_2_27_x86_64 + cp38-abi3-manylinux_2_26_x86_64 + cp38-abi3-manylinux_2_25_x86_64 + cp38-abi3-manylinux_2_24_x86_64 + cp38-abi3-manylinux_2_23_x86_64 + cp38-abi3-manylinux_2_22_x86_64 + cp38-abi3-manylinux_2_21_x86_64 + cp38-abi3-manylinux_2_20_x86_64 + cp38-abi3-manylinux_2_19_x86_64 + cp38-abi3-manylinux_2_18_x86_64 + cp38-abi3-manylinux_2_17_x86_64 + cp38-abi3-manylinux2014_x86_64 + cp38-abi3-manylinux_2_16_x86_64 + cp38-abi3-manylinux_2_15_x86_64 + cp38-abi3-manylinux_2_14_x86_64 + cp38-abi3-manylinux_2_13_x86_64 + cp38-abi3-manylinux_2_12_x86_64 + cp38-abi3-manylinux2010_x86_64 + cp38-abi3-manylinux_2_11_x86_64 + cp38-abi3-manylinux_2_10_x86_64 + cp38-abi3-manylinux_2_9_x86_64 + cp38-abi3-manylinux_2_8_x86_64 + cp38-abi3-manylinux_2_7_x86_64 + cp38-abi3-manylinux_2_6_x86_64 + cp38-abi3-manylinux_2_5_x86_64 + cp38-abi3-manylinux1_x86_64 + cp38-abi3-linux_x86_64 + cp37-abi3-manylinux_2_28_x86_64 + cp37-abi3-manylinux_2_27_x86_64 + cp37-abi3-manylinux_2_26_x86_64 + cp37-abi3-manylinux_2_25_x86_64 + cp37-abi3-manylinux_2_24_x86_64 + cp37-abi3-manylinux_2_23_x86_64 + cp37-abi3-manylinux_2_22_x86_64 + cp37-abi3-manylinux_2_21_x86_64 + cp37-abi3-manylinux_2_20_x86_64 + cp37-abi3-manylinux_2_19_x86_64 + cp37-abi3-manylinux_2_18_x86_64 + cp37-abi3-manylinux_2_17_x86_64 + cp37-abi3-manylinux2014_x86_64 + cp37-abi3-manylinux_2_16_x86_64 + cp37-abi3-manylinux_2_15_x86_64 + cp37-abi3-manylinux_2_14_x86_64 + cp37-abi3-manylinux_2_13_x86_64 + cp37-abi3-manylinux_2_12_x86_64 + cp37-abi3-manylinux2010_x86_64 + cp37-abi3-manylinux_2_11_x86_64 + cp37-abi3-manylinux_2_10_x86_64 + cp37-abi3-manylinux_2_9_x86_64 + cp37-abi3-manylinux_2_8_x86_64 + cp37-abi3-manylinux_2_7_x86_64 + cp37-abi3-manylinux_2_6_x86_64 + cp37-abi3-manylinux_2_5_x86_64 + cp37-abi3-manylinux1_x86_64 + cp37-abi3-linux_x86_64 + cp36-abi3-manylinux_2_28_x86_64 + cp36-abi3-manylinux_2_27_x86_64 + cp36-abi3-manylinux_2_26_x86_64 + cp36-abi3-manylinux_2_25_x86_64 + cp36-abi3-manylinux_2_24_x86_64 + cp36-abi3-manylinux_2_23_x86_64 + cp36-abi3-manylinux_2_22_x86_64 + cp36-abi3-manylinux_2_21_x86_64 + cp36-abi3-manylinux_2_20_x86_64 + cp36-abi3-manylinux_2_19_x86_64 + cp36-abi3-manylinux_2_18_x86_64 + cp36-abi3-manylinux_2_17_x86_64 + cp36-abi3-manylinux2014_x86_64 + cp36-abi3-manylinux_2_16_x86_64 + cp36-abi3-manylinux_2_15_x86_64 + cp36-abi3-manylinux_2_14_x86_64 + cp36-abi3-manylinux_2_13_x86_64 + cp36-abi3-manylinux_2_12_x86_64 + cp36-abi3-manylinux2010_x86_64 + cp36-abi3-manylinux_2_11_x86_64 + cp36-abi3-manylinux_2_10_x86_64 + cp36-abi3-manylinux_2_9_x86_64 + cp36-abi3-manylinux_2_8_x86_64 + cp36-abi3-manylinux_2_7_x86_64 + cp36-abi3-manylinux_2_6_x86_64 + cp36-abi3-manylinux_2_5_x86_64 + cp36-abi3-manylinux1_x86_64 + cp36-abi3-linux_x86_64 + cp35-abi3-manylinux_2_28_x86_64 + cp35-abi3-manylinux_2_27_x86_64 + cp35-abi3-manylinux_2_26_x86_64 + cp35-abi3-manylinux_2_25_x86_64 + cp35-abi3-manylinux_2_24_x86_64 + cp35-abi3-manylinux_2_23_x86_64 + cp35-abi3-manylinux_2_22_x86_64 + cp35-abi3-manylinux_2_21_x86_64 + cp35-abi3-manylinux_2_20_x86_64 + cp35-abi3-manylinux_2_19_x86_64 + cp35-abi3-manylinux_2_18_x86_64 + cp35-abi3-manylinux_2_17_x86_64 + cp35-abi3-manylinux2014_x86_64 + cp35-abi3-manylinux_2_16_x86_64 + cp35-abi3-manylinux_2_15_x86_64 + cp35-abi3-manylinux_2_14_x86_64 + cp35-abi3-manylinux_2_13_x86_64 + cp35-abi3-manylinux_2_12_x86_64 + cp35-abi3-manylinux2010_x86_64 + cp35-abi3-manylinux_2_11_x86_64 + cp35-abi3-manylinux_2_10_x86_64 + cp35-abi3-manylinux_2_9_x86_64 + cp35-abi3-manylinux_2_8_x86_64 + cp35-abi3-manylinux_2_7_x86_64 + cp35-abi3-manylinux_2_6_x86_64 + cp35-abi3-manylinux_2_5_x86_64 + cp35-abi3-manylinux1_x86_64 + cp35-abi3-linux_x86_64 + cp34-abi3-manylinux_2_28_x86_64 + cp34-abi3-manylinux_2_27_x86_64 + cp34-abi3-manylinux_2_26_x86_64 + cp34-abi3-manylinux_2_25_x86_64 + cp34-abi3-manylinux_2_24_x86_64 + cp34-abi3-manylinux_2_23_x86_64 + cp34-abi3-manylinux_2_22_x86_64 + cp34-abi3-manylinux_2_21_x86_64 + cp34-abi3-manylinux_2_20_x86_64 + cp34-abi3-manylinux_2_19_x86_64 + cp34-abi3-manylinux_2_18_x86_64 + cp34-abi3-manylinux_2_17_x86_64 + cp34-abi3-manylinux2014_x86_64 + cp34-abi3-manylinux_2_16_x86_64 + cp34-abi3-manylinux_2_15_x86_64 + cp34-abi3-manylinux_2_14_x86_64 + cp34-abi3-manylinux_2_13_x86_64 + cp34-abi3-manylinux_2_12_x86_64 + cp34-abi3-manylinux2010_x86_64 + cp34-abi3-manylinux_2_11_x86_64 + cp34-abi3-manylinux_2_10_x86_64 + cp34-abi3-manylinux_2_9_x86_64 + cp34-abi3-manylinux_2_8_x86_64 + cp34-abi3-manylinux_2_7_x86_64 + cp34-abi3-manylinux_2_6_x86_64 + cp34-abi3-manylinux_2_5_x86_64 + cp34-abi3-manylinux1_x86_64 + cp34-abi3-linux_x86_64 + cp33-abi3-manylinux_2_28_x86_64 + cp33-abi3-manylinux_2_27_x86_64 + cp33-abi3-manylinux_2_26_x86_64 + cp33-abi3-manylinux_2_25_x86_64 + cp33-abi3-manylinux_2_24_x86_64 + cp33-abi3-manylinux_2_23_x86_64 + cp33-abi3-manylinux_2_22_x86_64 + cp33-abi3-manylinux_2_21_x86_64 + cp33-abi3-manylinux_2_20_x86_64 + cp33-abi3-manylinux_2_19_x86_64 + cp33-abi3-manylinux_2_18_x86_64 + cp33-abi3-manylinux_2_17_x86_64 + cp33-abi3-manylinux2014_x86_64 + cp33-abi3-manylinux_2_16_x86_64 + cp33-abi3-manylinux_2_15_x86_64 + cp33-abi3-manylinux_2_14_x86_64 + cp33-abi3-manylinux_2_13_x86_64 + cp33-abi3-manylinux_2_12_x86_64 + cp33-abi3-manylinux2010_x86_64 + cp33-abi3-manylinux_2_11_x86_64 + cp33-abi3-manylinux_2_10_x86_64 + cp33-abi3-manylinux_2_9_x86_64 + cp33-abi3-manylinux_2_8_x86_64 + cp33-abi3-manylinux_2_7_x86_64 + cp33-abi3-manylinux_2_6_x86_64 + cp33-abi3-manylinux_2_5_x86_64 + cp33-abi3-manylinux1_x86_64 + cp33-abi3-linux_x86_64 + cp32-abi3-manylinux_2_28_x86_64 + cp32-abi3-manylinux_2_27_x86_64 + cp32-abi3-manylinux_2_26_x86_64 + cp32-abi3-manylinux_2_25_x86_64 + cp32-abi3-manylinux_2_24_x86_64 + cp32-abi3-manylinux_2_23_x86_64 + cp32-abi3-manylinux_2_22_x86_64 + cp32-abi3-manylinux_2_21_x86_64 + cp32-abi3-manylinux_2_20_x86_64 + cp32-abi3-manylinux_2_19_x86_64 + cp32-abi3-manylinux_2_18_x86_64 + cp32-abi3-manylinux_2_17_x86_64 + cp32-abi3-manylinux2014_x86_64 + cp32-abi3-manylinux_2_16_x86_64 + cp32-abi3-manylinux_2_15_x86_64 + cp32-abi3-manylinux_2_14_x86_64 + cp32-abi3-manylinux_2_13_x86_64 + cp32-abi3-manylinux_2_12_x86_64 + cp32-abi3-manylinux2010_x86_64 + cp32-abi3-manylinux_2_11_x86_64 + cp32-abi3-manylinux_2_10_x86_64 + cp32-abi3-manylinux_2_9_x86_64 + cp32-abi3-manylinux_2_8_x86_64 + cp32-abi3-manylinux_2_7_x86_64 + cp32-abi3-manylinux_2_6_x86_64 + cp32-abi3-manylinux_2_5_x86_64 + cp32-abi3-manylinux1_x86_64 + cp32-abi3-linux_x86_64 + py39-none-manylinux_2_28_x86_64 + py39-none-manylinux_2_27_x86_64 + py39-none-manylinux_2_26_x86_64 + py39-none-manylinux_2_25_x86_64 + py39-none-manylinux_2_24_x86_64 + py39-none-manylinux_2_23_x86_64 + py39-none-manylinux_2_22_x86_64 + py39-none-manylinux_2_21_x86_64 + py39-none-manylinux_2_20_x86_64 + py39-none-manylinux_2_19_x86_64 + py39-none-manylinux_2_18_x86_64 + py39-none-manylinux_2_17_x86_64 + py39-none-manylinux2014_x86_64 + py39-none-manylinux_2_16_x86_64 + py39-none-manylinux_2_15_x86_64 + py39-none-manylinux_2_14_x86_64 + py39-none-manylinux_2_13_x86_64 + py39-none-manylinux_2_12_x86_64 + py39-none-manylinux2010_x86_64 + py39-none-manylinux_2_11_x86_64 + py39-none-manylinux_2_10_x86_64 + py39-none-manylinux_2_9_x86_64 + py39-none-manylinux_2_8_x86_64 + py39-none-manylinux_2_7_x86_64 + py39-none-manylinux_2_6_x86_64 + py39-none-manylinux_2_5_x86_64 + py39-none-manylinux1_x86_64 + py39-none-linux_x86_64 + py3-none-manylinux_2_28_x86_64 + py3-none-manylinux_2_27_x86_64 + py3-none-manylinux_2_26_x86_64 + py3-none-manylinux_2_25_x86_64 + py3-none-manylinux_2_24_x86_64 + py3-none-manylinux_2_23_x86_64 + py3-none-manylinux_2_22_x86_64 + py3-none-manylinux_2_21_x86_64 + py3-none-manylinux_2_20_x86_64 + py3-none-manylinux_2_19_x86_64 + py3-none-manylinux_2_18_x86_64 + py3-none-manylinux_2_17_x86_64 + py3-none-manylinux2014_x86_64 + py3-none-manylinux_2_16_x86_64 + py3-none-manylinux_2_15_x86_64 + py3-none-manylinux_2_14_x86_64 + py3-none-manylinux_2_13_x86_64 + py3-none-manylinux_2_12_x86_64 + py3-none-manylinux2010_x86_64 + py3-none-manylinux_2_11_x86_64 + py3-none-manylinux_2_10_x86_64 + py3-none-manylinux_2_9_x86_64 + py3-none-manylinux_2_8_x86_64 + py3-none-manylinux_2_7_x86_64 + py3-none-manylinux_2_6_x86_64 + py3-none-manylinux_2_5_x86_64 + py3-none-manylinux1_x86_64 + py3-none-linux_x86_64 + py38-none-manylinux_2_28_x86_64 + py38-none-manylinux_2_27_x86_64 + py38-none-manylinux_2_26_x86_64 + py38-none-manylinux_2_25_x86_64 + py38-none-manylinux_2_24_x86_64 + py38-none-manylinux_2_23_x86_64 + py38-none-manylinux_2_22_x86_64 + py38-none-manylinux_2_21_x86_64 + py38-none-manylinux_2_20_x86_64 + py38-none-manylinux_2_19_x86_64 + py38-none-manylinux_2_18_x86_64 + py38-none-manylinux_2_17_x86_64 + py38-none-manylinux2014_x86_64 + py38-none-manylinux_2_16_x86_64 + py38-none-manylinux_2_15_x86_64 + py38-none-manylinux_2_14_x86_64 + py38-none-manylinux_2_13_x86_64 + py38-none-manylinux_2_12_x86_64 + py38-none-manylinux2010_x86_64 + py38-none-manylinux_2_11_x86_64 + py38-none-manylinux_2_10_x86_64 + py38-none-manylinux_2_9_x86_64 + py38-none-manylinux_2_8_x86_64 + py38-none-manylinux_2_7_x86_64 + py38-none-manylinux_2_6_x86_64 + py38-none-manylinux_2_5_x86_64 + py38-none-manylinux1_x86_64 + py38-none-linux_x86_64 + py37-none-manylinux_2_28_x86_64 + py37-none-manylinux_2_27_x86_64 + py37-none-manylinux_2_26_x86_64 + py37-none-manylinux_2_25_x86_64 + py37-none-manylinux_2_24_x86_64 + py37-none-manylinux_2_23_x86_64 + py37-none-manylinux_2_22_x86_64 + py37-none-manylinux_2_21_x86_64 + py37-none-manylinux_2_20_x86_64 + py37-none-manylinux_2_19_x86_64 + py37-none-manylinux_2_18_x86_64 + py37-none-manylinux_2_17_x86_64 + py37-none-manylinux2014_x86_64 + py37-none-manylinux_2_16_x86_64 + py37-none-manylinux_2_15_x86_64 + py37-none-manylinux_2_14_x86_64 + py37-none-manylinux_2_13_x86_64 + py37-none-manylinux_2_12_x86_64 + py37-none-manylinux2010_x86_64 + py37-none-manylinux_2_11_x86_64 + py37-none-manylinux_2_10_x86_64 + py37-none-manylinux_2_9_x86_64 + py37-none-manylinux_2_8_x86_64 + py37-none-manylinux_2_7_x86_64 + py37-none-manylinux_2_6_x86_64 + py37-none-manylinux_2_5_x86_64 + py37-none-manylinux1_x86_64 + py37-none-linux_x86_64 + py36-none-manylinux_2_28_x86_64 + py36-none-manylinux_2_27_x86_64 + py36-none-manylinux_2_26_x86_64 + py36-none-manylinux_2_25_x86_64 + py36-none-manylinux_2_24_x86_64 + py36-none-manylinux_2_23_x86_64 + py36-none-manylinux_2_22_x86_64 + py36-none-manylinux_2_21_x86_64 + py36-none-manylinux_2_20_x86_64 + py36-none-manylinux_2_19_x86_64 + py36-none-manylinux_2_18_x86_64 + py36-none-manylinux_2_17_x86_64 + py36-none-manylinux2014_x86_64 + py36-none-manylinux_2_16_x86_64 + py36-none-manylinux_2_15_x86_64 + py36-none-manylinux_2_14_x86_64 + py36-none-manylinux_2_13_x86_64 + py36-none-manylinux_2_12_x86_64 + py36-none-manylinux2010_x86_64 + py36-none-manylinux_2_11_x86_64 + py36-none-manylinux_2_10_x86_64 + py36-none-manylinux_2_9_x86_64 + py36-none-manylinux_2_8_x86_64 + py36-none-manylinux_2_7_x86_64 + py36-none-manylinux_2_6_x86_64 + py36-none-manylinux_2_5_x86_64 + py36-none-manylinux1_x86_64 + py36-none-linux_x86_64 + py35-none-manylinux_2_28_x86_64 + py35-none-manylinux_2_27_x86_64 + py35-none-manylinux_2_26_x86_64 + py35-none-manylinux_2_25_x86_64 + py35-none-manylinux_2_24_x86_64 + py35-none-manylinux_2_23_x86_64 + py35-none-manylinux_2_22_x86_64 + py35-none-manylinux_2_21_x86_64 + py35-none-manylinux_2_20_x86_64 + py35-none-manylinux_2_19_x86_64 + py35-none-manylinux_2_18_x86_64 + py35-none-manylinux_2_17_x86_64 + py35-none-manylinux2014_x86_64 + py35-none-manylinux_2_16_x86_64 + py35-none-manylinux_2_15_x86_64 + py35-none-manylinux_2_14_x86_64 + py35-none-manylinux_2_13_x86_64 + py35-none-manylinux_2_12_x86_64 + py35-none-manylinux2010_x86_64 + py35-none-manylinux_2_11_x86_64 + py35-none-manylinux_2_10_x86_64 + py35-none-manylinux_2_9_x86_64 + py35-none-manylinux_2_8_x86_64 + py35-none-manylinux_2_7_x86_64 + py35-none-manylinux_2_6_x86_64 + py35-none-manylinux_2_5_x86_64 + py35-none-manylinux1_x86_64 + py35-none-linux_x86_64 + py34-none-manylinux_2_28_x86_64 + py34-none-manylinux_2_27_x86_64 + py34-none-manylinux_2_26_x86_64 + py34-none-manylinux_2_25_x86_64 + py34-none-manylinux_2_24_x86_64 + py34-none-manylinux_2_23_x86_64 + py34-none-manylinux_2_22_x86_64 + py34-none-manylinux_2_21_x86_64 + py34-none-manylinux_2_20_x86_64 + py34-none-manylinux_2_19_x86_64 + py34-none-manylinux_2_18_x86_64 + py34-none-manylinux_2_17_x86_64 + py34-none-manylinux2014_x86_64 + py34-none-manylinux_2_16_x86_64 + py34-none-manylinux_2_15_x86_64 + py34-none-manylinux_2_14_x86_64 + py34-none-manylinux_2_13_x86_64 + py34-none-manylinux_2_12_x86_64 + py34-none-manylinux2010_x86_64 + py34-none-manylinux_2_11_x86_64 + py34-none-manylinux_2_10_x86_64 + py34-none-manylinux_2_9_x86_64 + py34-none-manylinux_2_8_x86_64 + py34-none-manylinux_2_7_x86_64 + py34-none-manylinux_2_6_x86_64 + py34-none-manylinux_2_5_x86_64 + py34-none-manylinux1_x86_64 + py34-none-linux_x86_64 + py33-none-manylinux_2_28_x86_64 + py33-none-manylinux_2_27_x86_64 + py33-none-manylinux_2_26_x86_64 + py33-none-manylinux_2_25_x86_64 + py33-none-manylinux_2_24_x86_64 + py33-none-manylinux_2_23_x86_64 + py33-none-manylinux_2_22_x86_64 + py33-none-manylinux_2_21_x86_64 + py33-none-manylinux_2_20_x86_64 + py33-none-manylinux_2_19_x86_64 + py33-none-manylinux_2_18_x86_64 + py33-none-manylinux_2_17_x86_64 + py33-none-manylinux2014_x86_64 + py33-none-manylinux_2_16_x86_64 + py33-none-manylinux_2_15_x86_64 + py33-none-manylinux_2_14_x86_64 + py33-none-manylinux_2_13_x86_64 + py33-none-manylinux_2_12_x86_64 + py33-none-manylinux2010_x86_64 + py33-none-manylinux_2_11_x86_64 + py33-none-manylinux_2_10_x86_64 + py33-none-manylinux_2_9_x86_64 + py33-none-manylinux_2_8_x86_64 + py33-none-manylinux_2_7_x86_64 + py33-none-manylinux_2_6_x86_64 + py33-none-manylinux_2_5_x86_64 + py33-none-manylinux1_x86_64 + py33-none-linux_x86_64 + py32-none-manylinux_2_28_x86_64 + py32-none-manylinux_2_27_x86_64 + py32-none-manylinux_2_26_x86_64 + py32-none-manylinux_2_25_x86_64 + py32-none-manylinux_2_24_x86_64 + py32-none-manylinux_2_23_x86_64 + py32-none-manylinux_2_22_x86_64 + py32-none-manylinux_2_21_x86_64 + py32-none-manylinux_2_20_x86_64 + py32-none-manylinux_2_19_x86_64 + py32-none-manylinux_2_18_x86_64 + py32-none-manylinux_2_17_x86_64 + py32-none-manylinux2014_x86_64 + py32-none-manylinux_2_16_x86_64 + py32-none-manylinux_2_15_x86_64 + py32-none-manylinux_2_14_x86_64 + py32-none-manylinux_2_13_x86_64 + py32-none-manylinux_2_12_x86_64 + py32-none-manylinux2010_x86_64 + py32-none-manylinux_2_11_x86_64 + py32-none-manylinux_2_10_x86_64 + py32-none-manylinux_2_9_x86_64 + py32-none-manylinux_2_8_x86_64 + py32-none-manylinux_2_7_x86_64 + py32-none-manylinux_2_6_x86_64 + py32-none-manylinux_2_5_x86_64 + py32-none-manylinux1_x86_64 + py32-none-linux_x86_64 + py31-none-manylinux_2_28_x86_64 + py31-none-manylinux_2_27_x86_64 + py31-none-manylinux_2_26_x86_64 + py31-none-manylinux_2_25_x86_64 + py31-none-manylinux_2_24_x86_64 + py31-none-manylinux_2_23_x86_64 + py31-none-manylinux_2_22_x86_64 + py31-none-manylinux_2_21_x86_64 + py31-none-manylinux_2_20_x86_64 + py31-none-manylinux_2_19_x86_64 + py31-none-manylinux_2_18_x86_64 + py31-none-manylinux_2_17_x86_64 + py31-none-manylinux2014_x86_64 + py31-none-manylinux_2_16_x86_64 + py31-none-manylinux_2_15_x86_64 + py31-none-manylinux_2_14_x86_64 + py31-none-manylinux_2_13_x86_64 + py31-none-manylinux_2_12_x86_64 + py31-none-manylinux2010_x86_64 + py31-none-manylinux_2_11_x86_64 + py31-none-manylinux_2_10_x86_64 + py31-none-manylinux_2_9_x86_64 + py31-none-manylinux_2_8_x86_64 + py31-none-manylinux_2_7_x86_64 + py31-none-manylinux_2_6_x86_64 + py31-none-manylinux_2_5_x86_64 + py31-none-manylinux1_x86_64 + py31-none-linux_x86_64 + py30-none-manylinux_2_28_x86_64 + py30-none-manylinux_2_27_x86_64 + py30-none-manylinux_2_26_x86_64 + py30-none-manylinux_2_25_x86_64 + py30-none-manylinux_2_24_x86_64 + py30-none-manylinux_2_23_x86_64 + py30-none-manylinux_2_22_x86_64 + py30-none-manylinux_2_21_x86_64 + py30-none-manylinux_2_20_x86_64 + py30-none-manylinux_2_19_x86_64 + py30-none-manylinux_2_18_x86_64 + py30-none-manylinux_2_17_x86_64 + py30-none-manylinux2014_x86_64 + py30-none-manylinux_2_16_x86_64 + py30-none-manylinux_2_15_x86_64 + py30-none-manylinux_2_14_x86_64 + py30-none-manylinux_2_13_x86_64 + py30-none-manylinux_2_12_x86_64 + py30-none-manylinux2010_x86_64 + py30-none-manylinux_2_11_x86_64 + py30-none-manylinux_2_10_x86_64 + py30-none-manylinux_2_9_x86_64 + py30-none-manylinux_2_8_x86_64 + py30-none-manylinux_2_7_x86_64 + py30-none-manylinux_2_6_x86_64 + py30-none-manylinux_2_5_x86_64 + py30-none-manylinux1_x86_64 + py30-none-linux_x86_64 + cp39-none-any + py39-none-any + py3-none-any + py38-none-any + py37-none-any + py36-none-any + py35-none-any + py34-none-any + py33-none-any + py32-none-any + py31-none-any + py30-none-any + "### + ); +} + +#[test] +fn test_system_tags_macos() { + let tags = Tags::from_env( + &Platform::new( + Os::Macos { + major: 14, + minor: 0, + }, + Arch::Aarch64, + ), + (3, 9), + "cpython", + (3, 9), + false, + false, + ) + .unwrap(); + assert_snapshot!( + tags, + @r###" + cp39-cp39-macosx_14_0_arm64 + cp39-cp39-macosx_14_0_universal2 + cp39-cp39-macosx_13_0_arm64 + cp39-cp39-macosx_13_0_universal2 + cp39-cp39-macosx_12_0_arm64 + cp39-cp39-macosx_12_0_universal2 + cp39-cp39-macosx_11_0_arm64 + cp39-cp39-macosx_11_0_universal2 + cp39-cp39-macosx_10_16_universal2 + cp39-cp39-macosx_10_15_universal2 + cp39-cp39-macosx_10_14_universal2 + cp39-cp39-macosx_10_13_universal2 + cp39-cp39-macosx_10_12_universal2 + cp39-cp39-macosx_10_11_universal2 + cp39-cp39-macosx_10_10_universal2 + cp39-cp39-macosx_10_9_universal2 + cp39-cp39-macosx_10_8_universal2 + cp39-cp39-macosx_10_7_universal2 + cp39-cp39-macosx_10_6_universal2 + cp39-cp39-macosx_10_5_universal2 + cp39-cp39-macosx_10_4_universal2 + cp39-abi3-macosx_14_0_arm64 + cp39-abi3-macosx_14_0_universal2 + cp39-abi3-macosx_13_0_arm64 + cp39-abi3-macosx_13_0_universal2 + cp39-abi3-macosx_12_0_arm64 + cp39-abi3-macosx_12_0_universal2 + cp39-abi3-macosx_11_0_arm64 + cp39-abi3-macosx_11_0_universal2 + cp39-abi3-macosx_10_16_universal2 + cp39-abi3-macosx_10_15_universal2 + cp39-abi3-macosx_10_14_universal2 + cp39-abi3-macosx_10_13_universal2 + cp39-abi3-macosx_10_12_universal2 + cp39-abi3-macosx_10_11_universal2 + cp39-abi3-macosx_10_10_universal2 + cp39-abi3-macosx_10_9_universal2 + cp39-abi3-macosx_10_8_universal2 + cp39-abi3-macosx_10_7_universal2 + cp39-abi3-macosx_10_6_universal2 + cp39-abi3-macosx_10_5_universal2 + cp39-abi3-macosx_10_4_universal2 + cp39-none-macosx_14_0_arm64 + cp39-none-macosx_14_0_universal2 + cp39-none-macosx_13_0_arm64 + cp39-none-macosx_13_0_universal2 + cp39-none-macosx_12_0_arm64 + cp39-none-macosx_12_0_universal2 + cp39-none-macosx_11_0_arm64 + cp39-none-macosx_11_0_universal2 + cp39-none-macosx_10_16_universal2 + cp39-none-macosx_10_15_universal2 + cp39-none-macosx_10_14_universal2 + cp39-none-macosx_10_13_universal2 + cp39-none-macosx_10_12_universal2 + cp39-none-macosx_10_11_universal2 + cp39-none-macosx_10_10_universal2 + cp39-none-macosx_10_9_universal2 + cp39-none-macosx_10_8_universal2 + cp39-none-macosx_10_7_universal2 + cp39-none-macosx_10_6_universal2 + cp39-none-macosx_10_5_universal2 + cp39-none-macosx_10_4_universal2 + cp38-abi3-macosx_14_0_arm64 + cp38-abi3-macosx_14_0_universal2 + cp38-abi3-macosx_13_0_arm64 + cp38-abi3-macosx_13_0_universal2 + cp38-abi3-macosx_12_0_arm64 + cp38-abi3-macosx_12_0_universal2 + cp38-abi3-macosx_11_0_arm64 + cp38-abi3-macosx_11_0_universal2 + cp38-abi3-macosx_10_16_universal2 + cp38-abi3-macosx_10_15_universal2 + cp38-abi3-macosx_10_14_universal2 + cp38-abi3-macosx_10_13_universal2 + cp38-abi3-macosx_10_12_universal2 + cp38-abi3-macosx_10_11_universal2 + cp38-abi3-macosx_10_10_universal2 + cp38-abi3-macosx_10_9_universal2 + cp38-abi3-macosx_10_8_universal2 + cp38-abi3-macosx_10_7_universal2 + cp38-abi3-macosx_10_6_universal2 + cp38-abi3-macosx_10_5_universal2 + cp38-abi3-macosx_10_4_universal2 + cp37-abi3-macosx_14_0_arm64 + cp37-abi3-macosx_14_0_universal2 + cp37-abi3-macosx_13_0_arm64 + cp37-abi3-macosx_13_0_universal2 + cp37-abi3-macosx_12_0_arm64 + cp37-abi3-macosx_12_0_universal2 + cp37-abi3-macosx_11_0_arm64 + cp37-abi3-macosx_11_0_universal2 + cp37-abi3-macosx_10_16_universal2 + cp37-abi3-macosx_10_15_universal2 + cp37-abi3-macosx_10_14_universal2 + cp37-abi3-macosx_10_13_universal2 + cp37-abi3-macosx_10_12_universal2 + cp37-abi3-macosx_10_11_universal2 + cp37-abi3-macosx_10_10_universal2 + cp37-abi3-macosx_10_9_universal2 + cp37-abi3-macosx_10_8_universal2 + cp37-abi3-macosx_10_7_universal2 + cp37-abi3-macosx_10_6_universal2 + cp37-abi3-macosx_10_5_universal2 + cp37-abi3-macosx_10_4_universal2 + cp36-abi3-macosx_14_0_arm64 + cp36-abi3-macosx_14_0_universal2 + cp36-abi3-macosx_13_0_arm64 + cp36-abi3-macosx_13_0_universal2 + cp36-abi3-macosx_12_0_arm64 + cp36-abi3-macosx_12_0_universal2 + cp36-abi3-macosx_11_0_arm64 + cp36-abi3-macosx_11_0_universal2 + cp36-abi3-macosx_10_16_universal2 + cp36-abi3-macosx_10_15_universal2 + cp36-abi3-macosx_10_14_universal2 + cp36-abi3-macosx_10_13_universal2 + cp36-abi3-macosx_10_12_universal2 + cp36-abi3-macosx_10_11_universal2 + cp36-abi3-macosx_10_10_universal2 + cp36-abi3-macosx_10_9_universal2 + cp36-abi3-macosx_10_8_universal2 + cp36-abi3-macosx_10_7_universal2 + cp36-abi3-macosx_10_6_universal2 + cp36-abi3-macosx_10_5_universal2 + cp36-abi3-macosx_10_4_universal2 + cp35-abi3-macosx_14_0_arm64 + cp35-abi3-macosx_14_0_universal2 + cp35-abi3-macosx_13_0_arm64 + cp35-abi3-macosx_13_0_universal2 + cp35-abi3-macosx_12_0_arm64 + cp35-abi3-macosx_12_0_universal2 + cp35-abi3-macosx_11_0_arm64 + cp35-abi3-macosx_11_0_universal2 + cp35-abi3-macosx_10_16_universal2 + cp35-abi3-macosx_10_15_universal2 + cp35-abi3-macosx_10_14_universal2 + cp35-abi3-macosx_10_13_universal2 + cp35-abi3-macosx_10_12_universal2 + cp35-abi3-macosx_10_11_universal2 + cp35-abi3-macosx_10_10_universal2 + cp35-abi3-macosx_10_9_universal2 + cp35-abi3-macosx_10_8_universal2 + cp35-abi3-macosx_10_7_universal2 + cp35-abi3-macosx_10_6_universal2 + cp35-abi3-macosx_10_5_universal2 + cp35-abi3-macosx_10_4_universal2 + cp34-abi3-macosx_14_0_arm64 + cp34-abi3-macosx_14_0_universal2 + cp34-abi3-macosx_13_0_arm64 + cp34-abi3-macosx_13_0_universal2 + cp34-abi3-macosx_12_0_arm64 + cp34-abi3-macosx_12_0_universal2 + cp34-abi3-macosx_11_0_arm64 + cp34-abi3-macosx_11_0_universal2 + cp34-abi3-macosx_10_16_universal2 + cp34-abi3-macosx_10_15_universal2 + cp34-abi3-macosx_10_14_universal2 + cp34-abi3-macosx_10_13_universal2 + cp34-abi3-macosx_10_12_universal2 + cp34-abi3-macosx_10_11_universal2 + cp34-abi3-macosx_10_10_universal2 + cp34-abi3-macosx_10_9_universal2 + cp34-abi3-macosx_10_8_universal2 + cp34-abi3-macosx_10_7_universal2 + cp34-abi3-macosx_10_6_universal2 + cp34-abi3-macosx_10_5_universal2 + cp34-abi3-macosx_10_4_universal2 + cp33-abi3-macosx_14_0_arm64 + cp33-abi3-macosx_14_0_universal2 + cp33-abi3-macosx_13_0_arm64 + cp33-abi3-macosx_13_0_universal2 + cp33-abi3-macosx_12_0_arm64 + cp33-abi3-macosx_12_0_universal2 + cp33-abi3-macosx_11_0_arm64 + cp33-abi3-macosx_11_0_universal2 + cp33-abi3-macosx_10_16_universal2 + cp33-abi3-macosx_10_15_universal2 + cp33-abi3-macosx_10_14_universal2 + cp33-abi3-macosx_10_13_universal2 + cp33-abi3-macosx_10_12_universal2 + cp33-abi3-macosx_10_11_universal2 + cp33-abi3-macosx_10_10_universal2 + cp33-abi3-macosx_10_9_universal2 + cp33-abi3-macosx_10_8_universal2 + cp33-abi3-macosx_10_7_universal2 + cp33-abi3-macosx_10_6_universal2 + cp33-abi3-macosx_10_5_universal2 + cp33-abi3-macosx_10_4_universal2 + cp32-abi3-macosx_14_0_arm64 + cp32-abi3-macosx_14_0_universal2 + cp32-abi3-macosx_13_0_arm64 + cp32-abi3-macosx_13_0_universal2 + cp32-abi3-macosx_12_0_arm64 + cp32-abi3-macosx_12_0_universal2 + cp32-abi3-macosx_11_0_arm64 + cp32-abi3-macosx_11_0_universal2 + cp32-abi3-macosx_10_16_universal2 + cp32-abi3-macosx_10_15_universal2 + cp32-abi3-macosx_10_14_universal2 + cp32-abi3-macosx_10_13_universal2 + cp32-abi3-macosx_10_12_universal2 + cp32-abi3-macosx_10_11_universal2 + cp32-abi3-macosx_10_10_universal2 + cp32-abi3-macosx_10_9_universal2 + cp32-abi3-macosx_10_8_universal2 + cp32-abi3-macosx_10_7_universal2 + cp32-abi3-macosx_10_6_universal2 + cp32-abi3-macosx_10_5_universal2 + cp32-abi3-macosx_10_4_universal2 + py39-none-macosx_14_0_arm64 + py39-none-macosx_14_0_universal2 + py39-none-macosx_13_0_arm64 + py39-none-macosx_13_0_universal2 + py39-none-macosx_12_0_arm64 + py39-none-macosx_12_0_universal2 + py39-none-macosx_11_0_arm64 + py39-none-macosx_11_0_universal2 + py39-none-macosx_10_16_universal2 + py39-none-macosx_10_15_universal2 + py39-none-macosx_10_14_universal2 + py39-none-macosx_10_13_universal2 + py39-none-macosx_10_12_universal2 + py39-none-macosx_10_11_universal2 + py39-none-macosx_10_10_universal2 + py39-none-macosx_10_9_universal2 + py39-none-macosx_10_8_universal2 + py39-none-macosx_10_7_universal2 + py39-none-macosx_10_6_universal2 + py39-none-macosx_10_5_universal2 + py39-none-macosx_10_4_universal2 + py3-none-macosx_14_0_arm64 + py3-none-macosx_14_0_universal2 + py3-none-macosx_13_0_arm64 + py3-none-macosx_13_0_universal2 + py3-none-macosx_12_0_arm64 + py3-none-macosx_12_0_universal2 + py3-none-macosx_11_0_arm64 + py3-none-macosx_11_0_universal2 + py3-none-macosx_10_16_universal2 + py3-none-macosx_10_15_universal2 + py3-none-macosx_10_14_universal2 + py3-none-macosx_10_13_universal2 + py3-none-macosx_10_12_universal2 + py3-none-macosx_10_11_universal2 + py3-none-macosx_10_10_universal2 + py3-none-macosx_10_9_universal2 + py3-none-macosx_10_8_universal2 + py3-none-macosx_10_7_universal2 + py3-none-macosx_10_6_universal2 + py3-none-macosx_10_5_universal2 + py3-none-macosx_10_4_universal2 + py38-none-macosx_14_0_arm64 + py38-none-macosx_14_0_universal2 + py38-none-macosx_13_0_arm64 + py38-none-macosx_13_0_universal2 + py38-none-macosx_12_0_arm64 + py38-none-macosx_12_0_universal2 + py38-none-macosx_11_0_arm64 + py38-none-macosx_11_0_universal2 + py38-none-macosx_10_16_universal2 + py38-none-macosx_10_15_universal2 + py38-none-macosx_10_14_universal2 + py38-none-macosx_10_13_universal2 + py38-none-macosx_10_12_universal2 + py38-none-macosx_10_11_universal2 + py38-none-macosx_10_10_universal2 + py38-none-macosx_10_9_universal2 + py38-none-macosx_10_8_universal2 + py38-none-macosx_10_7_universal2 + py38-none-macosx_10_6_universal2 + py38-none-macosx_10_5_universal2 + py38-none-macosx_10_4_universal2 + py37-none-macosx_14_0_arm64 + py37-none-macosx_14_0_universal2 + py37-none-macosx_13_0_arm64 + py37-none-macosx_13_0_universal2 + py37-none-macosx_12_0_arm64 + py37-none-macosx_12_0_universal2 + py37-none-macosx_11_0_arm64 + py37-none-macosx_11_0_universal2 + py37-none-macosx_10_16_universal2 + py37-none-macosx_10_15_universal2 + py37-none-macosx_10_14_universal2 + py37-none-macosx_10_13_universal2 + py37-none-macosx_10_12_universal2 + py37-none-macosx_10_11_universal2 + py37-none-macosx_10_10_universal2 + py37-none-macosx_10_9_universal2 + py37-none-macosx_10_8_universal2 + py37-none-macosx_10_7_universal2 + py37-none-macosx_10_6_universal2 + py37-none-macosx_10_5_universal2 + py37-none-macosx_10_4_universal2 + py36-none-macosx_14_0_arm64 + py36-none-macosx_14_0_universal2 + py36-none-macosx_13_0_arm64 + py36-none-macosx_13_0_universal2 + py36-none-macosx_12_0_arm64 + py36-none-macosx_12_0_universal2 + py36-none-macosx_11_0_arm64 + py36-none-macosx_11_0_universal2 + py36-none-macosx_10_16_universal2 + py36-none-macosx_10_15_universal2 + py36-none-macosx_10_14_universal2 + py36-none-macosx_10_13_universal2 + py36-none-macosx_10_12_universal2 + py36-none-macosx_10_11_universal2 + py36-none-macosx_10_10_universal2 + py36-none-macosx_10_9_universal2 + py36-none-macosx_10_8_universal2 + py36-none-macosx_10_7_universal2 + py36-none-macosx_10_6_universal2 + py36-none-macosx_10_5_universal2 + py36-none-macosx_10_4_universal2 + py35-none-macosx_14_0_arm64 + py35-none-macosx_14_0_universal2 + py35-none-macosx_13_0_arm64 + py35-none-macosx_13_0_universal2 + py35-none-macosx_12_0_arm64 + py35-none-macosx_12_0_universal2 + py35-none-macosx_11_0_arm64 + py35-none-macosx_11_0_universal2 + py35-none-macosx_10_16_universal2 + py35-none-macosx_10_15_universal2 + py35-none-macosx_10_14_universal2 + py35-none-macosx_10_13_universal2 + py35-none-macosx_10_12_universal2 + py35-none-macosx_10_11_universal2 + py35-none-macosx_10_10_universal2 + py35-none-macosx_10_9_universal2 + py35-none-macosx_10_8_universal2 + py35-none-macosx_10_7_universal2 + py35-none-macosx_10_6_universal2 + py35-none-macosx_10_5_universal2 + py35-none-macosx_10_4_universal2 + py34-none-macosx_14_0_arm64 + py34-none-macosx_14_0_universal2 + py34-none-macosx_13_0_arm64 + py34-none-macosx_13_0_universal2 + py34-none-macosx_12_0_arm64 + py34-none-macosx_12_0_universal2 + py34-none-macosx_11_0_arm64 + py34-none-macosx_11_0_universal2 + py34-none-macosx_10_16_universal2 + py34-none-macosx_10_15_universal2 + py34-none-macosx_10_14_universal2 + py34-none-macosx_10_13_universal2 + py34-none-macosx_10_12_universal2 + py34-none-macosx_10_11_universal2 + py34-none-macosx_10_10_universal2 + py34-none-macosx_10_9_universal2 + py34-none-macosx_10_8_universal2 + py34-none-macosx_10_7_universal2 + py34-none-macosx_10_6_universal2 + py34-none-macosx_10_5_universal2 + py34-none-macosx_10_4_universal2 + py33-none-macosx_14_0_arm64 + py33-none-macosx_14_0_universal2 + py33-none-macosx_13_0_arm64 + py33-none-macosx_13_0_universal2 + py33-none-macosx_12_0_arm64 + py33-none-macosx_12_0_universal2 + py33-none-macosx_11_0_arm64 + py33-none-macosx_11_0_universal2 + py33-none-macosx_10_16_universal2 + py33-none-macosx_10_15_universal2 + py33-none-macosx_10_14_universal2 + py33-none-macosx_10_13_universal2 + py33-none-macosx_10_12_universal2 + py33-none-macosx_10_11_universal2 + py33-none-macosx_10_10_universal2 + py33-none-macosx_10_9_universal2 + py33-none-macosx_10_8_universal2 + py33-none-macosx_10_7_universal2 + py33-none-macosx_10_6_universal2 + py33-none-macosx_10_5_universal2 + py33-none-macosx_10_4_universal2 + py32-none-macosx_14_0_arm64 + py32-none-macosx_14_0_universal2 + py32-none-macosx_13_0_arm64 + py32-none-macosx_13_0_universal2 + py32-none-macosx_12_0_arm64 + py32-none-macosx_12_0_universal2 + py32-none-macosx_11_0_arm64 + py32-none-macosx_11_0_universal2 + py32-none-macosx_10_16_universal2 + py32-none-macosx_10_15_universal2 + py32-none-macosx_10_14_universal2 + py32-none-macosx_10_13_universal2 + py32-none-macosx_10_12_universal2 + py32-none-macosx_10_11_universal2 + py32-none-macosx_10_10_universal2 + py32-none-macosx_10_9_universal2 + py32-none-macosx_10_8_universal2 + py32-none-macosx_10_7_universal2 + py32-none-macosx_10_6_universal2 + py32-none-macosx_10_5_universal2 + py32-none-macosx_10_4_universal2 + py31-none-macosx_14_0_arm64 + py31-none-macosx_14_0_universal2 + py31-none-macosx_13_0_arm64 + py31-none-macosx_13_0_universal2 + py31-none-macosx_12_0_arm64 + py31-none-macosx_12_0_universal2 + py31-none-macosx_11_0_arm64 + py31-none-macosx_11_0_universal2 + py31-none-macosx_10_16_universal2 + py31-none-macosx_10_15_universal2 + py31-none-macosx_10_14_universal2 + py31-none-macosx_10_13_universal2 + py31-none-macosx_10_12_universal2 + py31-none-macosx_10_11_universal2 + py31-none-macosx_10_10_universal2 + py31-none-macosx_10_9_universal2 + py31-none-macosx_10_8_universal2 + py31-none-macosx_10_7_universal2 + py31-none-macosx_10_6_universal2 + py31-none-macosx_10_5_universal2 + py31-none-macosx_10_4_universal2 + py30-none-macosx_14_0_arm64 + py30-none-macosx_14_0_universal2 + py30-none-macosx_13_0_arm64 + py30-none-macosx_13_0_universal2 + py30-none-macosx_12_0_arm64 + py30-none-macosx_12_0_universal2 + py30-none-macosx_11_0_arm64 + py30-none-macosx_11_0_universal2 + py30-none-macosx_10_16_universal2 + py30-none-macosx_10_15_universal2 + py30-none-macosx_10_14_universal2 + py30-none-macosx_10_13_universal2 + py30-none-macosx_10_12_universal2 + py30-none-macosx_10_11_universal2 + py30-none-macosx_10_10_universal2 + py30-none-macosx_10_9_universal2 + py30-none-macosx_10_8_universal2 + py30-none-macosx_10_7_universal2 + py30-none-macosx_10_6_universal2 + py30-none-macosx_10_5_universal2 + py30-none-macosx_10_4_universal2 + cp39-none-any + py39-none-any + py3-none-any + py38-none-any + py37-none-any + py36-none-any + py35-none-any + py34-none-any + py33-none-any + py32-none-any + py31-none-any + py30-none-any + "### + ); +} diff --git a/crates/uv-pubgrub/Cargo.toml b/crates/uv-pubgrub/Cargo.toml index 907856145..8c9753112 100644 --- a/crates/uv-pubgrub/Cargo.toml +++ b/crates/uv-pubgrub/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" edition = "2021" description = "Common uv pubgrub types." +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index 9348924bf..383c15499 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true authors.workspace = true license.workspace = true +[lib] +doctest = false + [dependencies] uv-client = { workspace = true } uv-configuration = { workspace = true } diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 8a90a8952..734ef6038 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -640,275 +640,4 @@ async fn handle_response(registry: &Url, response: Response) -> Result) -> usize { - 0 - } - fn on_download_progress(&self, _id: usize, _inc: u64) {} - fn on_download_complete(&self, _id: usize) {} - } - - /// Snapshot the data we send for an upload request for a source distribution. - #[tokio::test] - async fn upload_request_source_dist() { - let filename = "tqdm-999.0.0.tar.gz"; - let file = PathBuf::from("../../scripts/links/").join(filename); - let filename = DistFilename::try_from_normalized_filename(filename).unwrap(); - - let form_metadata = form_metadata(&file, &filename).await.unwrap(); - - let formatted_metadata = form_metadata - .iter() - .map(|(k, v)| format!("{k}: {v}")) - .join("\n"); - assert_snapshot!(&formatted_metadata, @r###" - :action: file_upload - sha256_digest: 89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b - protocol_version: 1 - metadata_version: 2.3 - name: tqdm - version: 999.0.0 - filetype: sdist - pyversion: source - description: # tqdm - - [![PyPI - Version](https://img.shields.io/pypi/v/tqdm.svg)](https://pypi.org/project/tqdm) - [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tqdm.svg)](https://pypi.org/project/tqdm) - - ----- - - **Table of Contents** - - - [Installation](#installation) - - [License](#license) - - ## Installation - - ```console - pip install tqdm - ``` - - ## License - - `tqdm` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. - - description_content_type: text/markdown - author_email: Charlie Marsh - requires_python: >=3.8 - classifiers: Development Status :: 4 - Beta - classifiers: Programming Language :: Python - classifiers: Programming Language :: Python :: 3.8 - classifiers: Programming Language :: Python :: 3.9 - classifiers: Programming Language :: Python :: 3.10 - classifiers: Programming Language :: Python :: 3.11 - classifiers: Programming Language :: Python :: 3.12 - classifiers: Programming Language :: Python :: Implementation :: CPython - classifiers: Programming Language :: Python :: Implementation :: PyPy - project_urls: Documentation, https://github.com/unknown/tqdm#readme - project_urls: Issues, https://github.com/unknown/tqdm/issues - project_urls: Source, https://github.com/unknown/tqdm - "###); - - let (request, _) = build_request( - &file, - &filename, - &Url::parse("https://example.org/upload").unwrap(), - &BaseClientBuilder::new().build().client(), - Some("ferris"), - Some("F3RR!S"), - &form_metadata, - Arc::new(DummyReporter), - ) - .await - .unwrap(); - - insta::with_settings!({ - filters => [("boundary=[0-9a-f-]+", "boundary=[...]")], - }, { - assert_debug_snapshot!(&request, @r###" - RequestBuilder { - inner: RequestBuilder { - method: POST, - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "example.org", - ), - ), - port: None, - path: "/upload", - query: None, - fragment: None, - }, - headers: { - "content-type": "multipart/form-data; boundary=[...]", - "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7", - "authorization": "Basic ZmVycmlzOkYzUlIhUw==", - }, - }, - .. - } - "###); - }); - } - - /// Snapshot the data we send for an upload request for a wheel. - #[tokio::test] - async fn upload_request_wheel() { - let filename = "tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl"; - let file = PathBuf::from("../../scripts/links/").join(filename); - let filename = DistFilename::try_from_normalized_filename(filename).unwrap(); - - let form_metadata = form_metadata(&file, &filename).await.unwrap(); - - let formatted_metadata = form_metadata - .iter() - .map(|(k, v)| format!("{k}: {v}")) - .join("\n"); - assert_snapshot!(&formatted_metadata, @r###" - :action: file_upload - sha256_digest: 0d88ca657bc6b64995ca416e0c59c71af85cc10015d940fa446c42a8b485ee1c - protocol_version: 1 - metadata_version: 2.1 - name: tqdm - version: 4.66.1 - filetype: bdist_wheel - pyversion: py3 - summary: Fast, Extensible Progress Meter - description_content_type: text/x-rst - maintainer_email: tqdm developers - license: MPL-2.0 AND MIT - keywords: progressbar,progressmeter,progress,bar,meter,rate,eta,console,terminal,time - requires_python: >=3.7 - classifiers: Development Status :: 5 - Production/Stable - classifiers: Environment :: Console - classifiers: Environment :: MacOS X - classifiers: Environment :: Other Environment - classifiers: Environment :: Win32 (MS Windows) - classifiers: Environment :: X11 Applications - classifiers: Framework :: IPython - classifiers: Framework :: Jupyter - classifiers: Intended Audience :: Developers - classifiers: Intended Audience :: Education - classifiers: Intended Audience :: End Users/Desktop - classifiers: Intended Audience :: Other Audience - classifiers: Intended Audience :: System Administrators - classifiers: License :: OSI Approved :: MIT License - classifiers: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) - classifiers: Operating System :: MacOS - classifiers: Operating System :: MacOS :: MacOS X - classifiers: Operating System :: Microsoft - classifiers: Operating System :: Microsoft :: MS-DOS - classifiers: Operating System :: Microsoft :: Windows - classifiers: Operating System :: POSIX - classifiers: Operating System :: POSIX :: BSD - classifiers: Operating System :: POSIX :: BSD :: FreeBSD - classifiers: Operating System :: POSIX :: Linux - classifiers: Operating System :: POSIX :: SunOS/Solaris - classifiers: Operating System :: Unix - classifiers: Programming Language :: Python - classifiers: Programming Language :: Python :: 3 - classifiers: Programming Language :: Python :: 3.7 - classifiers: Programming Language :: Python :: 3.8 - classifiers: Programming Language :: Python :: 3.9 - classifiers: Programming Language :: Python :: 3.10 - classifiers: Programming Language :: Python :: 3.11 - classifiers: Programming Language :: Python :: 3 :: Only - classifiers: Programming Language :: Python :: Implementation - classifiers: Programming Language :: Python :: Implementation :: IronPython - classifiers: Programming Language :: Python :: Implementation :: PyPy - classifiers: Programming Language :: Unix Shell - classifiers: Topic :: Desktop Environment - classifiers: Topic :: Education :: Computer Aided Instruction (CAI) - classifiers: Topic :: Education :: Testing - classifiers: Topic :: Office/Business - classifiers: Topic :: Other/Nonlisted Topic - classifiers: Topic :: Software Development :: Build Tools - classifiers: Topic :: Software Development :: Libraries - classifiers: Topic :: Software Development :: Libraries :: Python Modules - classifiers: Topic :: Software Development :: Pre-processors - classifiers: Topic :: Software Development :: User Interfaces - classifiers: Topic :: System :: Installation/Setup - classifiers: Topic :: System :: Logging - classifiers: Topic :: System :: Monitoring - classifiers: Topic :: System :: Shells - classifiers: Topic :: Terminals - classifiers: Topic :: Utilities - requires_dist: colorama ; platform_system == "Windows" - requires_dist: pytest >=6 ; extra == 'dev' - requires_dist: pytest-cov ; extra == 'dev' - requires_dist: pytest-timeout ; extra == 'dev' - requires_dist: pytest-xdist ; extra == 'dev' - requires_dist: ipywidgets >=6 ; extra == 'notebook' - requires_dist: slack-sdk ; extra == 'slack' - requires_dist: requests ; extra == 'telegram' - project_urls: homepage, https://tqdm.github.io - project_urls: repository, https://github.com/tqdm/tqdm - project_urls: changelog, https://tqdm.github.io/releases - project_urls: wiki, https://github.com/tqdm/tqdm/wiki - "###); - - let (request, _) = build_request( - &file, - &filename, - &Url::parse("https://example.org/upload").unwrap(), - &BaseClientBuilder::new().build().client(), - Some("ferris"), - Some("F3RR!S"), - &form_metadata, - Arc::new(DummyReporter), - ) - .await - .unwrap(); - - insta::with_settings!({ - filters => [("boundary=[0-9a-f-]+", "boundary=[...]")], - }, { - assert_debug_snapshot!(&request, @r###" - RequestBuilder { - inner: RequestBuilder { - method: POST, - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "example.org", - ), - ), - port: None, - path: "/upload", - query: None, - fragment: None, - }, - headers: { - "content-type": "multipart/form-data; boundary=[...]", - "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7", - "authorization": "Basic ZmVycmlzOkYzUlIhUw==", - }, - }, - .. - } - "###); - }); - } -} +mod tests; diff --git a/crates/uv-publish/src/tests.rs b/crates/uv-publish/src/tests.rs new file mode 100644 index 000000000..26b052ef0 --- /dev/null +++ b/crates/uv-publish/src/tests.rs @@ -0,0 +1,271 @@ +use crate::{build_request, form_metadata, Reporter}; +use insta::{assert_debug_snapshot, assert_snapshot}; +use itertools::Itertools; +use std::path::PathBuf; +use std::sync::Arc; +use url::Url; +use uv_client::BaseClientBuilder; +use uv_distribution_filename::DistFilename; + +struct DummyReporter; + +impl Reporter for DummyReporter { + fn on_progress(&self, _name: &str, _id: usize) {} + fn on_download_start(&self, _name: &str, _size: Option) -> usize { + 0 + } + fn on_download_progress(&self, _id: usize, _inc: u64) {} + fn on_download_complete(&self, _id: usize) {} +} + +/// Snapshot the data we send for an upload request for a source distribution. +#[tokio::test] +async fn upload_request_source_dist() { + let filename = "tqdm-999.0.0.tar.gz"; + let file = PathBuf::from("../../scripts/links/").join(filename); + let filename = DistFilename::try_from_normalized_filename(filename).unwrap(); + + let form_metadata = form_metadata(&file, &filename).await.unwrap(); + + let formatted_metadata = form_metadata + .iter() + .map(|(k, v)| format!("{k}: {v}")) + .join("\n"); + assert_snapshot!(&formatted_metadata, @r###" + :action: file_upload + sha256_digest: 89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b + protocol_version: 1 + metadata_version: 2.3 + name: tqdm + version: 999.0.0 + filetype: sdist + pyversion: source + description: # tqdm + + [![PyPI - Version](https://img.shields.io/pypi/v/tqdm.svg)](https://pypi.org/project/tqdm) + [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/tqdm.svg)](https://pypi.org/project/tqdm) + + ----- + + **Table of Contents** + + - [Installation](#installation) + - [License](#license) + + ## Installation + + ```console + pip install tqdm + ``` + + ## License + + `tqdm` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. + + description_content_type: text/markdown + author_email: Charlie Marsh + requires_python: >=3.8 + classifiers: Development Status :: 4 - Beta + classifiers: Programming Language :: Python + classifiers: Programming Language :: Python :: 3.8 + classifiers: Programming Language :: Python :: 3.9 + classifiers: Programming Language :: Python :: 3.10 + classifiers: Programming Language :: Python :: 3.11 + classifiers: Programming Language :: Python :: 3.12 + classifiers: Programming Language :: Python :: Implementation :: CPython + classifiers: Programming Language :: Python :: Implementation :: PyPy + project_urls: Documentation, https://github.com/unknown/tqdm#readme + project_urls: Issues, https://github.com/unknown/tqdm/issues + project_urls: Source, https://github.com/unknown/tqdm + "###); + + let (request, _) = build_request( + &file, + &filename, + &Url::parse("https://example.org/upload").unwrap(), + &BaseClientBuilder::new().build().client(), + Some("ferris"), + Some("F3RR!S"), + &form_metadata, + Arc::new(DummyReporter), + ) + .await + .unwrap(); + + insta::with_settings!({ + filters => [("boundary=[0-9a-f-]+", "boundary=[...]")], + }, { + assert_debug_snapshot!(&request, @r###" + RequestBuilder { + inner: RequestBuilder { + method: POST, + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.org", + ), + ), + port: None, + path: "/upload", + query: None, + fragment: None, + }, + headers: { + "content-type": "multipart/form-data; boundary=[...]", + "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7", + "authorization": "Basic ZmVycmlzOkYzUlIhUw==", + }, + }, + .. + } + "###); + }); +} + +/// Snapshot the data we send for an upload request for a wheel. +#[tokio::test] +async fn upload_request_wheel() { + let filename = + "tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl"; + let file = PathBuf::from("../../scripts/links/").join(filename); + let filename = DistFilename::try_from_normalized_filename(filename).unwrap(); + + let form_metadata = form_metadata(&file, &filename).await.unwrap(); + + let formatted_metadata = form_metadata + .iter() + .map(|(k, v)| format!("{k}: {v}")) + .join("\n"); + assert_snapshot!(&formatted_metadata, @r###" + :action: file_upload + sha256_digest: 0d88ca657bc6b64995ca416e0c59c71af85cc10015d940fa446c42a8b485ee1c + protocol_version: 1 + metadata_version: 2.1 + name: tqdm + version: 4.66.1 + filetype: bdist_wheel + pyversion: py3 + summary: Fast, Extensible Progress Meter + description_content_type: text/x-rst + maintainer_email: tqdm developers + license: MPL-2.0 AND MIT + keywords: progressbar,progressmeter,progress,bar,meter,rate,eta,console,terminal,time + requires_python: >=3.7 + classifiers: Development Status :: 5 - Production/Stable + classifiers: Environment :: Console + classifiers: Environment :: MacOS X + classifiers: Environment :: Other Environment + classifiers: Environment :: Win32 (MS Windows) + classifiers: Environment :: X11 Applications + classifiers: Framework :: IPython + classifiers: Framework :: Jupyter + classifiers: Intended Audience :: Developers + classifiers: Intended Audience :: Education + classifiers: Intended Audience :: End Users/Desktop + classifiers: Intended Audience :: Other Audience + classifiers: Intended Audience :: System Administrators + classifiers: License :: OSI Approved :: MIT License + classifiers: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) + classifiers: Operating System :: MacOS + classifiers: Operating System :: MacOS :: MacOS X + classifiers: Operating System :: Microsoft + classifiers: Operating System :: Microsoft :: MS-DOS + classifiers: Operating System :: Microsoft :: Windows + classifiers: Operating System :: POSIX + classifiers: Operating System :: POSIX :: BSD + classifiers: Operating System :: POSIX :: BSD :: FreeBSD + classifiers: Operating System :: POSIX :: Linux + classifiers: Operating System :: POSIX :: SunOS/Solaris + classifiers: Operating System :: Unix + classifiers: Programming Language :: Python + classifiers: Programming Language :: Python :: 3 + classifiers: Programming Language :: Python :: 3.7 + classifiers: Programming Language :: Python :: 3.8 + classifiers: Programming Language :: Python :: 3.9 + classifiers: Programming Language :: Python :: 3.10 + classifiers: Programming Language :: Python :: 3.11 + classifiers: Programming Language :: Python :: 3 :: Only + classifiers: Programming Language :: Python :: Implementation + classifiers: Programming Language :: Python :: Implementation :: IronPython + classifiers: Programming Language :: Python :: Implementation :: PyPy + classifiers: Programming Language :: Unix Shell + classifiers: Topic :: Desktop Environment + classifiers: Topic :: Education :: Computer Aided Instruction (CAI) + classifiers: Topic :: Education :: Testing + classifiers: Topic :: Office/Business + classifiers: Topic :: Other/Nonlisted Topic + classifiers: Topic :: Software Development :: Build Tools + classifiers: Topic :: Software Development :: Libraries + classifiers: Topic :: Software Development :: Libraries :: Python Modules + classifiers: Topic :: Software Development :: Pre-processors + classifiers: Topic :: Software Development :: User Interfaces + classifiers: Topic :: System :: Installation/Setup + classifiers: Topic :: System :: Logging + classifiers: Topic :: System :: Monitoring + classifiers: Topic :: System :: Shells + classifiers: Topic :: Terminals + classifiers: Topic :: Utilities + requires_dist: colorama ; platform_system == "Windows" + requires_dist: pytest >=6 ; extra == 'dev' + requires_dist: pytest-cov ; extra == 'dev' + requires_dist: pytest-timeout ; extra == 'dev' + requires_dist: pytest-xdist ; extra == 'dev' + requires_dist: ipywidgets >=6 ; extra == 'notebook' + requires_dist: slack-sdk ; extra == 'slack' + requires_dist: requests ; extra == 'telegram' + project_urls: homepage, https://tqdm.github.io + project_urls: repository, https://github.com/tqdm/tqdm + project_urls: changelog, https://tqdm.github.io/releases + project_urls: wiki, https://github.com/tqdm/tqdm/wiki + "###); + + let (request, _) = build_request( + &file, + &filename, + &Url::parse("https://example.org/upload").unwrap(), + &BaseClientBuilder::new().build().client(), + Some("ferris"), + Some("F3RR!S"), + &form_metadata, + Arc::new(DummyReporter), + ) + .await + .unwrap(); + + insta::with_settings!({ + filters => [("boundary=[0-9a-f-]+", "boundary=[...]")], + }, { + assert_debug_snapshot!(&request, @r###" + RequestBuilder { + inner: RequestBuilder { + method: POST, + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "example.org", + ), + ), + port: None, + path: "/upload", + query: None, + fragment: None, + }, + headers: { + "content-type": "multipart/form-data; boundary=[...]", + "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7", + "authorization": "Basic ZmVycmlzOkYzUlIhUw==", + }, + }, + .. + } + "###); + }); +} diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index 5d2197667..a89b29d4b 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-pypi-types/src/lenient_requirement.rs b/crates/uv-pypi-types/src/lenient_requirement.rs index 8baaf96ed..9dafcbce8 100644 --- a/crates/uv-pypi-types/src/lenient_requirement.rs +++ b/crates/uv-pypi-types/src/lenient_requirement.rs @@ -163,261 +163,4 @@ impl<'de> Deserialize<'de> for LenientVersionSpecifiers { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use uv_pep440::VersionSpecifiers; - use uv_pep508::Requirement; - - use crate::LenientVersionSpecifiers; - - use super::LenientRequirement; - - #[test] - fn requirement_missing_comma() { - let actual: Requirement = LenientRequirement::from_str("elasticsearch-dsl (>=7.2.0<8.0.0)") - .unwrap() - .into(); - let expected: Requirement = - Requirement::from_str("elasticsearch-dsl (>=7.2.0,<8.0.0)").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn requirement_not_equal_tile() { - let actual: Requirement = LenientRequirement::from_str("jupyter-core (!=~5.0,>=4.12)") - .unwrap() - .into(); - let expected: Requirement = Requirement::from_str("jupyter-core (!=5.0.*,>=4.12)").unwrap(); - assert_eq!(actual, expected); - - let actual: Requirement = LenientRequirement::from_str("jupyter-core (!=~5,>=4.12)") - .unwrap() - .into(); - let expected: Requirement = Requirement::from_str("jupyter-core (!=5.*,>=4.12)").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn requirement_greater_than_star() { - let actual: Requirement = LenientRequirement::from_str("torch (>=1.9.*)") - .unwrap() - .into(); - let expected: Requirement = Requirement::from_str("torch (>=1.9)").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn requirement_missing_dot() { - let actual: Requirement = - LenientRequirement::from_str("pyzmq (>=2.7,!=3.0*,!=3.1*,!=3.2*)") - .unwrap() - .into(); - let expected: Requirement = - Requirement::from_str("pyzmq (>=2.7,!=3.0.*,!=3.1.*,!=3.2.*)").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn requirement_trailing_comma() { - let actual: Requirement = LenientRequirement::from_str("pyzmq >=3.6,").unwrap().into(); - let expected: Requirement = Requirement::from_str("pyzmq >=3.6").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn specifier_missing_comma() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=7.2.0<8.0.0") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=7.2.0,<8.0.0").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn specifier_not_equal_tile() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str("!=~5.0,>=4.12") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str("!=5.0.*,>=4.12").unwrap(); - assert_eq!(actual, expected); - - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str("!=~5,>=4.12") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str("!=5.*,>=4.12").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn specifier_greater_than_star() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=1.9.*") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=1.9").unwrap(); - assert_eq!(actual, expected); - - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=1.*").unwrap().into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=1").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn specifier_missing_dot() { - let actual: VersionSpecifiers = - LenientVersionSpecifiers::from_str(">=2.7,!=3.0*,!=3.1*,!=3.2*") - .unwrap() - .into(); - let expected: VersionSpecifiers = - VersionSpecifiers::from_str(">=2.7,!=3.0.*,!=3.1.*,!=3.2.*").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn specifier_trailing_comma() { - let actual: VersionSpecifiers = - LenientVersionSpecifiers::from_str(">=3.6,").unwrap().into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); - assert_eq!(actual, expected); - } - - #[test] - fn specifier_trailing_comma_trailing_space() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=3.6, ") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn specifier_invalid_single_quotes() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">= '2.7'") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">= 2.7").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn specifier_invalid_double_quotes() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=\"3.6\"") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn specifier_multi_fix() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str( - ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*,", - ) - .unwrap() - .into(); - let expected: VersionSpecifiers = - VersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*") - .unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn smaller_than_star() { - let actual: VersionSpecifiers = - LenientVersionSpecifiers::from_str(">=2.7,!=3.0.*,!=3.1.*,<3.4.*") - .unwrap() - .into(); - let expected: VersionSpecifiers = - VersionSpecifiers::from_str(">=2.7,!=3.0.*,!=3.1.*,<3.4").unwrap(); - assert_eq!(actual, expected); - } - - /// - /// - #[test] - fn stray_quote() { - let actual: VersionSpecifiers = - LenientVersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*', !=3.2.*, !=3.3.*'") - .unwrap() - .into(); - let expected: VersionSpecifiers = - VersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*").unwrap(); - assert_eq!(actual, expected); - let actual: VersionSpecifiers = - LenientVersionSpecifiers::from_str(">=3.6'").unwrap().into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn trailing_comma_after_quote() { - let actual: Requirement = LenientRequirement::from_str("botocore>=1.3.0,<1.4.0',") - .unwrap() - .into(); - let expected: Requirement = Requirement::from_str("botocore>=1.3.0,<1.4.0").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn greater_than_dev() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">dev").unwrap().into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">0.0.0dev").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn trailing_alpha_zero() { - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=9.0.0a1.0") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=9.0.0a1").unwrap(); - assert_eq!(actual, expected); - - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=9.0a1.0") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=9.0a1").unwrap(); - assert_eq!(actual, expected); - - let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=9a1.0") - .unwrap() - .into(); - let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=9a1").unwrap(); - assert_eq!(actual, expected); - } - - /// - #[test] - fn stray_quote_preserve_marker() { - let actual: Requirement = - LenientRequirement::from_str("numpy >=1.19; python_version >= \"3.7\"") - .unwrap() - .into(); - let expected: Requirement = - Requirement::from_str("numpy >=1.19; python_version >= \"3.7\"").unwrap(); - assert_eq!(actual, expected); - - let actual: Requirement = - LenientRequirement::from_str("numpy \">=1.19\"; python_version >= \"3.7\"") - .unwrap() - .into(); - let expected: Requirement = - Requirement::from_str("numpy >=1.19; python_version >= \"3.7\"").unwrap(); - assert_eq!(actual, expected); - - let actual: Requirement = - LenientRequirement::from_str("'numpy' >=1.19\"; python_version >= \"3.7\"") - .unwrap() - .into(); - let expected: Requirement = - Requirement::from_str("numpy >=1.19; python_version >= \"3.7\"").unwrap(); - assert_eq!(actual, expected); - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/lenient_requirement/tests.rs b/crates/uv-pypi-types/src/lenient_requirement/tests.rs new file mode 100644 index 000000000..64b8b58cf --- /dev/null +++ b/crates/uv-pypi-types/src/lenient_requirement/tests.rs @@ -0,0 +1,251 @@ +use std::str::FromStr; + +use uv_pep440::VersionSpecifiers; +use uv_pep508::Requirement; + +use crate::LenientVersionSpecifiers; + +use super::LenientRequirement; + +#[test] +fn requirement_missing_comma() { + let actual: Requirement = LenientRequirement::from_str("elasticsearch-dsl (>=7.2.0<8.0.0)") + .unwrap() + .into(); + let expected: Requirement = + Requirement::from_str("elasticsearch-dsl (>=7.2.0,<8.0.0)").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn requirement_not_equal_tile() { + let actual: Requirement = LenientRequirement::from_str("jupyter-core (!=~5.0,>=4.12)") + .unwrap() + .into(); + let expected: Requirement = Requirement::from_str("jupyter-core (!=5.0.*,>=4.12)").unwrap(); + assert_eq!(actual, expected); + + let actual: Requirement = LenientRequirement::from_str("jupyter-core (!=~5,>=4.12)") + .unwrap() + .into(); + let expected: Requirement = Requirement::from_str("jupyter-core (!=5.*,>=4.12)").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn requirement_greater_than_star() { + let actual: Requirement = LenientRequirement::from_str("torch (>=1.9.*)") + .unwrap() + .into(); + let expected: Requirement = Requirement::from_str("torch (>=1.9)").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn requirement_missing_dot() { + let actual: Requirement = LenientRequirement::from_str("pyzmq (>=2.7,!=3.0*,!=3.1*,!=3.2*)") + .unwrap() + .into(); + let expected: Requirement = + Requirement::from_str("pyzmq (>=2.7,!=3.0.*,!=3.1.*,!=3.2.*)").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn requirement_trailing_comma() { + let actual: Requirement = LenientRequirement::from_str("pyzmq >=3.6,").unwrap().into(); + let expected: Requirement = Requirement::from_str("pyzmq >=3.6").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn specifier_missing_comma() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=7.2.0<8.0.0") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=7.2.0,<8.0.0").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn specifier_not_equal_tile() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str("!=~5.0,>=4.12") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str("!=5.0.*,>=4.12").unwrap(); + assert_eq!(actual, expected); + + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str("!=~5,>=4.12") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str("!=5.*,>=4.12").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn specifier_greater_than_star() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=1.9.*") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=1.9").unwrap(); + assert_eq!(actual, expected); + + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=1.*").unwrap().into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=1").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn specifier_missing_dot() { + let actual: VersionSpecifiers = + LenientVersionSpecifiers::from_str(">=2.7,!=3.0*,!=3.1*,!=3.2*") + .unwrap() + .into(); + let expected: VersionSpecifiers = + VersionSpecifiers::from_str(">=2.7,!=3.0.*,!=3.1.*,!=3.2.*").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn specifier_trailing_comma() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=3.6,").unwrap().into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn specifier_trailing_comma_trailing_space() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=3.6, ") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn specifier_invalid_single_quotes() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">= '2.7'") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">= 2.7").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn specifier_invalid_double_quotes() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=\"3.6\"") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn specifier_multi_fix() { + let actual: VersionSpecifiers = + LenientVersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*,") + .unwrap() + .into(); + let expected: VersionSpecifiers = + VersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn smaller_than_star() { + let actual: VersionSpecifiers = + LenientVersionSpecifiers::from_str(">=2.7,!=3.0.*,!=3.1.*,<3.4.*") + .unwrap() + .into(); + let expected: VersionSpecifiers = + VersionSpecifiers::from_str(">=2.7,!=3.0.*,!=3.1.*,<3.4").unwrap(); + assert_eq!(actual, expected); +} + +/// +/// +#[test] +fn stray_quote() { + let actual: VersionSpecifiers = + LenientVersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*', !=3.2.*, !=3.3.*'") + .unwrap() + .into(); + let expected: VersionSpecifiers = + VersionSpecifiers::from_str(">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*").unwrap(); + assert_eq!(actual, expected); + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=3.6'").unwrap().into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=3.6").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn trailing_comma_after_quote() { + let actual: Requirement = LenientRequirement::from_str("botocore>=1.3.0,<1.4.0',") + .unwrap() + .into(); + let expected: Requirement = Requirement::from_str("botocore>=1.3.0,<1.4.0").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn greater_than_dev() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">dev").unwrap().into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">0.0.0dev").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn trailing_alpha_zero() { + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=9.0.0a1.0") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=9.0.0a1").unwrap(); + assert_eq!(actual, expected); + + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=9.0a1.0") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=9.0a1").unwrap(); + assert_eq!(actual, expected); + + let actual: VersionSpecifiers = LenientVersionSpecifiers::from_str(">=9a1.0") + .unwrap() + .into(); + let expected: VersionSpecifiers = VersionSpecifiers::from_str(">=9a1").unwrap(); + assert_eq!(actual, expected); +} + +/// +#[test] +fn stray_quote_preserve_marker() { + let actual: Requirement = + LenientRequirement::from_str("numpy >=1.19; python_version >= \"3.7\"") + .unwrap() + .into(); + let expected: Requirement = + Requirement::from_str("numpy >=1.19; python_version >= \"3.7\"").unwrap(); + assert_eq!(actual, expected); + + let actual: Requirement = + LenientRequirement::from_str("numpy \">=1.19\"; python_version >= \"3.7\"") + .unwrap() + .into(); + let expected: Requirement = + Requirement::from_str("numpy >=1.19; python_version >= \"3.7\"").unwrap(); + assert_eq!(actual, expected); + + let actual: Requirement = + LenientRequirement::from_str("'numpy' >=1.19\"; python_version >= \"3.7\"") + .unwrap() + .into(); + let expected: Requirement = + Requirement::from_str("numpy >=1.19; python_version >= \"3.7\"").unwrap(); + assert_eq!(actual, expected); +} diff --git a/crates/uv-pypi-types/src/metadata/metadata23.rs b/crates/uv-pypi-types/src/metadata/metadata23.rs index f10a73968..8a1bfd9cc 100644 --- a/crates/uv-pypi-types/src/metadata/metadata23.rs +++ b/crates/uv-pypi-types/src/metadata/metadata23.rs @@ -275,37 +275,4 @@ impl FromStr for Metadata23 { } #[cfg(test)] -mod tests { - use super::*; - use crate::MetadataError; - - #[test] - fn test_parse_from_str() { - let s = "Metadata-Version: 1.0"; - let meta: Result = s.parse(); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name")))); - - let s = "Metadata-Version: 1.0\nName: asdf"; - let meta = Metadata23::parse(s.as_bytes()); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); - - let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0"; - let meta = Metadata23::parse(s.as_bytes()).unwrap(); - assert_eq!(meta.metadata_version, "1.0"); - assert_eq!(meta.name, "asdf"); - assert_eq!(meta.version, "1.0"); - - let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nDescription: a Python package"; - let meta: Metadata23 = s.parse().unwrap(); - assert_eq!(meta.description.as_deref(), Some("a Python package")); - - let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\n\na Python package"; - let meta: Metadata23 = s.parse().unwrap(); - assert_eq!(meta.description.as_deref(), Some("a Python package")); - - let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包"; - let meta: Metadata23 = s.parse().unwrap(); - assert_eq!(meta.author.as_deref(), Some("中文")); - assert_eq!(meta.description.as_deref(), Some("一个 Python 包")); - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/metadata/metadata23/tests.rs b/crates/uv-pypi-types/src/metadata/metadata23/tests.rs new file mode 100644 index 000000000..be2f358cf --- /dev/null +++ b/crates/uv-pypi-types/src/metadata/metadata23/tests.rs @@ -0,0 +1,32 @@ +use super::*; +use crate::MetadataError; + +#[test] +fn test_parse_from_str() { + let s = "Metadata-Version: 1.0"; + let meta: Result = s.parse(); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name")))); + + let s = "Metadata-Version: 1.0\nName: asdf"; + let meta = Metadata23::parse(s.as_bytes()); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); + + let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0"; + let meta = Metadata23::parse(s.as_bytes()).unwrap(); + assert_eq!(meta.metadata_version, "1.0"); + assert_eq!(meta.name, "asdf"); + assert_eq!(meta.version, "1.0"); + + let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nDescription: a Python package"; + let meta: Metadata23 = s.parse().unwrap(); + assert_eq!(meta.description.as_deref(), Some("a Python package")); + + let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\n\na Python package"; + let meta: Metadata23 = s.parse().unwrap(); + assert_eq!(meta.description.as_deref(), Some("a Python package")); + + let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包"; + let meta: Metadata23 = s.parse().unwrap(); + assert_eq!(meta.author.as_deref(), Some("中文")); + assert_eq!(meta.description.as_deref(), Some("一个 Python 包")); +} diff --git a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs index da68a2e46..9d854f7c7 100644 --- a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs +++ b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs @@ -157,73 +157,4 @@ impl ResolutionMetadata { } #[cfg(test)] -mod tests { - use super::*; - use crate::MetadataError; - use std::str::FromStr; - use uv_normalize::PackageName; - use uv_pep440::Version; - - #[test] - fn test_parse_metadata() { - let s = "Metadata-Version: 1.0"; - let meta = ResolutionMetadata::parse_metadata(s.as_bytes()); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name")))); - - let s = "Metadata-Version: 1.0\nName: asdf"; - let meta = ResolutionMetadata::parse_metadata(s.as_bytes()); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); - - let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0"; - let meta = ResolutionMetadata::parse_metadata(s.as_bytes()).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - - let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包"; - let meta = ResolutionMetadata::parse_metadata(s.as_bytes()).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - - let s = "Metadata-Version: 1.0\nName: =?utf-8?q?foobar?=\nVersion: 1.0"; - let meta = ResolutionMetadata::parse_metadata(s.as_bytes()).unwrap(); - assert_eq!(meta.name, PackageName::from_str("foobar").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - - let s = "Metadata-Version: 1.0\nName: =?utf-8?q?=C3=A4_space?= \nVersion: 1.0"; - let meta = ResolutionMetadata::parse_metadata(s.as_bytes()); - assert!(matches!(meta, Err(MetadataError::InvalidName(_)))); - } - - #[test] - fn test_parse_pkg_info() { - let s = "Metadata-Version: 2.1"; - let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()); - assert!(matches!( - meta, - Err(MetadataError::UnsupportedMetadataVersion(_)) - )); - - let s = "Metadata-Version: 2.2\nName: asdf"; - let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); - - let s = "Metadata-Version: 2.3\nName: asdf"; - let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); - - let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0"; - let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - - let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nDynamic: Requires-Dist"; - let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()).unwrap_err(); - assert!(matches!(meta, MetadataError::DynamicField("Requires-Dist"))); - - let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nRequires-Dist: foo"; - let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]); - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/metadata/metadata_resolver/tests.rs b/crates/uv-pypi-types/src/metadata/metadata_resolver/tests.rs new file mode 100644 index 000000000..e4a0e90e5 --- /dev/null +++ b/crates/uv-pypi-types/src/metadata/metadata_resolver/tests.rs @@ -0,0 +1,68 @@ +use super::*; +use crate::MetadataError; +use std::str::FromStr; +use uv_normalize::PackageName; +use uv_pep440::Version; + +#[test] +fn test_parse_metadata() { + let s = "Metadata-Version: 1.0"; + let meta = ResolutionMetadata::parse_metadata(s.as_bytes()); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name")))); + + let s = "Metadata-Version: 1.0\nName: asdf"; + let meta = ResolutionMetadata::parse_metadata(s.as_bytes()); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); + + let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0"; + let meta = ResolutionMetadata::parse_metadata(s.as_bytes()).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + + let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0\nAuthor: 中文\n\n一个 Python 包"; + let meta = ResolutionMetadata::parse_metadata(s.as_bytes()).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + + let s = "Metadata-Version: 1.0\nName: =?utf-8?q?foobar?=\nVersion: 1.0"; + let meta = ResolutionMetadata::parse_metadata(s.as_bytes()).unwrap(); + assert_eq!(meta.name, PackageName::from_str("foobar").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + + let s = "Metadata-Version: 1.0\nName: =?utf-8?q?=C3=A4_space?= \nVersion: 1.0"; + let meta = ResolutionMetadata::parse_metadata(s.as_bytes()); + assert!(matches!(meta, Err(MetadataError::InvalidName(_)))); +} + +#[test] +fn test_parse_pkg_info() { + let s = "Metadata-Version: 2.1"; + let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()); + assert!(matches!( + meta, + Err(MetadataError::UnsupportedMetadataVersion(_)) + )); + + let s = "Metadata-Version: 2.2\nName: asdf"; + let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); + + let s = "Metadata-Version: 2.3\nName: asdf"; + let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); + + let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0"; + let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + + let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nDynamic: Requires-Dist"; + let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()).unwrap_err(); + assert!(matches!(meta, MetadataError::DynamicField("Requires-Dist"))); + + let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nRequires-Dist: foo"; + let meta = ResolutionMetadata::parse_pkg_info(s.as_bytes()).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]); +} diff --git a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs index 1371a3498..0cf74e9fb 100644 --- a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs +++ b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs @@ -225,90 +225,4 @@ impl RequiresDist { } #[cfg(test)] -mod tests { - use crate::metadata::pyproject_toml::parse_pyproject_toml; - use crate::MetadataError; - use std::str::FromStr; - use uv_normalize::PackageName; - use uv_pep440::Version; - - #[test] - fn test_parse_pyproject_toml() { - let s = r#" - [project] - name = "asdf" - "#; - let meta = parse_pyproject_toml(s); - assert!(matches!(meta, Err(MetadataError::FieldNotFound("version")))); - - let s = r#" - [project] - name = "asdf" - dynamic = ["version"] - "#; - let meta = parse_pyproject_toml(s); - assert!(matches!(meta, Err(MetadataError::DynamicField("version")))); - - let s = r#" - [project] - name = "asdf" - version = "1.0" - "#; - let meta = parse_pyproject_toml(s).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - assert!(meta.requires_python.is_none()); - assert!(meta.requires_dist.is_empty()); - assert!(meta.provides_extras.is_empty()); - - let s = r#" - [project] - name = "asdf" - version = "1.0" - requires-python = ">=3.6" - "#; - let meta = parse_pyproject_toml(s).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap())); - assert!(meta.requires_dist.is_empty()); - assert!(meta.provides_extras.is_empty()); - - let s = r#" - [project] - name = "asdf" - version = "1.0" - requires-python = ">=3.6" - dependencies = ["foo"] - "#; - let meta = parse_pyproject_toml(s).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap())); - assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]); - assert!(meta.provides_extras.is_empty()); - - let s = r#" - [project] - name = "asdf" - version = "1.0" - requires-python = ">=3.6" - dependencies = ["foo"] - - [project.optional-dependencies] - dotenv = ["bar"] - "#; - let meta = parse_pyproject_toml(s).unwrap(); - assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); - assert_eq!(meta.version, Version::new([1, 0])); - assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap())); - assert_eq!( - meta.requires_dist, - vec![ - "foo".parse().unwrap(), - "bar; extra == \"dotenv\"".parse().unwrap() - ] - ); - assert_eq!(meta.provides_extras, vec!["dotenv".parse().unwrap()]); - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs b/crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs new file mode 100644 index 000000000..c137fb99b --- /dev/null +++ b/crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs @@ -0,0 +1,85 @@ +use crate::metadata::pyproject_toml::parse_pyproject_toml; +use crate::MetadataError; +use std::str::FromStr; +use uv_normalize::PackageName; +use uv_pep440::Version; + +#[test] +fn test_parse_pyproject_toml() { + let s = r#" + [project] + name = "asdf" + "#; + let meta = parse_pyproject_toml(s); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("version")))); + + let s = r#" + [project] + name = "asdf" + dynamic = ["version"] + "#; + let meta = parse_pyproject_toml(s); + assert!(matches!(meta, Err(MetadataError::DynamicField("version")))); + + let s = r#" + [project] + name = "asdf" + version = "1.0" + "#; + let meta = parse_pyproject_toml(s).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + assert!(meta.requires_python.is_none()); + assert!(meta.requires_dist.is_empty()); + assert!(meta.provides_extras.is_empty()); + + let s = r#" + [project] + name = "asdf" + version = "1.0" + requires-python = ">=3.6" + "#; + let meta = parse_pyproject_toml(s).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap())); + assert!(meta.requires_dist.is_empty()); + assert!(meta.provides_extras.is_empty()); + + let s = r#" + [project] + name = "asdf" + version = "1.0" + requires-python = ">=3.6" + dependencies = ["foo"] + "#; + let meta = parse_pyproject_toml(s).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap())); + assert_eq!(meta.requires_dist, vec!["foo".parse().unwrap()]); + assert!(meta.provides_extras.is_empty()); + + let s = r#" + [project] + name = "asdf" + version = "1.0" + requires-python = ">=3.6" + dependencies = ["foo"] + + [project.optional-dependencies] + dotenv = ["bar"] + "#; + let meta = parse_pyproject_toml(s).unwrap(); + assert_eq!(meta.name, PackageName::from_str("asdf").unwrap()); + assert_eq!(meta.version, Version::new([1, 0])); + assert_eq!(meta.requires_python, Some(">=3.6".parse().unwrap())); + assert_eq!( + meta.requires_dist, + vec![ + "foo".parse().unwrap(), + "bar; extra == \"dotenv\"".parse().unwrap() + ] + ); + assert_eq!(meta.provides_extras, vec!["dotenv".parse().unwrap()]); +} diff --git a/crates/uv-pypi-types/src/metadata/requires_txt.rs b/crates/uv-pypi-types/src/metadata/requires_txt.rs index 083e0a9ba..fbfa4fe6d 100644 --- a/crates/uv-pypi-types/src/metadata/requires_txt.rs +++ b/crates/uv-pypi-types/src/metadata/requires_txt.rs @@ -110,61 +110,4 @@ impl RequiresTxt { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_requires_txt() { - let s = r" -Werkzeug>=0.14 -Jinja2>=2.10 - -[dev] -pytest>=3 -sphinx - -[dotenv] -python-dotenv - "; - let meta = RequiresTxt::parse(s.as_bytes()).unwrap(); - assert_eq!( - meta.requires_dist, - vec![ - "Werkzeug>=0.14".parse().unwrap(), - "Jinja2>=2.10".parse().unwrap(), - "pytest>=3; extra == \"dev\"".parse().unwrap(), - "sphinx; extra == \"dev\"".parse().unwrap(), - "python-dotenv; extra == \"dotenv\"".parse().unwrap(), - ] - ); - - let s = r" -Werkzeug>=0.14 - -[dev:] -Jinja2>=2.10 - -[:sys_platform == 'win32'] -pytest>=3 - -[] -sphinx - -[dotenv:sys_platform == 'darwin'] -python-dotenv - "; - let meta = RequiresTxt::parse(s.as_bytes()).unwrap(); - assert_eq!( - meta.requires_dist, - vec![ - "Werkzeug>=0.14".parse().unwrap(), - "Jinja2>=2.10 ; extra == \"dev\"".parse().unwrap(), - "pytest>=3; sys_platform == 'win32'".parse().unwrap(), - "sphinx".parse().unwrap(), - "python-dotenv; sys_platform == 'darwin' and extra == \"dotenv\"" - .parse() - .unwrap(), - ] - ); - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/metadata/requires_txt/tests.rs b/crates/uv-pypi-types/src/metadata/requires_txt/tests.rs new file mode 100644 index 000000000..ee20fd62e --- /dev/null +++ b/crates/uv-pypi-types/src/metadata/requires_txt/tests.rs @@ -0,0 +1,56 @@ +use super::*; + +#[test] +fn test_requires_txt() { + let s = r" +Werkzeug>=0.14 +Jinja2>=2.10 + +[dev] +pytest>=3 +sphinx + +[dotenv] +python-dotenv + "; + let meta = RequiresTxt::parse(s.as_bytes()).unwrap(); + assert_eq!( + meta.requires_dist, + vec![ + "Werkzeug>=0.14".parse().unwrap(), + "Jinja2>=2.10".parse().unwrap(), + "pytest>=3; extra == \"dev\"".parse().unwrap(), + "sphinx; extra == \"dev\"".parse().unwrap(), + "python-dotenv; extra == \"dotenv\"".parse().unwrap(), + ] + ); + + let s = r" +Werkzeug>=0.14 + +[dev:] +Jinja2>=2.10 + +[:sys_platform == 'win32'] +pytest>=3 + +[] +sphinx + +[dotenv:sys_platform == 'darwin'] +python-dotenv + "; + let meta = RequiresTxt::parse(s.as_bytes()).unwrap(); + assert_eq!( + meta.requires_dist, + vec![ + "Werkzeug>=0.14".parse().unwrap(), + "Jinja2>=2.10 ; extra == \"dev\"".parse().unwrap(), + "pytest>=3; sys_platform == 'win32'".parse().unwrap(), + "sphinx".parse().unwrap(), + "python-dotenv; sys_platform == 'darwin' and extra == \"dotenv\"" + .parse() + .unwrap(), + ] + ); +} diff --git a/crates/uv-pypi-types/src/parsed_url.rs b/crates/uv-pypi-types/src/parsed_url.rs index ecc1031b5..3afcadd70 100644 --- a/crates/uv-pypi-types/src/parsed_url.rs +++ b/crates/uv-pypi-types/src/parsed_url.rs @@ -507,46 +507,4 @@ impl From for Url { } #[cfg(test)] -mod tests { - use anyhow::Result; - use url::Url; - - use crate::parsed_url::ParsedUrl; - - #[test] - fn direct_url_from_url() -> Result<()> { - let expected = Url::parse("git+https://github.com/pallets/flask.git")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - - let expected = Url::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - - let expected = Url::parse("git+https://github.com/pallets/flask.git@2.0.0")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - - let expected = - Url::parse("git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - - // TODO(charlie): Preserve other fragments. - let expected = - Url::parse("git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_ne!(expected, actual); - - Ok(()) - } - - #[test] - #[cfg(unix)] - fn direct_url_from_url_absolute() -> Result<()> { - let expected = Url::parse("file:///path/to/directory")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - Ok(()) - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/parsed_url/tests.rs b/crates/uv-pypi-types/src/parsed_url/tests.rs new file mode 100644 index 000000000..b5f606bbc --- /dev/null +++ b/crates/uv-pypi-types/src/parsed_url/tests.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use url::Url; + +use crate::parsed_url::ParsedUrl; + +#[test] +fn direct_url_from_url() -> Result<()> { + let expected = Url::parse("git+https://github.com/pallets/flask.git")?; + let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + + let expected = Url::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?; + let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + + let expected = Url::parse("git+https://github.com/pallets/flask.git@2.0.0")?; + let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + + let expected = + Url::parse("git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir")?; + let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + + // TODO(charlie): Preserve other fragments. + let expected = + Url::parse("git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir")?; + let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + assert_ne!(expected, actual); + + Ok(()) +} + +#[test] +#[cfg(unix)] +fn direct_url_from_url_absolute() -> Result<()> { + let expected = Url::parse("file:///path/to/directory")?; + let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + Ok(()) +} diff --git a/crates/uv-pypi-types/src/requirement.rs b/crates/uv-pypi-types/src/requirement.rs index 499b77411..ee930c573 100644 --- a/crates/uv-pypi-types/src/requirement.rs +++ b/crates/uv-pypi-types/src/requirement.rs @@ -827,50 +827,4 @@ pub fn redact_git_credentials(url: &mut Url) { } #[cfg(test)] -mod tests { - use std::path::PathBuf; - - use uv_pep508::{MarkerTree, VerbatimUrl}; - - use crate::{Requirement, RequirementSource}; - - #[test] - fn roundtrip() { - let requirement = Requirement { - name: "foo".parse().unwrap(), - extras: vec![], - marker: MarkerTree::TRUE, - source: RequirementSource::Registry { - specifier: ">1,<2".parse().unwrap(), - index: None, - }, - origin: None, - }; - - let raw = toml::to_string(&requirement).unwrap(); - let deserialized: Requirement = toml::from_str(&raw).unwrap(); - assert_eq!(requirement, deserialized); - - let path = if cfg!(windows) { - "C:\\home\\ferris\\foo" - } else { - "/home/ferris/foo" - }; - let requirement = Requirement { - name: "foo".parse().unwrap(), - extras: vec![], - marker: MarkerTree::TRUE, - source: RequirementSource::Directory { - install_path: PathBuf::from(path), - editable: false, - r#virtual: false, - url: VerbatimUrl::from_absolute_path(path).unwrap(), - }, - origin: None, - }; - - let raw = toml::to_string(&requirement).unwrap(); - let deserialized: Requirement = toml::from_str(&raw).unwrap(); - assert_eq!(requirement, deserialized); - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/requirement/tests.rs b/crates/uv-pypi-types/src/requirement/tests.rs new file mode 100644 index 000000000..24aa27aa3 --- /dev/null +++ b/crates/uv-pypi-types/src/requirement/tests.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; + +use uv_pep508::{MarkerTree, VerbatimUrl}; + +use crate::{Requirement, RequirementSource}; + +#[test] +fn roundtrip() { + let requirement = Requirement { + name: "foo".parse().unwrap(), + extras: vec![], + marker: MarkerTree::TRUE, + source: RequirementSource::Registry { + specifier: ">1,<2".parse().unwrap(), + index: None, + }, + origin: None, + }; + + let raw = toml::to_string(&requirement).unwrap(); + let deserialized: Requirement = toml::from_str(&raw).unwrap(); + assert_eq!(requirement, deserialized); + + let path = if cfg!(windows) { + "C:\\home\\ferris\\foo" + } else { + "/home/ferris/foo" + }; + let requirement = Requirement { + name: "foo".parse().unwrap(), + extras: vec![], + marker: MarkerTree::TRUE, + source: RequirementSource::Directory { + install_path: PathBuf::from(path), + editable: false, + r#virtual: false, + url: VerbatimUrl::from_absolute_path(path).unwrap(), + }, + origin: None, + }; + + let raw = toml::to_string(&requirement).unwrap(); + let deserialized: Requirement = toml::from_str(&raw).unwrap(); + assert_eq!(requirement, deserialized); +} diff --git a/crates/uv-pypi-types/src/simple_json.rs b/crates/uv-pypi-types/src/simple_json.rs index 5d6868e8d..efba207de 100644 --- a/crates/uv-pypi-types/src/simple_json.rs +++ b/crates/uv-pypi-types/src/simple_json.rs @@ -425,75 +425,4 @@ pub enum HashError { } #[cfg(test)] -mod tests { - use crate::{HashError, Hashes}; - - #[test] - fn parse_hashes() -> Result<(), HashError> { - let hashes: Hashes = - "sha512:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; - assert_eq!( - hashes, - Hashes { - md5: None, - sha256: None, - sha384: None, - sha512: Some( - "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into() - ), - } - ); - - let hashes: Hashes = - "sha384:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; - assert_eq!( - hashes, - Hashes { - md5: None, - sha256: None, - sha384: Some( - "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into() - ), - sha512: None - } - ); - - let hashes: Hashes = - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; - assert_eq!( - hashes, - Hashes { - md5: None, - sha256: Some( - "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into() - ), - sha384: None, - sha512: None - } - ); - - let hashes: Hashes = - "md5:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".parse()?; - assert_eq!( - hashes, - Hashes { - md5: Some( - "090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".into() - ), - sha256: None, - sha384: None, - sha512: None - } - ); - - let result = "sha256=40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f" - .parse::(); - assert!(result.is_err()); - - let result = "blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619" - .parse::(); - assert!(result.is_err()); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-pypi-types/src/simple_json/tests.rs b/crates/uv-pypi-types/src/simple_json/tests.rs new file mode 100644 index 000000000..cbf94b52b --- /dev/null +++ b/crates/uv-pypi-types/src/simple_json/tests.rs @@ -0,0 +1,62 @@ +use crate::{HashError, Hashes}; + +#[test] +fn parse_hashes() -> Result<(), HashError> { + let hashes: Hashes = + "sha512:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; + assert_eq!( + hashes, + Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: Some("40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into()), + } + ); + + let hashes: Hashes = + "sha384:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; + assert_eq!( + hashes, + Hashes { + md5: None, + sha256: None, + sha384: Some("40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into()), + sha512: None + } + ); + + let hashes: Hashes = + "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; + assert_eq!( + hashes, + Hashes { + md5: None, + sha256: Some("40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into()), + sha384: None, + sha512: None + } + ); + + let hashes: Hashes = + "md5:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".parse()?; + assert_eq!( + hashes, + Hashes { + md5: Some("090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".into()), + sha256: None, + sha384: None, + sha512: None + } + ); + + let result = + "sha256=40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse::(); + assert!(result.is_err()); + + let result = + "blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619".parse::(); + assert!(result.is_err()); + + Ok(()) +} diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index c41c21775..7c5589e2a 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 8b237aecd..24abeaf7b 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -2245,534 +2245,4 @@ fn split_wheel_tag_release_version(version: Version) -> Version { } #[cfg(test)] -mod tests { - use std::{path::PathBuf, str::FromStr}; - - use assert_fs::{prelude::*, TempDir}; - use test_log::test; - use uv_pep440::{Prerelease, PrereleaseKind, VersionSpecifiers}; - - use crate::{ - discovery::{PythonRequest, VersionRequest}, - implementation::ImplementationName, - }; - - use super::{Error, PythonVariant}; - - #[test] - fn interpreter_request_from_str() { - assert_eq!(PythonRequest::parse("any"), PythonRequest::Any); - assert_eq!(PythonRequest::parse("default"), PythonRequest::Default); - assert_eq!( - PythonRequest::parse("3.12"), - PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()) - ); - assert_eq!( - PythonRequest::parse(">=3.12"), - PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap()) - ); - assert_eq!( - PythonRequest::parse(">=3.12,<3.13"), - PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap()) - ); - assert_eq!( - PythonRequest::parse(">=3.12,<3.13"), - PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap()) - ); - - assert_eq!( - PythonRequest::parse("3.13.0a1"), - PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap()) - ); - assert_eq!( - PythonRequest::parse("3.13.0b5"), - PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap()) - ); - assert_eq!( - PythonRequest::parse("3.13.0rc1"), - PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap()) - ); - assert_eq!( - PythonRequest::parse("3.13.1rc1"), - PythonRequest::ExecutableName("3.13.1rc1".to_string()), - "Pre-release version requests require a patch version of zero" - ); - assert_eq!( - PythonRequest::parse("3rc1"), - PythonRequest::ExecutableName("3rc1".to_string()), - "Pre-release version requests require a minor version" - ); - - assert_eq!( - PythonRequest::parse("cpython"), - PythonRequest::Implementation(ImplementationName::CPython) - ); - assert_eq!( - PythonRequest::parse("cpython3.12.2"), - PythonRequest::ImplementationVersion( - ImplementationName::CPython, - VersionRequest::from_str("3.12.2").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("pypy"), - PythonRequest::Implementation(ImplementationName::PyPy) - ); - assert_eq!( - PythonRequest::parse("pp"), - PythonRequest::Implementation(ImplementationName::PyPy) - ); - assert_eq!( - PythonRequest::parse("graalpy"), - PythonRequest::Implementation(ImplementationName::GraalPy) - ); - assert_eq!( - PythonRequest::parse("gp"), - PythonRequest::Implementation(ImplementationName::GraalPy) - ); - assert_eq!( - PythonRequest::parse("cp"), - PythonRequest::Implementation(ImplementationName::CPython) - ); - assert_eq!( - PythonRequest::parse("pypy3.10"), - PythonRequest::ImplementationVersion( - ImplementationName::PyPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("pp310"), - PythonRequest::ImplementationVersion( - ImplementationName::PyPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("graalpy3.10"), - PythonRequest::ImplementationVersion( - ImplementationName::GraalPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("gp310"), - PythonRequest::ImplementationVersion( - ImplementationName::GraalPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("cp38"), - PythonRequest::ImplementationVersion( - ImplementationName::CPython, - VersionRequest::from_str("3.8").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("pypy@3.10"), - PythonRequest::ImplementationVersion( - ImplementationName::PyPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("pypy310"), - PythonRequest::ImplementationVersion( - ImplementationName::PyPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("graalpy@3.10"), - PythonRequest::ImplementationVersion( - ImplementationName::GraalPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - assert_eq!( - PythonRequest::parse("graalpy310"), - PythonRequest::ImplementationVersion( - ImplementationName::GraalPy, - VersionRequest::from_str("3.10").unwrap(), - ) - ); - - let tempdir = TempDir::new().unwrap(); - assert_eq!( - PythonRequest::parse(tempdir.path().to_str().unwrap()), - PythonRequest::Directory(tempdir.path().to_path_buf()), - "An existing directory is treated as a directory" - ); - assert_eq!( - PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()), - PythonRequest::File(tempdir.child("foo").path().to_path_buf()), - "A path that does not exist is treated as a file" - ); - tempdir.child("bar").touch().unwrap(); - assert_eq!( - PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()), - PythonRequest::File(tempdir.child("bar").path().to_path_buf()), - "An existing file is treated as a file" - ); - assert_eq!( - PythonRequest::parse("./foo"), - PythonRequest::File(PathBuf::from_str("./foo").unwrap()), - "A string with a file system separator is treated as a file" - ); - assert_eq!( - PythonRequest::parse("3.13t"), - PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap()) - ); - } - - #[test] - fn interpreter_request_to_canonical_string() { - assert_eq!(PythonRequest::Default.to_canonical_string(), "default"); - assert_eq!(PythonRequest::Any.to_canonical_string(), "any"); - assert_eq!( - PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(), - "3.12" - ); - assert_eq!( - PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap()) - .to_canonical_string(), - ">=3.12" - ); - assert_eq!( - PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap()) - .to_canonical_string(), - ">=3.12, <3.13" - ); - - assert_eq!( - PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap()) - .to_canonical_string(), - "3.13a1" - ); - - assert_eq!( - PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap()) - .to_canonical_string(), - "3.13b5" - ); - - assert_eq!( - PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap()) - .to_canonical_string(), - "3.13rc1" - ); - - assert_eq!( - PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap()) - .to_canonical_string(), - "3.13rc4" - ); - - assert_eq!( - PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(), - "foo" - ); - assert_eq!( - PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(), - "cpython" - ); - assert_eq!( - PythonRequest::ImplementationVersion( - ImplementationName::CPython, - VersionRequest::from_str("3.12.2").unwrap(), - ) - .to_canonical_string(), - "cpython@3.12.2" - ); - assert_eq!( - PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(), - "pypy" - ); - assert_eq!( - PythonRequest::ImplementationVersion( - ImplementationName::PyPy, - VersionRequest::from_str("3.10").unwrap(), - ) - .to_canonical_string(), - "pypy@3.10" - ); - assert_eq!( - PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(), - "graalpy" - ); - assert_eq!( - PythonRequest::ImplementationVersion( - ImplementationName::GraalPy, - VersionRequest::from_str("3.10").unwrap(), - ) - .to_canonical_string(), - "graalpy@3.10" - ); - - let tempdir = TempDir::new().unwrap(); - assert_eq!( - PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(), - tempdir.path().to_str().unwrap(), - "An existing directory is treated as a directory" - ); - assert_eq!( - PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(), - tempdir.child("foo").path().to_str().unwrap(), - "A path that does not exist is treated as a file" - ); - tempdir.child("bar").touch().unwrap(); - assert_eq!( - PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(), - tempdir.child("bar").path().to_str().unwrap(), - "An existing file is treated as a file" - ); - assert_eq!( - PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(), - "./foo", - "A string with a file system separator is treated as a file" - ); - } - - #[test] - fn version_request_from_str() { - assert_eq!( - VersionRequest::from_str("3").unwrap(), - VersionRequest::Major(3, PythonVariant::Default) - ); - assert_eq!( - VersionRequest::from_str("3.12").unwrap(), - VersionRequest::MajorMinor(3, 12, PythonVariant::Default) - ); - assert_eq!( - VersionRequest::from_str("3.12.1").unwrap(), - VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default) - ); - assert!(VersionRequest::from_str("1.foo.1").is_err()); - assert_eq!( - VersionRequest::from_str("3").unwrap(), - VersionRequest::Major(3, PythonVariant::Default) - ); - assert_eq!( - VersionRequest::from_str("38").unwrap(), - VersionRequest::MajorMinor(3, 8, PythonVariant::Default) - ); - assert_eq!( - VersionRequest::from_str("312").unwrap(), - VersionRequest::MajorMinor(3, 12, PythonVariant::Default) - ); - assert_eq!( - VersionRequest::from_str("3100").unwrap(), - VersionRequest::MajorMinor(3, 100, PythonVariant::Default) - ); - assert_eq!( - VersionRequest::from_str("3.13a1").unwrap(), - VersionRequest::MajorMinorPrerelease( - 3, - 13, - Prerelease { - kind: PrereleaseKind::Alpha, - number: 1 - }, - PythonVariant::Default - ) - ); - assert_eq!( - VersionRequest::from_str("313b1").unwrap(), - VersionRequest::MajorMinorPrerelease( - 3, - 13, - Prerelease { - kind: PrereleaseKind::Beta, - number: 1 - }, - PythonVariant::Default - ) - ); - assert_eq!( - VersionRequest::from_str("3.13.0b2").unwrap(), - VersionRequest::MajorMinorPrerelease( - 3, - 13, - Prerelease { - kind: PrereleaseKind::Beta, - number: 2 - }, - PythonVariant::Default - ) - ); - assert_eq!( - VersionRequest::from_str("3.13.0rc3").unwrap(), - VersionRequest::MajorMinorPrerelease( - 3, - 13, - Prerelease { - kind: PrereleaseKind::Rc, - number: 3 - }, - PythonVariant::Default - ) - ); - assert!( - matches!( - VersionRequest::from_str("3rc1"), - Err(Error::InvalidVersionRequest(_)) - ), - "Pre-release version requests require a minor version" - ); - assert!( - matches!( - VersionRequest::from_str("3.13.2rc1"), - Err(Error::InvalidVersionRequest(_)) - ), - "Pre-release version requests require a patch version of zero" - ); - assert!( - matches!( - VersionRequest::from_str("3.12-dev"), - Err(Error::InvalidVersionRequest(_)) - ), - "Development version segments are not allowed" - ); - assert!( - matches!( - VersionRequest::from_str("3.12+local"), - Err(Error::InvalidVersionRequest(_)) - ), - "Local version segments are not allowed" - ); - assert!( - matches!( - VersionRequest::from_str("3.12.post0"), - Err(Error::InvalidVersionRequest(_)) - ), - "Post version segments are not allowed" - ); - assert!( - // Test for overflow - matches!( - VersionRequest::from_str("31000"), - Err(Error::InvalidVersionRequest(_)) - ) - ); - assert_eq!( - VersionRequest::from_str("3t").unwrap(), - VersionRequest::Major(3, PythonVariant::Freethreaded) - ); - assert_eq!( - VersionRequest::from_str("313t").unwrap(), - VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded) - ); - assert_eq!( - VersionRequest::from_str("3.13t").unwrap(), - VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded) - ); - assert_eq!( - VersionRequest::from_str(">=3.13t").unwrap(), - VersionRequest::Range( - VersionSpecifiers::from_str(">=3.13").unwrap(), - PythonVariant::Freethreaded - ) - ); - assert_eq!( - VersionRequest::from_str(">=3.13").unwrap(), - VersionRequest::Range( - VersionSpecifiers::from_str(">=3.13").unwrap(), - PythonVariant::Default - ) - ); - assert_eq!( - VersionRequest::from_str(">=3.12,<3.14t").unwrap(), - VersionRequest::Range( - VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(), - PythonVariant::Freethreaded - ) - ); - assert!(matches!( - VersionRequest::from_str("3.13tt"), - Err(Error::InvalidVersionRequest(_)) - )); - } - - #[test] - fn executable_names_from_request() { - fn case(request: &str, expected: &[&str]) { - let (implementation, version) = match PythonRequest::parse(request) { - PythonRequest::Any => (None, VersionRequest::Any), - PythonRequest::Default => (None, VersionRequest::Default), - PythonRequest::Version(version) => (None, version), - PythonRequest::ImplementationVersion(implementation, version) => { - (Some(implementation), version) - } - PythonRequest::Implementation(implementation) => { - (Some(implementation), VersionRequest::Default) - } - result => { - panic!("Test cases should request versions or implementations; got {result:?}") - } - }; - - let result: Vec<_> = version - .executable_names(implementation.as_ref()) - .into_iter() - .map(|name| name.to_string()) - .collect(); - - let expected: Vec<_> = expected - .iter() - .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX)) - .collect(); - - assert_eq!(result, expected, "mismatch for case \"{request}\""); - } - - case( - "any", - &[ - "python", "python3", "cpython", "pypy", "graalpy", "cpython3", "pypy3", "graalpy3", - ], - ); - - case("default", &["python", "python3"]); - - case("3", &["python", "python3"]); - - case("4", &["python", "python4"]); - - case("3.13", &["python", "python3", "python3.13"]); - - case( - "pypy@3.10", - &[ - "python", - "python3", - "python3.10", - "pypy", - "pypy3", - "pypy3.10", - ], - ); - - case( - "3.13t", - &[ - "python", - "python3", - "python3.13", - "pythont", - "python3t", - "python3.13t", - ], - ); - - case( - "3.13.2", - &["python", "python3", "python3.13", "python3.13.2"], - ); - - case( - "3.13rc2", - &["python", "python3", "python3.13", "python3.13rc2"], - ); - } -} +mod tests; diff --git a/crates/uv-python/src/discovery/tests.rs b/crates/uv-python/src/discovery/tests.rs new file mode 100644 index 000000000..9dbd688f3 --- /dev/null +++ b/crates/uv-python/src/discovery/tests.rs @@ -0,0 +1,525 @@ +use std::{path::PathBuf, str::FromStr}; + +use assert_fs::{prelude::*, TempDir}; +use test_log::test; +use uv_pep440::{Prerelease, PrereleaseKind, VersionSpecifiers}; + +use crate::{ + discovery::{PythonRequest, VersionRequest}, + implementation::ImplementationName, +}; + +use super::{Error, PythonVariant}; + +#[test] +fn interpreter_request_from_str() { + assert_eq!(PythonRequest::parse("any"), PythonRequest::Any); + assert_eq!(PythonRequest::parse("default"), PythonRequest::Default); + assert_eq!( + PythonRequest::parse("3.12"), + PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()) + ); + assert_eq!( + PythonRequest::parse(">=3.12"), + PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap()) + ); + assert_eq!( + PythonRequest::parse(">=3.12,<3.13"), + PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap()) + ); + assert_eq!( + PythonRequest::parse(">=3.12,<3.13"), + PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap()) + ); + + assert_eq!( + PythonRequest::parse("3.13.0a1"), + PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap()) + ); + assert_eq!( + PythonRequest::parse("3.13.0b5"), + PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap()) + ); + assert_eq!( + PythonRequest::parse("3.13.0rc1"), + PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap()) + ); + assert_eq!( + PythonRequest::parse("3.13.1rc1"), + PythonRequest::ExecutableName("3.13.1rc1".to_string()), + "Pre-release version requests require a patch version of zero" + ); + assert_eq!( + PythonRequest::parse("3rc1"), + PythonRequest::ExecutableName("3rc1".to_string()), + "Pre-release version requests require a minor version" + ); + + assert_eq!( + PythonRequest::parse("cpython"), + PythonRequest::Implementation(ImplementationName::CPython) + ); + assert_eq!( + PythonRequest::parse("cpython3.12.2"), + PythonRequest::ImplementationVersion( + ImplementationName::CPython, + VersionRequest::from_str("3.12.2").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("pypy"), + PythonRequest::Implementation(ImplementationName::PyPy) + ); + assert_eq!( + PythonRequest::parse("pp"), + PythonRequest::Implementation(ImplementationName::PyPy) + ); + assert_eq!( + PythonRequest::parse("graalpy"), + PythonRequest::Implementation(ImplementationName::GraalPy) + ); + assert_eq!( + PythonRequest::parse("gp"), + PythonRequest::Implementation(ImplementationName::GraalPy) + ); + assert_eq!( + PythonRequest::parse("cp"), + PythonRequest::Implementation(ImplementationName::CPython) + ); + assert_eq!( + PythonRequest::parse("pypy3.10"), + PythonRequest::ImplementationVersion( + ImplementationName::PyPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("pp310"), + PythonRequest::ImplementationVersion( + ImplementationName::PyPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("graalpy3.10"), + PythonRequest::ImplementationVersion( + ImplementationName::GraalPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("gp310"), + PythonRequest::ImplementationVersion( + ImplementationName::GraalPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("cp38"), + PythonRequest::ImplementationVersion( + ImplementationName::CPython, + VersionRequest::from_str("3.8").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("pypy@3.10"), + PythonRequest::ImplementationVersion( + ImplementationName::PyPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("pypy310"), + PythonRequest::ImplementationVersion( + ImplementationName::PyPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("graalpy@3.10"), + PythonRequest::ImplementationVersion( + ImplementationName::GraalPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + assert_eq!( + PythonRequest::parse("graalpy310"), + PythonRequest::ImplementationVersion( + ImplementationName::GraalPy, + VersionRequest::from_str("3.10").unwrap(), + ) + ); + + let tempdir = TempDir::new().unwrap(); + assert_eq!( + PythonRequest::parse(tempdir.path().to_str().unwrap()), + PythonRequest::Directory(tempdir.path().to_path_buf()), + "An existing directory is treated as a directory" + ); + assert_eq!( + PythonRequest::parse(tempdir.child("foo").path().to_str().unwrap()), + PythonRequest::File(tempdir.child("foo").path().to_path_buf()), + "A path that does not exist is treated as a file" + ); + tempdir.child("bar").touch().unwrap(); + assert_eq!( + PythonRequest::parse(tempdir.child("bar").path().to_str().unwrap()), + PythonRequest::File(tempdir.child("bar").path().to_path_buf()), + "An existing file is treated as a file" + ); + assert_eq!( + PythonRequest::parse("./foo"), + PythonRequest::File(PathBuf::from_str("./foo").unwrap()), + "A string with a file system separator is treated as a file" + ); + assert_eq!( + PythonRequest::parse("3.13t"), + PythonRequest::Version(VersionRequest::from_str("3.13t").unwrap()) + ); +} + +#[test] +fn interpreter_request_to_canonical_string() { + assert_eq!(PythonRequest::Default.to_canonical_string(), "default"); + assert_eq!(PythonRequest::Any.to_canonical_string(), "any"); + assert_eq!( + PythonRequest::Version(VersionRequest::from_str("3.12").unwrap()).to_canonical_string(), + "3.12" + ); + assert_eq!( + PythonRequest::Version(VersionRequest::from_str(">=3.12").unwrap()).to_canonical_string(), + ">=3.12" + ); + assert_eq!( + PythonRequest::Version(VersionRequest::from_str(">=3.12,<3.13").unwrap()) + .to_canonical_string(), + ">=3.12, <3.13" + ); + + assert_eq!( + PythonRequest::Version(VersionRequest::from_str("3.13.0a1").unwrap()).to_canonical_string(), + "3.13a1" + ); + + assert_eq!( + PythonRequest::Version(VersionRequest::from_str("3.13.0b5").unwrap()).to_canonical_string(), + "3.13b5" + ); + + assert_eq!( + PythonRequest::Version(VersionRequest::from_str("3.13.0rc1").unwrap()) + .to_canonical_string(), + "3.13rc1" + ); + + assert_eq!( + PythonRequest::Version(VersionRequest::from_str("313rc4").unwrap()).to_canonical_string(), + "3.13rc4" + ); + + assert_eq!( + PythonRequest::ExecutableName("foo".to_string()).to_canonical_string(), + "foo" + ); + assert_eq!( + PythonRequest::Implementation(ImplementationName::CPython).to_canonical_string(), + "cpython" + ); + assert_eq!( + PythonRequest::ImplementationVersion( + ImplementationName::CPython, + VersionRequest::from_str("3.12.2").unwrap(), + ) + .to_canonical_string(), + "cpython@3.12.2" + ); + assert_eq!( + PythonRequest::Implementation(ImplementationName::PyPy).to_canonical_string(), + "pypy" + ); + assert_eq!( + PythonRequest::ImplementationVersion( + ImplementationName::PyPy, + VersionRequest::from_str("3.10").unwrap(), + ) + .to_canonical_string(), + "pypy@3.10" + ); + assert_eq!( + PythonRequest::Implementation(ImplementationName::GraalPy).to_canonical_string(), + "graalpy" + ); + assert_eq!( + PythonRequest::ImplementationVersion( + ImplementationName::GraalPy, + VersionRequest::from_str("3.10").unwrap(), + ) + .to_canonical_string(), + "graalpy@3.10" + ); + + let tempdir = TempDir::new().unwrap(); + assert_eq!( + PythonRequest::Directory(tempdir.path().to_path_buf()).to_canonical_string(), + tempdir.path().to_str().unwrap(), + "An existing directory is treated as a directory" + ); + assert_eq!( + PythonRequest::File(tempdir.child("foo").path().to_path_buf()).to_canonical_string(), + tempdir.child("foo").path().to_str().unwrap(), + "A path that does not exist is treated as a file" + ); + tempdir.child("bar").touch().unwrap(); + assert_eq!( + PythonRequest::File(tempdir.child("bar").path().to_path_buf()).to_canonical_string(), + tempdir.child("bar").path().to_str().unwrap(), + "An existing file is treated as a file" + ); + assert_eq!( + PythonRequest::File(PathBuf::from_str("./foo").unwrap()).to_canonical_string(), + "./foo", + "A string with a file system separator is treated as a file" + ); +} + +#[test] +fn version_request_from_str() { + assert_eq!( + VersionRequest::from_str("3").unwrap(), + VersionRequest::Major(3, PythonVariant::Default) + ); + assert_eq!( + VersionRequest::from_str("3.12").unwrap(), + VersionRequest::MajorMinor(3, 12, PythonVariant::Default) + ); + assert_eq!( + VersionRequest::from_str("3.12.1").unwrap(), + VersionRequest::MajorMinorPatch(3, 12, 1, PythonVariant::Default) + ); + assert!(VersionRequest::from_str("1.foo.1").is_err()); + assert_eq!( + VersionRequest::from_str("3").unwrap(), + VersionRequest::Major(3, PythonVariant::Default) + ); + assert_eq!( + VersionRequest::from_str("38").unwrap(), + VersionRequest::MajorMinor(3, 8, PythonVariant::Default) + ); + assert_eq!( + VersionRequest::from_str("312").unwrap(), + VersionRequest::MajorMinor(3, 12, PythonVariant::Default) + ); + assert_eq!( + VersionRequest::from_str("3100").unwrap(), + VersionRequest::MajorMinor(3, 100, PythonVariant::Default) + ); + assert_eq!( + VersionRequest::from_str("3.13a1").unwrap(), + VersionRequest::MajorMinorPrerelease( + 3, + 13, + Prerelease { + kind: PrereleaseKind::Alpha, + number: 1 + }, + PythonVariant::Default + ) + ); + assert_eq!( + VersionRequest::from_str("313b1").unwrap(), + VersionRequest::MajorMinorPrerelease( + 3, + 13, + Prerelease { + kind: PrereleaseKind::Beta, + number: 1 + }, + PythonVariant::Default + ) + ); + assert_eq!( + VersionRequest::from_str("3.13.0b2").unwrap(), + VersionRequest::MajorMinorPrerelease( + 3, + 13, + Prerelease { + kind: PrereleaseKind::Beta, + number: 2 + }, + PythonVariant::Default + ) + ); + assert_eq!( + VersionRequest::from_str("3.13.0rc3").unwrap(), + VersionRequest::MajorMinorPrerelease( + 3, + 13, + Prerelease { + kind: PrereleaseKind::Rc, + number: 3 + }, + PythonVariant::Default + ) + ); + assert!( + matches!( + VersionRequest::from_str("3rc1"), + Err(Error::InvalidVersionRequest(_)) + ), + "Pre-release version requests require a minor version" + ); + assert!( + matches!( + VersionRequest::from_str("3.13.2rc1"), + Err(Error::InvalidVersionRequest(_)) + ), + "Pre-release version requests require a patch version of zero" + ); + assert!( + matches!( + VersionRequest::from_str("3.12-dev"), + Err(Error::InvalidVersionRequest(_)) + ), + "Development version segments are not allowed" + ); + assert!( + matches!( + VersionRequest::from_str("3.12+local"), + Err(Error::InvalidVersionRequest(_)) + ), + "Local version segments are not allowed" + ); + assert!( + matches!( + VersionRequest::from_str("3.12.post0"), + Err(Error::InvalidVersionRequest(_)) + ), + "Post version segments are not allowed" + ); + assert!( + // Test for overflow + matches!( + VersionRequest::from_str("31000"), + Err(Error::InvalidVersionRequest(_)) + ) + ); + assert_eq!( + VersionRequest::from_str("3t").unwrap(), + VersionRequest::Major(3, PythonVariant::Freethreaded) + ); + assert_eq!( + VersionRequest::from_str("313t").unwrap(), + VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded) + ); + assert_eq!( + VersionRequest::from_str("3.13t").unwrap(), + VersionRequest::MajorMinor(3, 13, PythonVariant::Freethreaded) + ); + assert_eq!( + VersionRequest::from_str(">=3.13t").unwrap(), + VersionRequest::Range( + VersionSpecifiers::from_str(">=3.13").unwrap(), + PythonVariant::Freethreaded + ) + ); + assert_eq!( + VersionRequest::from_str(">=3.13").unwrap(), + VersionRequest::Range( + VersionSpecifiers::from_str(">=3.13").unwrap(), + PythonVariant::Default + ) + ); + assert_eq!( + VersionRequest::from_str(">=3.12,<3.14t").unwrap(), + VersionRequest::Range( + VersionSpecifiers::from_str(">=3.12,<3.14").unwrap(), + PythonVariant::Freethreaded + ) + ); + assert!(matches!( + VersionRequest::from_str("3.13tt"), + Err(Error::InvalidVersionRequest(_)) + )); +} + +#[test] +fn executable_names_from_request() { + fn case(request: &str, expected: &[&str]) { + let (implementation, version) = match PythonRequest::parse(request) { + PythonRequest::Any => (None, VersionRequest::Any), + PythonRequest::Default => (None, VersionRequest::Default), + PythonRequest::Version(version) => (None, version), + PythonRequest::ImplementationVersion(implementation, version) => { + (Some(implementation), version) + } + PythonRequest::Implementation(implementation) => { + (Some(implementation), VersionRequest::Default) + } + result => { + panic!("Test cases should request versions or implementations; got {result:?}") + } + }; + + let result: Vec<_> = version + .executable_names(implementation.as_ref()) + .into_iter() + .map(|name| name.to_string()) + .collect(); + + let expected: Vec<_> = expected + .iter() + .map(|name| format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX)) + .collect(); + + assert_eq!(result, expected, "mismatch for case \"{request}\""); + } + + case( + "any", + &[ + "python", "python3", "cpython", "pypy", "graalpy", "cpython3", "pypy3", "graalpy3", + ], + ); + + case("default", &["python", "python3"]); + + case("3", &["python", "python3"]); + + case("4", &["python", "python4"]); + + case("3.13", &["python", "python3", "python3.13"]); + + case( + "pypy@3.10", + &[ + "python", + "python3", + "python3.10", + "pypy", + "pypy3", + "pypy3.10", + ], + ); + + case( + "3.13t", + &[ + "python", + "python3", + "python3.13", + "pythont", + "python3t", + "python3.13t", + ], + ); + + case( + "3.13.2", + &["python", "python3", "python3.13", "python3.13.2"], + ); + + case( + "3.13rc2", + &["python", "python3", "python3.13", "python3.13rc2"], + ); +} diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 3ce148eb6..a792566a5 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -792,108 +792,4 @@ impl InterpreterInfo { #[cfg(unix)] #[cfg(test)] -mod tests { - use std::str::FromStr; - - use fs_err as fs; - use indoc::{formatdoc, indoc}; - use tempfile::tempdir; - - use uv_cache::Cache; - use uv_pep440::Version; - - use crate::Interpreter; - - #[test] - fn test_cache_invalidation() { - let mock_dir = tempdir().unwrap(); - let mocked_interpreter = mock_dir.path().join("python"); - let json = indoc! {r##" - { - "result": "success", - "platform": { - "os": { - "name": "manylinux", - "major": 2, - "minor": 38 - }, - "arch": "x86_64" - }, - "manylinux_compatible": false, - "markers": { - "implementation_name": "cpython", - "implementation_version": "3.12.0", - "os_name": "posix", - "platform_machine": "x86_64", - "platform_python_implementation": "CPython", - "platform_release": "6.5.0-13-generic", - "platform_system": "Linux", - "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023", - "python_full_version": "3.12.0", - "python_version": "3.12", - "sys_platform": "linux" - }, - "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0", - "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0", - "sys_prefix": "/home/ferris/projects/uv/.venv", - "sys_executable": "/home/ferris/projects/uv/.venv/bin/python", - "sys_path": [ - "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12", - "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages" - ], - "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12", - "scheme": { - "data": "/home/ferris/.pyenv/versions/3.12.0", - "include": "/home/ferris/.pyenv/versions/3.12.0/include", - "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages", - "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages", - "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin" - }, - "virtualenv": { - "data": "", - "include": "include", - "platlib": "lib/python3.12/site-packages", - "purelib": "lib/python3.12/site-packages", - "scripts": "bin" - }, - "pointer_size": "64", - "gil_disabled": true - } - "##}; - - let cache = Cache::temp().unwrap().init().unwrap(); - - fs::write( - &mocked_interpreter, - formatdoc! {r##" - #!/bin/bash - echo '{json}' - "##}, - ) - .unwrap(); - - fs::set_permissions( - &mocked_interpreter, - std::os::unix::fs::PermissionsExt::from_mode(0o770), - ) - .unwrap(); - let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap(); - assert_eq!( - interpreter.markers.python_version().version, - Version::from_str("3.12").unwrap() - ); - fs::write( - &mocked_interpreter, - formatdoc! {r##" - #!/bin/bash - echo '{}' - "##, json.replace("3.12", "3.13")}, - ) - .unwrap(); - let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap(); - assert_eq!( - interpreter.markers.python_version().version, - Version::from_str("3.13").unwrap() - ); - } -} +mod tests; diff --git a/crates/uv-python/src/interpreter/tests.rs b/crates/uv-python/src/interpreter/tests.rs new file mode 100644 index 000000000..100d0d1a1 --- /dev/null +++ b/crates/uv-python/src/interpreter/tests.rs @@ -0,0 +1,103 @@ +use std::str::FromStr; + +use fs_err as fs; +use indoc::{formatdoc, indoc}; +use tempfile::tempdir; + +use uv_cache::Cache; +use uv_pep440::Version; + +use crate::Interpreter; + +#[test] +fn test_cache_invalidation() { + let mock_dir = tempdir().unwrap(); + let mocked_interpreter = mock_dir.path().join("python"); + let json = indoc! {r##" + { + "result": "success", + "platform": { + "os": { + "name": "manylinux", + "major": 2, + "minor": 38 + }, + "arch": "x86_64" + }, + "manylinux_compatible": false, + "markers": { + "implementation_name": "cpython", + "implementation_version": "3.12.0", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.5.0-13-generic", + "platform_system": "Linux", + "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023", + "python_full_version": "3.12.0", + "python_version": "3.12", + "sys_platform": "linux" + }, + "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/3.12.0", + "sys_base_prefix": "/home/ferris/.pyenv/versions/3.12.0", + "sys_prefix": "/home/ferris/projects/uv/.venv", + "sys_executable": "/home/ferris/projects/uv/.venv/bin/python", + "sys_path": [ + "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12", + "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages" + ], + "stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12", + "scheme": { + "data": "/home/ferris/.pyenv/versions/3.12.0", + "include": "/home/ferris/.pyenv/versions/3.12.0/include", + "platlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages", + "purelib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages", + "scripts": "/home/ferris/.pyenv/versions/3.12.0/bin" + }, + "virtualenv": { + "data": "", + "include": "include", + "platlib": "lib/python3.12/site-packages", + "purelib": "lib/python3.12/site-packages", + "scripts": "bin" + }, + "pointer_size": "64", + "gil_disabled": true + } + "##}; + + let cache = Cache::temp().unwrap().init().unwrap(); + + fs::write( + &mocked_interpreter, + formatdoc! {r##" + #!/bin/bash + echo '{json}' + "##}, + ) + .unwrap(); + + fs::set_permissions( + &mocked_interpreter, + std::os::unix::fs::PermissionsExt::from_mode(0o770), + ) + .unwrap(); + let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap(); + assert_eq!( + interpreter.markers.python_version().version, + Version::from_str("3.12").unwrap() + ); + fs::write( + &mocked_interpreter, + formatdoc! {r##" + #!/bin/bash + echo '{}' + "##, json.replace("3.12", "3.13")}, + ) + .unwrap(); + let interpreter = Interpreter::query(&mocked_interpreter, &cache).unwrap(); + assert_eq!( + interpreter.markers.python_version().version, + Version::from_str("3.13").unwrap() + ); +} diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 55063f9fb..bbea150c3 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -85,2263 +85,4 @@ pub enum Error { // The mock interpreters are not valid on Windows so we don't have unit test coverage there // TODO(zanieb): We should write a mock interpreter script that works on Windows #[cfg(all(test, unix))] -mod tests { - use std::{ - env, - ffi::{OsStr, OsString}, - path::{Path, PathBuf}, - str::FromStr, - }; - - use anyhow::Result; - use assert_fs::{fixture::ChildPath, prelude::*, TempDir}; - use indoc::{formatdoc, indoc}; - use temp_env::with_vars; - use test_log::test; - - use uv_cache::Cache; - - use crate::{ - discovery::{ - find_best_python_installation, find_python_installation, EnvironmentPreference, - }, - PythonPreference, - }; - use crate::{ - implementation::ImplementationName, installation::PythonInstallation, - managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable, - PythonNotFound, PythonRequest, PythonSource, PythonVersion, - }; - - struct TestContext { - tempdir: TempDir, - cache: Cache, - installations: ManagedPythonInstallations, - search_path: Option>, - workdir: ChildPath, - } - - impl TestContext { - fn new() -> Result { - let tempdir = TempDir::new()?; - let workdir = tempdir.child("workdir"); - workdir.create_dir_all()?; - - Ok(Self { - tempdir, - cache: Cache::temp()?, - installations: ManagedPythonInstallations::temp()?, - search_path: None, - workdir, - }) - } - - /// Clear the search path. - fn reset_search_path(&mut self) { - self.search_path = None; - } - - /// Add a directory to the search path. - fn add_to_search_path(&mut self, path: PathBuf) { - match self.search_path.as_mut() { - Some(paths) => paths.push(path), - None => self.search_path = Some(vec![path]), - }; - } - - /// Create a new directory and add it to the search path. - fn new_search_path_directory(&mut self, name: impl AsRef) -> Result { - let child = self.tempdir.child(name); - child.create_dir_all()?; - self.add_to_search_path(child.to_path_buf()); - Ok(child) - } - - fn run(&self, closure: F) -> R - where - F: FnOnce() -> R, - { - self.run_with_vars(&[], closure) - } - - fn run_with_vars(&self, vars: &[(&str, Option<&OsStr>)], closure: F) -> R - where - F: FnOnce() -> R, - { - let path = self - .search_path - .as_ref() - .map(|paths| env::join_paths(paths).unwrap()); - - let mut run_vars = vec![ - // Ensure `PATH` is used - ("UV_TEST_PYTHON_PATH", None), - // Ignore active virtual environments (i.e. that the dev is using) - ("VIRTUAL_ENV", None), - ("PATH", path.as_deref()), - // Use the temporary python directory - ( - "UV_PYTHON_INSTALL_DIR", - Some(self.installations.root().as_os_str()), - ), - // Set a working directory - ("PWD", Some(self.workdir.path().as_os_str())), - ]; - for (key, value) in vars { - run_vars.push((key, *value)); - } - with_vars(&run_vars, closure) - } - - /// Create a fake Python interpreter executable which returns fixed metadata mocking our interpreter - /// query script output. - fn create_mock_interpreter( - path: &Path, - version: &PythonVersion, - implementation: ImplementationName, - system: bool, - free_threaded: bool, - ) -> Result<()> { - let json = indoc! {r##" - { - "result": "success", - "platform": { - "os": { - "name": "manylinux", - "major": 2, - "minor": 38 - }, - "arch": "x86_64" - }, - "manylinux_compatible": true, - "markers": { - "implementation_name": "{IMPLEMENTATION}", - "implementation_version": "{FULL_VERSION}", - "os_name": "posix", - "platform_machine": "x86_64", - "platform_python_implementation": "{IMPLEMENTATION}", - "platform_release": "6.5.0-13-generic", - "platform_system": "Linux", - "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023", - "python_full_version": "{FULL_VERSION}", - "python_version": "{VERSION}", - "sys_platform": "linux" - }, - "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}", - "sys_base_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}", - "sys_prefix": "{PREFIX}", - "sys_executable": "{PATH}", - "sys_path": [ - "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/lib/python{VERSION}", - "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages" - ], - "stdlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}", - "scheme": { - "data": "/home/ferris/.pyenv/versions/{FULL_VERSION}", - "include": "/home/ferris/.pyenv/versions/{FULL_VERSION}/include", - "platlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages", - "purelib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages", - "scripts": "/home/ferris/.pyenv/versions/{FULL_VERSION}/bin" - }, - "virtualenv": { - "data": "", - "include": "include", - "platlib": "lib/python{VERSION}/site-packages", - "purelib": "lib/python{VERSION}/site-packages", - "scripts": "bin" - }, - "pointer_size": "64", - "gil_disabled": {FREE_THREADED} - } - "##}; - - let json = if system { - json.replace("{PREFIX}", "/home/ferris/.pyenv/versions/{FULL_VERSION}") - } else { - json.replace("{PREFIX}", "/home/ferris/projects/uv/.venv") - }; - - let json = json - .replace( - "{PATH}", - path.to_str().expect("Path can be represented as string"), - ) - .replace("{FULL_VERSION}", &version.to_string()) - .replace("{VERSION}", &version.without_patch().to_string()) - .replace("{FREE_THREADED}", &free_threaded.to_string()) - .replace("{IMPLEMENTATION}", (&implementation).into()); - - fs_err::create_dir_all(path.parent().unwrap())?; - fs_err::write( - path, - formatdoc! {r##" - #!/bin/bash - echo '{json}' - "##}, - )?; - - fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?; - - Ok(()) - } - - /// Create a mock Python 2 interpreter executable which returns a fixed error message mocking - /// invocation of Python 2 with the `-I` flag as done by our query script. - fn create_mock_python2_interpreter(path: &Path) -> Result<()> { - let output = indoc! { r" - Unknown option: -I - usage: /usr/bin/python [option] ... [-c cmd | -m mod | file | -] [arg] ... - Try `python -h` for more information. - "}; - - fs_err::write( - path, - formatdoc! {r##" - #!/bin/bash - echo '{output}' 1>&2 - "##}, - )?; - - fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?; - - Ok(()) - } - - /// Create child directories in a temporary directory. - fn new_search_path_directories( - &mut self, - names: &[impl AsRef], - ) -> Result> { - let paths = names - .iter() - .map(|name| self.new_search_path_directory(name)) - .collect::>>()?; - Ok(paths) - } - - /// Create fake Python interpreters the given Python versions. - /// - /// Adds them to the test context search path. - fn add_python_to_workdir(&self, name: &str, version: &str) -> Result<()> { - TestContext::create_mock_interpreter( - self.workdir.child(name).as_ref(), - &PythonVersion::from_str(version).expect("Test uses valid version"), - ImplementationName::default(), - true, - false, - ) - } - - /// Create fake Python interpreters the given Python versions. - /// - /// Adds them to the test context search path. - fn add_python_versions(&mut self, versions: &[&'static str]) -> Result<()> { - let interpreters: Vec<_> = versions - .iter() - .map(|version| (true, ImplementationName::default(), "python", *version)) - .collect(); - self.add_python_interpreters(interpreters.as_slice()) - } - - /// Create fake Python interpreters the given Python implementations and versions. - /// - /// Adds them to the test context search path. - fn add_python_interpreters( - &mut self, - kinds: &[(bool, ImplementationName, &'static str, &'static str)], - ) -> Result<()> { - // Generate a "unique" folder name for each interpreter - let names: Vec = kinds - .iter() - .map(|(system, implementation, name, version)| { - OsString::from_str(&format!("{system}-{implementation}-{name}-{version}")) - .unwrap() - }) - .collect(); - let paths = self.new_search_path_directories(names.as_slice())?; - for (path, (system, implementation, executable, version)) in - itertools::zip_eq(&paths, kinds) - { - let python = format!("{executable}{}", env::consts::EXE_SUFFIX); - Self::create_mock_interpreter( - &path.join(python), - &PythonVersion::from_str(version).unwrap(), - *implementation, - *system, - false, - )?; - } - Ok(()) - } - - /// Create a mock virtual environment at the given directory - fn mock_venv(path: impl AsRef, version: &'static str) -> Result<()> { - let executable = virtualenv_python_executable(path.as_ref()); - fs_err::create_dir_all( - executable - .parent() - .expect("A Python executable path should always have a parent"), - )?; - TestContext::create_mock_interpreter( - &executable, - &PythonVersion::from_str(version) - .expect("A valid Python version is used for tests"), - ImplementationName::default(), - false, - false, - )?; - ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?; - Ok(()) - } - - /// Create a mock conda prefix at the given directory. - /// - /// These are like virtual environments but they look like system interpreters because `prefix` and `base_prefix` are equal. - fn mock_conda_prefix(path: impl AsRef, version: &'static str) -> Result<()> { - let executable = virtualenv_python_executable(&path); - fs_err::create_dir_all( - executable - .parent() - .expect("A Python executable path should always have a parent"), - )?; - TestContext::create_mock_interpreter( - &executable, - &PythonVersion::from_str(version) - .expect("A valid Python version is used for tests"), - ImplementationName::default(), - true, - false, - )?; - ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?; - Ok(()) - } - } - - #[test] - fn find_python_empty_path() -> Result<()> { - let mut context = TestContext::new()?; - - context.search_path = Some(vec![]); - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - }); - assert!( - matches!(result, Ok(Err(PythonNotFound { .. }))), - "With an empty path, no Python installation should be detected got {result:?}" - ); - - context.search_path = None; - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - }); - assert!( - matches!(result, Ok(Err(PythonNotFound { .. }))), - "With an unset path, no Python installation should be detected got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_unexecutable_file() -> Result<()> { - let mut context = TestContext::new()?; - context - .new_search_path_directory("path")? - .child(format!("python{}", env::consts::EXE_SUFFIX)) - .touch()?; - - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - }); - assert!( - matches!( - result, - Ok(Err(PythonNotFound { .. })) - ), - "With an non-executable Python, no Python installation should be detected; got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_valid_executable() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.12.1"])?; - - let interpreter = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - })??; - assert!( - matches!( - interpreter, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find the valid executable; got {interpreter:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_valid_executable_after_invalid() -> Result<()> { - let mut context = TestContext::new()?; - let children = context.new_search_path_directories(&[ - "query-parse-error", - "not-executable", - "empty", - "good", - ])?; - - // An executable file with a bad response - #[cfg(unix)] - fs_err::write( - children[0].join(format!("python{}", env::consts::EXE_SUFFIX)), - formatdoc! {r##" - #!/bin/bash - echo 'foo' - "##}, - )?; - fs_err::set_permissions( - children[0].join(format!("python{}", env::consts::EXE_SUFFIX)), - std::os::unix::fs::PermissionsExt::from_mode(0o770), - )?; - - // A non-executable file - ChildPath::new(children[1].join(format!("python{}", env::consts::EXE_SUFFIX))).touch()?; - - // An empty directory at `children[2]` - - // An good interpreter! - let python_path = children[3].join(format!("python{}", env::consts::EXE_SUFFIX)); - TestContext::create_mock_interpreter( - &python_path, - &PythonVersion::from_str("3.12.1").unwrap(), - ImplementationName::default(), - true, - false, - )?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - })??; - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should skip the bad executables in favor of the good one; got {python:?}" - ); - assert_eq!(python.interpreter().sys_executable(), python_path); - - Ok(()) - } - - #[test] - fn find_python_only_python2_executable() -> Result<()> { - let mut context = TestContext::new()?; - let python = context - .new_search_path_directory("python2")? - .child(format!("python{}", env::consts::EXE_SUFFIX)); - TestContext::create_mock_python2_interpreter(&python)?; - - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - // TODO(zanieb): We could improve the error handling to hint this to the user - "If only Python 2 is available, we should not find a python; got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_skip_python2_executable() -> Result<()> { - let mut context = TestContext::new()?; - - let python2 = context - .new_search_path_directory("python2")? - .child(format!("python{}", env::consts::EXE_SUFFIX)); - TestContext::create_mock_python2_interpreter(&python2)?; - - let python3 = context - .new_search_path_directory("python3")? - .child(format!("python{}", env::consts::EXE_SUFFIX)); - TestContext::create_mock_interpreter( - &python3, - &PythonVersion::from_str("3.12.1").unwrap(), - ImplementationName::default(), - true, - false, - )?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::default(), - &context.cache, - ) - })??; - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should skip the Python 2 installation and find the Python 3 interpreter; got {python:?}" - ); - assert_eq!(python.interpreter().sys_executable(), python3.path()); - - Ok(()) - } - - #[test] - fn find_python_system_python_allowed() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (false, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::CPython, "python", "3.10.1"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "Should find the first interpreter regardless of system" - ); - - // Reverse the order of the virtual environment and system - context.reset_search_path(); - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.1"), - (false, ImplementationName::CPython, "python", "3.10.0"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "Should find the first interpreter regardless of system" - ); - - Ok(()) - } - - #[test] - fn find_python_system_python_required() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (false, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::CPython, "python", "3.10.1"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "Should skip the virtual environment" - ); - - Ok(()) - } - - #[test] - fn find_python_system_python_disallowed() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.0"), - (false, ImplementationName::CPython, "python", "3.10.1"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "Should skip the system Python" - ); - - Ok(()) - } - - #[test] - fn find_python_version_minor() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.11"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find a python; got {python:?}" - ); - assert_eq!( - &python.interpreter().python_full_version().to_string(), - "3.11.2", - "We should find the correct interpreter for the request" - ); - - Ok(()) - } - - #[test] - fn find_python_version_patch() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.3", "3.11.2", "3.12.3"])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.11.2"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find a python; got {python:?}" - ); - assert_eq!( - &python.interpreter().python_full_version().to_string(), - "3.11.2", - "We should find the correct interpreter for the request" - ); - - Ok(()) - } - - #[test] - fn find_python_version_minor_no_match() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?; - - let result = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.9"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find a python; got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_version_patch_no_match() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?; - - let result = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.11.9"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find a python; got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_best_python_version_patch_exact() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?; - - let python = context.run(|| { - find_best_python_installation( - &PythonRequest::parse("3.11.3"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find a python; got {python:?}" - ); - assert_eq!( - &python.interpreter().python_full_version().to_string(), - "3.11.3", - "We should prefer the exact request" - ); - - Ok(()) - } - - #[test] - fn find_best_python_version_patch_fallback() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?; - - let python = context.run(|| { - find_best_python_installation( - &PythonRequest::parse("3.11.11"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find a python; got {python:?}" - ); - assert_eq!( - &python.interpreter().python_full_version().to_string(), - "3.11.2", - "We should fallback to the first matching minor" - ); - - Ok(()) - } - - #[test] - fn find_best_python_skips_source_without_match() -> Result<()> { - let mut context = TestContext::new()?; - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(&venv, "3.12.0")?; - context.add_python_versions(&["3.10.1"])?; - - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_best_python_installation( - &PythonRequest::parse("3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should skip the active environment in favor of the requested version; got {python:?}" - ); - - Ok(()) - } - - #[test] - fn find_best_python_returns_to_earlier_source_on_fallback() -> Result<()> { - let mut context = TestContext::new()?; - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(&venv, "3.10.1")?; - context.add_python_versions(&["3.10.3"])?; - - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_best_python_installation( - &PythonRequest::parse("3.10.2"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::ActiveEnvironment, - interpreter: _ - } - ), - "We should prefer the active environment after relaxing; got {python:?}" - ); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should prefer the active environment" - ); - - Ok(()) - } - - #[test] - fn find_python_from_active_python() -> Result<()> { - let context = TestContext::new()?; - let venv = context.tempdir.child("some-venv"); - TestContext::mock_venv(&venv, "3.12.0")?; - - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should prefer the active environment" - ); - - Ok(()) - } - - #[test] - fn find_python_from_active_python_prerelease() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.12.0"])?; - let venv = context.tempdir.child("some-venv"); - TestContext::mock_venv(&venv, "3.13.0rc1")?; - - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.13.0rc1", - "We should prefer the active environment" - ); - - Ok(()) - } - - #[test] - fn find_python_from_conda_prefix() -> Result<()> { - let context = TestContext::new()?; - let condaenv = context.tempdir.child("condaenv"); - TestContext::mock_conda_prefix(&condaenv, "3.12.0")?; - - let python = - context.run_with_vars(&[("CONDA_PREFIX", Some(condaenv.as_os_str()))], || { - // Note this python is not treated as a system interpreter - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlyVirtual, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should allow the active conda python" - ); - - Ok(()) - } - - #[test] - fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> { - let context = TestContext::new()?; - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(&venv, "3.12.0")?; - let condaenv = context.tempdir.child("condaenv"); - TestContext::mock_conda_prefix(&condaenv, "3.12.1")?; - - let python = context.run_with_vars( - &[ - ("VIRTUAL_ENV", Some(venv.as_os_str())), - ("CONDA_PREFIX", Some(condaenv.as_os_str())), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should prefer the non-conda python" - ); - - // Put a virtual environment in the working directory - let venv = context.workdir.child(".venv"); - TestContext::mock_venv(venv, "3.12.2")?; - let python = - context.run_with_vars(&[("CONDA_PREFIX", Some(condaenv.as_os_str()))], || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.1", - "We should prefer the conda python over inactive virtual environments" - ); - - Ok(()) - } - - #[test] - fn find_python_from_discovered_python() -> Result<()> { - let mut context = TestContext::new()?; - - // Create a virtual environment in a parent of the workdir - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(venv, "3.12.0")?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should find the python" - ); - - // Add some system versions to ensure we don't use those - context.add_python_versions(&["3.12.1", "3.12.2"])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should prefer the discovered virtual environment over available system versions" - ); - - Ok(()) - } - - #[test] - fn find_python_skips_broken_active_python() -> Result<()> { - let context = TestContext::new()?; - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(&venv, "3.12.0")?; - - // Delete the pyvenv cfg to break the virtualenv - fs_err::remove_file(venv.join("pyvenv.cfg"))?; - - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - // TODO(zanieb): We should skip this python, why don't we? - "We should prefer the active environment" - ); - - Ok(()) - } - - #[test] - fn find_python_from_parent_interpreter() -> Result<()> { - let mut context = TestContext::new()?; - - let parent = context.tempdir.child("python").to_path_buf(); - TestContext::create_mock_interpreter( - &parent, - &PythonVersion::from_str("3.12.0").unwrap(), - ImplementationName::CPython, - // Note we mark this as a system interpreter instead of a virtual environment - true, - false, - )?; - - let python = context.run_with_vars( - &[("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str()))], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should find the parent interpreter" - ); - - // Parent interpreters are preferred over virtual environments and system interpreters - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(&venv, "3.12.2")?; - context.add_python_versions(&["3.12.3"])?; - let python = context.run_with_vars( - &[ - ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), - ("VIRTUAL_ENV", Some(venv.as_os_str())), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should prefer the parent interpreter" - ); - - // Test with `EnvironmentPreference::ExplicitSystem` - let python = context.run_with_vars( - &[ - ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), - ("VIRTUAL_ENV", Some(venv.as_os_str())), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::ExplicitSystem, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should prefer the parent interpreter" - ); - - // Test with `EnvironmentPreference::OnlySystem` - let python = context.run_with_vars( - &[ - ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), - ("VIRTUAL_ENV", Some(venv.as_os_str())), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should prefer the parent interpreter since it's not virtual" - ); - - // Test with `EnvironmentPreference::OnlyVirtual` - let python = context.run_with_vars( - &[ - ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), - ("VIRTUAL_ENV", Some(venv.as_os_str())), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlyVirtual, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.2", - "We find the virtual environment Python because a system is explicitly not allowed" - ); - - Ok(()) - } - - #[test] - fn find_python_from_parent_interpreter_prerelease() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.12.0"])?; - let parent = context.tempdir.child("python").to_path_buf(); - TestContext::create_mock_interpreter( - &parent, - &PythonVersion::from_str("3.13.0rc2").unwrap(), - ImplementationName::CPython, - // Note we mark this as a system interpreter instead of a virtual environment - true, - false, - )?; - - let python = context.run_with_vars( - &[("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str()))], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.13.0rc2", - "We should find the parent interpreter" - ); - - Ok(()) - } - - #[test] - fn find_python_active_python_skipped_if_system_required() -> Result<()> { - let mut context = TestContext::new()?; - let venv = context.tempdir.child(".venv"); - TestContext::mock_venv(&venv, "3.9.0")?; - context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?; - - // Without a specific request - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should skip the active environment" - ); - - // With a requested minor version - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::parse("3.12"), - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.2", - "We should skip the active environment" - ); - - // With a patch version that cannot be python - let result = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::parse("3.12.3"), - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - result.is_err(), - "We should not find an python; got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_versions(&["3.10.1", "3.11.2"])?; - - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlyVirtual, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find an python; got {result:?}" - ); - - // With an invalid virtual environment variable - let result = context.run_with_vars( - &[("VIRTUAL_ENV", Some(context.tempdir.as_os_str()))], - || { - find_python_installation( - &PythonRequest::parse("3.12.3"), - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find an python; got {result:?}" - ); - Ok(()) - } - - #[test] - fn find_python_allows_name_in_working_directory() -> Result<()> { - let context = TestContext::new()?; - context.add_python_to_workdir("foobar", "3.10.0")?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("foobar"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the named executable" - ); - - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find it without a specific request" - ); - - let result = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.10.0"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find it via a matching version request" - ); - - Ok(()) - } - - #[test] - fn find_python_allows_relative_file_path() -> Result<()> { - let mut context = TestContext::new()?; - let python = context.workdir.child("foo").join("bar"); - TestContext::create_mock_interpreter( - &python, - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::default(), - true, - false, - )?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("./foo/bar"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the `bar` executable" - ); - - context.add_python_versions(&["3.11.1"])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("./foo/bar"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should prefer the `bar` executable over the system and virtualenvs" - ); - - Ok(()) - } - - #[test] - fn find_python_allows_absolute_file_path() -> Result<()> { - let mut context = TestContext::new()?; - let python_path = context.tempdir.child("foo").join("bar"); - TestContext::create_mock_interpreter( - &python_path, - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::default(), - true, - false, - )?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(python_path.to_str().unwrap()), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the `bar` executable" - ); - - // With `EnvironmentPreference::ExplicitSystem` - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(python_path.to_str().unwrap()), - EnvironmentPreference::ExplicitSystem, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should allow the `bar` executable with explicit system" - ); - - // With `EnvironmentPreference::OnlyVirtual` - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(python_path.to_str().unwrap()), - EnvironmentPreference::OnlyVirtual, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should allow the `bar` executable and verify it is virtual" - ); - - context.add_python_versions(&["3.11.1"])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(python_path.to_str().unwrap()), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should prefer the `bar` executable over the system and virtualenvs" - ); - - Ok(()) - } - - #[test] - fn find_python_allows_venv_directory_path() -> Result<()> { - let mut context = TestContext::new()?; - - let venv = context.tempdir.child("foo").child(".venv"); - TestContext::mock_venv(&venv, "3.10.0")?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("../foo/.venv"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the relative venv path" - ); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(venv.to_str().unwrap()), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the absolute venv path" - ); - - // We should allow it to be a directory that _looks_ like a virtual environment. - let python_path = context.tempdir.child("bar").join("bin").join("python"); - TestContext::create_mock_interpreter( - &python_path, - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::default(), - true, - false, - )?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the executable in the directory" - ); - - let other_venv = context.tempdir.child("foobar").child(".venv"); - TestContext::mock_venv(&other_venv, "3.11.1")?; - context.add_python_versions(&["3.12.2"])?; - let python = - context.run_with_vars(&[("VIRTUAL_ENV", Some(other_venv.as_os_str()))], || { - find_python_installation( - &PythonRequest::parse(venv.to_str().unwrap()), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should prefer the requested directory over the system and active virtual environments" - ); - - Ok(()) - } - - #[test] - fn find_python_venv_symlink() -> Result<()> { - let context = TestContext::new()?; - - let venv = context.tempdir.child("target").child("env"); - TestContext::mock_venv(&venv, "3.10.6")?; - let symlink = context.tempdir.child("proj").child(".venv"); - context.tempdir.child("proj").create_dir_all()?; - symlink.symlink_to_dir(venv)?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("../proj/.venv"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.6", - "We should find the symlinked venv" - ); - Ok(()) - } - - #[test] - fn find_python_treats_missing_file_path_as_file() -> Result<()> { - let context = TestContext::new()?; - context.workdir.child("foo").create_dir_all()?; - - let result = context.run(|| { - find_python_installation( - &PythonRequest::parse("./foo/bar"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find the file; got {result:?}" - ); - - Ok(()) - } - - #[test] - fn find_python_executable_name_in_search_path() -> Result<()> { - let mut context = TestContext::new()?; - let python = context.tempdir.child("foo").join("bar"); - TestContext::create_mock_interpreter( - &python, - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::default(), - true, - false, - )?; - context.add_to_search_path(context.tempdir.child("foo").to_path_buf()); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("bar"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the `bar` executable" - ); - - // With [`EnvironmentPreference::OnlyVirtual`], we should not allow the interpreter - let result = context.run(|| { - find_python_installation( - &PythonRequest::parse("bar"), - EnvironmentPreference::ExplicitSystem, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not allow a system interpreter; got {result:?}" - ); - - // Unless it's a virtual environment interpreter - let mut context = TestContext::new()?; - let python = context.tempdir.child("foo").join("bar"); - TestContext::create_mock_interpreter( - &python, - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::default(), - false, // Not a system interpreter - false, - )?; - context.add_to_search_path(context.tempdir.child("foo").to_path_buf()); - - let python = context - .run(|| { - find_python_installation( - &PythonRequest::parse("bar"), - EnvironmentPreference::ExplicitSystem, - PythonPreference::OnlySystem, - &context.cache, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should find the `bar` executable" - ); - - Ok(()) - } - - #[test] - fn find_python_pypy() -> Result<()> { - let mut context = TestContext::new()?; - - context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?; - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not find the pypy interpreter if not named `python` or requested; got {result:?}" - ); - - // But we should find it - context.reset_search_path(); - context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should find the pypy interpreter if it's the only one" - ); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("pypy"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should find the pypy interpreter if it's requested" - ); - - Ok(()) - } - - #[test] - fn find_python_pypy_request_ignores_cpython() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::PyPy, "pypy", "3.10.1"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("pypy"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should skip the CPython interpreter" - ); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should take the first interpreter without a specific request" - ); - - Ok(()) - } - - #[test] - fn find_python_pypy_request_skips_wrong_versions() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::PyPy, "pypy", "3.9"), - (true, ImplementationName::PyPy, "pypy", "3.10.1"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("pypy3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should skip the first interpreter" - ); - - Ok(()) - } - - #[test] - fn find_python_pypy_finds_executable_with_version_name() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), // We don't consider this one because of the executable name - (true, ImplementationName::PyPy, "pypy3.10", "3.10.1"), - (true, ImplementationName::PyPy, "pypy", "3.10.2"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("pypy@3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should find the requested interpreter version" - ); - - Ok(()) - } - - #[test] - fn find_python_all_minors() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::CPython, "python3", "3.10.0"), - (true, ImplementationName::CPython, "python3.12", "3.12.0"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(">= 3.11"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0", - "We should find matching minor version even if they aren't called `python` or `python3`" - ); - - Ok(()) - } - - #[test] - fn find_python_all_minors_prerelease() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::CPython, "python3", "3.10.0"), - (true, ImplementationName::CPython, "python3.11", "3.11.0b0"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(">= 3.11"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.11.0b0", - "We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases" - ); - - Ok(()) - } - - #[test] - fn find_python_all_minors_prerelease_next() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::CPython, "python3", "3.10.0"), - (true, ImplementationName::CPython, "python3.12", "3.12.0b0"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse(">= 3.11"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.0b0", - "We should find the 3.12 prerelease" - ); - - Ok(()) - } - - #[test] - fn find_python_graalpy() -> Result<()> { - let mut context = TestContext::new()?; - - context.add_python_interpreters(&[( - true, - ImplementationName::GraalPy, - "graalpy", - "3.10.0", - )])?; - let result = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })?; - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not the graalpy interpreter if not named `python` or requested; got {result:?}" - ); - - // But we should find it - context.reset_search_path(); - context.add_python_interpreters(&[( - true, - ImplementationName::GraalPy, - "python", - "3.10.1", - )])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should find the graalpy interpreter if it's the only one" - ); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("graalpy"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should find the graalpy interpreter if it's requested" - ); - - Ok(()) - } - - #[test] - fn find_python_graalpy_request_ignores_cpython() -> Result<()> { - let mut context = TestContext::new()?; - context.add_python_interpreters(&[ - (true, ImplementationName::CPython, "python", "3.10.0"), - (true, ImplementationName::GraalPy, "graalpy", "3.10.1"), - ])?; - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("graalpy"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should skip the CPython interpreter" - ); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should take the first interpreter without a specific request" - ); - - Ok(()) - } - - #[test] - fn find_python_prefers_generic_executable_over_implementation_name() -> Result<()> { - let mut context = TestContext::new()?; - - // We prefer `python` executables over `graalpy` executables in the same directory - // if they are both GraalPy - TestContext::create_mock_interpreter( - &context.tempdir.join("python"), - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::GraalPy, - true, - false, - )?; - TestContext::create_mock_interpreter( - &context.tempdir.join("graalpy"), - &PythonVersion::from_str("3.10.1").unwrap(), - ImplementationName::GraalPy, - true, - false, - )?; - context.add_to_search_path(context.tempdir.to_path_buf()); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("graalpy@3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - ); - - // And `python` executables earlier in the search path will take precedence - context.reset_search_path(); - context.add_python_interpreters(&[ - (true, ImplementationName::GraalPy, "python", "3.10.2"), - (true, ImplementationName::GraalPy, "graalpy", "3.10.3"), - ])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("graalpy@3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.2", - ); - - // But `graalpy` executables earlier in the search path will take precedence - context.reset_search_path(); - context.add_python_interpreters(&[ - (true, ImplementationName::GraalPy, "graalpy", "3.10.3"), - (true, ImplementationName::GraalPy, "python", "3.10.2"), - ])?; - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("graalpy@3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.3", - ); - - Ok(()) - } - - #[test] - fn find_python_prefers_generic_executable_over_one_with_version() -> Result<()> { - let mut context = TestContext::new()?; - TestContext::create_mock_interpreter( - &context.tempdir.join("pypy3.10"), - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::PyPy, - true, - false, - )?; - TestContext::create_mock_interpreter( - &context.tempdir.join("pypy"), - &PythonVersion::from_str("3.10.1").unwrap(), - ImplementationName::PyPy, - true, - false, - )?; - context.add_to_search_path(context.tempdir.to_path_buf()); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("pypy@3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should prefer the generic executable over one with the version number" - ); - - let mut context = TestContext::new()?; - TestContext::create_mock_interpreter( - &context.tempdir.join("python3.10"), - &PythonVersion::from_str("3.10.0").unwrap(), - ImplementationName::PyPy, - true, - false, - )?; - TestContext::create_mock_interpreter( - &context.tempdir.join("pypy"), - &PythonVersion::from_str("3.10.1").unwrap(), - ImplementationName::PyPy, - true, - false, - )?; - context.add_to_search_path(context.tempdir.to_path_buf()); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("pypy@3.10"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should prefer the generic name with a version over one the implementation name" - ); - - Ok(()) - } - - #[test] - fn find_python_version_free_threaded() -> Result<()> { - let mut context = TestContext::new()?; - - TestContext::create_mock_interpreter( - &context.tempdir.join("python"), - &PythonVersion::from_str("3.13.1").unwrap(), - ImplementationName::CPython, - true, - false, - )?; - TestContext::create_mock_interpreter( - &context.tempdir.join("python3.13t"), - &PythonVersion::from_str("3.13.0").unwrap(), - ImplementationName::CPython, - true, - true, - )?; - context.add_to_search_path(context.tempdir.to_path_buf()); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.13t"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find a python; got {python:?}" - ); - assert_eq!( - &python.interpreter().python_full_version().to_string(), - "3.13.0", - "We should find the correct interpreter for the request" - ); - assert!( - &python.interpreter().gil_disabled(), - "We should find a python without the GIL" - ); - - Ok(()) - } - - #[test] - fn find_python_version_prefer_non_free_threaded() -> Result<()> { - let mut context = TestContext::new()?; - - TestContext::create_mock_interpreter( - &context.tempdir.join("python"), - &PythonVersion::from_str("3.13.0").unwrap(), - ImplementationName::CPython, - true, - false, - )?; - TestContext::create_mock_interpreter( - &context.tempdir.join("python3.13t"), - &PythonVersion::from_str("3.13.0").unwrap(), - ImplementationName::CPython, - true, - true, - )?; - context.add_to_search_path(context.tempdir.to_path_buf()); - - let python = context.run(|| { - find_python_installation( - &PythonRequest::parse("3.13"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - })??; - - assert!( - matches!( - python, - PythonInstallation { - source: PythonSource::SearchPath, - interpreter: _ - } - ), - "We should find a python; got {python:?}" - ); - assert_eq!( - &python.interpreter().python_full_version().to_string(), - "3.13.0", - "We should find the correct interpreter for the request" - ); - assert!( - !&python.interpreter().gil_disabled(), - "We should prefer a python with the GIL" - ); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-python/src/libc.rs b/crates/uv-python/src/libc.rs index 7e60780f6..15f058999 100644 --- a/crates/uv-python/src/libc.rs +++ b/crates/uv-python/src/libc.rs @@ -235,45 +235,4 @@ fn find_ld_path_at(path: impl AsRef) -> Option { } #[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - - #[test] - fn parse_ldd_output() { - let ver_str = glibc_ldd_output_to_version( - "stdout", - indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39. - Copyright (C) 2024 Free Software Foundation, Inc. - This is free software; see the source for copying conditions. - There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A - PARTICULAR PURPOSE. - "}, - ) - .unwrap(); - assert_eq!( - ver_str, - LibcVersion::Manylinux { - major: 2, - minor: 39 - } - ); - } - - #[test] - fn parse_musl_ld_output() { - // This output was generated by running `/lib/ld-musl-x86_64.so.1` - // in an Alpine Docker image. The Alpine version: - // - // # cat /etc/alpine-release - // 3.19.1 - let output = b"\ -musl libc (x86_64) -Version 1.2.4_git20230717 -Dynamic Program Loader -Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\ - "; - let got = musl_ld_output_to_version("stderr", output).unwrap(); - assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 }); - } -} +mod tests; diff --git a/crates/uv-python/src/libc/tests.rs b/crates/uv-python/src/libc/tests.rs new file mode 100644 index 000000000..f85e97237 --- /dev/null +++ b/crates/uv-python/src/libc/tests.rs @@ -0,0 +1,40 @@ +use super::*; +use indoc::indoc; + +#[test] +fn parse_ldd_output() { + let ver_str = glibc_ldd_output_to_version( + "stdout", + indoc! {br"ld.so (Ubuntu GLIBC 2.39-0ubuntu8.3) stable release version 2.39. + Copyright (C) 2024 Free Software Foundation, Inc. + This is free software; see the source for copying conditions. + There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A + PARTICULAR PURPOSE. + "}, + ) + .unwrap(); + assert_eq!( + ver_str, + LibcVersion::Manylinux { + major: 2, + minor: 39 + } + ); +} + +#[test] +fn parse_musl_ld_output() { + // This output was generated by running `/lib/ld-musl-x86_64.so.1` + // in an Alpine Docker image. The Alpine version: + // + // # cat /etc/alpine-release + // 3.19.1 + let output = b"\ +musl libc (x86_64) +Version 1.2.4_git20230717 +Dynamic Program Loader +Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname [args]\ + "; + let got = musl_ld_output_to_version("stderr", output).unwrap(); + assert_eq!(got, LibcVersion::Musllinux { major: 1, minor: 2 }); +} diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index b64f6ba6f..0e5920b05 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -168,37 +168,4 @@ impl PythonVersion { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use uv_pep440::{Prerelease, PrereleaseKind, Version}; - - use crate::PythonVersion; - - #[test] - fn python_markers() { - let version = PythonVersion::from_str("3.11.0").expect("valid python version"); - assert_eq!(version.python_version(), Version::new([3, 11])); - assert_eq!(version.python_version().to_string(), "3.11"); - assert_eq!(version.python_full_version(), Version::new([3, 11, 0])); - assert_eq!(version.python_full_version().to_string(), "3.11.0"); - - let version = PythonVersion::from_str("3.11").expect("valid python version"); - assert_eq!(version.python_version(), Version::new([3, 11])); - assert_eq!(version.python_version().to_string(), "3.11"); - assert_eq!(version.python_full_version(), Version::new([3, 11, 0])); - assert_eq!(version.python_full_version().to_string(), "3.11.0"); - - let version = PythonVersion::from_str("3.11.8a1").expect("valid python version"); - assert_eq!(version.python_version(), Version::new([3, 11])); - assert_eq!(version.python_version().to_string(), "3.11"); - assert_eq!( - version.python_full_version(), - Version::new([3, 11, 8]).with_pre(Some(Prerelease { - kind: PrereleaseKind::Alpha, - number: 1 - })) - ); - assert_eq!(version.python_full_version().to_string(), "3.11.8a1"); - } -} +mod tests; diff --git a/crates/uv-python/src/python_version/tests.rs b/crates/uv-python/src/python_version/tests.rs new file mode 100644 index 000000000..7065b1ae0 --- /dev/null +++ b/crates/uv-python/src/python_version/tests.rs @@ -0,0 +1,32 @@ +use std::str::FromStr; + +use uv_pep440::{Prerelease, PrereleaseKind, Version}; + +use crate::PythonVersion; + +#[test] +fn python_markers() { + let version = PythonVersion::from_str("3.11.0").expect("valid python version"); + assert_eq!(version.python_version(), Version::new([3, 11])); + assert_eq!(version.python_version().to_string(), "3.11"); + assert_eq!(version.python_full_version(), Version::new([3, 11, 0])); + assert_eq!(version.python_full_version().to_string(), "3.11.0"); + + let version = PythonVersion::from_str("3.11").expect("valid python version"); + assert_eq!(version.python_version(), Version::new([3, 11])); + assert_eq!(version.python_version().to_string(), "3.11"); + assert_eq!(version.python_full_version(), Version::new([3, 11, 0])); + assert_eq!(version.python_full_version().to_string(), "3.11.0"); + + let version = PythonVersion::from_str("3.11.8a1").expect("valid python version"); + assert_eq!(version.python_version(), Version::new([3, 11])); + assert_eq!(version.python_version().to_string(), "3.11"); + assert_eq!( + version.python_full_version(), + Version::new([3, 11, 8]).with_pre(Some(Prerelease { + kind: PrereleaseKind::Alpha, + number: 1 + })) + ); + assert_eq!(version.python_full_version().to_string(), "3.11.8a1"); +} diff --git a/crates/uv-python/src/tests.rs b/crates/uv-python/src/tests.rs new file mode 100644 index 000000000..d99ece619 --- /dev/null +++ b/crates/uv-python/src/tests.rs @@ -0,0 +1,2233 @@ +use std::{ + env, + ffi::{OsStr, OsString}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::Result; +use assert_fs::{fixture::ChildPath, prelude::*, TempDir}; +use indoc::{formatdoc, indoc}; +use temp_env::with_vars; +use test_log::test; + +use uv_cache::Cache; + +use crate::{ + discovery::{find_best_python_installation, find_python_installation, EnvironmentPreference}, + PythonPreference, +}; +use crate::{ + implementation::ImplementationName, installation::PythonInstallation, + managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable, PythonNotFound, + PythonRequest, PythonSource, PythonVersion, +}; + +struct TestContext { + tempdir: TempDir, + cache: Cache, + installations: ManagedPythonInstallations, + search_path: Option>, + workdir: ChildPath, +} + +impl TestContext { + fn new() -> Result { + let tempdir = TempDir::new()?; + let workdir = tempdir.child("workdir"); + workdir.create_dir_all()?; + + Ok(Self { + tempdir, + cache: Cache::temp()?, + installations: ManagedPythonInstallations::temp()?, + search_path: None, + workdir, + }) + } + + /// Clear the search path. + fn reset_search_path(&mut self) { + self.search_path = None; + } + + /// Add a directory to the search path. + fn add_to_search_path(&mut self, path: PathBuf) { + match self.search_path.as_mut() { + Some(paths) => paths.push(path), + None => self.search_path = Some(vec![path]), + }; + } + + /// Create a new directory and add it to the search path. + fn new_search_path_directory(&mut self, name: impl AsRef) -> Result { + let child = self.tempdir.child(name); + child.create_dir_all()?; + self.add_to_search_path(child.to_path_buf()); + Ok(child) + } + + fn run(&self, closure: F) -> R + where + F: FnOnce() -> R, + { + self.run_with_vars(&[], closure) + } + + fn run_with_vars(&self, vars: &[(&str, Option<&OsStr>)], closure: F) -> R + where + F: FnOnce() -> R, + { + let path = self + .search_path + .as_ref() + .map(|paths| env::join_paths(paths).unwrap()); + + let mut run_vars = vec![ + // Ensure `PATH` is used + ("UV_TEST_PYTHON_PATH", None), + // Ignore active virtual environments (i.e. that the dev is using) + ("VIRTUAL_ENV", None), + ("PATH", path.as_deref()), + // Use the temporary python directory + ( + "UV_PYTHON_INSTALL_DIR", + Some(self.installations.root().as_os_str()), + ), + // Set a working directory + ("PWD", Some(self.workdir.path().as_os_str())), + ]; + for (key, value) in vars { + run_vars.push((key, *value)); + } + with_vars(&run_vars, closure) + } + + /// Create a fake Python interpreter executable which returns fixed metadata mocking our interpreter + /// query script output. + fn create_mock_interpreter( + path: &Path, + version: &PythonVersion, + implementation: ImplementationName, + system: bool, + free_threaded: bool, + ) -> Result<()> { + let json = indoc! {r##" + { + "result": "success", + "platform": { + "os": { + "name": "manylinux", + "major": 2, + "minor": 38 + }, + "arch": "x86_64" + }, + "manylinux_compatible": true, + "markers": { + "implementation_name": "{IMPLEMENTATION}", + "implementation_version": "{FULL_VERSION}", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "{IMPLEMENTATION}", + "platform_release": "6.5.0-13-generic", + "platform_system": "Linux", + "platform_version": "#13-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 3 12:16:05 UTC 2023", + "python_full_version": "{FULL_VERSION}", + "python_version": "{VERSION}", + "sys_platform": "linux" + }, + "sys_base_exec_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}", + "sys_base_prefix": "/home/ferris/.pyenv/versions/{FULL_VERSION}", + "sys_prefix": "{PREFIX}", + "sys_executable": "{PATH}", + "sys_path": [ + "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/lib/python{VERSION}", + "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages" + ], + "stdlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}", + "scheme": { + "data": "/home/ferris/.pyenv/versions/{FULL_VERSION}", + "include": "/home/ferris/.pyenv/versions/{FULL_VERSION}/include", + "platlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages", + "purelib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages", + "scripts": "/home/ferris/.pyenv/versions/{FULL_VERSION}/bin" + }, + "virtualenv": { + "data": "", + "include": "include", + "platlib": "lib/python{VERSION}/site-packages", + "purelib": "lib/python{VERSION}/site-packages", + "scripts": "bin" + }, + "pointer_size": "64", + "gil_disabled": {FREE_THREADED} + } + "##}; + + let json = if system { + json.replace("{PREFIX}", "/home/ferris/.pyenv/versions/{FULL_VERSION}") + } else { + json.replace("{PREFIX}", "/home/ferris/projects/uv/.venv") + }; + + let json = json + .replace( + "{PATH}", + path.to_str().expect("Path can be represented as string"), + ) + .replace("{FULL_VERSION}", &version.to_string()) + .replace("{VERSION}", &version.without_patch().to_string()) + .replace("{FREE_THREADED}", &free_threaded.to_string()) + .replace("{IMPLEMENTATION}", (&implementation).into()); + + fs_err::create_dir_all(path.parent().unwrap())?; + fs_err::write( + path, + formatdoc! {r##" + #!/bin/bash + echo '{json}' + "##}, + )?; + + fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?; + + Ok(()) + } + + /// Create a mock Python 2 interpreter executable which returns a fixed error message mocking + /// invocation of Python 2 with the `-I` flag as done by our query script. + fn create_mock_python2_interpreter(path: &Path) -> Result<()> { + let output = indoc! { r" + Unknown option: -I + usage: /usr/bin/python [option] ... [-c cmd | -m mod | file | -] [arg] ... + Try `python -h` for more information. + "}; + + fs_err::write( + path, + formatdoc! {r##" + #!/bin/bash + echo '{output}' 1>&2 + "##}, + )?; + + fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?; + + Ok(()) + } + + /// Create child directories in a temporary directory. + fn new_search_path_directories( + &mut self, + names: &[impl AsRef], + ) -> Result> { + let paths = names + .iter() + .map(|name| self.new_search_path_directory(name)) + .collect::>>()?; + Ok(paths) + } + + /// Create fake Python interpreters the given Python versions. + /// + /// Adds them to the test context search path. + fn add_python_to_workdir(&self, name: &str, version: &str) -> Result<()> { + TestContext::create_mock_interpreter( + self.workdir.child(name).as_ref(), + &PythonVersion::from_str(version).expect("Test uses valid version"), + ImplementationName::default(), + true, + false, + ) + } + + /// Create fake Python interpreters the given Python versions. + /// + /// Adds them to the test context search path. + fn add_python_versions(&mut self, versions: &[&'static str]) -> Result<()> { + let interpreters: Vec<_> = versions + .iter() + .map(|version| (true, ImplementationName::default(), "python", *version)) + .collect(); + self.add_python_interpreters(interpreters.as_slice()) + } + + /// Create fake Python interpreters the given Python implementations and versions. + /// + /// Adds them to the test context search path. + fn add_python_interpreters( + &mut self, + kinds: &[(bool, ImplementationName, &'static str, &'static str)], + ) -> Result<()> { + // Generate a "unique" folder name for each interpreter + let names: Vec = kinds + .iter() + .map(|(system, implementation, name, version)| { + OsString::from_str(&format!("{system}-{implementation}-{name}-{version}")).unwrap() + }) + .collect(); + let paths = self.new_search_path_directories(names.as_slice())?; + for (path, (system, implementation, executable, version)) in + itertools::zip_eq(&paths, kinds) + { + let python = format!("{executable}{}", env::consts::EXE_SUFFIX); + Self::create_mock_interpreter( + &path.join(python), + &PythonVersion::from_str(version).unwrap(), + *implementation, + *system, + false, + )?; + } + Ok(()) + } + + /// Create a mock virtual environment at the given directory + fn mock_venv(path: impl AsRef, version: &'static str) -> Result<()> { + let executable = virtualenv_python_executable(path.as_ref()); + fs_err::create_dir_all( + executable + .parent() + .expect("A Python executable path should always have a parent"), + )?; + TestContext::create_mock_interpreter( + &executable, + &PythonVersion::from_str(version).expect("A valid Python version is used for tests"), + ImplementationName::default(), + false, + false, + )?; + ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?; + Ok(()) + } + + /// Create a mock conda prefix at the given directory. + /// + /// These are like virtual environments but they look like system interpreters because `prefix` and `base_prefix` are equal. + fn mock_conda_prefix(path: impl AsRef, version: &'static str) -> Result<()> { + let executable = virtualenv_python_executable(&path); + fs_err::create_dir_all( + executable + .parent() + .expect("A Python executable path should always have a parent"), + )?; + TestContext::create_mock_interpreter( + &executable, + &PythonVersion::from_str(version).expect("A valid Python version is used for tests"), + ImplementationName::default(), + true, + false, + )?; + ChildPath::new(path.as_ref().join("pyvenv.cfg")).touch()?; + Ok(()) + } +} + +#[test] +fn find_python_empty_path() -> Result<()> { + let mut context = TestContext::new()?; + + context.search_path = Some(vec![]); + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + }); + assert!( + matches!(result, Ok(Err(PythonNotFound { .. }))), + "With an empty path, no Python installation should be detected got {result:?}" + ); + + context.search_path = None; + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + }); + assert!( + matches!(result, Ok(Err(PythonNotFound { .. }))), + "With an unset path, no Python installation should be detected got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_unexecutable_file() -> Result<()> { + let mut context = TestContext::new()?; + context + .new_search_path_directory("path")? + .child(format!("python{}", env::consts::EXE_SUFFIX)) + .touch()?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + }); + assert!( + matches!(result, Ok(Err(PythonNotFound { .. }))), + "With an non-executable Python, no Python installation should be detected; got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_valid_executable() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.12.1"])?; + + let interpreter = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + })??; + assert!( + matches!( + interpreter, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find the valid executable; got {interpreter:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_valid_executable_after_invalid() -> Result<()> { + let mut context = TestContext::new()?; + let children = context.new_search_path_directories(&[ + "query-parse-error", + "not-executable", + "empty", + "good", + ])?; + + // An executable file with a bad response + #[cfg(unix)] + fs_err::write( + children[0].join(format!("python{}", env::consts::EXE_SUFFIX)), + formatdoc! {r##" + #!/bin/bash + echo 'foo' + "##}, + )?; + fs_err::set_permissions( + children[0].join(format!("python{}", env::consts::EXE_SUFFIX)), + std::os::unix::fs::PermissionsExt::from_mode(0o770), + )?; + + // A non-executable file + ChildPath::new(children[1].join(format!("python{}", env::consts::EXE_SUFFIX))).touch()?; + + // An empty directory at `children[2]` + + // An good interpreter! + let python_path = children[3].join(format!("python{}", env::consts::EXE_SUFFIX)); + TestContext::create_mock_interpreter( + &python_path, + &PythonVersion::from_str("3.12.1").unwrap(), + ImplementationName::default(), + true, + false, + )?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + })??; + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should skip the bad executables in favor of the good one; got {python:?}" + ); + assert_eq!(python.interpreter().sys_executable(), python_path); + + Ok(()) +} + +#[test] +fn find_python_only_python2_executable() -> Result<()> { + let mut context = TestContext::new()?; + let python = context + .new_search_path_directory("python2")? + .child(format!("python{}", env::consts::EXE_SUFFIX)); + TestContext::create_mock_python2_interpreter(&python)?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + // TODO(zanieb): We could improve the error handling to hint this to the user + "If only Python 2 is available, we should not find a python; got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_skip_python2_executable() -> Result<()> { + let mut context = TestContext::new()?; + + let python2 = context + .new_search_path_directory("python2")? + .child(format!("python{}", env::consts::EXE_SUFFIX)); + TestContext::create_mock_python2_interpreter(&python2)?; + + let python3 = context + .new_search_path_directory("python3")? + .child(format!("python{}", env::consts::EXE_SUFFIX)); + TestContext::create_mock_interpreter( + &python3, + &PythonVersion::from_str("3.12.1").unwrap(), + ImplementationName::default(), + true, + false, + )?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::default(), + &context.cache, + ) + })??; + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should skip the Python 2 installation and find the Python 3 interpreter; got {python:?}" + ); + assert_eq!(python.interpreter().sys_executable(), python3.path()); + + Ok(()) +} + +#[test] +fn find_python_system_python_allowed() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (false, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::CPython, "python", "3.10.1"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "Should find the first interpreter regardless of system" + ); + + // Reverse the order of the virtual environment and system + context.reset_search_path(); + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.1"), + (false, ImplementationName::CPython, "python", "3.10.0"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "Should find the first interpreter regardless of system" + ); + + Ok(()) +} + +#[test] +fn find_python_system_python_required() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (false, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::CPython, "python", "3.10.1"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "Should skip the virtual environment" + ); + + Ok(()) +} + +#[test] +fn find_python_system_python_disallowed() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.0"), + (false, ImplementationName::CPython, "python", "3.10.1"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "Should skip the system Python" + ); + + Ok(()) +} + +#[test] +fn find_python_version_minor() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.11"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find a python; got {python:?}" + ); + assert_eq!( + &python.interpreter().python_full_version().to_string(), + "3.11.2", + "We should find the correct interpreter for the request" + ); + + Ok(()) +} + +#[test] +fn find_python_version_patch() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.3", "3.11.2", "3.12.3"])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.11.2"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find a python; got {python:?}" + ); + assert_eq!( + &python.interpreter().python_full_version().to_string(), + "3.11.2", + "We should find the correct interpreter for the request" + ); + + Ok(()) +} + +#[test] +fn find_python_version_minor_no_match() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.9"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find a python; got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_version_patch_no_match() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.2", "3.12.3"])?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.11.9"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find a python; got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_best_python_version_patch_exact() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?; + + let python = context.run(|| { + find_best_python_installation( + &PythonRequest::parse("3.11.3"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find a python; got {python:?}" + ); + assert_eq!( + &python.interpreter().python_full_version().to_string(), + "3.11.3", + "We should prefer the exact request" + ); + + Ok(()) +} + +#[test] +fn find_best_python_version_patch_fallback() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.2", "3.11.4", "3.11.3", "3.12.5"])?; + + let python = context.run(|| { + find_best_python_installation( + &PythonRequest::parse("3.11.11"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find a python; got {python:?}" + ); + assert_eq!( + &python.interpreter().python_full_version().to_string(), + "3.11.2", + "We should fallback to the first matching minor" + ); + + Ok(()) +} + +#[test] +fn find_best_python_skips_source_without_match() -> Result<()> { + let mut context = TestContext::new()?; + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&venv, "3.12.0")?; + context.add_python_versions(&["3.10.1"])?; + + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_best_python_installation( + &PythonRequest::parse("3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should skip the active environment in favor of the requested version; got {python:?}" + ); + + Ok(()) +} + +#[test] +fn find_best_python_returns_to_earlier_source_on_fallback() -> Result<()> { + let mut context = TestContext::new()?; + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&venv, "3.10.1")?; + context.add_python_versions(&["3.10.3"])?; + + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_best_python_installation( + &PythonRequest::parse("3.10.2"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::ActiveEnvironment, + interpreter: _ + } + ), + "We should prefer the active environment after relaxing; got {python:?}" + ); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should prefer the active environment" + ); + + Ok(()) +} + +#[test] +fn find_python_from_active_python() -> Result<()> { + let context = TestContext::new()?; + let venv = context.tempdir.child("some-venv"); + TestContext::mock_venv(&venv, "3.12.0")?; + + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should prefer the active environment" + ); + + Ok(()) +} + +#[test] +fn find_python_from_active_python_prerelease() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.12.0"])?; + let venv = context.tempdir.child("some-venv"); + TestContext::mock_venv(&venv, "3.13.0rc1")?; + + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.13.0rc1", + "We should prefer the active environment" + ); + + Ok(()) +} + +#[test] +fn find_python_from_conda_prefix() -> Result<()> { + let context = TestContext::new()?; + let condaenv = context.tempdir.child("condaenv"); + TestContext::mock_conda_prefix(&condaenv, "3.12.0")?; + + let python = + context.run_with_vars(&[("CONDA_PREFIX", Some(condaenv.as_os_str()))], || { + // Note this python is not treated as a system interpreter + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should allow the active conda python" + ); + + Ok(()) +} + +#[test] +fn find_python_from_conda_prefix_and_virtualenv() -> Result<()> { + let context = TestContext::new()?; + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&venv, "3.12.0")?; + let condaenv = context.tempdir.child("condaenv"); + TestContext::mock_conda_prefix(&condaenv, "3.12.1")?; + + let python = context.run_with_vars( + &[ + ("VIRTUAL_ENV", Some(venv.as_os_str())), + ("CONDA_PREFIX", Some(condaenv.as_os_str())), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should prefer the non-conda python" + ); + + // Put a virtual environment in the working directory + let venv = context.workdir.child(".venv"); + TestContext::mock_venv(venv, "3.12.2")?; + let python = + context.run_with_vars(&[("CONDA_PREFIX", Some(condaenv.as_os_str()))], || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.1", + "We should prefer the conda python over inactive virtual environments" + ); + + Ok(()) +} + +#[test] +fn find_python_from_discovered_python() -> Result<()> { + let mut context = TestContext::new()?; + + // Create a virtual environment in a parent of the workdir + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(venv, "3.12.0")?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should find the python" + ); + + // Add some system versions to ensure we don't use those + context.add_python_versions(&["3.12.1", "3.12.2"])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should prefer the discovered virtual environment over available system versions" + ); + + Ok(()) +} + +#[test] +fn find_python_skips_broken_active_python() -> Result<()> { + let context = TestContext::new()?; + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&venv, "3.12.0")?; + + // Delete the pyvenv cfg to break the virtualenv + fs_err::remove_file(venv.join("pyvenv.cfg"))?; + + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + // TODO(zanieb): We should skip this python, why don't we? + "We should prefer the active environment" + ); + + Ok(()) +} + +#[test] +fn find_python_from_parent_interpreter() -> Result<()> { + let mut context = TestContext::new()?; + + let parent = context.tempdir.child("python").to_path_buf(); + TestContext::create_mock_interpreter( + &parent, + &PythonVersion::from_str("3.12.0").unwrap(), + ImplementationName::CPython, + // Note we mark this as a system interpreter instead of a virtual environment + true, + false, + )?; + + let python = context.run_with_vars( + &[("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str()))], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should find the parent interpreter" + ); + + // Parent interpreters are preferred over virtual environments and system interpreters + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&venv, "3.12.2")?; + context.add_python_versions(&["3.12.3"])?; + let python = context.run_with_vars( + &[ + ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), + ("VIRTUAL_ENV", Some(venv.as_os_str())), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should prefer the parent interpreter" + ); + + // Test with `EnvironmentPreference::ExplicitSystem` + let python = context.run_with_vars( + &[ + ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), + ("VIRTUAL_ENV", Some(venv.as_os_str())), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::ExplicitSystem, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should prefer the parent interpreter" + ); + + // Test with `EnvironmentPreference::OnlySystem` + let python = context.run_with_vars( + &[ + ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), + ("VIRTUAL_ENV", Some(venv.as_os_str())), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should prefer the parent interpreter since it's not virtual" + ); + + // Test with `EnvironmentPreference::OnlyVirtual` + let python = context.run_with_vars( + &[ + ("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str())), + ("VIRTUAL_ENV", Some(venv.as_os_str())), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.2", + "We find the virtual environment Python because a system is explicitly not allowed" + ); + + Ok(()) +} + +#[test] +fn find_python_from_parent_interpreter_prerelease() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.12.0"])?; + let parent = context.tempdir.child("python").to_path_buf(); + TestContext::create_mock_interpreter( + &parent, + &PythonVersion::from_str("3.13.0rc2").unwrap(), + ImplementationName::CPython, + // Note we mark this as a system interpreter instead of a virtual environment + true, + false, + )?; + + let python = context.run_with_vars( + &[("UV_INTERNAL__PARENT_INTERPRETER", Some(parent.as_os_str()))], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.13.0rc2", + "We should find the parent interpreter" + ); + + Ok(()) +} + +#[test] +fn find_python_active_python_skipped_if_system_required() -> Result<()> { + let mut context = TestContext::new()?; + let venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&venv, "3.9.0")?; + context.add_python_versions(&["3.10.0", "3.11.1", "3.12.2"])?; + + // Without a specific request + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should skip the active environment" + ); + + // With a requested minor version + let python = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::parse("3.12"), + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.2", + "We should skip the active environment" + ); + + // With a patch version that cannot be python + let result = context.run_with_vars(&[("VIRTUAL_ENV", Some(venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::parse("3.12.3"), + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + result.is_err(), + "We should not find an python; got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_fails_if_no_virtualenv_and_system_not_allowed() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.10.1", "3.11.2"])?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find an python; got {result:?}" + ); + + // With an invalid virtual environment variable + let result = context.run_with_vars( + &[("VIRTUAL_ENV", Some(context.tempdir.as_os_str()))], + || { + find_python_installation( + &PythonRequest::parse("3.12.3"), + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find an python; got {result:?}" + ); + Ok(()) +} + +#[test] +fn find_python_allows_name_in_working_directory() -> Result<()> { + let context = TestContext::new()?; + context.add_python_to_workdir("foobar", "3.10.0")?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("foobar"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the named executable" + ); + + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find it without a specific request" + ); + + let result = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.10.0"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find it via a matching version request" + ); + + Ok(()) +} + +#[test] +fn find_python_allows_relative_file_path() -> Result<()> { + let mut context = TestContext::new()?; + let python = context.workdir.child("foo").join("bar"); + TestContext::create_mock_interpreter( + &python, + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::default(), + true, + false, + )?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("./foo/bar"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the `bar` executable" + ); + + context.add_python_versions(&["3.11.1"])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("./foo/bar"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should prefer the `bar` executable over the system and virtualenvs" + ); + + Ok(()) +} + +#[test] +fn find_python_allows_absolute_file_path() -> Result<()> { + let mut context = TestContext::new()?; + let python_path = context.tempdir.child("foo").join("bar"); + TestContext::create_mock_interpreter( + &python_path, + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::default(), + true, + false, + )?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(python_path.to_str().unwrap()), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the `bar` executable" + ); + + // With `EnvironmentPreference::ExplicitSystem` + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(python_path.to_str().unwrap()), + EnvironmentPreference::ExplicitSystem, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should allow the `bar` executable with explicit system" + ); + + // With `EnvironmentPreference::OnlyVirtual` + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(python_path.to_str().unwrap()), + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should allow the `bar` executable and verify it is virtual" + ); + + context.add_python_versions(&["3.11.1"])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(python_path.to_str().unwrap()), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should prefer the `bar` executable over the system and virtualenvs" + ); + + Ok(()) +} + +#[test] +fn find_python_allows_venv_directory_path() -> Result<()> { + let mut context = TestContext::new()?; + + let venv = context.tempdir.child("foo").child(".venv"); + TestContext::mock_venv(&venv, "3.10.0")?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("../foo/.venv"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the relative venv path" + ); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(venv.to_str().unwrap()), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the absolute venv path" + ); + + // We should allow it to be a directory that _looks_ like a virtual environment. + let python_path = context.tempdir.child("bar").join("bin").join("python"); + TestContext::create_mock_interpreter( + &python_path, + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::default(), + true, + false, + )?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(context.tempdir.child("bar").to_str().unwrap()), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the executable in the directory" + ); + + let other_venv = context.tempdir.child("foobar").child(".venv"); + TestContext::mock_venv(&other_venv, "3.11.1")?; + context.add_python_versions(&["3.12.2"])?; + let python = + context.run_with_vars(&[("VIRTUAL_ENV", Some(other_venv.as_os_str()))], || { + find_python_installation( + &PythonRequest::parse(venv.to_str().unwrap()), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should prefer the requested directory over the system and active virtual environments" + ); + + Ok(()) +} + +#[test] +fn find_python_venv_symlink() -> Result<()> { + let context = TestContext::new()?; + + let venv = context.tempdir.child("target").child("env"); + TestContext::mock_venv(&venv, "3.10.6")?; + let symlink = context.tempdir.child("proj").child(".venv"); + context.tempdir.child("proj").create_dir_all()?; + symlink.symlink_to_dir(venv)?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("../proj/.venv"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.6", + "We should find the symlinked venv" + ); + Ok(()) +} + +#[test] +fn find_python_treats_missing_file_path_as_file() -> Result<()> { + let context = TestContext::new()?; + context.workdir.child("foo").create_dir_all()?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::parse("./foo/bar"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find the file; got {result:?}" + ); + + Ok(()) +} + +#[test] +fn find_python_executable_name_in_search_path() -> Result<()> { + let mut context = TestContext::new()?; + let python = context.tempdir.child("foo").join("bar"); + TestContext::create_mock_interpreter( + &python, + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::default(), + true, + false, + )?; + context.add_to_search_path(context.tempdir.child("foo").to_path_buf()); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("bar"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the `bar` executable" + ); + + // With [`EnvironmentPreference::OnlyVirtual`], we should not allow the interpreter + let result = context.run(|| { + find_python_installation( + &PythonRequest::parse("bar"), + EnvironmentPreference::ExplicitSystem, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not allow a system interpreter; got {result:?}" + ); + + // Unless it's a virtual environment interpreter + let mut context = TestContext::new()?; + let python = context.tempdir.child("foo").join("bar"); + TestContext::create_mock_interpreter( + &python, + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::default(), + false, // Not a system interpreter + false, + )?; + context.add_to_search_path(context.tempdir.child("foo").to_path_buf()); + + let python = context + .run(|| { + find_python_installation( + &PythonRequest::parse("bar"), + EnvironmentPreference::ExplicitSystem, + PythonPreference::OnlySystem, + &context.cache, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should find the `bar` executable" + ); + + Ok(()) +} + +#[test] +fn find_python_pypy() -> Result<()> { + let mut context = TestContext::new()?; + + context.add_python_interpreters(&[(true, ImplementationName::PyPy, "pypy", "3.10.0")])?; + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not find the pypy interpreter if not named `python` or requested; got {result:?}" + ); + + // But we should find it + context.reset_search_path(); + context.add_python_interpreters(&[(true, ImplementationName::PyPy, "python", "3.10.1")])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should find the pypy interpreter if it's the only one" + ); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("pypy"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should find the pypy interpreter if it's requested" + ); + + Ok(()) +} + +#[test] +fn find_python_pypy_request_ignores_cpython() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::PyPy, "pypy", "3.10.1"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("pypy"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should skip the CPython interpreter" + ); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should take the first interpreter without a specific request" + ); + + Ok(()) +} + +#[test] +fn find_python_pypy_request_skips_wrong_versions() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::PyPy, "pypy", "3.9"), + (true, ImplementationName::PyPy, "pypy", "3.10.1"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("pypy3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should skip the first interpreter" + ); + + Ok(()) +} + +#[test] +fn find_python_pypy_finds_executable_with_version_name() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::PyPy, "pypy3.9", "3.10.0"), // We don't consider this one because of the executable name + (true, ImplementationName::PyPy, "pypy3.10", "3.10.1"), + (true, ImplementationName::PyPy, "pypy", "3.10.2"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("pypy@3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should find the requested interpreter version" + ); + + Ok(()) +} + +#[test] +fn find_python_all_minors() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::CPython, "python3", "3.10.0"), + (true, ImplementationName::CPython, "python3.12", "3.12.0"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(">= 3.11"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0", + "We should find matching minor version even if they aren't called `python` or `python3`" + ); + + Ok(()) +} + +#[test] +fn find_python_all_minors_prerelease() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::CPython, "python3", "3.10.0"), + (true, ImplementationName::CPython, "python3.11", "3.11.0b0"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(">= 3.11"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.11.0b0", + "We should find the 3.11 prerelease even though >=3.11 would normally exclude prereleases" + ); + + Ok(()) +} + +#[test] +fn find_python_all_minors_prerelease_next() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::CPython, "python3", "3.10.0"), + (true, ImplementationName::CPython, "python3.12", "3.12.0b0"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse(">= 3.11"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.0b0", + "We should find the 3.12 prerelease" + ); + + Ok(()) +} + +#[test] +fn find_python_graalpy() -> Result<()> { + let mut context = TestContext::new()?; + + context.add_python_interpreters(&[(true, ImplementationName::GraalPy, "graalpy", "3.10.0")])?; + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })?; + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not the graalpy interpreter if not named `python` or requested; got {result:?}" + ); + + // But we should find it + context.reset_search_path(); + context.add_python_interpreters(&[(true, ImplementationName::GraalPy, "python", "3.10.1")])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should find the graalpy interpreter if it's the only one" + ); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("graalpy"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should find the graalpy interpreter if it's requested" + ); + + Ok(()) +} + +#[test] +fn find_python_graalpy_request_ignores_cpython() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_interpreters(&[ + (true, ImplementationName::CPython, "python", "3.10.0"), + (true, ImplementationName::GraalPy, "graalpy", "3.10.1"), + ])?; + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("graalpy"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should skip the CPython interpreter" + ); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should take the first interpreter without a specific request" + ); + + Ok(()) +} + +#[test] +fn find_python_prefers_generic_executable_over_implementation_name() -> Result<()> { + let mut context = TestContext::new()?; + + // We prefer `python` executables over `graalpy` executables in the same directory + // if they are both GraalPy + TestContext::create_mock_interpreter( + &context.tempdir.join("python"), + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::GraalPy, + true, + false, + )?; + TestContext::create_mock_interpreter( + &context.tempdir.join("graalpy"), + &PythonVersion::from_str("3.10.1").unwrap(), + ImplementationName::GraalPy, + true, + false, + )?; + context.add_to_search_path(context.tempdir.to_path_buf()); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("graalpy@3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + ); + + // And `python` executables earlier in the search path will take precedence + context.reset_search_path(); + context.add_python_interpreters(&[ + (true, ImplementationName::GraalPy, "python", "3.10.2"), + (true, ImplementationName::GraalPy, "graalpy", "3.10.3"), + ])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("graalpy@3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.2", + ); + + // But `graalpy` executables earlier in the search path will take precedence + context.reset_search_path(); + context.add_python_interpreters(&[ + (true, ImplementationName::GraalPy, "graalpy", "3.10.3"), + (true, ImplementationName::GraalPy, "python", "3.10.2"), + ])?; + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("graalpy@3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.3", + ); + + Ok(()) +} + +#[test] +fn find_python_prefers_generic_executable_over_one_with_version() -> Result<()> { + let mut context = TestContext::new()?; + TestContext::create_mock_interpreter( + &context.tempdir.join("pypy3.10"), + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::PyPy, + true, + false, + )?; + TestContext::create_mock_interpreter( + &context.tempdir.join("pypy"), + &PythonVersion::from_str("3.10.1").unwrap(), + ImplementationName::PyPy, + true, + false, + )?; + context.add_to_search_path(context.tempdir.to_path_buf()); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("pypy@3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should prefer the generic executable over one with the version number" + ); + + let mut context = TestContext::new()?; + TestContext::create_mock_interpreter( + &context.tempdir.join("python3.10"), + &PythonVersion::from_str("3.10.0").unwrap(), + ImplementationName::PyPy, + true, + false, + )?; + TestContext::create_mock_interpreter( + &context.tempdir.join("pypy"), + &PythonVersion::from_str("3.10.1").unwrap(), + ImplementationName::PyPy, + true, + false, + )?; + context.add_to_search_path(context.tempdir.to_path_buf()); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("pypy@3.10"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should prefer the generic name with a version over one the implementation name" + ); + + Ok(()) +} + +#[test] +fn find_python_version_free_threaded() -> Result<()> { + let mut context = TestContext::new()?; + + TestContext::create_mock_interpreter( + &context.tempdir.join("python"), + &PythonVersion::from_str("3.13.1").unwrap(), + ImplementationName::CPython, + true, + false, + )?; + TestContext::create_mock_interpreter( + &context.tempdir.join("python3.13t"), + &PythonVersion::from_str("3.13.0").unwrap(), + ImplementationName::CPython, + true, + true, + )?; + context.add_to_search_path(context.tempdir.to_path_buf()); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.13t"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find a python; got {python:?}" + ); + assert_eq!( + &python.interpreter().python_full_version().to_string(), + "3.13.0", + "We should find the correct interpreter for the request" + ); + assert!( + &python.interpreter().gil_disabled(), + "We should find a python without the GIL" + ); + + Ok(()) +} + +#[test] +fn find_python_version_prefer_non_free_threaded() -> Result<()> { + let mut context = TestContext::new()?; + + TestContext::create_mock_interpreter( + &context.tempdir.join("python"), + &PythonVersion::from_str("3.13.0").unwrap(), + ImplementationName::CPython, + true, + false, + )?; + TestContext::create_mock_interpreter( + &context.tempdir.join("python3.13t"), + &PythonVersion::from_str("3.13.0").unwrap(), + ImplementationName::CPython, + true, + true, + )?; + context.add_to_search_path(context.tempdir.to_path_buf()); + + let python = context.run(|| { + find_python_installation( + &PythonRequest::parse("3.13"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + })??; + + assert!( + matches!( + python, + PythonInstallation { + source: PythonSource::SearchPath, + interpreter: _ + } + ), + "We should find a python; got {python:?}" + ); + assert_eq!( + &python.interpreter().python_full_version().to_string(), + "3.13.0", + "We should find the correct interpreter for the request" + ); + assert!( + !&python.interpreter().gil_disabled(), + "We should prefer a python with the GIL" + ); + + Ok(()) +} diff --git a/crates/uv-requirements-txt/Cargo.toml b/crates/uv-requirements-txt/Cargo.toml index 86d72cf0b..8ab3cb6b3 100644 --- a/crates/uv-requirements-txt/Cargo.toml +++ b/crates/uv-requirements-txt/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index 2bff81ff5..d3fbf929b 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -9,6 +9,9 @@ repository.workspace = true authors.workspace = true license.workspace = true +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index a50aaae47..780878b79 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 588347160..c48480ff7 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -4122,286 +4122,4 @@ fn each_element_on_its_line_array(elements: impl Iterator = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn missing_dependency_version_unambiguous() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "a" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "b" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package.dependencies]] -name = "a" -source = { registry = "https://pypi.org/simple" } -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn missing_dependency_source_version_unambiguous() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "a" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "b" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package.dependencies]] -name = "a" -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn missing_dependency_source_ambiguous() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "a" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "a" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "b" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package.dependencies]] -name = "a" -version = "0.1.0" -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn missing_dependency_version_ambiguous() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "a" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "a" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "b" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package.dependencies]] -name = "a" -source = { registry = "https://pypi.org/simple" } -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn missing_dependency_source_version_ambiguous() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "a" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "a" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package]] -name = "b" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } - -[[package.dependencies]] -name = "a" -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn hash_optional_missing() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }] -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn hash_optional_present() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }] -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn hash_required_present() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { path = "file:///foo/bar" } -wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }] -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn source_direct_no_subdir() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { url = "https://burntsushi.net" } -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn source_direct_has_subdir() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" } -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn source_directory() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { directory = "path/to/dir" } -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } - - #[test] - fn source_editable() { - let data = r#" -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "anyio" -version = "4.3.0" -source = { editable = "path/to/dir" } -"#; - let result: Result = toml::from_str(data); - insta::assert_debug_snapshot!(result); - } -} +mod tests; diff --git a/crates/uv-resolver/src/lock/tests.rs b/crates/uv-resolver/src/lock/tests.rs new file mode 100644 index 000000000..40e03a027 --- /dev/null +++ b/crates/uv-resolver/src/lock/tests.rs @@ -0,0 +1,281 @@ +use super::*; + +#[test] +fn missing_dependency_source_unambiguous() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +version = "0.1.0" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn missing_dependency_version_unambiguous() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +source = { registry = "https://pypi.org/simple" } +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn missing_dependency_source_version_unambiguous() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn missing_dependency_source_ambiguous() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "a" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +version = "0.1.0" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn missing_dependency_version_ambiguous() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "a" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +source = { registry = "https://pypi.org/simple" } +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn missing_dependency_source_version_ambiguous() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "a" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "a" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package]] +name = "b" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 } + +[[package.dependencies]] +name = "a" +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn hash_optional_missing() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }] +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn hash_optional_present() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }] +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn hash_required_present() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { path = "file:///foo/bar" } +wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }] +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn source_direct_no_subdir() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { url = "https://burntsushi.net" } +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn source_direct_has_subdir() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" } +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn source_directory() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { directory = "path/to/dir" } +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} + +#[test] +fn source_editable() { + let data = r#" +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.3.0" +source = { editable = "path/to/dir" } +"#; + let result: Result = toml::from_str(data); + insta::assert_debug_snapshot!(result); +} diff --git a/crates/uv-resolver/src/redirect.rs b/crates/uv-resolver/src/redirect.rs index a4a7652f4..60004c394 100644 --- a/crates/uv-resolver/src/redirect.rs +++ b/crates/uv-resolver/src/redirect.rs @@ -86,65 +86,4 @@ fn apply_redirect(url: &VerbatimUrl, redirect: Url) -> VerbatimUrl { } #[cfg(test)] -mod tests { - use url::Url; - - use uv_pep508::VerbatimUrl; - - use crate::redirect::apply_redirect; - - #[test] - fn test_apply_redirect() -> Result<(), url::ParseError> { - // If there's no `@` in the original representation, we can just append the precise suffix - // to the given representation. - let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git")? - .with_given("git+https://github.com/flask.git"); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; - - let expected = VerbatimUrl::parse_url( - "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", - )? - .with_given("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe"); - assert_eq!(apply_redirect(&verbatim, redirect), expected); - - // If there's an `@` in the original representation, and it's stable between the parsed and - // given representations, we preserve everything that precedes the `@` in the precise - // representation. - let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? - .with_given("git+https://${DOMAIN}.com/flask.git@main"); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; - - let expected = VerbatimUrl::parse_url( - "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", - )? - .with_given("https://${DOMAIN}.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe"); - assert_eq!(apply_redirect(&verbatim, redirect), expected); - - // If there's a conflict after the `@`, discard the original representation. - let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? - .with_given("git+https://github.com/flask.git@${TAG}".to_string()); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; - - let expected = VerbatimUrl::parse_url( - "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", - )?; - assert_eq!(apply_redirect(&verbatim, redirect), expected); - - // We should preserve subdirectory fragments. - let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git#subdirectory=src")? - .with_given("git+https://github.com/flask.git#subdirectory=src"); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src")?; - - let expected = VerbatimUrl::parse_url( - "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src", - )?.with_given("git+https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src"); - - assert_eq!(apply_redirect(&verbatim, redirect), expected); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-resolver/src/redirect/tests.rs b/crates/uv-resolver/src/redirect/tests.rs new file mode 100644 index 000000000..6a151adc8 --- /dev/null +++ b/crates/uv-resolver/src/redirect/tests.rs @@ -0,0 +1,61 @@ +use url::Url; + +use uv_pep508::VerbatimUrl; + +use crate::redirect::apply_redirect; + +#[test] +fn test_apply_redirect() -> Result<(), url::ParseError> { + // If there's no `@` in the original representation, we can just append the precise suffix + // to the given representation. + let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git")? + .with_given("git+https://github.com/flask.git"); + let redirect = + Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; + + let expected = VerbatimUrl::parse_url( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", + )? + .with_given("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe"); + assert_eq!(apply_redirect(&verbatim, redirect), expected); + + // If there's an `@` in the original representation, and it's stable between the parsed and + // given representations, we preserve everything that precedes the `@` in the precise + // representation. + let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? + .with_given("git+https://${DOMAIN}.com/flask.git@main"); + let redirect = + Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; + + let expected = VerbatimUrl::parse_url( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", + )? + .with_given("https://${DOMAIN}.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe"); + assert_eq!(apply_redirect(&verbatim, redirect), expected); + + // If there's a conflict after the `@`, discard the original representation. + let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? + .with_given("git+https://github.com/flask.git@${TAG}".to_string()); + let redirect = + Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; + + let expected = VerbatimUrl::parse_url( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", + )?; + assert_eq!(apply_redirect(&verbatim, redirect), expected); + + // We should preserve subdirectory fragments. + let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git#subdirectory=src")? + .with_given("git+https://github.com/flask.git#subdirectory=src"); + let redirect = Url::parse( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src", + )?; + + let expected = VerbatimUrl::parse_url( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src", + )?.with_given("git+https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src"); + + assert_eq!(apply_redirect(&verbatim, redirect), expected); + + Ok(()) +} diff --git a/crates/uv-resolver/src/requires_python.rs b/crates/uv-resolver/src/requires_python.rs index e2f49aaa5..a2519d20a 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-resolver/src/requires_python.rs @@ -784,121 +784,4 @@ impl From for Bound { } #[cfg(test)] -mod tests { - use std::cmp::Ordering; - use std::collections::Bound; - use std::str::FromStr; - - use uv_distribution_filename::WheelFilename; - use uv_pep440::{Version, VersionSpecifiers}; - - use crate::requires_python::{LowerBound, UpperBound}; - use crate::RequiresPython; - - #[test] - fn requires_python_included() { - let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); - let wheel_names = &[ - "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", - "black-24.4.2-cp310-cp310-win_amd64.whl", - "black-24.4.2-cp310-none-win_amd64.whl", - "cbor2-5.6.4-py3-none-any.whl", - "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl", - "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl", - "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl", - "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", - ]; - for wheel_name in wheel_names { - assert!( - requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), - "{wheel_name}" - ); - } - - let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); - let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"]; - for wheel_name in wheel_names { - assert!( - requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), - "{wheel_name}" - ); - } - } - - #[test] - fn requires_python_dropped() { - let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); - let wheel_names = &[ - "PySocks-1.7.1-py27-none-any.whl", - "black-24.4.2-cp39-cp39-win_amd64.whl", - "dearpygui-1.11.1-cp312-cp312-win_amd64.whl", - "psutil-6.0.0-cp27-none-win32.whl", - "psutil-6.0.0-cp36-cp36m-win32.whl", - "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", - "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl", - "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl", - "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl", - ]; - for wheel_name in wheel_names { - assert!( - !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), - "{wheel_name}" - ); - } - - let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); - let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"]; - for wheel_name in wheel_names { - assert!( - !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), - "{wheel_name}" - ); - } - } - - #[test] - fn lower_bound_ordering() { - let versions = &[ - // No bound - LowerBound::new(Bound::Unbounded), - // >=3.8 - LowerBound::new(Bound::Included(Version::new([3, 8]))), - // >3.8 - LowerBound::new(Bound::Excluded(Version::new([3, 8]))), - // >=3.8.1 - LowerBound::new(Bound::Included(Version::new([3, 8, 1]))), - // >3.8.1 - LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))), - ]; - for (i, v1) in versions.iter().enumerate() { - for v2 in &versions[i + 1..] { - assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); - } - } - } - - #[test] - fn upper_bound_ordering() { - let versions = &[ - // <3.8 - UpperBound::new(Bound::Excluded(Version::new([3, 8]))), - // <=3.8 - UpperBound::new(Bound::Included(Version::new([3, 8]))), - // <3.8.1 - UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))), - // <=3.8.1 - UpperBound::new(Bound::Included(Version::new([3, 8, 1]))), - // No bound - UpperBound::new(Bound::Unbounded), - ]; - for (i, v1) in versions.iter().enumerate() { - for v2 in &versions[i + 1..] { - assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); - } - } - } -} +mod tests; diff --git a/crates/uv-resolver/src/requires_python/tests.rs b/crates/uv-resolver/src/requires_python/tests.rs new file mode 100644 index 000000000..b369db15e --- /dev/null +++ b/crates/uv-resolver/src/requires_python/tests.rs @@ -0,0 +1,116 @@ +use std::cmp::Ordering; +use std::collections::Bound; +use std::str::FromStr; + +use uv_distribution_filename::WheelFilename; +use uv_pep440::{Version, VersionSpecifiers}; + +use crate::requires_python::{LowerBound, UpperBound}; +use crate::RequiresPython; + +#[test] +fn requires_python_included() { + let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let wheel_names = &[ + "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", + "black-24.4.2-cp310-cp310-win_amd64.whl", + "black-24.4.2-cp310-none-win_amd64.whl", + "cbor2-5.6.4-py3-none-any.whl", + "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl", + "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl", + "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl", + "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", + ]; + for wheel_name in wheel_names { + assert!( + requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), + "{wheel_name}" + ); + } + + let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"]; + for wheel_name in wheel_names { + assert!( + requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), + "{wheel_name}" + ); + } +} + +#[test] +fn requires_python_dropped() { + let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let wheel_names = &[ + "PySocks-1.7.1-py27-none-any.whl", + "black-24.4.2-cp39-cp39-win_amd64.whl", + "dearpygui-1.11.1-cp312-cp312-win_amd64.whl", + "psutil-6.0.0-cp27-none-win32.whl", + "psutil-6.0.0-cp36-cp36m-win32.whl", + "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", + "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl", + "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl", + "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl", + ]; + for wheel_name in wheel_names { + assert!( + !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), + "{wheel_name}" + ); + } + + let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers).unwrap(); + let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"]; + for wheel_name in wheel_names { + assert!( + !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()), + "{wheel_name}" + ); + } +} + +#[test] +fn lower_bound_ordering() { + let versions = &[ + // No bound + LowerBound::new(Bound::Unbounded), + // >=3.8 + LowerBound::new(Bound::Included(Version::new([3, 8]))), + // >3.8 + LowerBound::new(Bound::Excluded(Version::new([3, 8]))), + // >=3.8.1 + LowerBound::new(Bound::Included(Version::new([3, 8, 1]))), + // >3.8.1 + LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))), + ]; + for (i, v1) in versions.iter().enumerate() { + for v2 in &versions[i + 1..] { + assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); + } + } +} + +#[test] +fn upper_bound_ordering() { + let versions = &[ + // <3.8 + UpperBound::new(Bound::Excluded(Version::new([3, 8]))), + // <=3.8 + UpperBound::new(Bound::Included(Version::new([3, 8]))), + // <3.8.1 + UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))), + // <=3.8.1 + UpperBound::new(Bound::Included(Version::new([3, 8, 1]))), + // No bound + UpperBound::new(Bound::Unbounded), + ]; + for (i, v1) in versions.iter().enumerate() { + for v2 in &versions[i + 1..] { + assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}"); + } + } +} diff --git a/crates/uv-resolver/src/resolver/locals.rs b/crates/uv-resolver/src/resolver/locals.rs index c570c9ad3..dedd9ad9b 100644 --- a/crates/uv-resolver/src/resolver/locals.rs +++ b/crates/uv-resolver/src/resolver/locals.rs @@ -198,136 +198,4 @@ pub(crate) fn from_source(source: &RequirementSource) -> Option { } #[cfg(test)] -mod tests { - use std::str::FromStr; - - use anyhow::Result; - use url::Url; - - use uv_pep440::{Operator, Version, VersionSpecifier, VersionSpecifiers}; - use uv_pep508::VerbatimUrl; - use uv_pypi_types::ParsedUrl; - use uv_pypi_types::RequirementSource; - - use super::{from_source, Locals}; - - #[test] - fn extract_locals() -> Result<()> { - // Extract from a source distribution in a URL. - let url = VerbatimUrl::from_url(Url::parse("https://example.com/foo-1.0.0+local.tar.gz")?); - let source = - RequirementSource::from_parsed_url(ParsedUrl::try_from(url.to_url()).unwrap(), url); - let locals: Vec<_> = from_source(&source).into_iter().collect(); - assert_eq!(locals, vec![Version::from_str("1.0.0+local")?]); - - // Extract from a wheel in a URL. - let url = VerbatimUrl::from_url(Url::parse( - "https://example.com/foo-1.0.0+local-cp39-cp39-linux_x86_64.whl", - )?); - let source = - RequirementSource::from_parsed_url(ParsedUrl::try_from(url.to_url()).unwrap(), url); - let locals: Vec<_> = from_source(&source).into_iter().collect(); - assert_eq!(locals, vec![Version::from_str("1.0.0+local")?]); - - // Don't extract anything if the URL is opaque. - let url = VerbatimUrl::from_url(Url::parse("git+https://example.com/foo/bar")?); - let source = - RequirementSource::from_parsed_url(ParsedUrl::try_from(url.to_url()).unwrap(), url); - let locals: Vec<_> = from_source(&source).into_iter().collect(); - assert!(locals.is_empty()); - - // Extract from `==` specifiers. - let version = VersionSpecifiers::from_iter([ - VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)?, - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)?, - ]); - let source = RequirementSource::Registry { - specifier: version, - index: None, - }; - let locals: Vec<_> = from_source(&source).into_iter().collect(); - assert_eq!(locals, vec![Version::from_str("1.0.0+local")?]); - - // Ignore other specifiers. - let version = VersionSpecifiers::from_iter([VersionSpecifier::from_version( - Operator::NotEqual, - Version::from_str("1.0.0+local")?, - )?]); - let source = RequirementSource::Registry { - specifier: version, - index: None, - }; - let locals: Vec<_> = from_source(&source).into_iter().collect(); - assert!(locals.is_empty()); - - Ok(()) - } - - #[test] - fn map_version() -> Result<()> { - // Given `==1.0.0`, if the local version is `1.0.0+local`, map to `==1.0.0+local`. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? - ); - - // Given `!=1.0.0`, if the local version is `1.0.0+local`, map to `!=1.0.0+local`. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::NotEqual, Version::from_str("1.0.0")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::NotEqual, Version::from_str("1.0.0+local")?)? - ); - - // Given `<=1.0.0`, if the local version is `1.0.0+local`, map to `==1.0.0+local`. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::LessThanEqual, Version::from_str("1.0.0")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? - ); - - // Given `>1.0.0`, `1.0.0+local` is already (correctly) disallowed. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)? - ); - - // Given `===1.0.0`, `1.0.0+local` is already (correctly) disallowed. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::ExactEqual, Version::from_str("1.0.0")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::ExactEqual, Version::from_str("1.0.0")?)? - ); - - // Given `==1.0.0+local`, `1.0.0+local` is already (correctly) allowed. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? - ); - - // Given `==1.0.0+other`, `1.0.0+local` is already (correctly) disallowed. - let local = Version::from_str("1.0.0+local")?; - let specifier = - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+other")?)?; - assert_eq!( - Locals::map(&local, &specifier)?, - VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+other")?)? - ); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-resolver/src/resolver/locals/tests.rs b/crates/uv-resolver/src/resolver/locals/tests.rs new file mode 100644 index 000000000..7ff7bfe3d --- /dev/null +++ b/crates/uv-resolver/src/resolver/locals/tests.rs @@ -0,0 +1,130 @@ +use std::str::FromStr; + +use anyhow::Result; +use url::Url; + +use uv_pep440::{Operator, Version, VersionSpecifier, VersionSpecifiers}; +use uv_pep508::VerbatimUrl; +use uv_pypi_types::ParsedUrl; +use uv_pypi_types::RequirementSource; + +use super::{from_source, Locals}; + +#[test] +fn extract_locals() -> Result<()> { + // Extract from a source distribution in a URL. + let url = VerbatimUrl::from_url(Url::parse("https://example.com/foo-1.0.0+local.tar.gz")?); + let source = + RequirementSource::from_parsed_url(ParsedUrl::try_from(url.to_url()).unwrap(), url); + let locals: Vec<_> = from_source(&source).into_iter().collect(); + assert_eq!(locals, vec![Version::from_str("1.0.0+local")?]); + + // Extract from a wheel in a URL. + let url = VerbatimUrl::from_url(Url::parse( + "https://example.com/foo-1.0.0+local-cp39-cp39-linux_x86_64.whl", + )?); + let source = + RequirementSource::from_parsed_url(ParsedUrl::try_from(url.to_url()).unwrap(), url); + let locals: Vec<_> = from_source(&source).into_iter().collect(); + assert_eq!(locals, vec![Version::from_str("1.0.0+local")?]); + + // Don't extract anything if the URL is opaque. + let url = VerbatimUrl::from_url(Url::parse("git+https://example.com/foo/bar")?); + let source = + RequirementSource::from_parsed_url(ParsedUrl::try_from(url.to_url()).unwrap(), url); + let locals: Vec<_> = from_source(&source).into_iter().collect(); + assert!(locals.is_empty()); + + // Extract from `==` specifiers. + let version = VersionSpecifiers::from_iter([ + VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)?, + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)?, + ]); + let source = RequirementSource::Registry { + specifier: version, + index: None, + }; + let locals: Vec<_> = from_source(&source).into_iter().collect(); + assert_eq!(locals, vec![Version::from_str("1.0.0+local")?]); + + // Ignore other specifiers. + let version = VersionSpecifiers::from_iter([VersionSpecifier::from_version( + Operator::NotEqual, + Version::from_str("1.0.0+local")?, + )?]); + let source = RequirementSource::Registry { + specifier: version, + index: None, + }; + let locals: Vec<_> = from_source(&source).into_iter().collect(); + assert!(locals.is_empty()); + + Ok(()) +} + +#[test] +fn map_version() -> Result<()> { + // Given `==1.0.0`, if the local version is `1.0.0+local`, map to `==1.0.0+local`. + let local = Version::from_str("1.0.0+local")?; + let specifier = VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? + ); + + // Given `!=1.0.0`, if the local version is `1.0.0+local`, map to `!=1.0.0+local`. + let local = Version::from_str("1.0.0+local")?; + let specifier = + VersionSpecifier::from_version(Operator::NotEqual, Version::from_str("1.0.0")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::NotEqual, Version::from_str("1.0.0+local")?)? + ); + + // Given `<=1.0.0`, if the local version is `1.0.0+local`, map to `==1.0.0+local`. + let local = Version::from_str("1.0.0+local")?; + let specifier = + VersionSpecifier::from_version(Operator::LessThanEqual, Version::from_str("1.0.0")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? + ); + + // Given `>1.0.0`, `1.0.0+local` is already (correctly) disallowed. + let local = Version::from_str("1.0.0+local")?; + let specifier = + VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::GreaterThan, Version::from_str("1.0.0")?)? + ); + + // Given `===1.0.0`, `1.0.0+local` is already (correctly) disallowed. + let local = Version::from_str("1.0.0+local")?; + let specifier = + VersionSpecifier::from_version(Operator::ExactEqual, Version::from_str("1.0.0")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::ExactEqual, Version::from_str("1.0.0")?)? + ); + + // Given `==1.0.0+local`, `1.0.0+local` is already (correctly) allowed. + let local = Version::from_str("1.0.0+local")?; + let specifier = + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+local")?)? + ); + + // Given `==1.0.0+other`, `1.0.0+local` is already (correctly) disallowed. + let local = Version::from_str("1.0.0+local")?; + let specifier = + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+other")?)?; + assert_eq!( + Locals::map(&local, &specifier)?, + VersionSpecifier::from_version(Operator::Equal, Version::from_str("1.0.0+other")?)? + ); + + Ok(()) +} diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index 1a109dfcb..a92848d4d 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" edition = "2021" description = "Parse PEP 723-style Python scripts." +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 79f9dac57..59ce3685f 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -481,233 +481,4 @@ fn serialize_metadata(metadata: &str) -> String { } #[cfg(test)] -mod tests { - use crate::{serialize_metadata, Pep723Error, ScriptTag}; - - #[test] - fn missing_space() { - let contents = indoc::indoc! {r" - # /// script - #requires-python = '>=3.11' - # /// - "}; - - assert!(matches!( - ScriptTag::parse(contents.as_bytes()), - Err(Pep723Error::UnclosedBlock) - )); - } - - #[test] - fn no_closing_pragma() { - let contents = indoc::indoc! {r" - # /// script - # requires-python = '>=3.11' - # dependencies = [ - # 'requests<3', - # 'rich', - # ] - "}; - - assert!(matches!( - ScriptTag::parse(contents.as_bytes()), - Err(Pep723Error::UnclosedBlock) - )); - } - - #[test] - fn leading_content() { - let contents = indoc::indoc! {r" - pass # /// script - # requires-python = '>=3.11' - # dependencies = [ - # 'requests<3', - # 'rich', - # ] - # /// - # - # - "}; - - assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); - } - - #[test] - fn simple() { - let contents = indoc::indoc! {r" - # /// script - # requires-python = '>=3.11' - # dependencies = [ - # 'requests<3', - # 'rich', - # ] - # /// - - import requests - from rich.pretty import pprint - - resp = requests.get('https://peps.python.org/api/peps.json') - data = resp.json() - "}; - - let expected_metadata = indoc::indoc! {r" - requires-python = '>=3.11' - dependencies = [ - 'requests<3', - 'rich', - ] - "}; - - let expected_data = indoc::indoc! {r" - - import requests - from rich.pretty import pprint - - resp = requests.get('https://peps.python.org/api/peps.json') - data = resp.json() - "}; - - let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap(); - - assert_eq!(actual.prelude, String::new()); - assert_eq!(actual.metadata, expected_metadata); - assert_eq!(actual.postlude, expected_data); - } - - #[test] - fn simple_with_shebang() { - let contents = indoc::indoc! {r" - #!/usr/bin/env python3 - # /// script - # requires-python = '>=3.11' - # dependencies = [ - # 'requests<3', - # 'rich', - # ] - # /// - - import requests - from rich.pretty import pprint - - resp = requests.get('https://peps.python.org/api/peps.json') - data = resp.json() - "}; - - let expected_metadata = indoc::indoc! {r" - requires-python = '>=3.11' - dependencies = [ - 'requests<3', - 'rich', - ] - "}; - - let expected_data = indoc::indoc! {r" - - import requests - from rich.pretty import pprint - - resp = requests.get('https://peps.python.org/api/peps.json') - data = resp.json() - "}; - - let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap(); - - assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string()); - assert_eq!(actual.metadata, expected_metadata); - assert_eq!(actual.postlude, expected_data); - } - #[test] - fn embedded_comment() { - let contents = indoc::indoc! {r" - # /// script - # embedded-csharp = ''' - # /// - # /// text - # /// - # /// - # public class MyClass { } - # ''' - # /// - "}; - - let expected = indoc::indoc! {r" - embedded-csharp = ''' - /// - /// text - /// - /// - public class MyClass { } - ''' - "}; - - let actual = ScriptTag::parse(contents.as_bytes()) - .unwrap() - .unwrap() - .metadata; - - assert_eq!(actual, expected); - } - - #[test] - fn trailing_lines() { - let contents = indoc::indoc! {r" - # /// script - # requires-python = '>=3.11' - # dependencies = [ - # 'requests<3', - # 'rich', - # ] - # /// - # - # - "}; - - let expected = indoc::indoc! {r" - requires-python = '>=3.11' - dependencies = [ - 'requests<3', - 'rich', - ] - "}; - - let actual = ScriptTag::parse(contents.as_bytes()) - .unwrap() - .unwrap() - .metadata; - - assert_eq!(actual, expected); - } - - #[test] - fn test_serialize_metadata_formatting() { - let metadata = indoc::indoc! {r" - requires-python = '>=3.11' - dependencies = [ - 'requests<3', - 'rich', - ] - "}; - - let expected_output = indoc::indoc! {r" - # /// script - # requires-python = '>=3.11' - # dependencies = [ - # 'requests<3', - # 'rich', - # ] - # /// - "}; - - let result = serialize_metadata(metadata); - assert_eq!(result, expected_output); - } - - #[test] - fn test_serialize_metadata_empty() { - let metadata = ""; - let expected_output = "# /// script\n# ///\n"; - - let result = serialize_metadata(metadata); - assert_eq!(result, expected_output); - } -} +mod tests; diff --git a/crates/uv-scripts/src/tests.rs b/crates/uv-scripts/src/tests.rs new file mode 100644 index 000000000..caf508792 --- /dev/null +++ b/crates/uv-scripts/src/tests.rs @@ -0,0 +1,228 @@ +use crate::{serialize_metadata, Pep723Error, ScriptTag}; + +#[test] +fn missing_space() { + let contents = indoc::indoc! {r" + # /// script + #requires-python = '>=3.11' + # /// + "}; + + assert!(matches!( + ScriptTag::parse(contents.as_bytes()), + Err(Pep723Error::UnclosedBlock) + )); +} + +#[test] +fn no_closing_pragma() { + let contents = indoc::indoc! {r" + # /// script + # requires-python = '>=3.11' + # dependencies = [ + # 'requests<3', + # 'rich', + # ] + "}; + + assert!(matches!( + ScriptTag::parse(contents.as_bytes()), + Err(Pep723Error::UnclosedBlock) + )); +} + +#[test] +fn leading_content() { + let contents = indoc::indoc! {r" + pass # /// script + # requires-python = '>=3.11' + # dependencies = [ + # 'requests<3', + # 'rich', + # ] + # /// + # + # + "}; + + assert_eq!(ScriptTag::parse(contents.as_bytes()).unwrap(), None); +} + +#[test] +fn simple() { + let contents = indoc::indoc! {r" + # /// script + # requires-python = '>=3.11' + # dependencies = [ + # 'requests<3', + # 'rich', + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get('https://peps.python.org/api/peps.json') + data = resp.json() + "}; + + let expected_metadata = indoc::indoc! {r" + requires-python = '>=3.11' + dependencies = [ + 'requests<3', + 'rich', + ] + "}; + + let expected_data = indoc::indoc! {r" + + import requests + from rich.pretty import pprint + + resp = requests.get('https://peps.python.org/api/peps.json') + data = resp.json() + "}; + + let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap(); + + assert_eq!(actual.prelude, String::new()); + assert_eq!(actual.metadata, expected_metadata); + assert_eq!(actual.postlude, expected_data); +} + +#[test] +fn simple_with_shebang() { + let contents = indoc::indoc! {r" + #!/usr/bin/env python3 + # /// script + # requires-python = '>=3.11' + # dependencies = [ + # 'requests<3', + # 'rich', + # ] + # /// + + import requests + from rich.pretty import pprint + + resp = requests.get('https://peps.python.org/api/peps.json') + data = resp.json() + "}; + + let expected_metadata = indoc::indoc! {r" + requires-python = '>=3.11' + dependencies = [ + 'requests<3', + 'rich', + ] + "}; + + let expected_data = indoc::indoc! {r" + + import requests + from rich.pretty import pprint + + resp = requests.get('https://peps.python.org/api/peps.json') + data = resp.json() + "}; + + let actual = ScriptTag::parse(contents.as_bytes()).unwrap().unwrap(); + + assert_eq!(actual.prelude, "#!/usr/bin/env python3\n".to_string()); + assert_eq!(actual.metadata, expected_metadata); + assert_eq!(actual.postlude, expected_data); +} +#[test] +fn embedded_comment() { + let contents = indoc::indoc! {r" + # /// script + # embedded-csharp = ''' + # /// + # /// text + # /// + # /// + # public class MyClass { } + # ''' + # /// + "}; + + let expected = indoc::indoc! {r" + embedded-csharp = ''' + /// + /// text + /// + /// + public class MyClass { } + ''' + "}; + + let actual = ScriptTag::parse(contents.as_bytes()) + .unwrap() + .unwrap() + .metadata; + + assert_eq!(actual, expected); +} + +#[test] +fn trailing_lines() { + let contents = indoc::indoc! {r" + # /// script + # requires-python = '>=3.11' + # dependencies = [ + # 'requests<3', + # 'rich', + # ] + # /// + # + # + "}; + + let expected = indoc::indoc! {r" + requires-python = '>=3.11' + dependencies = [ + 'requests<3', + 'rich', + ] + "}; + + let actual = ScriptTag::parse(contents.as_bytes()) + .unwrap() + .unwrap() + .metadata; + + assert_eq!(actual, expected); +} + +#[test] +fn test_serialize_metadata_formatting() { + let metadata = indoc::indoc! {r" + requires-python = '>=3.11' + dependencies = [ + 'requests<3', + 'rich', + ] + "}; + + let expected_output = indoc::indoc! {r" + # /// script + # requires-python = '>=3.11' + # dependencies = [ + # 'requests<3', + # 'rich', + # ] + # /// + "}; + + let result = serialize_metadata(metadata); + assert_eq!(result, expected_output); +} + +#[test] +fn test_serialize_metadata_empty() { + let metadata = ""; + let expected_output = "# /// script\n# ///\n"; + + let result = serialize_metadata(metadata); + assert_eq!(result, expected_output); +} diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 220fd0082..b666d4e41 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-shell/Cargo.toml b/crates/uv-shell/Cargo.toml index a6040c07a..3d13f8a8a 100644 --- a/crates/uv-shell/Cargo.toml +++ b/crates/uv-shell/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" edition = "2021" description = "Utilities for detecting and manipulating shell environments" +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-state/Cargo.toml b/crates/uv-state/Cargo.toml index 3b3bd2c1f..f90413be4 100644 --- a/crates/uv-state/Cargo.toml +++ b/crates/uv-state/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index 10261c528..a980f8846 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-trampoline/Cargo.toml b/crates/uv-trampoline/Cargo.toml index d6de598d4..92d5fa87e 100644 --- a/crates/uv-trampoline/Cargo.toml +++ b/crates/uv-trampoline/Cargo.toml @@ -5,6 +5,9 @@ authors = ["Nathaniel J. Smith "] license = "MIT OR Apache-2.0" edition = "2021" +[lib] +doctest = false + # Need to optimize etc. or else build fails [profile.dev] lto = true diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 6b47a31ce..57ac4571a 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 54c5c539c..2553673e5 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-version/src/lib.rs b/crates/uv-version/src/lib.rs index 20a8940a1..bf17cf1e8 100644 --- a/crates/uv-version/src/lib.rs +++ b/crates/uv-version/src/lib.rs @@ -6,11 +6,4 @@ pub fn version() -> &'static str { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_version() { - assert_eq!(version().to_string(), env!("CARGO_PKG_VERSION").to_string()); - } -} +mod tests; diff --git a/crates/uv-version/src/tests.rs b/crates/uv-version/src/tests.rs new file mode 100644 index 000000000..7887964c5 --- /dev/null +++ b/crates/uv-version/src/tests.rs @@ -0,0 +1,6 @@ +use super::*; + +#[test] +fn test_get_version() { + assert_eq!(version().to_string(), env!("CARGO_PKG_VERSION").to_string()); +} diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index fed03e0c9..959a17e20 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -13,6 +13,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-warnings/Cargo.toml b/crates/uv-warnings/Cargo.toml index c5b281e2e..74fdd61b8 100644 --- a/crates/uv-warnings/Cargo.toml +++ b/crates/uv-warnings/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index fe6125ce2..330a2fd9b 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -9,6 +9,9 @@ repository = { workspace = true } authors = { workspace = true } license = { workspace = true } +[lib] +doctest = false + [lints] workspace = true diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index d74d345ea..1fe6f4ae6 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1523,834 +1523,4 @@ impl<'env> From<&'env VirtualProject> for InstallTarget<'env> { #[cfg(test)] #[cfg(unix)] // Avoid path escaping for the unit tests -mod tests { - use std::env; - - use std::path::Path; - - use anyhow::Result; - use assert_fs::fixture::ChildPath; - use assert_fs::prelude::*; - use insta::assert_json_snapshot; - - use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; - - async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { - let root_dir = env::current_dir() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .join("scripts") - .join("workspaces"); - let project = - ProjectWorkspace::discover(&root_dir.join(folder), &DiscoveryOptions::default()) - .await - .unwrap(); - let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref()); - (project, root_escaped) - } - - async fn temporary_test(folder: &Path) -> (ProjectWorkspace, String) { - let project = ProjectWorkspace::discover(folder, &DiscoveryOptions::default()) - .await - .unwrap(); - let root_escaped = regex::escape(folder.to_string_lossy().as_ref()); - (project, root_escaped) - } - - #[tokio::test] - async fn albatross_in_example() { - let (project, root_escaped) = - workspace_test("albatross-in-example/examples/bird-feeder").await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]/albatross-in-example/examples/bird-feeder", - "project_name": "bird-feeder", - "workspace": { - "install_path": "[ROOT]/albatross-in-example/examples/bird-feeder", - "packages": { - "bird-feeder": { - "root": "[ROOT]/albatross-in-example/examples/bird-feeder", - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "anyio>=4.3.0,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "anyio>=4.3.0,<5" - ], - "optional-dependencies": null - }, - "tool": null - } - } - } - "###); - }); - } - - #[tokio::test] - async fn albatross_project_in_excluded() { - let (project, root_escaped) = - workspace_test("albatross-project-in-excluded/excluded/bird-feeder").await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", - "project_name": "bird-feeder", - "workspace": { - "install_path": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", - "packages": { - "bird-feeder": { - "root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "anyio>=4.3.0,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "anyio>=4.3.0,<5" - ], - "optional-dependencies": null - }, - "tool": null - } - } - } - "###); - }); - } - - #[tokio::test] - async fn albatross_root_workspace() { - let (project, root_escaped) = workspace_test("albatross-root-workspace").await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]/albatross-root-workspace", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]/albatross-root-workspace", - "packages": { - "albatross": { - "root": "[ROOT]/albatross-root-workspace", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "bird-feeder", - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "bird-feeder": { - "root": "[ROOT]/albatross-root-workspace/packages/bird-feeder", - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.8", - "dependencies": [ - "anyio>=4.3.0,<5", - "seeds" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "seeds": { - "root": "[ROOT]/albatross-root-workspace/packages/seeds", - "project": { - "name": "seeds", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "idna==3.6" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": { - "bird-feeder": [ - { - "workspace": true - } - ] - }, - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "bird-feeder", - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": { - "bird-feeder": [ - { - "workspace": true - } - ] - }, - "workspace": { - "members": [ - "packages/*" - ], - "exclude": null - }, - "managed": null, - "package": null, - "dev-dependencies": null, - "environments": null, - "override-dependencies": null, - "constraint-dependencies": null - } - } - } - } - } - "###); - }); - } - - #[tokio::test] - async fn albatross_virtual_workspace() { - let (project, root_escaped) = - workspace_test("albatross-virtual-workspace/packages/albatross").await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]/albatross-virtual-workspace/packages/albatross", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]/albatross-virtual-workspace", - "packages": { - "albatross": { - "root": "[ROOT]/albatross-virtual-workspace/packages/albatross", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "bird-feeder", - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "bird-feeder": { - "root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder", - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "anyio>=4.3.0,<5", - "seeds" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "seeds": { - "root": "[ROOT]/albatross-virtual-workspace/packages/seeds", - "project": { - "name": "seeds", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "idna==3.6" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": null, - "tool": { - "uv": { - "sources": null, - "workspace": { - "members": [ - "packages/*" - ], - "exclude": null - }, - "managed": null, - "package": null, - "dev-dependencies": null, - "environments": null, - "override-dependencies": null, - "constraint-dependencies": null - } - } - } - } - } - "###); - }); - } - - #[tokio::test] - async fn albatross_just_project() { - let (project, root_escaped) = workspace_test("albatross-just-project").await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]/albatross-just-project", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]/albatross-just-project", - "packages": { - "albatross": { - "root": "[ROOT]/albatross-just-project", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": null - } - } - } - "###); - }); - } - #[tokio::test] - async fn exclude_package() -> Result<()> { - let root = tempfile::TempDir::new()?; - let root = ChildPath::new(root.path()); - - // Create the root. - root.child("pyproject.toml").write_str( - r#" - [project] - name = "albatross" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["tqdm>=4,<5"] - - [tool.uv.workspace] - members = ["packages/*"] - exclude = ["packages/bird-feeder"] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - )?; - root.child("albatross").child("__init__.py").touch()?; - - // Create an included package (`seeds`). - root.child("packages") - .child("seeds") - .child("pyproject.toml") - .write_str( - r#" - [project] - name = "seeds" - version = "1.0.0" - requires-python = ">=3.12" - dependencies = ["idna==3.6"] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - )?; - root.child("packages") - .child("seeds") - .child("seeds") - .child("__init__.py") - .touch()?; - - // Create an excluded package (`bird-feeder`). - root.child("packages") - .child("bird-feeder") - .child("pyproject.toml") - .write_str( - r#" - [project] - name = "bird-feeder" - version = "1.0.0" - requires-python = ">=3.12" - dependencies = ["anyio>=4.3.0,<5"] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - )?; - root.child("packages") - .child("bird-feeder") - .child("bird_feeder") - .child("__init__.py") - .touch()?; - - let (project, root_escaped) = temporary_test(root.as_ref()).await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]", - "packages": { - "albatross": { - "root": "[ROOT]", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "seeds": { - "root": "[ROOT]/packages/seeds", - "project": { - "name": "seeds", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "idna==3.6" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "workspace": { - "members": [ - "packages/*" - ], - "exclude": [ - "packages/bird-feeder" - ] - }, - "managed": null, - "package": null, - "dev-dependencies": null, - "environments": null, - "override-dependencies": null, - "constraint-dependencies": null - } - } - } - } - } - "###); - }); - - // Rewrite the members to both include and exclude `bird-feeder` by name. - root.child("pyproject.toml").write_str( - r#" - [project] - name = "albatross" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["tqdm>=4,<5"] - - [tool.uv.workspace] - members = ["packages/seeds", "packages/bird-feeder"] - exclude = ["packages/bird-feeder"] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - )?; - - // `bird-feeder` should still be excluded. - let (project, root_escaped) = temporary_test(root.as_ref()).await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]", - "packages": { - "albatross": { - "root": "[ROOT]", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "seeds": { - "root": "[ROOT]/packages/seeds", - "project": { - "name": "seeds", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "idna==3.6" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "workspace": { - "members": [ - "packages/seeds", - "packages/bird-feeder" - ], - "exclude": [ - "packages/bird-feeder" - ] - }, - "managed": null, - "package": null, - "dev-dependencies": null, - "environments": null, - "override-dependencies": null, - "constraint-dependencies": null - } - } - } - } - } - "###); - }); - - // Rewrite the exclusion to use the top-level directory (`packages`). - root.child("pyproject.toml").write_str( - r#" - [project] - name = "albatross" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["tqdm>=4,<5"] - - [tool.uv.workspace] - members = ["packages/seeds", "packages/bird-feeder"] - exclude = ["packages"] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - )?; - - // `bird-feeder` should now be included. - let (project, root_escaped) = temporary_test(root.as_ref()).await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]", - "packages": { - "albatross": { - "root": "[ROOT]", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "bird-feeder": { - "root": "[ROOT]/packages/bird-feeder", - "project": { - "name": "bird-feeder", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "anyio>=4.3.0,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - }, - "seeds": { - "root": "[ROOT]/packages/seeds", - "project": { - "name": "seeds", - "version": "1.0.0", - "requires-python": ">=3.12", - "dependencies": [ - "idna==3.6" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "workspace": { - "members": [ - "packages/seeds", - "packages/bird-feeder" - ], - "exclude": [ - "packages" - ] - }, - "managed": null, - "package": null, - "dev-dependencies": null, - "environments": null, - "override-dependencies": null, - "constraint-dependencies": null - } - } - } - } - } - "###); - }); - - // Rewrite the exclusion to use the top-level directory with a glob (`packages/*`). - root.child("pyproject.toml").write_str( - r#" - [project] - name = "albatross" - version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["tqdm>=4,<5"] - - [tool.uv.workspace] - members = ["packages/seeds", "packages/bird-feeder"] - exclude = ["packages/*"] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#, - )?; - - // `bird-feeder` and `seeds` should now be excluded. - let (project, root_escaped) = temporary_test(root.as_ref()).await; - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_json_snapshot!( - project, - { - ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" - }, - @r###" - { - "project_root": "[ROOT]", - "project_name": "albatross", - "workspace": { - "install_path": "[ROOT]", - "packages": { - "albatross": { - "root": "[ROOT]", - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "pyproject_toml": "[PYPROJECT_TOML]" - } - }, - "sources": {}, - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "workspace": { - "members": [ - "packages/seeds", - "packages/bird-feeder" - ], - "exclude": [ - "packages/*" - ] - }, - "managed": null, - "package": null, - "dev-dependencies": null, - "environments": null, - "override-dependencies": null, - "constraint-dependencies": null - } - } - } - } - } - "###); - }); - - Ok(()) - } -} +mod tests; diff --git a/crates/uv-workspace/src/workspace/tests.rs b/crates/uv-workspace/src/workspace/tests.rs new file mode 100644 index 000000000..f12d130ab --- /dev/null +++ b/crates/uv-workspace/src/workspace/tests.rs @@ -0,0 +1,827 @@ +use std::env; + +use std::path::Path; + +use anyhow::Result; +use assert_fs::fixture::ChildPath; +use assert_fs::prelude::*; +use insta::assert_json_snapshot; + +use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; + +async fn workspace_test(folder: &str) -> (ProjectWorkspace, String) { + let root_dir = env::current_dir() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("scripts") + .join("workspaces"); + let project = ProjectWorkspace::discover(&root_dir.join(folder), &DiscoveryOptions::default()) + .await + .unwrap(); + let root_escaped = regex::escape(root_dir.to_string_lossy().as_ref()); + (project, root_escaped) +} + +async fn temporary_test(folder: &Path) -> (ProjectWorkspace, String) { + let project = ProjectWorkspace::discover(folder, &DiscoveryOptions::default()) + .await + .unwrap(); + let root_escaped = regex::escape(folder.to_string_lossy().as_ref()); + (project, root_escaped) +} + +#[tokio::test] +async fn albatross_in_example() { + let (project, root_escaped) = workspace_test("albatross-in-example/examples/bird-feeder").await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]/albatross-in-example/examples/bird-feeder", + "project_name": "bird-feeder", + "workspace": { + "install_path": "[ROOT]/albatross-in-example/examples/bird-feeder", + "packages": { + "bird-feeder": { + "root": "[ROOT]/albatross-in-example/examples/bird-feeder", + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "anyio>=4.3.0,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "anyio>=4.3.0,<5" + ], + "optional-dependencies": null + }, + "tool": null + } + } + } + "#); + }); +} + +#[tokio::test] +async fn albatross_project_in_excluded() { + let (project, root_escaped) = + workspace_test("albatross-project-in-excluded/excluded/bird-feeder").await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", + "project_name": "bird-feeder", + "workspace": { + "install_path": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", + "packages": { + "bird-feeder": { + "root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder", + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "anyio>=4.3.0,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "anyio>=4.3.0,<5" + ], + "optional-dependencies": null + }, + "tool": null + } + } + } + "#); + }); +} + +#[tokio::test] +async fn albatross_root_workspace() { + let (project, root_escaped) = workspace_test("albatross-root-workspace").await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]/albatross-root-workspace", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]/albatross-root-workspace", + "packages": { + "albatross": { + "root": "[ROOT]/albatross-root-workspace", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "bird-feeder", + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "bird-feeder": { + "root": "[ROOT]/albatross-root-workspace/packages/bird-feeder", + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.8", + "dependencies": [ + "anyio>=4.3.0,<5", + "seeds" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "seeds": { + "root": "[ROOT]/albatross-root-workspace/packages/seeds", + "project": { + "name": "seeds", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "idna==3.6" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": { + "bird-feeder": [ + { + "workspace": true + } + ] + }, + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "bird-feeder", + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": { + "bird-feeder": [ + { + "workspace": true + } + ] + }, + "workspace": { + "members": [ + "packages/*" + ], + "exclude": null + }, + "managed": null, + "package": null, + "dev-dependencies": null, + "environments": null, + "override-dependencies": null, + "constraint-dependencies": null + } + } + } + } + } + "#); + }); +} + +#[tokio::test] +async fn albatross_virtual_workspace() { + let (project, root_escaped) = + workspace_test("albatross-virtual-workspace/packages/albatross").await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]/albatross-virtual-workspace/packages/albatross", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]/albatross-virtual-workspace", + "packages": { + "albatross": { + "root": "[ROOT]/albatross-virtual-workspace/packages/albatross", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "bird-feeder", + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "bird-feeder": { + "root": "[ROOT]/albatross-virtual-workspace/packages/bird-feeder", + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "anyio>=4.3.0,<5", + "seeds" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "seeds": { + "root": "[ROOT]/albatross-virtual-workspace/packages/seeds", + "project": { + "name": "seeds", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "idna==3.6" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": null, + "tool": { + "uv": { + "sources": null, + "workspace": { + "members": [ + "packages/*" + ], + "exclude": null + }, + "managed": null, + "package": null, + "dev-dependencies": null, + "environments": null, + "override-dependencies": null, + "constraint-dependencies": null + } + } + } + } + } + "#); + }); +} + +#[tokio::test] +async fn albatross_just_project() { + let (project, root_escaped) = workspace_test("albatross-just-project").await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]/albatross-just-project", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]/albatross-just-project", + "packages": { + "albatross": { + "root": "[ROOT]/albatross-just-project", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": null + } + } + } + "#); + }); +} +#[tokio::test] +async fn exclude_package() -> Result<()> { + let root = tempfile::TempDir::new()?; + let root = ChildPath::new(root.path()); + + // Create the root. + root.child("pyproject.toml").write_str( + r#" + [project] + name = "albatross" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["tqdm>=4,<5"] + + [tool.uv.workspace] + members = ["packages/*"] + exclude = ["packages/bird-feeder"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + root.child("albatross").child("__init__.py").touch()?; + + // Create an included package (`seeds`). + root.child("packages") + .child("seeds") + .child("pyproject.toml") + .write_str( + r#" + [project] + name = "seeds" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = ["idna==3.6"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + root.child("packages") + .child("seeds") + .child("seeds") + .child("__init__.py") + .touch()?; + + // Create an excluded package (`bird-feeder`). + root.child("packages") + .child("bird-feeder") + .child("pyproject.toml") + .write_str( + r#" + [project] + name = "bird-feeder" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = ["anyio>=4.3.0,<5"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + root.child("packages") + .child("bird-feeder") + .child("bird_feeder") + .child("__init__.py") + .touch()?; + + let (project, root_escaped) = temporary_test(root.as_ref()).await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]", + "packages": { + "albatross": { + "root": "[ROOT]", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "seeds": { + "root": "[ROOT]/packages/seeds", + "project": { + "name": "seeds", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "idna==3.6" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "workspace": { + "members": [ + "packages/*" + ], + "exclude": [ + "packages/bird-feeder" + ] + }, + "managed": null, + "package": null, + "dev-dependencies": null, + "environments": null, + "override-dependencies": null, + "constraint-dependencies": null + } + } + } + } + } + "#); + }); + + // Rewrite the members to both include and exclude `bird-feeder` by name. + root.child("pyproject.toml").write_str( + r#" + [project] + name = "albatross" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["tqdm>=4,<5"] + + [tool.uv.workspace] + members = ["packages/seeds", "packages/bird-feeder"] + exclude = ["packages/bird-feeder"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + + // `bird-feeder` should still be excluded. + let (project, root_escaped) = temporary_test(root.as_ref()).await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]", + "packages": { + "albatross": { + "root": "[ROOT]", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "seeds": { + "root": "[ROOT]/packages/seeds", + "project": { + "name": "seeds", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "idna==3.6" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "workspace": { + "members": [ + "packages/seeds", + "packages/bird-feeder" + ], + "exclude": [ + "packages/bird-feeder" + ] + }, + "managed": null, + "package": null, + "dev-dependencies": null, + "environments": null, + "override-dependencies": null, + "constraint-dependencies": null + } + } + } + } + } + "#); + }); + + // Rewrite the exclusion to use the top-level directory (`packages`). + root.child("pyproject.toml").write_str( + r#" + [project] + name = "albatross" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["tqdm>=4,<5"] + + [tool.uv.workspace] + members = ["packages/seeds", "packages/bird-feeder"] + exclude = ["packages"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + + // `bird-feeder` should now be included. + let (project, root_escaped) = temporary_test(root.as_ref()).await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]", + "packages": { + "albatross": { + "root": "[ROOT]", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "bird-feeder": { + "root": "[ROOT]/packages/bird-feeder", + "project": { + "name": "bird-feeder", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "anyio>=4.3.0,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + }, + "seeds": { + "root": "[ROOT]/packages/seeds", + "project": { + "name": "seeds", + "version": "1.0.0", + "requires-python": ">=3.12", + "dependencies": [ + "idna==3.6" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "workspace": { + "members": [ + "packages/seeds", + "packages/bird-feeder" + ], + "exclude": [ + "packages" + ] + }, + "managed": null, + "package": null, + "dev-dependencies": null, + "environments": null, + "override-dependencies": null, + "constraint-dependencies": null + } + } + } + } + } + "#); + }); + + // Rewrite the exclusion to use the top-level directory with a glob (`packages/*`). + root.child("pyproject.toml").write_str( + r#" + [project] + name = "albatross" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["tqdm>=4,<5"] + + [tool.uv.workspace] + members = ["packages/seeds", "packages/bird-feeder"] + exclude = ["packages/*"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + + // `bird-feeder` and `seeds` should now be excluded. + let (project, root_escaped) = temporary_test(root.as_ref()).await; + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_json_snapshot!( + project, + { + ".workspace.packages.*.pyproject_toml" => "[PYPROJECT_TOML]" + }, + @r#" + { + "project_root": "[ROOT]", + "project_name": "albatross", + "workspace": { + "install_path": "[ROOT]", + "packages": { + "albatross": { + "root": "[ROOT]", + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "pyproject_toml": "[PYPROJECT_TOML]" + } + }, + "sources": {}, + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "workspace": { + "members": [ + "packages/seeds", + "packages/bird-feeder" + ], + "exclude": [ + "packages/*" + ] + }, + "managed": null, + "package": null, + "dev-dependencies": null, + "environments": null, + "override-dependencies": null, + "constraint-dependencies": null + } + } + } + } + } + "#); + }); + + Ok(()) +} diff --git a/crates/uv/src/version.rs b/crates/uv/src/version.rs index 323fc0a48..91ddf9771 100644 --- a/crates/uv/src/version.rs +++ b/crates/uv/src/version.rs @@ -76,73 +76,4 @@ pub(crate) fn version() -> VersionInfo { } #[cfg(test)] -mod tests { - use insta::{assert_json_snapshot, assert_snapshot}; - - use super::{CommitInfo, VersionInfo}; - - #[test] - fn version_formatting() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: None, - }; - assert_snapshot!(version, @"0.0.0"); - } - - #[test] - fn version_formatting_with_commit_info() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), - last_tag: Some("v0.0.1".to_string()), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 0, - }), - }; - assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); - } - - #[test] - fn version_formatting_with_commits_since_last_tag() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), - last_tag: Some("v0.0.1".to_string()), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 24, - }), - }; - assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); - } - - #[test] - fn version_serializable() { - let version = VersionInfo { - version: "0.0.0".to_string(), - commit_info: Some(CommitInfo { - short_commit_hash: "53b0f5d92".to_string(), - commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), - last_tag: Some("v0.0.1".to_string()), - commit_date: "2023-10-19".to_string(), - commits_since_last_tag: 0, - }), - }; - assert_json_snapshot!(version, @r###" - { - "version": "0.0.0", - "commit_info": { - "short_commit_hash": "53b0f5d92", - "commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7", - "commit_date": "2023-10-19", - "last_tag": "v0.0.1", - "commits_since_last_tag": 0 - } - } - "###); - } -} +mod tests; diff --git a/crates/uv/src/version/tests.rs b/crates/uv/src/version/tests.rs new file mode 100644 index 000000000..1627cb7d8 --- /dev/null +++ b/crates/uv/src/version/tests.rs @@ -0,0 +1,68 @@ +use insta::{assert_json_snapshot, assert_snapshot}; + +use super::{CommitInfo, VersionInfo}; + +#[test] +fn version_formatting() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: None, + }; + assert_snapshot!(version, @"0.0.0"); +} + +#[test] +fn version_formatting_with_commit_info() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), + last_tag: Some("v0.0.1".to_string()), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 0, + }), + }; + assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)"); +} + +#[test] +fn version_formatting_with_commits_since_last_tag() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), + last_tag: Some("v0.0.1".to_string()), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 24, + }), + }; + assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)"); +} + +#[test] +fn version_serializable() { + let version = VersionInfo { + version: "0.0.0".to_string(), + commit_info: Some(CommitInfo { + short_commit_hash: "53b0f5d92".to_string(), + commit_hash: "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7".to_string(), + last_tag: Some("v0.0.1".to_string()), + commit_date: "2023-10-19".to_string(), + commits_since_last_tag: 0, + }), + }; + assert_json_snapshot!(version, @r###" + { + "version": "0.0.0", + "commit_info": { + "short_commit_hash": "53b0f5d92", + "commit_hash": "53b0f5d924110e5b26fbf09f6fd3a03d67b475b7", + "commit_date": "2023-10-19", + "last_tag": "v0.0.1", + "commits_since_last_tag": 0 + } + } + "###); +} diff --git a/crates/uv/tests/branching_urls.rs b/crates/uv/tests/it/branching_urls.rs similarity index 99% rename from crates/uv/tests/branching_urls.rs rename to crates/uv/tests/it/branching_urls.rs index f3db79e85..1133065bf 100644 --- a/crates/uv/tests/branching_urls.rs +++ b/crates/uv/tests/it/branching_urls.rs @@ -6,8 +6,6 @@ use insta::assert_snapshot; use crate::common::{make_project, uv_snapshot, TestContext}; -mod common; - /// The root package has diverging URLs for disjoint markers: /// ```toml /// dependencies = [ diff --git a/crates/uv/tests/build.rs b/crates/uv/tests/it/build.rs similarity index 99% rename from crates/uv/tests/build.rs rename to crates/uv/tests/it/build.rs index 9099f3fcc..d6fa68292 100644 --- a/crates/uv/tests/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1,12 +1,8 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; use assert_fs::prelude::*; -use common::{uv_snapshot, TestContext}; use predicates::prelude::predicate; -mod common; - #[test] fn build() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/build_backend.rs b/crates/uv/tests/it/build_backend.rs similarity index 93% rename from crates/uv/tests/build_backend.rs rename to crates/uv/tests/it/build_backend.rs index cf1bd96fe..cef3e76cc 100644 --- a/crates/uv/tests/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -1,14 +1,10 @@ -#![cfg(feature = "python")] - +use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; use assert_cmd::assert::OutputAssertExt; -use common::{uv_snapshot, TestContext}; use std::env; use std::path::Path; use tempfile::TempDir; -mod common; - /// Test that build backend works if we invoke it directly. /// /// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel. diff --git a/crates/uv/tests/cache_clean.rs b/crates/uv/tests/it/cache_clean.rs similarity index 97% rename from crates/uv/tests/cache_clean.rs rename to crates/uv/tests/it/cache_clean.rs index ea0d77807..9bb20860e 100644 --- a/crates/uv/tests/cache_clean.rs +++ b/crates/uv/tests/it/cache_clean.rs @@ -1,14 +1,8 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; -use common::uv_snapshot; - -use crate::common::TestContext; - -mod common; +use crate::common::{uv_snapshot, TestContext}; /// `cache clean` should remove all packages. #[test] diff --git a/crates/uv/tests/cache_prune.rs b/crates/uv/tests/it/cache_prune.rs similarity index 99% rename from crates/uv/tests/cache_prune.rs rename to crates/uv/tests/it/cache_prune.rs index f7cffe0c1..7a5daf4da 100644 --- a/crates/uv/tests/cache_prune.rs +++ b/crates/uv/tests/it/cache_prune.rs @@ -1,15 +1,11 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::uv_snapshot; use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; -use common::uv_snapshot; use indoc::indoc; use crate::common::TestContext; -mod common; - /// `cache prune` should be a no-op if there's nothing out-of-date in the cache. #[test] fn prune_no_op() -> Result<()> { diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/it/common/mod.rs similarity index 99% rename from crates/uv/tests/common/mod.rs rename to crates/uv/tests/it/common/mod.rs index 41e94b982..75a327bea 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1309,19 +1309,19 @@ macro_rules! uv_snapshot { }}; ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{ // Take a reference for backwards compatibility with the vec-expecting insta filters. - let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, function_name!(), Some($crate::common::WindowsFilters::Platform)); + let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::common::WindowsFilters::Platform)); ::insta::assert_snapshot!(snapshot, @$snapshot); output }}; ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{ // Take a reference for backwards compatibility with the vec-expecting insta filters. - let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, function_name!(), None); + let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, $crate::function_name!(), None); ::insta::assert_snapshot!(snapshot, @$snapshot); output }}; ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{ // Take a reference for backwards compatibility with the vec-expecting insta filters. - let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, function_name!(), Some($crate::common::WindowsFilters::Universal)); + let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::common::WindowsFilters::Universal)); ::insta::assert_snapshot!(snapshot, @$snapshot); output }}; diff --git a/crates/uv/tests/ecosystem.rs b/crates/uv/tests/it/ecosystem.rs similarity index 97% rename from crates/uv/tests/ecosystem.rs rename to crates/uv/tests/it/ecosystem.rs index 6828a742c..97da78323 100644 --- a/crates/uv/tests/ecosystem.rs +++ b/crates/uv/tests/it/ecosystem.rs @@ -1,14 +1,9 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - -use crate::common::get_bin; +use crate::common::{self, get_bin, TestContext}; use anyhow::Result; -use common::TestContext; use insta::assert_snapshot; use std::path::Path; use std::process::Command; -mod common; - // These tests just run `uv lock` on an assorted of ecosystem // projects. // diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/it/edit.rs similarity index 99% rename from crates/uv/tests/edit.rs rename to crates/uv/tests/it/edit.rs index 8e8747d8c..70ef50b7e 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; @@ -7,10 +5,7 @@ use indoc::indoc; use insta::assert_snapshot; use std::path::Path; -use crate::common::{decode_token, packse_index_url}; -use common::{uv_snapshot, TestContext}; - -mod common; +use crate::common::{self, decode_token, packse_index_url, uv_snapshot, TestContext}; /// Add a PyPI requirement. #[test] diff --git a/crates/uv/tests/export.rs b/crates/uv/tests/it/export.rs similarity index 99% rename from crates/uv/tests/export.rs rename to crates/uv/tests/it/export.rs index cacfb3490..e56c300e8 100644 --- a/crates/uv/tests/export.rs +++ b/crates/uv/tests/it/export.rs @@ -1,14 +1,11 @@ -#![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::disallowed_types)] +use crate::common::{apply_filters, uv_snapshot, TestContext}; use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; -use common::{apply_filters, uv_snapshot, TestContext}; use std::process::Stdio; -mod common; - #[test] fn dependency() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/help.rs b/crates/uv/tests/it/help.rs similarity index 99% rename from crates/uv/tests/help.rs rename to crates/uv/tests/it/help.rs index 32013e59a..ea6811bfc 100644 --- a/crates/uv/tests/help.rs +++ b/crates/uv/tests/it/help.rs @@ -1,13 +1,11 @@ -use common::{uv_snapshot, TestContext}; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn help() { let context = TestContext::new_with_versions(&[]); // The `uv help` command should show the long help message - uv_snapshot!(context.filters(), context.help(), @r###" + uv_snapshot!(context.filters(), context.help(), @r#" success: true exit_code: 0 ----- stdout ----- @@ -69,14 +67,14 @@ fn help() { ----- stderr ----- - "###); + "#); } #[test] fn help_flag() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.command().arg("--help"), @r###" + uv_snapshot!(context.filters(), context.command().arg("--help"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -136,14 +134,14 @@ fn help_flag() { Use `uv help` for more details. ----- stderr ----- - "###); + "#); } #[test] fn help_short_flag() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.command().arg("-h"), @r###" + uv_snapshot!(context.filters(), context.command().arg("-h"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -203,14 +201,14 @@ fn help_short_flag() { Use `uv help` for more details. ----- stderr ----- - "###); + "#); } #[test] fn help_subcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("python"), @r###" + uv_snapshot!(context.filters(), context.help().arg("python"), @r##" success: true exit_code: 0 ----- stdout ----- @@ -391,14 +389,14 @@ fn help_subcommand() { ----- stderr ----- - "###); + "##); } #[test] fn help_subsubcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r###" + uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r##" success: true exit_code: 0 ----- stdout ----- @@ -559,7 +557,7 @@ fn help_subsubcommand() { ----- stderr ----- - "###); + "##); } #[test] @@ -674,7 +672,7 @@ fn help_flag_subsubcommand() { fn help_unknown_subcommand() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("foobar"), @r###" + uv_snapshot!(context.filters(), context.help().arg("foobar"), @r#" success: false exit_code: 2 ----- stdout ----- @@ -698,9 +696,9 @@ fn help_unknown_subcommand() { cache version generate-shell-completion - "###); + "#); - uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r###" + uv_snapshot!(context.filters(), context.help().arg("foo").arg("bar"), @r#" success: false exit_code: 2 ----- stdout ----- @@ -724,7 +722,7 @@ fn help_unknown_subcommand() { cache version generate-shell-completion - "###); + "#); } #[test] @@ -751,7 +749,7 @@ fn help_unknown_subsubcommand() { fn help_with_global_option() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("--no-cache"), @r###" + uv_snapshot!(context.filters(), context.help().arg("--no-cache"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -813,7 +811,7 @@ fn help_with_global_option() { ----- stderr ----- - "###); + "#); } #[test] @@ -855,7 +853,7 @@ fn help_with_no_pager() { // We can't really test whether the --no-pager option works with a snapshot test. // It's still nice to have a test for the option to confirm the option exists. - uv_snapshot!(context.filters(), context.help().arg("--no-pager"), @r###" + uv_snapshot!(context.filters(), context.help().arg("--no-pager"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -917,5 +915,5 @@ fn help_with_no_pager() { ----- stderr ----- - "###); + "#); } diff --git a/crates/uv/tests/init.rs b/crates/uv/tests/it/init.rs similarity index 99% rename from crates/uv/tests/init.rs rename to crates/uv/tests/it/init.rs index 8ec8d3d22..bce457b23 100644 --- a/crates/uv/tests/init.rs +++ b/crates/uv/tests/it/init.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use std::process::Command; use anyhow::Result; @@ -9,11 +7,8 @@ use indoc::indoc; use insta::assert_snapshot; use predicates::prelude::predicate; -use common::{uv_snapshot, TestContext}; +use crate::common::{uv_snapshot, TestContext}; -mod common; - -/// See [`init_application`] and [`init_library`] for more coverage. #[test] fn init() { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/it/lock.rs similarity index 99% rename from crates/uv/tests/lock.rs rename to crates/uv/tests/it/lock.rs index b8444a0d1..4fb537cb8 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; @@ -8,14 +6,11 @@ use insta::assert_snapshot; use std::io::BufReader; use url::Url; -use common::{uv_snapshot, TestContext}; +use crate::common::{ + self, build_vendor_links_url, decode_token, packse_index_url, uv_snapshot, TestContext, +}; use uv_fs::Simplified; -use crate::common::{build_vendor_links_url, decode_token, packse_index_url}; - -mod common; - -/// Lock a requirement from PyPI. #[test] fn lock_wheel_registry() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs similarity index 98% rename from crates/uv/tests/lock_scenarios.rs rename to crates/uv/tests/it/lock_scenarios.rs index e42027865..b00b82644 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -5,15 +5,15 @@ //! #![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::needless_raw_string_hashes)] +#![allow(clippy::doc_markdown)] +#![allow(clippy::doc_lazy_continuation)] use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use insta::assert_snapshot; -use common::{packse_index_url, uv_snapshot, TestContext}; - -mod common; +use crate::common::{packse_index_url, uv_snapshot, TestContext}; /// This test ensures that multiple non-conflicting but also /// non-overlapping dependency specifications with the same package name @@ -67,14 +67,14 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -82,7 +82,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -113,7 +113,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=1" }, ] - "### + "# ); }); @@ -184,14 +184,14 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -199,7 +199,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" @@ -225,7 +225,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { { name = "package-a", specifier = "<2" }, { name = "package-a", specifier = ">=1" }, ] - "### + "# ); }); @@ -283,14 +283,14 @@ fn fork_basic() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 3 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -298,7 +298,7 @@ fn fork_basic() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -345,7 +345,7 @@ fn fork_basic() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -417,7 +417,7 @@ fn conflict_in_fork() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: false exit_code: 1 ----- stdout ----- @@ -431,7 +431,7 @@ fn conflict_in_fork() -> Result<()> { package-a{sys_platform == 'darwin'}==1.0.0 package-a{sys_platform == 'darwin'}>2 and your project depends on package-a{sys_platform == 'darwin'}<2, we can conclude that your project's requirements are unsatisfiable. - "### + "# ); Ok(()) @@ -485,7 +485,7 @@ fn fork_conflict_unsatisfiable() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: false exit_code: 1 ----- stdout ----- @@ -493,7 +493,7 @@ fn fork_conflict_unsatisfiable() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because your project depends on package-a>=2 and package-a<2, we can conclude that your project's requirements are unsatisfiable. - "### + "# ); Ok(()) @@ -568,14 +568,14 @@ fn fork_filter_sibling_dependencies() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 7 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -583,7 +583,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -682,7 +682,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { { name = "package-b", marker = "sys_platform == 'linux'", specifier = "==1.0.0" }, { name = "package-c", marker = "sys_platform == 'darwin'", specifier = "==1.0.0" }, ] - "### + "# ); }); @@ -746,14 +746,14 @@ fn fork_upgrade() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 3 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -761,7 +761,7 @@ fn fork_upgrade() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" @@ -796,7 +796,7 @@ fn fork_upgrade() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-foo" }] - "### + "# ); }); @@ -866,14 +866,14 @@ fn fork_incomplete_markers() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -881,7 +881,7 @@ fn fork_incomplete_markers() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -951,7 +951,7 @@ fn fork_incomplete_markers() -> Result<()> { { name = "package-a", marker = "python_full_version >= '3.11'", specifier = "==2" }, { name = "package-b" }, ] - "### + "# ); }); @@ -1019,14 +1019,14 @@ fn fork_marker_accrue() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -1034,7 +1034,7 @@ fn fork_marker_accrue() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" @@ -1085,7 +1085,7 @@ fn fork_marker_accrue() -> Result<()> { { name = "package-a", marker = "implementation_name == 'cpython'", specifier = "==1.0.0" }, { name = "package-b", marker = "implementation_name == 'pypy'", specifier = "==1.0.0" }, ] - "### + "# ); }); @@ -1152,7 +1152,7 @@ fn fork_marker_disjoint() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: false exit_code: 1 ----- stdout ----- @@ -1160,7 +1160,7 @@ fn fork_marker_disjoint() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies for split (sys_platform == 'linux'): ╰─▶ Because your project depends on package-a{sys_platform == 'linux'}>=2 and package-a{sys_platform == 'linux'}<2, we can conclude that your project's requirements are unsatisfiable. - "### + "# ); Ok(()) @@ -1222,14 +1222,14 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 6 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -1237,7 +1237,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1328,7 +1328,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -1402,14 +1402,14 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -1417,7 +1417,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1496,7 +1496,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -1571,14 +1571,14 @@ fn fork_marker_inherit_combined() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -1586,7 +1586,7 @@ fn fork_marker_inherit_combined() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1665,7 +1665,7 @@ fn fork_marker_inherit_combined() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -1733,14 +1733,14 @@ fn fork_marker_inherit_isolated() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -1748,7 +1748,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1807,7 +1807,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -1881,14 +1881,14 @@ fn fork_marker_inherit_transitive() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -1896,7 +1896,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1967,7 +1967,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -2037,14 +2037,14 @@ fn fork_marker_inherit() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 3 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -2052,7 +2052,7 @@ fn fork_marker_inherit() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2099,7 +2099,7 @@ fn fork_marker_inherit() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -2175,14 +2175,14 @@ fn fork_marker_limited_inherit() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -2190,7 +2190,7 @@ fn fork_marker_limited_inherit() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2260,7 +2260,7 @@ fn fork_marker_limited_inherit() -> Result<()> { { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, { name = "package-b" }, ] - "### + "# ); }); @@ -2330,14 +2330,14 @@ fn fork_marker_selection() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -2345,7 +2345,7 @@ fn fork_marker_selection() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2403,7 +2403,7 @@ fn fork_marker_selection() -> Result<()> { { name = "package-b", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-b", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "### + "# ); }); @@ -2485,14 +2485,14 @@ fn fork_marker_track() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -2500,7 +2500,7 @@ fn fork_marker_track() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2570,7 +2570,7 @@ fn fork_marker_track() -> Result<()> { { name = "package-b", marker = "sys_platform == 'darwin'", specifier = "<2.8" }, { name = "package-b", marker = "sys_platform == 'linux'", specifier = ">=2.8" }, ] - "### + "# ); }); @@ -2637,14 +2637,14 @@ fn fork_non_fork_marker_transitive() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -2652,7 +2652,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" @@ -2703,7 +2703,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { { name = "package-a", specifier = "==1.0.0" }, { name = "package-b", specifier = "==1.0.0" }, ] - "### + "# ); }); @@ -2771,7 +2771,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: false exit_code: 1 ----- stdout ----- @@ -2780,7 +2780,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because package-b{sys_platform == 'darwin'}==1.0.0 depends on package-c>=2.0.0 and package-a{sys_platform == 'linux'}==1.0.0 depends on package-c<2.0.0, we can conclude that package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0 are incompatible. And because your project depends on package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0, we can conclude that your project's requirements are unsatisfiable. - "### + "# ); Ok(()) @@ -2843,7 +2843,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: false exit_code: 1 ----- stdout ----- @@ -2856,7 +2856,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { package-c{sys_platform == 'linux'}>2.0.0 and package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. And because your project depends on package-a==1.0.0 and package-b==1.0.0, we can conclude that your project's requirements are unsatisfiable. - "### + "# ); Ok(()) @@ -2936,14 +2936,14 @@ fn fork_overlapping_markers_basic() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -2951,7 +2951,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2983,7 +2983,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { { name = "package-a", marker = "python_full_version >= '3.10'", specifier = ">=1.1.0" }, { name = "package-a", marker = "python_full_version >= '3.11'", specifier = ">=1.2.0" }, ] - "### + "# ); }); @@ -3103,14 +3103,14 @@ fn preferences_dependent_forking_bistable() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 8 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -3118,7 +3118,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -3223,7 +3223,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-cleaver" }] - "### + "# ); }); @@ -3339,14 +3339,14 @@ fn preferences_dependent_forking_conflicting() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 6 packages in [TIME] - "### + "# ); Ok(()) @@ -3481,14 +3481,14 @@ fn preferences_dependent_forking_tristable() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 11 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -3496,7 +3496,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -3649,7 +3649,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { { name = "package-cleaver" }, { name = "package-foo" }, ] - "### + "# ); }); @@ -3764,14 +3764,14 @@ fn preferences_dependent_forking() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -3779,7 +3779,7 @@ fn preferences_dependent_forking() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -3850,7 +3850,7 @@ fn preferences_dependent_forking() -> Result<()> { { name = "package-cleaver" }, { name = "package-foo" }, ] - "### + "# ); }); @@ -3938,14 +3938,14 @@ fn fork_remaining_universe_partitioning() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -3953,7 +3953,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -4032,7 +4032,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { { name = "package-a", marker = "sys_platform == 'illumos'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'windows'", specifier = ">=2" }, ] - "### + "# ); }); @@ -4090,14 +4090,14 @@ fn fork_requires_python_full_prerelease() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4105,7 +4105,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.10" @@ -4116,7 +4116,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9'", specifier = "==1.0.0" }] - "### + "# ); }); @@ -4174,14 +4174,14 @@ fn fork_requires_python_full() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4189,7 +4189,7 @@ fn fork_requires_python_full() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.10" @@ -4200,7 +4200,7 @@ fn fork_requires_python_full() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9'", specifier = "==1.0.0" }] - "### + "# ); }); @@ -4222,10 +4222,10 @@ fn fork_requires_python_full() -> Result<()> { /// with a `python_version == '3.10'` marker. /// /// This is a regression test for the universal resolver where it would -/// convert a `Requires-Python: >=3.10.1` specifier into a -/// `python_version >= '3.10.1'` marker expression, which would be -/// considered disjoint with `python_version == '3.10'`. Thus, the -/// dependency `a` below was erroneously excluded. It should be included. +/// convert a `Requires-Python: >=3.10.1` specifier into a `python_version +/// >= '3.10.1'` marker expression, which would be considered disjoint +/// with `python_version == '3.10'`. Thus, the dependency `a` below was +/// erroneously excluded. It should be included. /// /// ```text /// fork-requires-python-patch-overlap @@ -4262,14 +4262,14 @@ fn fork_requires_python_patch_overlap() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4277,7 +4277,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.10.1" @@ -4300,7 +4300,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.10.*'", specifier = "==1.0.0" }] - "### + "# ); }); @@ -4355,14 +4355,14 @@ fn fork_requires_python() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4370,7 +4370,7 @@ fn fork_requires_python() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.10" @@ -4381,7 +4381,7 @@ fn fork_requires_python() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9.*'", specifier = "==1.0.0" }] - "### + "# ); }); @@ -4438,14 +4438,14 @@ fn unreachable_package() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4453,7 +4453,7 @@ fn unreachable_package() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" @@ -4476,7 +4476,7 @@ fn unreachable_package() -> Result<()> { wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_package_a-1.0.0-py3-none-any.whl", hash = "sha256:cc472ded9f3b260e6cda0e633fa407a13607e190422cb455f02beebd32d6751f" }, ] - "### + "# ); }); @@ -4539,14 +4539,14 @@ fn unreachable_wheels() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4554,7 +4554,7 @@ fn unreachable_wheels() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.8" @@ -4602,7 +4602,7 @@ fn unreachable_wheels() -> Result<()> { wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_c-1.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4b846c5b1646b04828a2bef6c9d180ff7cfd725866013dcec8933de7fb5f9e8d" }, ] - "### + "# ); }); @@ -4656,14 +4656,14 @@ fn requires_python_wheels() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove("UV_EXCLUDE_NEWER"); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r###" + uv_snapshot!(filters, cmd, @r#" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "### + "# ); let lock = context.read("uv.lock"); @@ -4671,7 +4671,7 @@ fn requires_python_wheels() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r###" + lock, @r#" version = 1 requires-python = ">=3.10" @@ -4695,7 +4695,7 @@ fn requires_python_wheels() -> Result<()> { { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp310-cp310-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp311-cp311-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, ] - "### + "# ); }); diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs new file mode 100644 index 000000000..0c95a0375 --- /dev/null +++ b/crates/uv/tests/it/main.rs @@ -0,0 +1,114 @@ +//! this is the single integration test, as documented by matklad +//! in + +pub(crate) mod common; + +mod branching_urls; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod build; + +#[cfg(feature = "python")] +mod build_backend; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod cache_clean; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod cache_prune; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod ecosystem; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod edit; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod export; + +mod help; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod init; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod lock; + +mod lock_scenarios; + +mod pip_check; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod pip_compile; + +mod pip_compile_scenarios; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod pip_freeze; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod pip_install; + +mod pip_install_scenarios; + +mod pip_list; + +mod pip_show; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod pip_sync; + +mod pip_tree; +mod pip_uninstall; + +#[cfg(feature = "pypi")] +mod publish; + +mod python_dir; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod python_find; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod python_pin; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod run; + +#[cfg(feature = "self-update")] +mod self_update; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod show_settings; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod sync; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tool_dir; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tool_install; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tool_list; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tool_run; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tool_uninstall; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tool_upgrade; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod tree; + +#[cfg(feature = "python")] +mod venv; + +#[cfg(all(feature = "python", feature = "pypi"))] +mod workflow; + +mod workspace; diff --git a/crates/uv/tests/pip_check.rs b/crates/uv/tests/it/pip_check.rs similarity index 99% rename from crates/uv/tests/pip_check.rs rename to crates/uv/tests/it/pip_check.rs index d81463255..7f4d9a459 100644 --- a/crates/uv/tests/pip_check.rs +++ b/crates/uv/tests/it/pip_check.rs @@ -2,12 +2,9 @@ use anyhow::Result; use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; -use common::uv_snapshot; - +use crate::common::uv_snapshot; use crate::common::TestContext; -mod common; - #[test] fn check_compatible_packages() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs similarity index 99% rename from crates/uv/tests/pip_compile.rs rename to crates/uv/tests/it/pip_compile.rs index 109dcc651..3bff95204 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -1,4 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::disallowed_types)] use std::env::current_dir; @@ -9,13 +8,10 @@ use anyhow::{bail, Context, Result}; use assert_fs::prelude::*; use indoc::indoc; use url::Url; - -use common::{uv_snapshot, TestContext}; use uv_fs::Simplified; -mod common; +use crate::common::{uv_snapshot, TestContext}; -/// Resolve a specific version of `anyio` from a `requirements.in` file. #[test] fn compile_requirements_in() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs similarity index 98% rename from crates/uv/tests/pip_compile_scenarios.rs rename to crates/uv/tests/it/pip_compile_scenarios.rs index 561a9f821..d0f748536 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -13,13 +13,11 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileWriteStr, PathChild}; use predicates::prelude::predicate; -use common::{ +use crate::common::{ build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions, uv_snapshot, TestContext, }; -mod common; - /// Provision python binaries and return a `pip compile` command with options shared across all scenarios. fn command(context: &TestContext, python_versions: &[&str]) -> Command { let python_path = python_path_with_versions(&context.temp_dir, python_versions) @@ -69,7 +67,7 @@ fn incompatible_python_compatible_override() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r###" + , @r##" success: true exit_code: 0 ----- stdout ----- @@ -81,7 +79,7 @@ fn incompatible_python_compatible_override() -> Result<()> { ----- stderr ----- warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. Resolved 1 package in [TIME] - "### + "## ); output.assert().success().stdout(predicate::str::contains( @@ -119,7 +117,7 @@ fn compatible_python_incompatible_override() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.9") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -131,7 +129,7 @@ fn compatible_python_incompatible_override() -> Result<()> { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. hint: The `--python-version` value (>=3.9.0) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.10). Consider using a higher `--python-version` value. - "### + "# ); output.assert().failure(); @@ -175,7 +173,7 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> // dependencies. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -185,7 +183,7 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> × No solution found when resolving dependencies: ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "### + "# ); output.assert().failure(); @@ -230,7 +228,7 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> { // used to build the source distributions. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r###" + , @r##" success: true exit_code: 0 ----- stdout ----- @@ -241,7 +239,7 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] - "### + "## ); output.assert().success().stdout(predicate::str::contains( @@ -287,7 +285,7 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> // determine its dependencies. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -297,7 +295,7 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> × No solution found when resolving dependencies: ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "### + "# ); output.assert().failure(); @@ -345,7 +343,7 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { // available, but is not compatible with the target version and cannot be used. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -364,7 +362,7 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { And because you require package-a, we can conclude that your requirements are unsatisfiable. hint: The `--python-version` value (>=3.11.0) includes Python versions that are not supported by your dependencies (e.g., package-a==2.0.0 only supports >=3.12). Consider using a higher `--python-version` value. - "### + "# ); output.assert().failure(); @@ -403,7 +401,7 @@ fn python_patch_override_no_patch() -> Result<()> { // requirement is treated as 3.8.0. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.8") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -414,7 +412,7 @@ fn python_patch_override_no_patch() -> Result<()> { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. hint: The `--python-version` value (>=3.8.0) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.8.4). Consider using a higher `--python-version` value. - "### + "# ); output.assert().failure(); @@ -451,7 +449,7 @@ fn python_patch_override_patch_compatible() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.8.0") - , @r###" + , @r##" success: true exit_code: 0 ----- stdout ----- @@ -463,7 +461,7 @@ fn python_patch_override_patch_compatible() -> Result<()> { ----- stderr ----- warning: The requested Python version 3.8.0 is not available; 3.8.18 will be used to build dependencies instead. Resolved 1 package in [TIME] - "### + "## ); output.assert().success().stdout(predicate::str::contains( diff --git a/crates/uv/tests/pip_freeze.rs b/crates/uv/tests/it/pip_freeze.rs similarity index 98% rename from crates/uv/tests/pip_freeze.rs rename to crates/uv/tests/it/pip_freeze.rs index 307e37741..985a6a8bf 100644 --- a/crates/uv/tests/pip_freeze.rs +++ b/crates/uv/tests/it/pip_freeze.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::fixture::ChildPath; @@ -7,9 +5,6 @@ use assert_fs::prelude::*; use crate::common::{uv_snapshot, TestContext}; -mod common; - -/// List multiple installed packages in a virtual environment. #[test] fn freeze_many() -> Result<()> { let context = TestContext::new("3.12"); @@ -77,7 +72,7 @@ fn freeze_duplicate() -> Result<()> { )?; // Run `pip freeze`. - uv_snapshot!(context1.filters(), context1.pip_freeze().arg("--strict"), @r###" + uv_snapshot!(context1.filters(), context1.pip_freeze().arg("--strict"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -88,7 +83,7 @@ fn freeze_duplicate() -> Result<()> { warning: The package `pip` has multiple installed distributions: - [SITE_PACKAGES]/pip-21.3.1.dist-info - [SITE_PACKAGES]/pip-22.1.1.dist-info - "### + "# ); Ok(()) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/it/pip_install.rs similarity index 99% rename from crates/uv/tests/pip_install.rs rename to crates/uv/tests/it/pip_install.rs index 610a3c3e0..6480bb31b 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use std::process::Command; use anyhow::Result; @@ -10,13 +8,11 @@ use indoc::indoc; use predicates::prelude::predicate; use url::Url; -use common::{uv_snapshot, TestContext}; +use crate::common::{ + self, build_vendor_links_url, decode_token, get_bin, uv_snapshot, venv_bin_path, TestContext, +}; use uv_fs::Simplified; -use crate::common::{build_vendor_links_url, decode_token, get_bin, venv_bin_path}; - -mod common; - #[test] fn missing_requirements_txt() { let context = TestContext::new("3.12"); @@ -1596,6 +1592,8 @@ fn install_git_public_https_missing_commit() { #[test] #[cfg(all(not(windows), feature = "git"))] fn install_git_private_https_pat() { + use crate::common::decode_token; + let context = TestContext::new("3.8"); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); @@ -7172,7 +7170,7 @@ fn sklearn() { let filters = std::iter::once((r"exit code: 1", "exit status: 1")) .chain(context.filters()) .collect::>(); - uv_snapshot!(filters, context.pip_install().arg("sklearn"), @r###" + uv_snapshot!(filters, context.pip_install().arg("sklearn"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -7199,6 +7197,6 @@ fn sklearn() { https://github.com/scikit-learn/sklearn-pypi-package help: `sklearn` is often confused for `scikit-learn` Did you mean to install `scikit-learn` instead? - "### + "# ); } diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs similarity index 98% rename from crates/uv/tests/pip_install_scenarios.rs rename to crates/uv/tests/it/pip_install_scenarios.rs index 5bc71a57d..b4a9621b5 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -11,11 +11,10 @@ use std::process::Command; use assert_cmd::assert::Assert; use assert_cmd::prelude::*; -use common::venv_to_interpreter; - -use crate::common::{build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, TestContext}; - -mod common; +use crate::common::{ + build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, venv_to_interpreter, + TestContext, +}; fn assert_command(venv: &Path, command: &str, temp_dir: &Path) -> Assert { Command::new(venv_to_interpreter(venv)) @@ -74,7 +73,7 @@ fn requires_package_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-package-does-not-exist-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -82,7 +81,7 @@ fn requires_package_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because package-a was not found in the package registry and you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -113,7 +112,7 @@ fn requires_exact_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-exact-version-does-not-exist-a==2.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -121,7 +120,7 @@ fn requires_exact_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==2.0.0 and you require package-a==2.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -154,7 +153,7 @@ fn requires_greater_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-greater-version-does-not-exist-a>1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -162,7 +161,7 @@ fn requires_greater_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a<=1.0.0 is available and you require package-a>1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -196,7 +195,7 @@ fn requires_less_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-less-version-does-not-exist-a<2.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -204,7 +203,7 @@ fn requires_less_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a>=2.0.0 is available and you require package-a<2.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -237,7 +236,7 @@ fn transitive_requires_package_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("transitive-requires-package-does-not-exist-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -246,7 +245,7 @@ fn transitive_requires_package_does_not_exist() { × No solution found when resolving dependencies: ╰─▶ Because package-b was not found in the package registry and package-a==1.0.0 depends on package-b, we can conclude that package-a==1.0.0 cannot be used. And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -278,7 +277,7 @@ fn excluded_only_version() { uv_snapshot!(filters, command(&context) .arg("excluded-only-version-a!=1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -289,7 +288,7 @@ fn excluded_only_version() { package-a<1.0.0 package-a>1.0.0 we can conclude that your requirements are unsatisfiable. - "###); + "#); // Only `a==1.0.0` is available but the user excluded it. assert_not_installed(&context.venv, "excluded_only_version_a", &context.temp_dir); @@ -334,7 +333,7 @@ fn excluded_only_compatible_version() { uv_snapshot!(filters, command(&context) .arg("excluded-only-compatible-version-a!=2.0.0") .arg("excluded-only-compatible-version-b<3.0.0,>=2.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -357,7 +356,7 @@ fn excluded_only_compatible_version() { package-a<2.0.0 package-a>2.0.0 and package-b>=2.0.0,<3.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require // incompatible versions of `b`. The user has excluded that version of `a` so @@ -440,7 +439,7 @@ fn dependency_excludes_range_of_compatible_versions() { .arg("dependency-excludes-range-of-compatible-versions-a") .arg("dependency-excludes-range-of-compatible-versions-b<3.0.0,>=2.0.0") .arg("dependency-excludes-range-of-compatible-versions-c") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -466,7 +465,7 @@ fn dependency_excludes_range_of_compatible_versions() { package-b>=3.0.0 And because you require package-b>=2.0.0,<3.0.0 and package-c, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` // require incompatible versions of `b`, but all available versions of `c` exclude @@ -565,7 +564,7 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-a") .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-b<3.0.0,>=2.0.0") .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-c") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -591,7 +590,7 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { package-b>=3.0.0 And because you require package-b>=2.0.0,<3.0.0 and package-c, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` // require incompatible versions of `b`, but all available versions of `c` exclude @@ -641,7 +640,7 @@ fn extra_required() { uv_snapshot!(filters, command(&context) .arg("extra-required-a[extra]") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -652,7 +651,7 @@ fn extra_required() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==1.0.0 - "###); + "#); assert_installed( &context.venv, @@ -691,7 +690,7 @@ fn missing_extra() { uv_snapshot!(filters, command(&context) .arg("missing-extra-a[extra]") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -702,7 +701,7 @@ fn missing_extra() { Installed 1 package in [TIME] + package-a==1.0.0 warning: The package `package-a==1.0.0` does not have an extra named `extra` - "###); + "#); // Missing extras are ignored during resolution. assert_installed(&context.venv, "missing_extra_a", "1.0.0", &context.temp_dir); @@ -742,7 +741,7 @@ fn multiple_extras_required() { uv_snapshot!(filters, command(&context) .arg("multiple-extras-required-a[extra_b,extra_c]") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -754,7 +753,7 @@ fn multiple_extras_required() { + package-a==1.0.0 + package-b==1.0.0 + package-c==1.0.0 - "###); + "#); assert_installed( &context.venv, @@ -822,7 +821,7 @@ fn all_extras_required() { uv_snapshot!(filters, command(&context) .arg("all-extras-required-a[all]") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -834,7 +833,7 @@ fn all_extras_required() { + package-a==1.0.0 + package-b==1.0.0 + package-c==1.0.0 - "###); + "#); assert_installed( &context.venv, @@ -890,7 +889,7 @@ fn extra_incompatible_with_extra() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-extra-a[extra_b,extra_c]") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -900,7 +899,7 @@ fn extra_incompatible_with_extra() { ╰─▶ Because only package-a[extra-c]==1.0.0 is available and package-a[extra-c]==1.0.0 depends on package-b==2.0.0, we can conclude that all versions of package-a[extra-c] depend on package-b==2.0.0. And because package-a[extra-b]==1.0.0 depends on package-b==1.0.0 and only package-a[extra-b]==1.0.0 is available, we can conclude that all versions of package-a[extra-b] and all versions of package-a[extra-c] are incompatible. And because you require package-a[extra-b] and package-a[extra-c], we can conclude that your requirements are unsatisfiable. - "###); + "#); // Because both `extra_b` and `extra_c` are requested and they require incompatible // versions of `b`, `a` cannot be installed. @@ -944,7 +943,7 @@ fn extra_incompatible_with_extra_not_requested() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-extra-not-requested-a[extra_c]") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -955,7 +954,7 @@ fn extra_incompatible_with_extra_not_requested() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0 - "###); + "#); // Because the user does not request both extras, it is okay that one is // incompatible with the other. @@ -1006,7 +1005,7 @@ fn extra_incompatible_with_root() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-root-a[extra]") .arg("extra-incompatible-with-root-b==2.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1015,7 +1014,7 @@ fn extra_incompatible_with_root() { × No solution found when resolving dependencies: ╰─▶ Because only package-a[extra]==1.0.0 is available and package-a[extra]==1.0.0 depends on package-b==1.0.0, we can conclude that all versions of package-a[extra] depend on package-b==1.0.0. And because you require package-a[extra] and package-b==2.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Because the user requested `b==2.0.0` but the requested extra requires // `b==1.0.0`, the dependencies cannot be satisfied. @@ -1064,7 +1063,7 @@ fn extra_does_not_exist_backtrack() { uv_snapshot!(filters, command(&context) .arg("extra-does-not-exist-backtrack-a[extra]") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1075,7 +1074,7 @@ fn extra_does_not_exist_backtrack() { Installed 1 package in [TIME] + package-a==3.0.0 warning: The package `package-a==3.0.0` does not have an extra named `extra` - "###); + "#); // The resolver should not backtrack to `a==1.0.0` because missing extras are // allowed during resolution. `b` should not be installed. @@ -1113,7 +1112,7 @@ fn direct_incompatible_versions() { uv_snapshot!(filters, command(&context) .arg("direct-incompatible-versions-a==1.0.0") .arg("direct-incompatible-versions-a==2.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1121,7 +1120,7 @@ fn direct_incompatible_versions() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because you require package-a==1.0.0 and package-a==2.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1166,7 +1165,7 @@ fn transitive_incompatible_with_root_version() { uv_snapshot!(filters, command(&context) .arg("transitive-incompatible-with-root-version-a") .arg("transitive-incompatible-with-root-version-b==1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1175,7 +1174,7 @@ fn transitive_incompatible_with_root_version() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b==2.0.0 and only package-a==1.0.0 is available, we can conclude that all versions of package-a depend on package-b==2.0.0. And because you require package-a and package-b==1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1224,7 +1223,7 @@ fn transitive_incompatible_with_transitive() { uv_snapshot!(filters, command(&context) .arg("transitive-incompatible-with-transitive-a") .arg("transitive-incompatible-with-transitive-b") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1234,7 +1233,7 @@ fn transitive_incompatible_with_transitive() { ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 depends on package-c==1.0.0, we can conclude that all versions of package-a depend on package-c==1.0.0. And because package-b==1.0.0 depends on package-c==2.0.0 and only package-b==1.0.0 is available, we can conclude that all versions of package-a and all versions of package-b are incompatible. And because you require package-a and package-b, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1275,7 +1274,7 @@ fn transitive_incompatible_versions() { uv_snapshot!(filters, command(&context) .arg("transitive-incompatible-versions-a==1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1284,7 +1283,7 @@ fn transitive_incompatible_versions() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and package-b==2.0.0, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1316,7 +1315,7 @@ fn local_simple() { uv_snapshot!(filters, command(&context) .arg("local-simple-a==1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1324,7 +1323,7 @@ fn local_simple() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. assert_not_installed(&context.venv, "local_simple_a", &context.temp_dir); @@ -1355,7 +1354,7 @@ fn local_not_used_with_sdist() { uv_snapshot!(filters, command(&context) .arg("local-not-used-with-sdist-a==1.2.3") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1365,7 +1364,7 @@ fn local_not_used_with_sdist() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3 - "###); + "#); // The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'. assert_installed( @@ -1402,7 +1401,7 @@ fn local_used_without_sdist() { uv_snapshot!(filters, command(&context) .arg("local-used-without-sdist-a==1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1410,7 +1409,7 @@ fn local_used_without_sdist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because package-a==1.2.3 has no wheels with a matching Python ABI tag and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. assert_not_installed( @@ -1447,7 +1446,7 @@ fn local_not_latest() { uv_snapshot!(filters, command(&context) .arg("local-not-latest-a>=1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1457,7 +1456,7 @@ fn local_not_latest() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.1+foo - "###); + "#); assert_installed( &context.venv, @@ -1497,7 +1496,7 @@ fn local_transitive() { uv_snapshot!(filters, command(&context) .arg("local-transitive-a") .arg("local-transitive-b==2.0.0+foo") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1508,7 +1507,7 @@ fn local_transitive() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0+foo - "###); + "#); // The version '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo. assert_installed( @@ -1555,7 +1554,7 @@ fn local_transitive_greater_than() { uv_snapshot!(filters, command(&context) .arg("local-transitive-greater-than-a") .arg("local-transitive-greater-than-b==2.0.0+foo") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1564,7 +1563,7 @@ fn local_transitive_greater_than() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b>2.0.0 and only package-a==1.0.0 is available, we can conclude that all versions of package-a depend on package-b>2.0.0. And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1608,7 +1607,7 @@ fn local_transitive_greater_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("local-transitive-greater-than-or-equal-a") .arg("local-transitive-greater-than-or-equal-b==2.0.0+foo") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1619,7 +1618,7 @@ fn local_transitive_greater_than_or_equal() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0+foo - "###); + "#); // The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo. assert_installed( @@ -1666,7 +1665,7 @@ fn local_transitive_less_than() { uv_snapshot!(filters, command(&context) .arg("local-transitive-less-than-a") .arg("local-transitive-less-than-b==2.0.0+foo") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1675,7 +1674,7 @@ fn local_transitive_less_than() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b<2.0.0 and only package-a==1.0.0 is available, we can conclude that all versions of package-a depend on package-b<2.0.0. And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1719,7 +1718,7 @@ fn local_transitive_less_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("local-transitive-less-than-or-equal-a") .arg("local-transitive-less-than-or-equal-b==2.0.0+foo") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1730,7 +1729,7 @@ fn local_transitive_less_than_or_equal() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0+foo - "###); + "#); // The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo. assert_installed( @@ -1776,7 +1775,7 @@ fn local_transitive_confounding() { uv_snapshot!(filters, command(&context) .arg("local-transitive-confounding-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1785,7 +1784,7 @@ fn local_transitive_confounding() { × No solution found when resolving dependencies: ╰─▶ Because package-b==2.0.0 has no wheels with a matching Python ABI tag and package-a==1.0.0 depends on package-b==2.0.0, we can conclude that package-a==1.0.0 cannot be used. And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); // The version '2.0.0+foo' satisfies the constraint '==2.0.0'. assert_not_installed( @@ -1825,7 +1824,7 @@ fn local_transitive_conflicting() { uv_snapshot!(filters, command(&context) .arg("local-transitive-conflicting-a") .arg("local-transitive-conflicting-b==2.0.0+foo") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1834,7 +1833,7 @@ fn local_transitive_conflicting() { × No solution found when resolving dependencies: ╰─▶ Because package-a==1.0.0 depends on package-b==2.0.0+bar and only package-a==1.0.0 is available, we can conclude that all versions of package-a depend on package-b==2.0.0+bar. And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -1884,7 +1883,7 @@ fn local_transitive_backtrack() { uv_snapshot!(filters, command(&context) .arg("local-transitive-backtrack-a") .arg("local-transitive-backtrack-b==2.0.0+foo") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1895,7 +1894,7 @@ fn local_transitive_backtrack() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0+foo - "###); + "#); // Backtracking to '1.0.0' gives us compatible local versions of b. assert_installed( @@ -1934,7 +1933,7 @@ fn local_greater_than() { uv_snapshot!(filters, command(&context) .arg("local-greater-than-a>1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -1942,7 +1941,7 @@ fn local_greater_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "local_greater_than_a", &context.temp_dir); } @@ -1969,7 +1968,7 @@ fn local_greater_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("local-greater-than-or-equal-a>=1.2.3") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1979,7 +1978,7 @@ fn local_greater_than_or_equal() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3+foo - "###); + "#); // The version '1.2.3+foo' satisfies the constraint '>=1.2.3'. assert_installed( @@ -2012,7 +2011,7 @@ fn local_less_than() { uv_snapshot!(filters, command(&context) .arg("local-less-than-a<1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2020,7 +2019,7 @@ fn local_less_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "local_less_than_a", &context.temp_dir); } @@ -2047,7 +2046,7 @@ fn local_less_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("local-less-than-or-equal-a<=1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2055,7 +2054,7 @@ fn local_less_than_or_equal() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); // The version '1.2.3+foo' satisfies the constraint '<=1.2.3'. assert_not_installed( @@ -2087,7 +2086,7 @@ fn post_simple() { uv_snapshot!(filters, command(&context) .arg("post-simple-a==1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2095,7 +2094,7 @@ fn post_simple() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "post_simple_a", &context.temp_dir); } @@ -2122,7 +2121,7 @@ fn post_greater_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-or-equal-a>=1.2.3") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2132,7 +2131,7 @@ fn post_greater_than_or_equal() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3.post1 - "###); + "#); // The version '1.2.3.post1' satisfies the constraint '>=1.2.3'. assert_installed( @@ -2165,7 +2164,7 @@ fn post_greater_than() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-a>1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2173,7 +2172,7 @@ fn post_greater_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "post_greater_than_a", &context.temp_dir); } @@ -2202,7 +2201,7 @@ fn post_greater_than_post() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-post-a>1.2.3.post0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2212,7 +2211,7 @@ fn post_greater_than_post() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3.post1 - "###); + "#); // The version '1.2.3.post1' satisfies the constraint '>1.2.3.post0'. assert_installed( @@ -2248,7 +2247,7 @@ fn post_greater_than_or_equal_post() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-or-equal-post-a>=1.2.3.post0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2258,7 +2257,7 @@ fn post_greater_than_or_equal_post() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3.post1 - "###); + "#); // The version '1.2.3.post1' satisfies the constraint '>=1.2.3.post0'. assert_installed( @@ -2291,7 +2290,7 @@ fn post_less_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("post-less-than-or-equal-a<=1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2299,7 +2298,7 @@ fn post_less_than_or_equal() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -2330,7 +2329,7 @@ fn post_less_than() { uv_snapshot!(filters, command(&context) .arg("post-less-than-a<1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2338,7 +2337,7 @@ fn post_less_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "post_less_than_a", &context.temp_dir); } @@ -2367,7 +2366,7 @@ fn post_local_greater_than() { uv_snapshot!(filters, command(&context) .arg("post-local-greater-than-a>1.2.3") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2375,7 +2374,7 @@ fn post_local_greater_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -2408,7 +2407,7 @@ fn post_local_greater_than_post() { uv_snapshot!(filters, command(&context) .arg("post-local-greater-than-post-a>1.2.3.post1") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2416,7 +2415,7 @@ fn post_local_greater_than_post() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>=1.2.3.post2, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -2449,7 +2448,7 @@ fn post_equal_not_available() { uv_snapshot!(filters, command(&context) .arg("post-equal-not-available-a==1.2.3.post0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2457,7 +2456,7 @@ fn post_equal_not_available() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==1.2.3.post0 and you require package-a==1.2.3.post0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -2490,7 +2489,7 @@ fn post_equal_available() { uv_snapshot!(filters, command(&context) .arg("post-equal-available-a==1.2.3.post0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2500,7 +2499,7 @@ fn post_equal_available() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3.post0 - "###); + "#); // The version '1.2.3.post0' satisfies the constraint '==1.2.3.post0'. assert_installed( @@ -2536,7 +2535,7 @@ fn post_greater_than_post_not_available() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-post-not-available-a>1.2.3.post2") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2544,7 +2543,7 @@ fn post_greater_than_post_not_available() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a<=1.2.3.post1 is available and you require package-a>=1.2.3.post3, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -2576,7 +2575,7 @@ fn package_only_prereleases() { uv_snapshot!(filters, command(&context) .arg("package-only-prereleases-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2586,7 +2585,7 @@ fn package_only_prereleases() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0a1 - "###); + "#); // Since there are only prerelease versions of `a` available, it should be // installed even though the user did not include a prerelease specifier. @@ -2622,7 +2621,7 @@ fn package_only_prereleases_in_range() { uv_snapshot!(filters, command(&context) .arg("package-only-prereleases-in-range-a>0.1.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -2632,7 +2631,7 @@ fn package_only_prereleases_in_range() { ╰─▶ Because only package-a<0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. hint: Pre-releases are available for package-a in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "###); + "#); // Since there are stable versions of `a` available, prerelease versions should not // be selected without explicit opt-in. @@ -2672,7 +2671,7 @@ fn requires_package_only_prereleases_in_range_global_opt_in() { uv_snapshot!(filters, command(&context) .arg("--prerelease=allow") .arg("requires-package-only-prereleases-in-range-global-opt-in-a>0.1.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2682,7 +2681,7 @@ fn requires_package_only_prereleases_in_range_global_opt_in() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0a1 - "###); + "#); assert_installed( &context.venv, @@ -2716,7 +2715,7 @@ fn requires_package_prerelease_and_final_any() { uv_snapshot!(filters, command(&context) .arg("requires-package-prerelease-and-final-any-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2726,7 +2725,7 @@ fn requires_package_prerelease_and_final_any() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.1.0 - "###); + "#); // Since the user did not provide a prerelease specifier, the older stable version // should be selected. @@ -2768,7 +2767,7 @@ fn package_prerelease_specified_only_final_available() { uv_snapshot!(filters, command(&context) .arg("package-prerelease-specified-only-final-available-a>=0.1.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2778,7 +2777,7 @@ fn package_prerelease_specified_only_final_available() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.3.0 - "###); + "#); // The latest stable version should be selected. assert_installed( @@ -2819,7 +2818,7 @@ fn package_prerelease_specified_only_prerelease_available() { uv_snapshot!(filters, command(&context) .arg("package-prerelease-specified-only-prerelease-available-a>=0.1.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2829,7 +2828,7 @@ fn package_prerelease_specified_only_prerelease_available() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.3.0a1 - "###); + "#); // The latest prerelease version should be selected. assert_installed( @@ -2869,7 +2868,7 @@ fn package_prerelease_specified_mixed_available() { uv_snapshot!(filters, command(&context) .arg("package-prerelease-specified-mixed-available-a>=0.1.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2879,7 +2878,7 @@ fn package_prerelease_specified_mixed_available() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0a1 - "###); + "#); // Since the user provided a prerelease specifier, the latest prerelease version // should be selected. @@ -2918,7 +2917,7 @@ fn package_multiple_prereleases_kinds() { uv_snapshot!(filters, command(&context) .arg("package-multiple-prereleases-kinds-a>=1.0.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2928,7 +2927,7 @@ fn package_multiple_prereleases_kinds() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0rc1 - "###); + "#); // Release candidates should be the highest precedence prerelease kind. assert_installed( @@ -2965,7 +2964,7 @@ fn package_multiple_prereleases_numbers() { uv_snapshot!(filters, command(&context) .arg("package-multiple-prereleases-numbers-a>=1.0.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2975,7 +2974,7 @@ fn package_multiple_prereleases_numbers() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0a3 - "###); + "#); // The latest alpha version should be selected. assert_installed( @@ -3013,7 +3012,7 @@ fn transitive_package_only_prereleases() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3024,7 +3023,7 @@ fn transitive_package_only_prereleases() { Installed 2 packages in [TIME] + package-a==0.1.0 + package-b==1.0.0a1 - "###); + "#); // Since there are only prerelease versions of `b` available, it should be selected // even though the user did not opt-in to prereleases. @@ -3070,7 +3069,7 @@ fn transitive_package_only_prereleases_in_range() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-in-range-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3081,7 +3080,7 @@ fn transitive_package_only_prereleases_in_range() { And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. hint: Pre-releases are available for package-b in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "###); + "#); // Since there are stable versions of `b` available, the prerelease version should // not be selected without explicit opt-in. The available version is excluded by @@ -3128,7 +3127,7 @@ fn transitive_package_only_prereleases_in_range_opt_in() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-in-range-opt-in-a") .arg("transitive-package-only-prereleases-in-range-opt-in-b>0.0.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3139,7 +3138,7 @@ fn transitive_package_only_prereleases_in_range_opt_in() { Installed 2 packages in [TIME] + package-a==0.1.0 + package-b==1.0.0a1 - "###); + "#); // Since the user included a dependency on `b` with a prerelease specifier, a // prerelease version can be selected. @@ -3192,7 +3191,7 @@ fn transitive_prerelease_and_stable_dependency() { uv_snapshot!(filters, command(&context) .arg("transitive-prerelease-and-stable-dependency-a") .arg("transitive-prerelease-and-stable-dependency-b") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3203,7 +3202,7 @@ fn transitive_prerelease_and_stable_dependency() { And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. hint: package-c was requested with a pre-release marker (e.g., package-c==2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "###); + "#); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. assert_not_installed( @@ -3261,7 +3260,7 @@ fn transitive_prerelease_and_stable_dependency_opt_in() { .arg("transitive-prerelease-and-stable-dependency-opt-in-a") .arg("transitive-prerelease-and-stable-dependency-opt-in-b") .arg("transitive-prerelease-and-stable-dependency-opt-in-c>=0.0.0a1") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3273,7 +3272,7 @@ fn transitive_prerelease_and_stable_dependency_opt_in() { + package-a==1.0.0 + package-b==1.0.0 + package-c==2.0.0b1 - "###); + "#); // Since the user explicitly opted-in to a prerelease for `c`, it can be installed. assert_installed( @@ -3359,7 +3358,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { uv_snapshot!(filters, command(&context) .arg("transitive-prerelease-and-stable-dependency-many-versions-a") .arg("transitive-prerelease-and-stable-dependency-many-versions-b") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3372,7 +3371,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { And because you require package-a and package-b, we can conclude that your requirements are unsatisfiable. hint: package-c was requested with a pre-release marker (e.g., package-c>=2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "###); + "#); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. assert_not_installed( @@ -3443,7 +3442,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() { uv_snapshot!(filters, command(&context) .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-a") .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-b") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3467,7 +3466,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions_holes() { package-c>2.0.0a7,<2.0.0b1 package-c>2.0.0b1,<2.0.0b5 ), but pre-releases weren't enabled (try: `--prerelease=allow`) - "###); + "#); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. assert_not_installed( @@ -3507,7 +3506,7 @@ fn package_only_prereleases_boundary() { uv_snapshot!(filters, command(&context) .arg("package-only-prereleases-boundary-a<0.2.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3517,7 +3516,7 @@ fn package_only_prereleases_boundary() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.1.0a1 - "###); + "#); // Since there are only prerelease versions of `a` available, a prerelease is // allowed. Since the user did not explicitly request a pre-release, pre-releases @@ -3556,7 +3555,7 @@ fn package_prereleases_boundary() { uv_snapshot!(filters, command(&context) .arg("--prerelease=allow") .arg("package-prereleases-boundary-a<0.2.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3566,7 +3565,7 @@ fn package_prereleases_boundary() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.1.0 - "###); + "#); // Since the user did not use a pre-release specifier, pre-releases at the boundary // should not be selected even though pre-releases are allowed. @@ -3604,7 +3603,7 @@ fn package_prereleases_global_boundary() { uv_snapshot!(filters, command(&context) .arg("--prerelease=allow") .arg("package-prereleases-global-boundary-a<0.2.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3614,7 +3613,7 @@ fn package_prereleases_global_boundary() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.1.0 - "###); + "#); // Since the user did not use a pre-release specifier, pre-releases at the boundary // should not be selected even though pre-releases are allowed. @@ -3654,7 +3653,7 @@ fn package_prereleases_specifier_boundary() { uv_snapshot!(filters, command(&context) .arg("package-prereleases-specifier-boundary-a<0.2.0a2") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3664,7 +3663,7 @@ fn package_prereleases_specifier_boundary() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.2.0a1 - "###); + "#); // Since the user used a pre-release specifier, pre-releases at the boundary should // be selected. @@ -3699,7 +3698,7 @@ fn python_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("python-version-does-not-exist-a==1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3708,7 +3707,7 @@ fn python_version_does_not_exist() { × No solution found when resolving dependencies: ╰─▶ Because the current Python version (3.8.[X]) does not satisfy Python>=3.30 and package-a==1.0.0 depends on Python>=3.30, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -3741,7 +3740,7 @@ fn python_less_than_current() { uv_snapshot!(filters, command(&context) .arg("python-less-than-current-a==1.0.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3751,7 +3750,7 @@ fn python_less_than_current() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); // We ignore the upper bound on Python requirements } @@ -3780,7 +3779,7 @@ fn python_greater_than_current() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-a==1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3789,7 +3788,7 @@ fn python_greater_than_current() { × No solution found when resolving dependencies: ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -3823,7 +3822,7 @@ fn python_greater_than_current_patch() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-patch-a==1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3832,7 +3831,7 @@ fn python_greater_than_current_patch() { × No solution found when resolving dependencies: ╰─▶ Because the current Python version (3.8.12) does not satisfy Python>=3.8.14 and package-a==1.0.0 depends on Python>=3.8.14, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -3887,7 +3886,7 @@ fn python_greater_than_current_many() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-many-a==1.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -3895,7 +3894,7 @@ fn python_greater_than_current_many() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==1.0.0 and you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -3936,7 +3935,7 @@ fn python_greater_than_current_backtrack() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-backtrack-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -3946,7 +3945,7 @@ fn python_greater_than_current_backtrack() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); assert_installed( &context.venv, @@ -3987,7 +3986,7 @@ fn python_greater_than_current_excluded() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-excluded-a>=2.0.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4012,7 +4011,7 @@ fn python_greater_than_current_excluded() { Because the current Python version (3.9.[X]) does not satisfy Python>=3.12 and package-a==4.0.0 depends on Python>=3.12, we can conclude that package-a==4.0.0 cannot be used. And because we know from (2) that package-a>=2.0.0,<4.0.0 cannot be used, we can conclude that package-a>=2.0.0 cannot be used. And because you require package-a>=2.0.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -4043,7 +4042,7 @@ fn specific_tag_and_default() { uv_snapshot!(filters, command(&context) .arg("specific-tag-and-default-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4053,7 +4052,7 @@ fn specific_tag_and_default() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); } /// No source distributions are available, only wheels. @@ -4078,7 +4077,7 @@ fn only_wheels() { uv_snapshot!(filters, command(&context) .arg("only-wheels-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4088,7 +4087,7 @@ fn only_wheels() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); } /// No wheels are available, only source distributions. @@ -4113,7 +4112,7 @@ fn no_wheels() { uv_snapshot!(filters, command(&context) .arg("no-wheels-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4123,7 +4122,7 @@ fn no_wheels() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); } /// No wheels with matching platform tags are available, just source distributions. @@ -4148,7 +4147,7 @@ fn no_wheels_with_matching_platform() { uv_snapshot!(filters, command(&context) .arg("no-wheels-with-matching-platform-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4158,7 +4157,7 @@ fn no_wheels_with_matching_platform() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); } /// No wheels with matching platform tags are available, nor are any source @@ -4184,7 +4183,7 @@ fn no_sdist_no_wheels_with_matching_platform() { uv_snapshot!(filters, command(&context) .arg("no-sdist-no-wheels-with-matching-platform-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4193,7 +4192,7 @@ fn no_sdist_no_wheels_with_matching_platform() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching platform tag, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -4225,7 +4224,7 @@ fn no_sdist_no_wheels_with_matching_python() { uv_snapshot!(filters, command(&context) .arg("no-sdist-no-wheels-with-matching-python-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4234,7 +4233,7 @@ fn no_sdist_no_wheels_with_matching_python() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python implementation tag, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -4266,7 +4265,7 @@ fn no_sdist_no_wheels_with_matching_abi() { uv_snapshot!(filters, command(&context) .arg("no-sdist-no-wheels-with-matching-abi-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4275,7 +4274,7 @@ fn no_sdist_no_wheels_with_matching_abi() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python ABI tag, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed( &context.venv, @@ -4309,7 +4308,7 @@ fn no_wheels_no_build() { .arg("--only-binary") .arg("no-wheels-no-build-a") .arg("no-wheels-no-build-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4318,7 +4317,7 @@ fn no_wheels_no_build() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no usable wheels and building from source is disabled, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir); } @@ -4348,7 +4347,7 @@ fn only_wheels_no_binary() { .arg("--no-binary") .arg("only-wheels-no-binary-a") .arg("only-wheels-no-binary-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4357,7 +4356,7 @@ fn only_wheels_no_binary() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no source distribution and using wheels is disabled, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); assert_not_installed(&context.venv, "only_wheels_no_binary_a", &context.temp_dir); } @@ -4387,7 +4386,7 @@ fn no_build() { .arg("--only-binary") .arg("no-build-a") .arg("no-build-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4397,7 +4396,7 @@ fn no_build() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); // The wheel should be used for install } @@ -4427,7 +4426,7 @@ fn no_binary() { .arg("--no-binary") .arg("no-binary-a") .arg("no-binary-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4437,7 +4436,7 @@ fn no_binary() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "###); + "#); // The source distribution should be used for install } @@ -4465,7 +4464,7 @@ fn package_only_yanked() { uv_snapshot!(filters, command(&context) .arg("package-only-yanked-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4474,7 +4473,7 @@ fn package_only_yanked() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Yanked versions should not be installed, even if they are the only one // available. @@ -4504,7 +4503,7 @@ fn package_only_yanked_in_range() { uv_snapshot!(filters, command(&context) .arg("package-only-yanked-in-range-a>0.1.0") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4516,7 +4515,7 @@ fn package_only_yanked_in_range() { package-a==1.0.0 and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-a>0.1.0 cannot be used. And because you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Since there are other versions of `a` available, yanked versions should not be // selected without explicit opt-in. @@ -4551,7 +4550,7 @@ fn requires_package_yanked_and_unyanked_any() { uv_snapshot!(filters, command(&context) .arg("requires-package-yanked-and-unyanked-any-a") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4561,7 +4560,7 @@ fn requires_package_yanked_and_unyanked_any() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.1.0 - "###); + "#); // The unyanked version should be selected. assert_installed( @@ -4599,7 +4598,7 @@ fn package_yanked_specified_mixed_available() { uv_snapshot!(filters, command(&context) .arg("package-yanked-specified-mixed-available-a>=0.1.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4609,7 +4608,7 @@ fn package_yanked_specified_mixed_available() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.3.0 - "###); + "#); // The latest unyanked version should be selected. assert_installed( @@ -4647,7 +4646,7 @@ fn transitive_package_only_yanked() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4657,7 +4656,7 @@ fn transitive_package_only_yanked() { ╰─▶ Because only package-b==1.0.0 is available and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that all versions of package-b cannot be used. And because package-a==0.1.0 depends on package-b, we can conclude that package-a==0.1.0 cannot be used. And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Yanked versions should not be installed, even if they are the only one // available. @@ -4696,7 +4695,7 @@ fn transitive_package_only_yanked_in_range() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-in-range-a") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4709,7 +4708,7 @@ fn transitive_package_only_yanked_in_range() { and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-b>0.1 cannot be used. And because package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Yanked versions should not be installed, even if they are the only valid version // in a range. @@ -4755,7 +4754,7 @@ fn transitive_package_only_yanked_in_range_opt_in() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-in-range-opt-in-a") .arg("transitive-package-only-yanked-in-range-opt-in-b==1.0.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4767,7 +4766,7 @@ fn transitive_package_only_yanked_in_range_opt_in() { + package-a==0.1.0 + package-b==1.0.0 warning: `package-b==1.0.0` is yanked (reason: "Yanked for testing") - "###); + "#); // Since the user included a dependency on `b` with an exact specifier, the yanked // version can be selected. @@ -4820,7 +4819,7 @@ fn transitive_yanked_and_unyanked_dependency() { uv_snapshot!(filters, command(&context) .arg("transitive-yanked-and-unyanked-dependency-a") .arg("transitive-yanked-and-unyanked-dependency-b") - , @r###" + , @r#" success: false exit_code: 1 ----- stdout ----- @@ -4829,7 +4828,7 @@ fn transitive_yanked_and_unyanked_dependency() { × No solution found when resolving dependencies: ╰─▶ Because package-c==2.0.0 was yanked (reason: Yanked for testing) and package-a==1.0.0 depends on package-c==2.0.0, we can conclude that package-a==1.0.0 cannot be used. And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "###); + "#); // Since the user did not explicitly select the yanked version, it cannot be used. assert_not_installed( @@ -4886,7 +4885,7 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { .arg("transitive-yanked-and-unyanked-dependency-opt-in-a") .arg("transitive-yanked-and-unyanked-dependency-opt-in-b") .arg("transitive-yanked-and-unyanked-dependency-opt-in-c==2.0.0") - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -4899,7 +4898,7 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { + package-b==1.0.0 + package-c==2.0.0 warning: `package-c==2.0.0` is yanked (reason: "Yanked for testing") - "###); + "#); // Since the user explicitly selected the yanked version of `c`, it can be // installed. diff --git a/crates/uv/tests/pip_list.rs b/crates/uv/tests/it/pip_list.rs similarity index 99% rename from crates/uv/tests/pip_list.rs rename to crates/uv/tests/it/pip_list.rs index b7db387d3..5f749706b 100644 --- a/crates/uv/tests/pip_list.rs +++ b/crates/uv/tests/it/pip_list.rs @@ -4,11 +4,7 @@ use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; use assert_fs::prelude::*; -use common::uv_snapshot; - -use crate::common::TestContext; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn list_empty_columns() { diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/it/pip_show.rs similarity index 99% rename from crates/uv/tests/pip_show.rs rename to crates/uv/tests/it/pip_show.rs index 4322d1e27..b8801c3c8 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/it/pip_show.rs @@ -6,11 +6,7 @@ use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; use indoc::indoc; -use common::uv_snapshot; - -use crate::common::TestContext; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn show_empty() { diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs similarity index 99% rename from crates/uv/tests/pip_sync.rs rename to crates/uv/tests/it/pip_sync.rs index c07f583f7..8fec7ab0d 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use std::env::consts::EXE_SUFFIX; use std::path::Path; use std::process::Command; @@ -13,13 +11,11 @@ use indoc::indoc; use predicates::Predicate; use url::Url; -use common::{uv_snapshot, venv_to_interpreter}; +use crate::common::{ + copy_dir_all, site_packages_path, uv_snapshot, venv_to_interpreter, TestContext, +}; use uv_fs::Simplified; -use crate::common::{copy_dir_all, site_packages_path, TestContext}; - -mod common; - fn check_command(venv: &Path, command: &str, temp_dir: &Path) { Command::new(venv_to_interpreter(venv)) // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files diff --git a/crates/uv/tests/pip_tree.rs b/crates/uv/tests/it/pip_tree.rs similarity index 99% rename from crates/uv/tests/pip_tree.rs rename to crates/uv/tests/it/pip_tree.rs index ef96dfb65..b984672c4 100644 --- a/crates/uv/tests/pip_tree.rs +++ b/crates/uv/tests/it/pip_tree.rs @@ -3,11 +3,8 @@ use std::process::Command; use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; -use common::uv_snapshot; - -use crate::common::{get_bin, TestContext}; - -mod common; +use crate::common::get_bin; +use crate::common::{uv_snapshot, TestContext}; #[test] fn no_package() { diff --git a/crates/uv/tests/pip_uninstall.rs b/crates/uv/tests/it/pip_uninstall.rs similarity index 99% rename from crates/uv/tests/pip_uninstall.rs rename to crates/uv/tests/it/pip_uninstall.rs index 5a4b67da3..903aa5e27 100644 --- a/crates/uv/tests/pip_uninstall.rs +++ b/crates/uv/tests/it/pip_uninstall.rs @@ -5,11 +5,7 @@ use assert_cmd::prelude::*; use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; -use common::uv_snapshot; - -use crate::common::{get_bin, venv_to_interpreter, TestContext}; - -mod common; +use crate::common::{get_bin, uv_snapshot, venv_to_interpreter, TestContext}; #[test] fn no_arguments() { diff --git a/crates/uv/tests/publish.rs b/crates/uv/tests/it/publish.rs similarity index 96% rename from crates/uv/tests/publish.rs rename to crates/uv/tests/it/publish.rs index ec1cceaf7..94039ba75 100644 --- a/crates/uv/tests/publish.rs +++ b/crates/uv/tests/it/publish.rs @@ -1,8 +1,4 @@ -#![cfg(feature = "pypi")] - -use common::{uv_snapshot, TestContext}; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn username_password_no_longer_supported() { diff --git a/crates/uv/tests/python_dir.rs b/crates/uv/tests/it/python_dir.rs similarity index 88% rename from crates/uv/tests/python_dir.rs rename to crates/uv/tests/it/python_dir.rs index 668215f2c..c05bd7b0f 100644 --- a/crates/uv/tests/python_dir.rs +++ b/crates/uv/tests/it/python_dir.rs @@ -1,8 +1,6 @@ use assert_fs::fixture::PathChild; -use common::{uv_snapshot, TestContext}; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn python_dir() { diff --git a/crates/uv/tests/python_find.rs b/crates/uv/tests/it/python_find.rs similarity index 99% rename from crates/uv/tests/python_find.rs rename to crates/uv/tests/it/python_find.rs index bd44fc1bd..5a3e62296 100644 --- a/crates/uv/tests/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -1,14 +1,10 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use assert_fs::prelude::PathChild; use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir}; use fs_err::remove_dir_all; use indoc::indoc; - -use common::{uv_snapshot, TestContext}; use uv_python::platform::{Arch, Os}; -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn python_find() { diff --git a/crates/uv/tests/python_pin.rs b/crates/uv/tests/it/python_pin.rs similarity index 99% rename from crates/uv/tests/python_pin.rs rename to crates/uv/tests/it/python_pin.rs index 19702a04a..aaa5b0494 100644 --- a/crates/uv/tests/python_pin.rs +++ b/crates/uv/tests/it/python_pin.rs @@ -1,16 +1,12 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; use assert_fs::fixture::{FileWriteStr, PathChild}; -use common::{uv_snapshot, TestContext}; use insta::assert_snapshot; use uv_python::{ platform::{Arch, Os}, PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME, }; -mod common; - #[test] fn python_pin() { let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]); diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/it/run.rs similarity index 99% rename from crates/uv/tests/run.rs rename to crates/uv/tests/it/run.rs index e10918d59..44e90cb5e 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1,4 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::disallowed_types)] use anyhow::Result; @@ -10,11 +9,8 @@ use std::path::Path; use uv_python::PYTHON_VERSION_FILENAME; -use common::{copy_dir_all, uv_snapshot, TestContext}; +use crate::common::{copy_dir_all, uv_snapshot, TestContext}; -mod common; - -/// Run with different python versions, which also depend on different dependency versions. #[test] fn run_with_python_version() -> Result<()> { let context = TestContext::new_with_versions(&["3.12", "3.11", "3.8"]); diff --git a/crates/uv/tests/self_update.rs b/crates/uv/tests/it/self_update.rs similarity index 96% rename from crates/uv/tests/self_update.rs rename to crates/uv/tests/it/self_update.rs index ddb2536b1..b378de0d0 100644 --- a/crates/uv/tests/self_update.rs +++ b/crates/uv/tests/it/self_update.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "self-update")] - use std::process::Command; use axoupdater::{ @@ -9,8 +7,6 @@ use axoupdater::{ use crate::common::get_bin; -mod common; - #[test] fn check_self_update() { // To maximally emulate behaviour in practice, this test actually modifies CARGO_HOME diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/it/show_settings.rs similarity index 99% rename from crates/uv/tests/show_settings.rs rename to crates/uv/tests/it/show_settings.rs index 816fe923c..376b9f8bc 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -1,14 +1,9 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use std::process::Command; use assert_fs::prelude::*; -use common::{uv_snapshot, TestContext}; +use crate::common::{uv_snapshot, TestContext}; -mod common; - -/// Create a `pip compile` command, overwriting defaults for any settings that vary based on machine /// and operating system. fn add_shared_args(mut command: Command) -> Command { command diff --git a/crates/uv/tests/snapshots/ecosystem__black-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__black-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__black-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__black-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__black-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__black-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__black-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__black-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/ecosystem__github-wikidata-bot-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__github-wikidata-bot-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__github-wikidata-bot-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__github-wikidata-bot-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__github-wikidata-bot-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/ecosystem__home-assistant-core-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__home-assistant-core-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__home-assistant-core-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__home-assistant-core-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__home-assistant-core-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/ecosystem__packse-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__packse-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__packse-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__packse-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__packse-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__packse-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__packse-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__packse-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/ecosystem__saleor-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__saleor-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__saleor-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__saleor-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__saleor-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__saleor-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__saleor-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/ecosystem__transformers-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__transformers-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__transformers-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__transformers-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__transformers-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__transformers-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__transformers-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/ecosystem__warehouse-lock-file.snap b/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__warehouse-lock-file.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__warehouse-lock-file.snap diff --git a/crates/uv/tests/snapshots/ecosystem__warehouse-uv-lock-output.snap b/crates/uv/tests/it/snapshots/it__ecosystem__warehouse-uv-lock-output.snap similarity index 100% rename from crates/uv/tests/snapshots/ecosystem__warehouse-uv-lock-output.snap rename to crates/uv/tests/it/snapshots/it__ecosystem__warehouse-uv-lock-output.snap diff --git a/crates/uv/tests/snapshots/workflow__jax_instability-2.snap b/crates/uv/tests/it/snapshots/it__workflow__jax_instability-2.snap similarity index 100% rename from crates/uv/tests/snapshots/workflow__jax_instability-2.snap rename to crates/uv/tests/it/snapshots/it__workflow__jax_instability-2.snap diff --git a/crates/uv/tests/snapshots/workflow__packse_add_remove_existing_package_noop-2.snap b/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_existing_package_noop-2.snap similarity index 100% rename from crates/uv/tests/snapshots/workflow__packse_add_remove_existing_package_noop-2.snap rename to crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_existing_package_noop-2.snap diff --git a/crates/uv/tests/snapshots/workflow__packse_add_remove_one_package-2.snap b/crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_one_package-2.snap similarity index 100% rename from crates/uv/tests/snapshots/workflow__packse_add_remove_one_package-2.snap rename to crates/uv/tests/it/snapshots/it__workflow__packse_add_remove_one_package-2.snap diff --git a/crates/uv/tests/snapshots/workflow__packse_promote_transitive_to_direct_then_remove-2.snap b/crates/uv/tests/it/snapshots/it__workflow__packse_promote_transitive_to_direct_then_remove-2.snap similarity index 100% rename from crates/uv/tests/snapshots/workflow__packse_promote_transitive_to_direct_then_remove-2.snap rename to crates/uv/tests/it/snapshots/it__workflow__packse_promote_transitive_to_direct_then_remove-2.snap diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/it/sync.rs similarity index 99% rename from crates/uv/tests/sync.rs rename to crates/uv/tests/it/sync.rs index 2d35682b5..c117fd8d7 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1,15 +1,11 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::{fixture::ChildPath, prelude::*}; use insta::assert_snapshot; - -use common::{uv_snapshot, venv_bin_path, TestContext}; -use predicates::prelude::predicate; use tempfile::tempdir_in; -mod common; +use crate::common::{uv_snapshot, venv_bin_path, TestContext}; +use predicates::prelude::predicate; #[test] fn sync() -> Result<()> { diff --git a/crates/uv/tests/tool_dir.rs b/crates/uv/tests/it/tool_dir.rs similarity index 89% rename from crates/uv/tests/tool_dir.rs rename to crates/uv/tests/it/tool_dir.rs index 105539e7e..e7ccaf95a 100644 --- a/crates/uv/tests/tool_dir.rs +++ b/crates/uv/tests/it/tool_dir.rs @@ -1,10 +1,6 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use assert_fs::fixture::PathChild; -use common::{uv_snapshot, TestContext}; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn tool_dir() { diff --git a/crates/uv/tests/tool_install.rs b/crates/uv/tests/it/tool_install.rs similarity index 99% rename from crates/uv/tests/tool_install.rs rename to crates/uv/tests/it/tool_install.rs index 1f5792f5c..1902b41b0 100644 --- a/crates/uv/tests/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -1,5 +1,3 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use std::process::Command; use anyhow::Result; @@ -11,11 +9,8 @@ use indoc::indoc; use insta::assert_snapshot; use predicates::prelude::predicate; -use common::{uv_snapshot, TestContext}; +use crate::common::{uv_snapshot, TestContext}; -mod common; - -/// Test installing a tool with `uv tool install` #[test] fn tool_install() { let context = TestContext::new("3.12") diff --git a/crates/uv/tests/tool_list.rs b/crates/uv/tests/it/tool_list.rs similarity index 98% rename from crates/uv/tests/tool_list.rs rename to crates/uv/tests/it/tool_list.rs index 3f0c26d90..e0e8d8760 100644 --- a/crates/uv/tests/tool_list.rs +++ b/crates/uv/tests/it/tool_list.rs @@ -1,14 +1,10 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::{self, uv_snapshot, TestContext}; use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::PathChild; -use common::{uv_snapshot, TestContext}; use fs_err as fs; use insta::assert_snapshot; -mod common; - #[test] fn tool_list() { let context = TestContext::new("3.12").with_filtered_exe_suffix(); diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/it/tool_run.rs similarity index 99% rename from crates/uv/tests/tool_run.rs rename to crates/uv/tests/it/tool_run.rs index cf443775b..bbff408bc 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/it/tool_run.rs @@ -1,12 +1,8 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::{copy_dir_all, uv_snapshot, TestContext}; use assert_cmd::prelude::*; use assert_fs::prelude::*; -use common::{copy_dir_all, uv_snapshot, TestContext}; use indoc::indoc; -mod common; - #[test] fn tool_run_args() { let context = TestContext::new("3.12").with_filtered_counts(); diff --git a/crates/uv/tests/tool_uninstall.rs b/crates/uv/tests/it/tool_uninstall.rs similarity index 98% rename from crates/uv/tests/tool_uninstall.rs rename to crates/uv/tests/it/tool_uninstall.rs index 9a9a3e7fb..25c06f16a 100644 --- a/crates/uv/tests/tool_uninstall.rs +++ b/crates/uv/tests/it/tool_uninstall.rs @@ -1,11 +1,7 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::PathChild; -use common::{uv_snapshot, TestContext}; - -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn tool_uninstall() { diff --git a/crates/uv/tests/tool_upgrade.rs b/crates/uv/tests/it/tool_upgrade.rs similarity index 99% rename from crates/uv/tests/tool_upgrade.rs rename to crates/uv/tests/it/tool_upgrade.rs index 92537534c..2a71f7512 100644 --- a/crates/uv/tests/tool_upgrade.rs +++ b/crates/uv/tests/it/tool_upgrade.rs @@ -1,11 +1,7 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - use assert_fs::prelude::*; - -use common::{uv_snapshot, TestContext}; use insta::assert_snapshot; -mod common; +use crate::common::{uv_snapshot, TestContext}; #[test] fn test_tool_upgrade_name() { diff --git a/crates/uv/tests/tree.rs b/crates/uv/tests/it/tree.rs similarity index 99% rename from crates/uv/tests/tree.rs rename to crates/uv/tests/it/tree.rs index cc1771807..70b4c37f2 100644 --- a/crates/uv/tests/tree.rs +++ b/crates/uv/tests/it/tree.rs @@ -1,13 +1,9 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; use assert_fs::prelude::*; -use common::{uv_snapshot, TestContext}; use indoc::formatdoc; use url::Url; -mod common; - #[test] fn nested_dependencies() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/it/venv.rs similarity index 99% rename from crates/uv/tests/venv.rs rename to crates/uv/tests/it/venv.rs index b7dd0d6cf..8738257b6 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/it/venv.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "python")] - use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; @@ -9,8 +7,6 @@ use uv_python::{PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME}; use crate::common::{uv_snapshot, TestContext}; -mod common; - #[test] fn create_venv() { let context = TestContext::new_with_versions(&["3.12"]); diff --git a/crates/uv/tests/workflow.rs b/crates/uv/tests/it/workflow.rs similarity index 99% rename from crates/uv/tests/workflow.rs rename to crates/uv/tests/it/workflow.rs index 7276ea258..204a2137e 100644 --- a/crates/uv/tests/workflow.rs +++ b/crates/uv/tests/it/workflow.rs @@ -1,13 +1,8 @@ -#![cfg(all(feature = "python", feature = "pypi"))] - +use crate::common::{diff_snapshot, uv_snapshot, TestContext}; use anyhow::Result; use assert_fs::fixture::{FileWriteStr, PathChild}; use insta::assert_snapshot; -use common::{diff_snapshot, uv_snapshot, TestContext}; - -mod common; - #[test] fn packse_add_remove_one_package() { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/workspace.rs b/crates/uv/tests/it/workspace.rs similarity index 99% rename from crates/uv/tests/workspace.rs rename to crates/uv/tests/it/workspace.rs index 1c00554b8..f66c856ec 100644 --- a/crates/uv/tests/workspace.rs +++ b/crates/uv/tests/it/workspace.rs @@ -13,9 +13,6 @@ use serde::{Deserialize, Serialize}; use crate::common::{copy_dir_ignore, make_project, uv_snapshot, TestContext}; -mod common; - -/// `pip install --preview -e ` fn install_workspace(context: &TestContext, current_dir: &Path) -> Command { let mut command = context.pip_install(); command.arg("-e").arg(current_dir); diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index f11107a9e..3b5759028 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -51,11 +51,11 @@ LOCK_TEMPLATE = TEMPLATES / "lock.mustache" PACKSE = TOOL_ROOT / "packse-scenarios" REQUIREMENTS = TOOL_ROOT / "requirements.txt" PROJECT_ROOT = TOOL_ROOT.parent.parent -TESTS = PROJECT_ROOT / "crates" / "uv" / "tests" +TESTS = PROJECT_ROOT / "crates" / "uv" / "tests" / "it" INSTALL_TESTS = TESTS / "pip_install_scenarios.rs" COMPILE_TESTS = TESTS / "pip_compile_scenarios.rs" LOCK_TESTS = TESTS / "lock_scenarios.rs" -TESTS_COMMON_MOD_RS = TESTS / "common/mod.rs" +TESTS_COMMON_MOD_RS = TESTS / "common" / "mod.rs" try: import packse diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index 1041d8616..84ac201d1 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -13,9 +13,7 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileWriteStr, PathChild}; use predicates::prelude::predicate; -use common::{build_vendor_links_url, packse_index_url, python_path_with_versions, get_bin, uv_snapshot, TestContext}; - -mod common; +use crate::common::{build_vendor_links_url, packse_index_url, python_path_with_versions, get_bin, uv_snapshot, TestContext}; /// Provision python binaries and return a `pip compile` command with options shared across all scenarios. fn command(context: &TestContext, python_versions: &[&str]) -> Command { diff --git a/scripts/scenarios/templates/install.mustache b/scripts/scenarios/templates/install.mustache index 2de0f7298..6e4d75648 100644 --- a/scripts/scenarios/templates/install.mustache +++ b/scripts/scenarios/templates/install.mustache @@ -11,11 +11,7 @@ use std::process::Command; use assert_cmd::assert::Assert; use assert_cmd::prelude::*; -use common::{venv_to_interpreter}; - -use crate::common::{build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, TestContext}; - -mod common; +use crate::common::{venv_to_interpreter, build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, TestContext}; fn assert_command(venv: &Path, command: &str, temp_dir: &Path) -> Assert { diff --git a/scripts/scenarios/templates/lock.mustache b/scripts/scenarios/templates/lock.mustache index 6bda8735a..8611751b9 100644 --- a/scripts/scenarios/templates/lock.mustache +++ b/scripts/scenarios/templates/lock.mustache @@ -5,15 +5,15 @@ //! #![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::needless_raw_string_hashes)] +#![allow(clippy::doc_markdown)] +#![allow(clippy::doc_lazy_continuation)] use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use insta::assert_snapshot; -use common::{packse_index_url, TestContext, uv_snapshot}; - -mod common; +use crate::common::{packse_index_url, TestContext, uv_snapshot}; {{#scenarios}} @@ -61,7 +61,7 @@ fn {{module_name}}() -> Result<()> { ); {{#expected.satisfiable}} - let lock = context.read("uv.lock")(); + let lock = context.read("uv.lock"); insta::with_settings!({ filters => filters, }, {