From d08bfee718372fc2a271a87e4c5b673874068554 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 18 Nov 2024 15:11:46 -0500 Subject: [PATCH] Remove separate test files in favor of same-file `mod tests` (#9199) ## Summary These were moved as part of a broader refactor to create a single integration test module. That "single integration test module" did indeed have a big impact on compile times, which is great! But we aren't seeing any benefit from moving these tests into their own files (despite the claim in [this blog post](https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html), I see the same compilation pattern regardless of where the tests are located). Plus, we don't have many of these, and same-file tests is such a strong Rust convention. --- crates/uv-auth/src/cache.rs | 75 +- crates/uv-auth/src/cache/tests.rs | 72 - crates/uv-auth/src/credentials.rs | 109 +- crates/uv-auth/src/credentials/tests.rs | 106 - crates/uv-auth/src/keyring.rs | 131 +- crates/uv-auth/src/keyring/tests.rs | 128 - crates/uv-auth/src/middleware.rs | 1081 +++++++- crates/uv-auth/src/middleware/tests.rs | 1079 -------- crates/uv-auth/src/realm.rs | 87 +- crates/uv-auth/src/realm/tests.rs | 84 - crates/uv-build-backend/src/lib.rs | 245 +- crates/uv-build-backend/src/metadata.rs | 406 ++- crates/uv-build-backend/src/metadata/tests.rs | 403 --- crates/uv-build-backend/src/tests.rs | 242 -- crates/uv-cache-key/src/canonical_url.rs | 142 +- .../uv-cache-key/src/canonical_url/tests.rs | 125 - crates/uv-cli/src/comma.rs | 64 +- crates/uv-cli/src/comma/tests.rs | 61 - crates/uv-cli/src/version.rs | 71 +- crates/uv-cli/src/version/tests.rs | 68 - crates/uv-client/src/html.rs | 998 ++++++- crates/uv-client/src/html/tests.rs | 995 ------- crates/uv-client/src/httpcache/control.rs | 324 ++- .../uv-client/src/httpcache/control/tests.rs | 320 --- crates/uv-client/src/registry_client.rs | 105 +- crates/uv-client/src/registry_client/tests.rs | 102 - crates/uv-configuration/src/build_options.rs | 64 +- .../src/build_options/tests.rs | 61 - .../uv-configuration/src/config_settings.rs | 80 +- .../src/config_settings/tests.rs | 77 - crates/uv-configuration/src/trusted_host.rs | 48 +- .../src/trusted_host/tests.rs | 45 - crates/uv-dev/src/generate_cli_reference.rs | 22 +- .../src/generate_cli_reference/tests.rs | 19 - .../uv-dev/src/generate_env_vars_reference.rs | 22 +- .../src/generate_env_vars_reference/tests.rs | 19 - crates/uv-dev/src/generate_json_schema.rs | 22 +- .../uv-dev/src/generate_json_schema/tests.rs | 19 - .../uv-dev/src/generate_options_reference.rs | 22 +- .../src/generate_options_reference/tests.rs | 19 - crates/uv-distribution-filename/src/egg.rs | 36 +- .../uv-distribution-filename/src/egg/tests.rs | 33 - .../src/source_dist.rs | 56 +- .../src/source_dist/tests.rs | 48 - crates/uv-distribution-filename/src/wheel.rs | 99 +- ..._filename__wheel__tests__ok_build_tag.snap | 27 - ...ename__wheel__tests__ok_multiple_tags.snap | 29 - ...ilename__wheel__tests__ok_single_tags.snap | 22 - .../src/wheel/tests.rs | 95 - crates/uv-fs/src/path.rs | 106 +- crates/uv-fs/src/path/tests.rs | 103 - crates/uv-git/src/sha.rs | 17 +- crates/uv-git/src/sha/tests.rs | 14 - crates/uv-normalize/src/dist_info_name.rs | 21 +- .../uv-normalize/src/dist_info_name/tests.rs | 18 - crates/uv-normalize/src/lib.rs | 77 +- crates/uv-normalize/src/tests.rs | 74 - crates/uv-options-metadata/src/lib.rs | 156 +- crates/uv-options-metadata/src/tests.rs | 153 -- crates/uv-pep440/src/lib.rs | 17 +- crates/uv-pep440/src/tests.rs | 11 - crates/uv-pep440/src/version.rs | 1384 +++++++++- crates/uv-pep440/src/version/tests.rs | 1380 ---------- crates/uv-pep440/src/version_specifier.rs | 961 ++++++- .../uv-pep440/src/version_specifier/tests.rs | 951 ------- crates/uv-pep508/src/lib.rs | 803 +++++- crates/uv-pep508/src/marker/algebra.rs | 87 +- crates/uv-pep508/src/marker/algebra/tests.rs | 84 - crates/uv-pep508/src/tests.rs | 797 ------ crates/uv-pep508/src/verbatim_url.rs | 59 +- crates/uv-pep508/src/verbatim_url/tests.rs | 56 - crates/uv-platform-tags/src/tags.rs | 1533 ++++++++++- crates/uv-platform-tags/src/tags/tests.rs | 1530 ----------- crates/uv-publish/src/lib.rs | 276 +- crates/uv-publish/src/tests.rs | 273 -- .../uv-pypi-types/src/lenient_requirement.rs | 259 +- .../src/lenient_requirement/tests.rs | 251 -- .../uv-pypi-types/src/metadata/metadata23.rs | 35 +- .../src/metadata/metadata23/tests.rs | 32 - .../src/metadata/metadata_resolver.rs | 73 +- .../src/metadata/metadata_resolver/tests.rs | 68 - .../src/metadata/pyproject_toml.rs | 88 +- .../src/metadata/pyproject_toml/tests.rs | 85 - .../src/metadata/requires_txt.rs | 59 +- .../src/metadata/requires_txt/tests.rs | 56 - crates/uv-pypi-types/src/parsed_url.rs | 44 +- crates/uv-pypi-types/src/parsed_url/tests.rs | 41 - crates/uv-pypi-types/src/requirement.rs | 48 +- crates/uv-pypi-types/src/requirement/tests.rs | 45 - crates/uv-pypi-types/src/simple_json.rs | 73 +- crates/uv-pypi-types/src/simple_json/tests.rs | 62 - crates/uv-python/src/discovery.rs | 535 +++- crates/uv-python/src/discovery/tests.rs | 528 ---- crates/uv-python/src/interpreter.rs | 106 +- crates/uv-python/src/interpreter/tests.rs | 103 - crates/uv-python/src/lib.rs | 2402 ++++++++++++++++- crates/uv-python/src/libc.rs | 43 +- crates/uv-python/src/libc/tests.rs | 40 - crates/uv-python/src/python_version.rs | 35 +- crates/uv-python/src/python_version/tests.rs | 32 - crates/uv-python/src/tests.rs | 2384 ---------------- crates/uv-resolver/src/lock/mod.rs | 284 +- crates/uv-resolver/src/lock/tests.rs | 281 -- crates/uv-resolver/src/redirect.rs | 64 +- crates/uv-resolver/src/redirect/tests.rs | 61 - crates/uv-resolver/src/requires_python.rs | 161 +- .../uv-resolver/src/requires_python/tests.rs | 158 -- crates/uv-scripts/src/lib.rs | 231 +- crates/uv-scripts/src/tests.rs | 228 -- crates/uv-version/src/lib.rs | 9 +- crates/uv-version/src/tests.rs | 6 - crates/uv-workspace/src/workspace.rs | 1022 ++++++- crates/uv-workspace/src/workspace/tests.rs | 1017 ------- crates/uv/src/version/tests.rs | 68 - 114 files changed, 15321 insertions(+), 15344 deletions(-) delete mode 100644 crates/uv-auth/src/cache/tests.rs delete mode 100644 crates/uv-auth/src/credentials/tests.rs delete mode 100644 crates/uv-auth/src/keyring/tests.rs delete mode 100644 crates/uv-auth/src/middleware/tests.rs delete mode 100644 crates/uv-auth/src/realm/tests.rs delete mode 100644 crates/uv-build-backend/src/metadata/tests.rs delete mode 100644 crates/uv-build-backend/src/tests.rs delete mode 100644 crates/uv-cache-key/src/canonical_url/tests.rs delete mode 100644 crates/uv-cli/src/comma/tests.rs delete mode 100644 crates/uv-cli/src/version/tests.rs delete mode 100644 crates/uv-client/src/html/tests.rs delete mode 100644 crates/uv-client/src/httpcache/control/tests.rs delete mode 100644 crates/uv-client/src/registry_client/tests.rs delete mode 100644 crates/uv-configuration/src/build_options/tests.rs delete mode 100644 crates/uv-configuration/src/config_settings/tests.rs delete mode 100644 crates/uv-configuration/src/trusted_host/tests.rs delete mode 100644 crates/uv-dev/src/generate_cli_reference/tests.rs delete mode 100644 crates/uv-dev/src/generate_env_vars_reference/tests.rs delete mode 100644 crates/uv-dev/src/generate_json_schema/tests.rs delete mode 100644 crates/uv-dev/src/generate_options_reference/tests.rs delete mode 100644 crates/uv-distribution-filename/src/egg/tests.rs delete mode 100644 crates/uv-distribution-filename/src/source_dist/tests.rs delete mode 100644 crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap delete mode 100644 crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap delete mode 100644 crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap delete mode 100644 crates/uv-distribution-filename/src/wheel/tests.rs delete mode 100644 crates/uv-fs/src/path/tests.rs delete mode 100644 crates/uv-git/src/sha/tests.rs delete mode 100644 crates/uv-normalize/src/dist_info_name/tests.rs delete mode 100644 crates/uv-normalize/src/tests.rs delete mode 100644 crates/uv-options-metadata/src/tests.rs delete mode 100644 crates/uv-pep440/src/tests.rs delete mode 100644 crates/uv-pep440/src/version/tests.rs delete mode 100644 crates/uv-pep440/src/version_specifier/tests.rs delete mode 100644 crates/uv-pep508/src/marker/algebra/tests.rs delete mode 100644 crates/uv-pep508/src/tests.rs delete mode 100644 crates/uv-pep508/src/verbatim_url/tests.rs delete mode 100644 crates/uv-platform-tags/src/tags/tests.rs delete mode 100644 crates/uv-publish/src/tests.rs delete mode 100644 crates/uv-pypi-types/src/lenient_requirement/tests.rs delete mode 100644 crates/uv-pypi-types/src/metadata/metadata23/tests.rs delete mode 100644 crates/uv-pypi-types/src/metadata/metadata_resolver/tests.rs delete mode 100644 crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs delete mode 100644 crates/uv-pypi-types/src/metadata/requires_txt/tests.rs delete mode 100644 crates/uv-pypi-types/src/parsed_url/tests.rs delete mode 100644 crates/uv-pypi-types/src/requirement/tests.rs delete mode 100644 crates/uv-pypi-types/src/simple_json/tests.rs delete mode 100644 crates/uv-python/src/discovery/tests.rs delete mode 100644 crates/uv-python/src/interpreter/tests.rs delete mode 100644 crates/uv-python/src/libc/tests.rs delete mode 100644 crates/uv-python/src/python_version/tests.rs delete mode 100644 crates/uv-python/src/tests.rs delete mode 100644 crates/uv-resolver/src/lock/tests.rs delete mode 100644 crates/uv-resolver/src/redirect/tests.rs delete mode 100644 crates/uv-resolver/src/requires_python/tests.rs delete mode 100644 crates/uv-scripts/src/tests.rs delete mode 100644 crates/uv-version/src/tests.rs delete mode 100644 crates/uv-workspace/src/workspace/tests.rs delete mode 100644 crates/uv/src/version/tests.rs diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index f62c69d0575a..008744ece8c0 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -215,4 +215,77 @@ impl TrieState { } #[cfg(test)] -mod tests; +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); + } +} diff --git a/crates/uv-auth/src/cache/tests.rs b/crates/uv-auth/src/cache/tests.rs deleted file mode 100644 index 2975e0e23d3d..000000000000 --- a/crates/uv-auth/src/cache/tests.rs +++ /dev/null @@ -1,72 +0,0 @@ -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 e79dfac9278f..753b9a244f9e 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -246,4 +246,111 @@ impl Credentials { } #[cfg(test)] -mod tests; +mod tests { + 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/credentials/tests.rs b/crates/uv-auth/src/credentials/tests.rs deleted file mode 100644 index dd4e373aeaaa..000000000000 --- a/crates/uv-auth/src/credentials/tests.rs +++ /dev/null @@ -1,106 +0,0 @@ -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 bc10c10c7498..7cc98a39c515 100644 --- a/crates/uv-auth/src/keyring.rs +++ b/crates/uv-auth/src/keyring.rs @@ -151,4 +151,133 @@ impl KeyringProvider { } #[cfg(test)] -mod tests; +mod tests { + 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/keyring/tests.rs b/crates/uv-auth/src/keyring/tests.rs deleted file mode 100644 index 6b1e8d6e22ec..000000000000 --- a/crates/uv-auth/src/keyring/tests.rs +++ /dev/null @@ -1,128 +0,0 @@ -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 046feaf20707..3c78600d865b 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -464,4 +464,1083 @@ fn tracing_url(request: &Request, credentials: Option<&Credentials>) -> String { } #[cfg(test)] -mod tests; +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. We should pull the username from the cache if not present on the + // URL. + 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(), + 200, + "The username is pulled from the cache, and the password from the keyring" + ); + + 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/middleware/tests.rs b/crates/uv-auth/src/middleware/tests.rs deleted file mode 100644 index 89b0f83b6633..000000000000 --- a/crates/uv-auth/src/middleware/tests.rs +++ /dev/null @@ -1,1079 +0,0 @@ -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. We should pull the username from the cache if not present on the - // URL. - 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(), - 200, - "The username is pulled from the cache, and the password from the keyring" - ); - - 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 fcda89ade470..92d73d5f8509 100644 --- a/crates/uv-auth/src/realm.rs +++ b/crates/uv-auth/src/realm.rs @@ -59,4 +59,89 @@ impl Display for Realm { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-auth/src/realm/tests.rs b/crates/uv-auth/src/realm/tests.rs deleted file mode 100644 index 753b37c090d1..000000000000 --- a/crates/uv-auth/src/realm/tests.rs +++ /dev/null @@ -1,84 +0,0 @@ -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-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 7ec480fa1858..4ad1f1ce758e 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -866,4 +866,247 @@ fn write_record( } #[cfg(test)] -mod tests; +mod tests { + use super::*; + use flate2::bufread::GzDecoder; + use insta::assert_snapshot; + use std::str::FromStr; + use tempfile::TempDir; + use uv_fs::copy_dir_all; + 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()], + }; + + assert_snapshot!(wheel_info(&filename, "1.0.0+test"), @r" + Wheel-Version: 1.0 + Generator: uv 1.0.0+test + Root-Is-Purelib: true + Tag: py2-none-any + Tag: py3-none-any + "); + } + + #[test] + fn test_record() { + let record = vec![RecordEntry { + path: "built_by_uv/__init__.py".to_string(), + hash: "89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865".to_string(), + size: 37, + }]; + + let mut writer = Vec::new(); + write_record(&mut writer, "built_by_uv-0.1.0", record).unwrap(); + assert_snapshot!(String::from_utf8(writer).unwrap(), @r" + built_by_uv/__init__.py,sha256=89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865,37 + built_by_uv-0.1.0/RECORD,, + "); + } + + /// Snapshot all files from the prepare metadata hook. + #[test] + fn test_prepare_metadata() { + let metadata_dir = TempDir::new().unwrap(); + let built_by_uv = Path::new("../../scripts/packages/built-by-uv"); + metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap(); + + let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) + .into_iter() + .map(|entry| { + entry + .unwrap() + .path() + .strip_prefix(metadata_dir.path()) + .expect("walkdir starts with root") + .portable_display() + .to_string() + }) + .filter(|path| !path.is_empty()) + .collect(); + files.sort(); + assert_snapshot!(files.join("\n"), @r" + built_by_uv-0.1.0.dist-info + built_by_uv-0.1.0.dist-info/METADATA + built_by_uv-0.1.0.dist-info/RECORD + built_by_uv-0.1.0.dist-info/WHEEL + "); + + let metadata_file = metadata_dir + .path() + .join("built_by_uv-0.1.0.dist-info/METADATA"); + assert_snapshot!(fs_err::read_to_string(metadata_file).unwrap(), @r###" + Metadata-Version: 2.4 + Name: built-by-uv + Version: 0.1.0 + Summary: A package to be built with the uv build backend that uses all features exposed by the build backend + Requires-Dist: anyio>=4,<5 + Requires-Python: >=3.12 + Description-Content-Type: text/markdown + + # built_by_uv + + A package to be built with the uv build backend that uses all features exposed by the build backend. + "###); + + let record_file = metadata_dir + .path() + .join("built_by_uv-0.1.0.dist-info/RECORD"); + assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r###" + built_by_uv-0.1.0.dist-info/WHEEL,sha256=3da1bfa0e8fd1b6cc246aa0b2b44a35815596c600cb485c39a6f8c106c3d5a8d,83 + built_by_uv-0.1.0.dist-info/METADATA,sha256=acb91f5a18cb53fa57b45eb4590ea13195a774c856a9dd8cf27cc5435d6451b6,372 + built_by_uv-0.1.0.dist-info/RECORD,, + "###); + + let wheel_file = metadata_dir + .path() + .join("built_by_uv-0.1.0.dist-info/WHEEL"); + assert_snapshot!(fs_err::read_to_string(wheel_file).unwrap(), @r###" + Wheel-Version: 1.0 + Generator: uv 1.0.0+test + Root-Is-Purelib: true + Tag: py3-none-any + "###); + } + + /// Test that source tree -> source dist -> wheel includes the right files and is stable and + /// deterministic in dependent of the build path. + #[test] + fn built_by_uv_building() { + let built_by_uv = Path::new("../../scripts/packages/built-by-uv"); + let src = TempDir::new().unwrap(); + for dir in ["src", "tests", "data-dir", "third-party-licenses"] { + copy_dir_all(built_by_uv.join(dir), src.path().join(dir)).unwrap(); + } + for dir in [ + "pyproject.toml", + "README.md", + "uv.lock", + "LICENSE-APACHE", + "LICENSE-MIT", + ] { + fs_err::copy(built_by_uv.join(dir), src.path().join(dir)).unwrap(); + } + + // Build a wheel from the source tree + let direct_output_dir = TempDir::new().unwrap(); + build_wheel( + src.path(), + direct_output_dir.path(), + None, + WheelSettings::default(), + "1.0.0+test", + ) + .unwrap(); + + let wheel = zip::ZipArchive::new( + File::open( + direct_output_dir + .path() + .join("built_by_uv-0.1.0-py3-none-any.whl"), + ) + .unwrap(), + ) + .unwrap(); + let mut direct_wheel_contents: Vec<_> = wheel.file_names().collect(); + direct_wheel_contents.sort_unstable(); + + // Build a source dist from the source tree + let source_dist_dir = TempDir::new().unwrap(); + build_source_dist( + src.path(), + source_dist_dir.path(), + SourceDistSettings::default(), + "1.0.0+test", + ) + .unwrap(); + + // Build a wheel from the source dist + let sdist_tree = TempDir::new().unwrap(); + let source_dist_path = source_dist_dir.path().join("built_by_uv-0.1.0.tar.gz"); + let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); + let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); + let mut source_dist_contents: Vec<_> = source_dist + .entries() + .unwrap() + .map(|entry| entry.unwrap().path().unwrap().to_str().unwrap().to_string()) + .collect(); + source_dist_contents.sort(); + // Reset the reader and unpack + let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); + let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); + source_dist.unpack(sdist_tree.path()).unwrap(); + drop(source_dist_dir); + + let indirect_output_dir = TempDir::new().unwrap(); + build_wheel( + &sdist_tree.path().join("built_by_uv-0.1.0"), + indirect_output_dir.path(), + None, + WheelSettings::default(), + "1.0.0+test", + ) + .unwrap(); + + // Check that we write deterministic wheels. + let wheel_filename = "built_by_uv-0.1.0-py3-none-any.whl"; + assert_eq!( + fs_err::read(direct_output_dir.path().join(wheel_filename)).unwrap(), + fs_err::read(indirect_output_dir.path().join(wheel_filename)).unwrap() + ); + + // Check the contained files and directories + assert_snapshot!(source_dist_contents.iter().map(|path| path.replace('\\', "/")).join("\n"), @r" + built_by_uv-0.1.0/LICENSE-APACHE + built_by_uv-0.1.0/LICENSE-MIT + built_by_uv-0.1.0/PKG-INFO + built_by_uv-0.1.0/README.md + built_by_uv-0.1.0/pyproject.toml + built_by_uv-0.1.0/src/built_by_uv + built_by_uv-0.1.0/src/built_by_uv/__init__.py + built_by_uv-0.1.0/src/built_by_uv/arithmetic + built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py + built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py + built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt + built_by_uv-0.1.0/third-party-licenses/PEP-401.txt + "); + + let wheel = zip::ZipArchive::new( + File::open( + indirect_output_dir + .path() + .join("built_by_uv-0.1.0-py3-none-any.whl"), + ) + .unwrap(), + ) + .unwrap(); + let mut indirect_wheel_contents: Vec<_> = wheel.file_names().collect(); + indirect_wheel_contents.sort_unstable(); + assert_eq!(indirect_wheel_contents, direct_wheel_contents); + + assert_snapshot!(indirect_wheel_contents.iter().map(|path| path.replace('\\', "/")).join("\n"), @r" + built_by_uv-0.1.0.dist-info/ + built_by_uv-0.1.0.dist-info/METADATA + built_by_uv-0.1.0.dist-info/RECORD + built_by_uv-0.1.0.dist-info/WHEEL + built_by_uv-0.1.0.dist-info/licenses/ + built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE + built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT + built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt + built_by_uv/ + built_by_uv/__init__.py + built_by_uv/arithmetic/ + built_by_uv/arithmetic/__init__.py + built_by_uv/arithmetic/circle.py + built_by_uv/arithmetic/pi.txt + "); + } +} diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index c7c4e61584b8..8a8c503ffbcd 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -680,4 +680,408 @@ struct BuildSystem { } #[cfg(test)] -mod tests; +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 + Author-email: 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 + Maintainer-email: 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("1.0.0+test")); + } + + #[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("1.0.0+test")); + } + + #[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("1.0.0+test")); + } + + #[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("1.0.0+test")); + } + + #[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("1.0.0+test")); + } + + #[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/metadata/tests.rs b/crates/uv-build-backend/src/metadata/tests.rs deleted file mode 100644 index abc4b9fd3000..000000000000 --- a/crates/uv-build-backend/src/metadata/tests.rs +++ /dev/null @@ -1,403 +0,0 @@ -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 - Author-email: 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 - Maintainer-email: 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("1.0.0+test")); -} - -#[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("1.0.0+test")); -} - -#[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("1.0.0+test")); -} - -#[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("1.0.0+test")); -} - -#[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("1.0.0+test")); -} - -#[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/tests.rs b/crates/uv-build-backend/src/tests.rs deleted file mode 100644 index 084493f5e9a7..000000000000 --- a/crates/uv-build-backend/src/tests.rs +++ /dev/null @@ -1,242 +0,0 @@ -use super::*; -use flate2::bufread::GzDecoder; -use insta::assert_snapshot; -use std::str::FromStr; -use tempfile::TempDir; -use uv_fs::copy_dir_all; -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()], - }; - - assert_snapshot!(wheel_info(&filename, "1.0.0+test"), @r" - Wheel-Version: 1.0 - Generator: uv 1.0.0+test - Root-Is-Purelib: true - Tag: py2-none-any - Tag: py3-none-any - "); -} - -#[test] -fn test_record() { - let record = vec![RecordEntry { - path: "built_by_uv/__init__.py".to_string(), - hash: "89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865".to_string(), - size: 37, - }]; - - let mut writer = Vec::new(); - write_record(&mut writer, "built_by_uv-0.1.0", record).unwrap(); - assert_snapshot!(String::from_utf8(writer).unwrap(), @r" - built_by_uv/__init__.py,sha256=89f869e53a3a0061a52c0233e6442d4d72de80a8a2d3406d9ea0bfd397ed7865,37 - built_by_uv-0.1.0/RECORD,, - "); -} - -/// Snapshot all files from the prepare metadata hook. -#[test] -fn test_prepare_metadata() { - let metadata_dir = TempDir::new().unwrap(); - let built_by_uv = Path::new("../../scripts/packages/built-by-uv"); - metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap(); - - let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) - .into_iter() - .map(|entry| { - entry - .unwrap() - .path() - .strip_prefix(metadata_dir.path()) - .expect("walkdir starts with root") - .portable_display() - .to_string() - }) - .filter(|path| !path.is_empty()) - .collect(); - files.sort(); - assert_snapshot!(files.join("\n"), @r" - built_by_uv-0.1.0.dist-info - built_by_uv-0.1.0.dist-info/METADATA - built_by_uv-0.1.0.dist-info/RECORD - built_by_uv-0.1.0.dist-info/WHEEL - "); - - let metadata_file = metadata_dir - .path() - .join("built_by_uv-0.1.0.dist-info/METADATA"); - assert_snapshot!(fs_err::read_to_string(metadata_file).unwrap(), @r###" - Metadata-Version: 2.4 - Name: built-by-uv - Version: 0.1.0 - Summary: A package to be built with the uv build backend that uses all features exposed by the build backend - Requires-Dist: anyio>=4,<5 - Requires-Python: >=3.12 - Description-Content-Type: text/markdown - - # built_by_uv - - A package to be built with the uv build backend that uses all features exposed by the build backend. - "###); - - let record_file = metadata_dir - .path() - .join("built_by_uv-0.1.0.dist-info/RECORD"); - assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r###" - built_by_uv-0.1.0.dist-info/WHEEL,sha256=3da1bfa0e8fd1b6cc246aa0b2b44a35815596c600cb485c39a6f8c106c3d5a8d,83 - built_by_uv-0.1.0.dist-info/METADATA,sha256=acb91f5a18cb53fa57b45eb4590ea13195a774c856a9dd8cf27cc5435d6451b6,372 - built_by_uv-0.1.0.dist-info/RECORD,, - "###); - - let wheel_file = metadata_dir - .path() - .join("built_by_uv-0.1.0.dist-info/WHEEL"); - assert_snapshot!(fs_err::read_to_string(wheel_file).unwrap(), @r###" - Wheel-Version: 1.0 - Generator: uv 1.0.0+test - Root-Is-Purelib: true - Tag: py3-none-any - "###); -} - -/// Test that source tree -> source dist -> wheel includes the right files and is stable and -/// deterministic in dependent of the build path. -#[test] -fn built_by_uv_building() { - let built_by_uv = Path::new("../../scripts/packages/built-by-uv"); - let src = TempDir::new().unwrap(); - for dir in ["src", "tests", "data-dir", "third-party-licenses"] { - copy_dir_all(built_by_uv.join(dir), src.path().join(dir)).unwrap(); - } - for dir in [ - "pyproject.toml", - "README.md", - "uv.lock", - "LICENSE-APACHE", - "LICENSE-MIT", - ] { - fs_err::copy(built_by_uv.join(dir), src.path().join(dir)).unwrap(); - } - - // Build a wheel from the source tree - let direct_output_dir = TempDir::new().unwrap(); - build_wheel( - src.path(), - direct_output_dir.path(), - None, - WheelSettings::default(), - "1.0.0+test", - ) - .unwrap(); - - let wheel = zip::ZipArchive::new( - File::open( - direct_output_dir - .path() - .join("built_by_uv-0.1.0-py3-none-any.whl"), - ) - .unwrap(), - ) - .unwrap(); - let mut direct_wheel_contents: Vec<_> = wheel.file_names().collect(); - direct_wheel_contents.sort_unstable(); - - // Build a source dist from the source tree - let source_dist_dir = TempDir::new().unwrap(); - build_source_dist( - src.path(), - source_dist_dir.path(), - SourceDistSettings::default(), - "1.0.0+test", - ) - .unwrap(); - - // Build a wheel from the source dist - let sdist_tree = TempDir::new().unwrap(); - let source_dist_path = source_dist_dir.path().join("built_by_uv-0.1.0.tar.gz"); - let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); - let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); - let mut source_dist_contents: Vec<_> = source_dist - .entries() - .unwrap() - .map(|entry| entry.unwrap().path().unwrap().to_str().unwrap().to_string()) - .collect(); - source_dist_contents.sort(); - // Reset the reader and unpack - let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); - let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); - source_dist.unpack(sdist_tree.path()).unwrap(); - drop(source_dist_dir); - - let indirect_output_dir = TempDir::new().unwrap(); - build_wheel( - &sdist_tree.path().join("built_by_uv-0.1.0"), - indirect_output_dir.path(), - None, - WheelSettings::default(), - "1.0.0+test", - ) - .unwrap(); - - // Check that we write deterministic wheels. - let wheel_filename = "built_by_uv-0.1.0-py3-none-any.whl"; - assert_eq!( - fs_err::read(direct_output_dir.path().join(wheel_filename)).unwrap(), - fs_err::read(indirect_output_dir.path().join(wheel_filename)).unwrap() - ); - - // Check the contained files and directories - assert_snapshot!(source_dist_contents.iter().map(|path| path.replace('\\', "/")).join("\n"), @r" - built_by_uv-0.1.0/LICENSE-APACHE - built_by_uv-0.1.0/LICENSE-MIT - built_by_uv-0.1.0/PKG-INFO - built_by_uv-0.1.0/README.md - built_by_uv-0.1.0/pyproject.toml - built_by_uv-0.1.0/src/built_by_uv - built_by_uv-0.1.0/src/built_by_uv/__init__.py - built_by_uv-0.1.0/src/built_by_uv/arithmetic - built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py - built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py - built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt - built_by_uv-0.1.0/third-party-licenses/PEP-401.txt - "); - - let wheel = zip::ZipArchive::new( - File::open( - indirect_output_dir - .path() - .join("built_by_uv-0.1.0-py3-none-any.whl"), - ) - .unwrap(), - ) - .unwrap(); - let mut indirect_wheel_contents: Vec<_> = wheel.file_names().collect(); - indirect_wheel_contents.sort_unstable(); - assert_eq!(indirect_wheel_contents, direct_wheel_contents); - - assert_snapshot!(indirect_wheel_contents.iter().map(|path| path.replace('\\', "/")).join("\n"), @r" - built_by_uv-0.1.0.dist-info/ - built_by_uv-0.1.0.dist-info/METADATA - built_by_uv-0.1.0.dist-info/RECORD - built_by_uv-0.1.0.dist-info/WHEEL - built_by_uv-0.1.0.dist-info/licenses/ - built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE - built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT - built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt - built_by_uv/ - built_by_uv/__init__.py - built_by_uv/arithmetic/ - built_by_uv/arithmetic/__init__.py - built_by_uv/arithmetic/circle.py - built_by_uv/arithmetic/pi.txt - "); -} diff --git a/crates/uv-cache-key/src/canonical_url.rs b/crates/uv-cache-key/src/canonical_url.rs index 9cc26d94ce71..b46d591bb6ef 100644 --- a/crates/uv-cache-key/src/canonical_url.rs +++ b/crates/uv-cache-key/src/canonical_url.rs @@ -181,4 +181,144 @@ impl std::fmt::Display for RepositoryUrl { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-cache-key/src/canonical_url/tests.rs b/crates/uv-cache-key/src/canonical_url/tests.rs deleted file mode 100644 index 0f6d157887e5..000000000000 --- a/crates/uv-cache-key/src/canonical_url/tests.rs +++ /dev/null @@ -1,125 +0,0 @@ -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-cli/src/comma.rs b/crates/uv-cli/src/comma.rs index 8c5d97b89a1e..d1c36c639d8d 100644 --- a/crates/uv-cli/src/comma.rs +++ b/crates/uv-cli/src/comma.rs @@ -60,4 +60,66 @@ impl FromStr for CommaSeparatedRequirements { } #[cfg(test)] -mod tests; +mod tests { + use super::CommaSeparatedRequirements; + use std::str::FromStr; + + #[test] + fn single() { + assert_eq!( + CommaSeparatedRequirements::from_str("flask").unwrap(), + CommaSeparatedRequirements(vec!["flask".to_string()]) + ); + } + + #[test] + fn double() { + assert_eq!( + CommaSeparatedRequirements::from_str("flask,anyio").unwrap(), + CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()]) + ); + } + + #[test] + fn empty() { + assert_eq!( + CommaSeparatedRequirements::from_str("flask,,anyio").unwrap(), + CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()]) + ); + } + + #[test] + fn single_extras() { + assert_eq!( + CommaSeparatedRequirements::from_str("psycopg[binary,pool]").unwrap(), + CommaSeparatedRequirements(vec!["psycopg[binary,pool]".to_string()]) + ); + } + + #[test] + fn double_extras() { + assert_eq!( + CommaSeparatedRequirements::from_str("psycopg[binary,pool], flask").unwrap(), + CommaSeparatedRequirements(vec![ + "psycopg[binary,pool]".to_string(), + "flask".to_string() + ]) + ); + } + + #[test] + fn single_specifiers() { + assert_eq!( + CommaSeparatedRequirements::from_str("requests>=2.1,<3").unwrap(), + CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string(),]) + ); + } + + #[test] + fn double_specifiers() { + assert_eq!( + CommaSeparatedRequirements::from_str("requests>=2.1,<3, flask").unwrap(), + CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string(), "flask".to_string()]) + ); + } +} diff --git a/crates/uv-cli/src/comma/tests.rs b/crates/uv-cli/src/comma/tests.rs deleted file mode 100644 index 4a98d73e07b3..000000000000 --- a/crates/uv-cli/src/comma/tests.rs +++ /dev/null @@ -1,61 +0,0 @@ -use super::CommaSeparatedRequirements; -use std::str::FromStr; - -#[test] -fn single() { - assert_eq!( - CommaSeparatedRequirements::from_str("flask").unwrap(), - CommaSeparatedRequirements(vec!["flask".to_string()]) - ); -} - -#[test] -fn double() { - assert_eq!( - CommaSeparatedRequirements::from_str("flask,anyio").unwrap(), - CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()]) - ); -} - -#[test] -fn empty() { - assert_eq!( - CommaSeparatedRequirements::from_str("flask,,anyio").unwrap(), - CommaSeparatedRequirements(vec!["flask".to_string(), "anyio".to_string()]) - ); -} - -#[test] -fn single_extras() { - assert_eq!( - CommaSeparatedRequirements::from_str("psycopg[binary,pool]").unwrap(), - CommaSeparatedRequirements(vec!["psycopg[binary,pool]".to_string()]) - ); -} - -#[test] -fn double_extras() { - assert_eq!( - CommaSeparatedRequirements::from_str("psycopg[binary,pool], flask").unwrap(), - CommaSeparatedRequirements(vec![ - "psycopg[binary,pool]".to_string(), - "flask".to_string() - ]) - ); -} - -#[test] -fn single_specifiers() { - assert_eq!( - CommaSeparatedRequirements::from_str("requests>=2.1,<3").unwrap(), - CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string(),]) - ); -} - -#[test] -fn double_specifiers() { - assert_eq!( - CommaSeparatedRequirements::from_str("requests>=2.1,<3, flask").unwrap(), - CommaSeparatedRequirements(vec!["requests>=2.1,<3".to_string(), "flask".to_string()]) - ); -} diff --git a/crates/uv-cli/src/version.rs b/crates/uv-cli/src/version.rs index 9eaa462b209e..076d6cf8b2f4 100644 --- a/crates/uv-cli/src/version.rs +++ b/crates/uv-cli/src/version.rs @@ -77,4 +77,73 @@ pub fn version() -> VersionInfo { } #[cfg(test)] -mod tests; +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 + } + } + "#); + } +} diff --git a/crates/uv-cli/src/version/tests.rs b/crates/uv-cli/src/version/tests.rs deleted file mode 100644 index de54bd6e1f13..000000000000 --- a/crates/uv-cli/src/version/tests.rs +++ /dev/null @@ -1,68 +0,0 @@ -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/src/html.rs b/crates/uv-client/src/html.rs index 020fed404b0c..648c5165a7bb 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -207,4 +207,1000 @@ pub enum Error { } #[cfg(test)] -mod tests; +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: `Jinja2-3.1.2-py3-none-any.whl`"); + } + + #[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: `Jinja2-3.1.2-py3-none-any.whl`"); + } + + #[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/html/tests.rs b/crates/uv-client/src/html/tests.rs deleted file mode 100644 index 4b0b35e6cf81..000000000000 --- a/crates/uv-client/src/html/tests.rs +++ /dev/null @@ -1,995 +0,0 @@ -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: `Jinja2-3.1.2-py3-none-any.whl`"); -} - -#[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: `Jinja2-3.1.2-py3-none-any.whl`"); -} - -#[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 e728d6f08e1d..6860386bff25 100644 --- a/crates/uv-client/src/httpcache/control.rs +++ b/crates/uv-client/src/httpcache/control.rs @@ -453,4 +453,326 @@ impl CacheControlDirective { } #[cfg(test)] -mod tests; +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![] + }, + ] + ); + } +} diff --git a/crates/uv-client/src/httpcache/control/tests.rs b/crates/uv-client/src/httpcache/control/tests.rs deleted file mode 100644 index 34cf770fa272..000000000000 --- a/crates/uv-client/src/httpcache/control/tests.rs +++ /dev/null @@ -1,320 +0,0 @@ -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 46b47d244518..58982426ae24 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -947,4 +947,107 @@ impl Connectivity { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-client/src/registry_client/tests.rs b/crates/uv-client/src/registry_client/tests.rs deleted file mode 100644 index 6e31a5cf35d0..000000000000 --- a/crates/uv-client/src/registry_client/tests.rs +++ /dev/null @@ -1,102 +0,0 @@ -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-configuration/src/build_options.rs b/crates/uv-configuration/src/build_options.rs index cfdb807aff7a..1a62a1a12983 100644 --- a/crates/uv-configuration/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -354,4 +354,66 @@ pub enum IndexStrategy { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-configuration/src/build_options/tests.rs b/crates/uv-configuration/src/build_options/tests.rs deleted file mode 100644 index 4eabc928bbbb..000000000000 --- a/crates/uv-configuration/src/build_options/tests.rs +++ /dev/null @@ -1,61 +0,0 @@ -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 a25521e49fb5..7d89074837d5 100644 --- a/crates/uv-configuration/src/config_settings.rs +++ b/crates/uv-configuration/src/config_settings.rs @@ -213,4 +213,82 @@ impl<'de> serde::Deserialize<'de> for ConfigSettings { } #[cfg(test)] -mod tests; +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"}"#); + } +} diff --git a/crates/uv-configuration/src/config_settings/tests.rs b/crates/uv-configuration/src/config_settings/tests.rs deleted file mode 100644 index 120a331cea91..000000000000 --- a/crates/uv-configuration/src/config_settings/tests.rs +++ /dev/null @@ -1,77 +0,0 @@ -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 326ef8602a04..9901b42cd95d 100644 --- a/crates/uv-configuration/src/trusted_host.rs +++ b/crates/uv-configuration/src/trusted_host.rs @@ -161,4 +161,50 @@ impl schemars::JsonSchema for TrustedHost { } #[cfg(test)] -mod tests; +mod tests { + #[test] + fn parse() { + assert_eq!( + "*".parse::().unwrap(), + super::TrustedHost::Wildcard + ); + + assert_eq!( + "example.com".parse::().unwrap(), + super::TrustedHost::Host { + scheme: None, + host: "example.com".to_string(), + port: None + } + ); + + assert_eq!( + "example.com:8080".parse::().unwrap(), + super::TrustedHost::Host { + scheme: None, + host: "example.com".to_string(), + port: Some(8080) + } + ); + + assert_eq!( + "https://example.com".parse::().unwrap(), + super::TrustedHost::Host { + scheme: Some("https".to_string()), + host: "example.com".to_string(), + port: None + } + ); + + assert_eq!( + "https://example.com/hello/world" + .parse::() + .unwrap(), + super::TrustedHost::Host { + scheme: Some("https".to_string()), + host: "example.com".to_string(), + port: None + } + ); + } +} diff --git a/crates/uv-configuration/src/trusted_host/tests.rs b/crates/uv-configuration/src/trusted_host/tests.rs deleted file mode 100644 index 6eef745a9d30..000000000000 --- a/crates/uv-configuration/src/trusted_host/tests.rs +++ /dev/null @@ -1,45 +0,0 @@ -#[test] -fn parse() { - assert_eq!( - "*".parse::().unwrap(), - super::TrustedHost::Wildcard - ); - - assert_eq!( - "example.com".parse::().unwrap(), - super::TrustedHost::Host { - scheme: None, - host: "example.com".to_string(), - port: None - } - ); - - assert_eq!( - "example.com:8080".parse::().unwrap(), - super::TrustedHost::Host { - scheme: None, - host: "example.com".to_string(), - port: Some(8080) - } - ); - - assert_eq!( - "https://example.com".parse::().unwrap(), - super::TrustedHost::Host { - scheme: Some("https".to_string()), - host: "example.com".to_string(), - port: None - } - ); - - assert_eq!( - "https://example.com/hello/world" - .parse::() - .unwrap(), - super::TrustedHost::Host { - scheme: Some("https".to_string()), - host: "example.com".to_string(), - port: None - } - ); -} diff --git a/crates/uv-dev/src/generate_cli_reference.rs b/crates/uv-dev/src/generate_cli_reference.rs index ec9a2eff5ccb..997b2d720923 100644 --- a/crates/uv-dev/src/generate_cli_reference.rs +++ b/crates/uv-dev/src/generate_cli_reference.rs @@ -330,4 +330,24 @@ fn emit_possible_options(opt: &clap::Arg, output: &mut String) { } #[cfg(test)] -mod tests; +mod tests { + use std::env; + + use anyhow::Result; + + use uv_static::EnvVars; + + use crate::generate_all::Mode; + + use super::{main, Args}; + + #[test] + fn test_generate_cli_reference() -> Result<()> { + let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) + } +} diff --git a/crates/uv-dev/src/generate_cli_reference/tests.rs b/crates/uv-dev/src/generate_cli_reference/tests.rs deleted file mode 100644 index ee626eb35b13..000000000000 --- a/crates/uv-dev/src/generate_cli_reference/tests.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::env; - -use anyhow::Result; - -use uv_static::EnvVars; - -use crate::generate_all::Mode; - -use super::{main, Args}; - -#[test] -fn test_generate_cli_reference() -> Result<()> { - let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { - Mode::Write - } else { - Mode::Check - }; - main(&Args { mode }) -} diff --git a/crates/uv-dev/src/generate_env_vars_reference.rs b/crates/uv-dev/src/generate_env_vars_reference.rs index 0f702a2306cf..a7a9e905625f 100644 --- a/crates/uv-dev/src/generate_env_vars_reference.rs +++ b/crates/uv-dev/src/generate_env_vars_reference.rs @@ -100,4 +100,24 @@ fn render(var: &str, doc: &str) -> String { } #[cfg(test)] -mod tests; +mod tests { + use std::env; + + use anyhow::Result; + + use uv_static::EnvVars; + + use crate::generate_all::Mode; + + use super::{main, Args}; + + #[test] + fn test_generate_env_vars_reference() -> Result<()> { + let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { + Mode::Write + } else { + Mode::Check + }; + main(&Args { mode }) + } +} diff --git a/crates/uv-dev/src/generate_env_vars_reference/tests.rs b/crates/uv-dev/src/generate_env_vars_reference/tests.rs deleted file mode 100644 index 896fb0ecdd4e..000000000000 --- a/crates/uv-dev/src/generate_env_vars_reference/tests.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::env; - -use anyhow::Result; - -use uv_static::EnvVars; - -use crate::generate_all::Mode; - -use super::{main, Args}; - -#[test] -fn test_generate_env_vars_reference() -> Result<()> { - let mode = if env::var(EnvVars::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 fcf112587ca4..82c5a7bd9222 100644 --- a/crates/uv-dev/src/generate_json_schema.rs +++ b/crates/uv-dev/src/generate_json_schema.rs @@ -107,4 +107,24 @@ fn generate() -> String { } #[cfg(test)] -mod tests; +mod tests { + use std::env; + + use anyhow::Result; + + use uv_static::EnvVars; + + use crate::generate_all::Mode; + + use super::{main, Args}; + + #[test] + fn test_generate_json_schema() -> Result<()> { + let mode = if env::var(EnvVars::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/tests.rs b/crates/uv-dev/src/generate_json_schema/tests.rs deleted file mode 100644 index 137172e5e793..000000000000 --- a/crates/uv-dev/src/generate_json_schema/tests.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::env; - -use anyhow::Result; - -use uv_static::EnvVars; - -use crate::generate_all::Mode; - -use super::{main, Args}; - -#[test] -fn test_generate_json_schema() -> Result<()> { - let mode = if env::var(EnvVars::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 2f97cca5c61a..96ae15340517 100644 --- a/crates/uv-dev/src/generate_options_reference.rs +++ b/crates/uv-dev/src/generate_options_reference.rs @@ -386,4 +386,24 @@ impl Visit for CollectOptionsVisitor { } #[cfg(test)] -mod tests; +mod tests { + use std::env; + + use anyhow::Result; + + use uv_static::EnvVars; + + use crate::generate_all::Mode; + + use super::{main, Args}; + + #[test] + fn test_generate_options_reference() -> Result<()> { + let mode = if env::var(EnvVars::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/tests.rs b/crates/uv-dev/src/generate_options_reference/tests.rs deleted file mode 100644 index dfcdd952f25c..000000000000 --- a/crates/uv-dev/src/generate_options_reference/tests.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::env; - -use anyhow::Result; - -use uv_static::EnvVars; - -use crate::generate_all::Mode; - -use super::{main, Args}; - -#[test] -fn test_generate_options_reference() -> Result<()> { - let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { - Mode::Write - } else { - Mode::Check - }; - main(&Args { mode }) -} diff --git a/crates/uv-distribution-filename/src/egg.rs b/crates/uv-distribution-filename/src/egg.rs index 84bbf3b6415e..f891169a3448 100644 --- a/crates/uv-distribution-filename/src/egg.rs +++ b/crates/uv-distribution-filename/src/egg.rs @@ -80,4 +80,38 @@ impl FromStr for EggInfoFilename { } #[cfg(test)] -mod tests; +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()); + } +} diff --git a/crates/uv-distribution-filename/src/egg/tests.rs b/crates/uv-distribution-filename/src/egg/tests.rs deleted file mode 100644 index 47c4dd56bdee..000000000000 --- a/crates/uv-distribution-filename/src/egg/tests.rs +++ /dev/null @@ -1,33 +0,0 @@ -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 2c1c26740cf8..a3920a32d7f4 100644 --- a/crates/uv-distribution-filename/src/source_dist.rs +++ b/crates/uv-distribution-filename/src/source_dist.rs @@ -170,4 +170,58 @@ enum SourceDistFilenameErrorKind { } #[cfg(test)] -mod tests; +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()); + } +} diff --git a/crates/uv-distribution-filename/src/source_dist/tests.rs b/crates/uv-distribution-filename/src/source_dist/tests.rs deleted file mode 100644 index f81f089f1b2e..000000000000 --- a/crates/uv-distribution-filename/src/source_dist/tests.rs +++ /dev/null @@ -1,48 +0,0 @@ -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 09b675b1158b..a91c3e1417b2 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -234,4 +234,101 @@ pub enum WheelFilenameError { } #[cfg(test)] -mod tests; +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 + ); + } + } +} 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 deleted file mode 100644 index ee6b96c713d0..000000000000 --- a/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_build_tag.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -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 deleted file mode 100644 index 811974f8b0e8..000000000000 --- a/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_multiple_tags.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -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 deleted file mode 100644 index ddd909a31e5f..000000000000 --- a/crates/uv-distribution-filename/src/wheel/snapshots/uv_distribution_filename__wheel__tests__ok_single_tags.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -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 deleted file mode 100644 index 59c94e4a3273..000000000000 --- a/crates/uv-distribution-filename/src/wheel/tests.rs +++ /dev/null @@ -1,95 +0,0 @@ -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-fs/src/path.rs b/crates/uv-fs/src/path.rs index 54941df43540..4e5e8049eb25 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -397,4 +397,108 @@ impl<'de> serde::de::Deserialize<'de> for PortablePathBuf { } #[cfg(test)] -mod tests; +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)); + } + } +} diff --git a/crates/uv-fs/src/path/tests.rs b/crates/uv-fs/src/path/tests.rs deleted file mode 100644 index 6d5f619dfc45..000000000000 --- a/crates/uv-fs/src/path/tests.rs +++ /dev/null @@ -1,103 +0,0 @@ -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/src/sha.rs b/crates/uv-git/src/sha.rs index 11a5c91ae64d..5ae7d3ec8e7b 100644 --- a/crates/uv-git/src/sha.rs +++ b/crates/uv-git/src/sha.rs @@ -112,4 +112,19 @@ impl Display for GitOid { } #[cfg(test)] -mod tests; +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) + ); + } +} diff --git a/crates/uv-git/src/sha/tests.rs b/crates/uv-git/src/sha/tests.rs deleted file mode 100644 index cac5e187dae6..000000000000 --- a/crates/uv-git/src/sha/tests.rs +++ /dev/null @@ -1,14 +0,0 @@ -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-normalize/src/dist_info_name.rs b/crates/uv-normalize/src/dist_info_name.rs index f885589e7657..0340cc6777bf 100644 --- a/crates/uv-normalize/src/dist_info_name.rs +++ b/crates/uv-normalize/src/dist_info_name.rs @@ -86,4 +86,23 @@ impl AsRef for DistInfoName<'_> { } #[cfg(test)] -mod tests; +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"); + } + } +} diff --git a/crates/uv-normalize/src/dist_info_name/tests.rs b/crates/uv-normalize/src/dist_info_name/tests.rs deleted file mode 100644 index 156b0887a68d..000000000000 --- a/crates/uv-normalize/src/dist_info_name/tests.rs +++ /dev/null @@ -1,18 +0,0 @@ -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 29c6480a8c5a..0360a5a1a161 100644 --- a/crates/uv-normalize/src/lib.rs +++ b/crates/uv-normalize/src/lib.rs @@ -120,4 +120,79 @@ impl Display for InvalidNameError { impl Error for InvalidNameError {} #[cfg(test)] -mod tests; +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()); + } + } +} diff --git a/crates/uv-normalize/src/tests.rs b/crates/uv-normalize/src/tests.rs deleted file mode 100644 index 96368fcc78f7..000000000000 --- a/crates/uv-normalize/src/tests.rs +++ /dev/null @@ -1,74 +0,0 @@ -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-options-metadata/src/lib.rs b/crates/uv-options-metadata/src/lib.rs index fad3b992138c..81f3ebafef8f 100644 --- a/crates/uv-options-metadata/src/lib.rs +++ b/crates/uv-options-metadata/src/lib.rs @@ -316,4 +316,158 @@ impl Display for PossibleValue { } #[cfg(test)] -mod tests; +mod tests { + 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-options-metadata/src/tests.rs b/crates/uv-options-metadata/src/tests.rs deleted file mode 100644 index 20374f0af646..000000000000 --- a/crates/uv-options-metadata/src/tests.rs +++ /dev/null @@ -1,153 +0,0 @@ -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/src/lib.rs b/crates/uv-pep440/src/lib.rs index 6fe6c531952d..ab8cf1e831e6 100644 --- a/crates/uv-pep440/src/lib.rs +++ b/crates/uv-pep440/src/lib.rs @@ -40,7 +40,20 @@ pub use { mod version; mod version_specifier; -#[cfg(test)] -mod tests; #[cfg(feature = "version-ranges")] mod version_ranges; + +#[cfg(test)] +mod tests { + 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/tests.rs b/crates/uv-pep440/src/tests.rs deleted file mode 100644 index 4495f0db713b..000000000000 --- a/crates/uv-pep440/src/tests.rs +++ /dev/null @@ -1,11 +0,0 @@ -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 b66de1a2f186..59962cbbb54e 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -2564,4 +2564,1386 @@ pub static MIN_VERSION: LazyLock = LazyLock::new(|| Version::from_str("0a0.dev0").unwrap()); #[cfg(test)] -mod tests; +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_segments(vec![LocalSegment::String("123abc".to_string())]), + ), + ( + "1.2+123abc456", + Version::new([1, 2]) + .with_local_segments(vec![LocalSegment::String("123abc456".to_string())]), + ), + ( + "1.2+abc", + Version::new([1, 2]) + .with_local_segments(vec![LocalSegment::String("abc".to_string())]), + ), + ( + "1.2+abc123", + Version::new([1, 2]) + .with_local_segments(vec![LocalSegment::String("abc123".to_string())]), + ), + ( + "1.2+abc123def", + Version::new([1, 2]) + .with_local_segments(vec![LocalSegment::String("abc123def".to_string())]), + ), + ( + "1.2+1234.abc", + Version::new([1, 2]).with_local_segments(vec![ + LocalSegment::Number(1234), + LocalSegment::String("abc".to_string()), + ]), + ), + ( + "1.2+123456", + Version::new([1, 2]).with_local_segments(vec![LocalSegment::Number(123_456)]), + ), + ( + "1.2.r32+123456", + Version::new([1, 2]) + .with_post(Some(32)) + .with_local_segments(vec![LocalSegment::Number(123_456)]), + ), + ( + "1.2.rev33+123456", + Version::new([1, 2]) + .with_post(Some(33)) + .with_local_segments(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_segments(vec![LocalSegment::String("123abc".to_string())]), + ), + ( + "1!1.2+123abc456", + Version::new([1, 2]) + .with_epoch(1) + .with_local_segments(vec![LocalSegment::String("123abc456".to_string())]), + ), + ( + "1!1.2+abc", + Version::new([1, 2]) + .with_epoch(1) + .with_local_segments(vec![LocalSegment::String("abc".to_string())]), + ), + ( + "1!1.2+abc123", + Version::new([1, 2]) + .with_epoch(1) + .with_local_segments(vec![LocalSegment::String("abc123".to_string())]), + ), + ( + "1!1.2+abc123def", + Version::new([1, 2]) + .with_epoch(1) + .with_local_segments(vec![LocalSegment::String("abc123def".to_string())]), + ), + ( + "1!1.2+1234.abc", + Version::new([1, 2]).with_epoch(1).with_local_segments(vec![ + LocalSegment::Number(1234), + LocalSegment::String("abc".to_string()), + ]), + ), + ( + "1!1.2+123456", + Version::new([1, 2]) + .with_epoch(1) + .with_local_segments(vec![LocalSegment::Number(123_456)]), + ), + ( + "1!1.2.r32+123456", + Version::new([1, 2]) + .with_epoch(1) + .with_post(Some(32)) + .with_local_segments(vec![LocalSegment::Number(123_456)]), + ), + ( + "1!1.2.rev33+123456", + Version::new([1, 2]) + .with_epoch(1) + .with_post(Some(33)) + .with_local_segments(vec![LocalSegment::Number(123_456)]), + ), + ( + "98765!1.2.rev33+123456", + Version::new([1, 2]) + .with_epoch(98765) + .with_post(Some(33)) + .with_local_segments(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_segments(vec![LocalSegment::Number(2)]) + ); + assert_eq!( + p("5+a"), + Version::new([5]).with_local_segments(vec![LocalSegment::String("a".to_string())]) + ); + assert_eq!( + p("5+abc.123"), + Version::new([5]).with_local_segments(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + ]) + ); + assert_eq!( + p("5+123.abc"), + Version::new([5]).with_local_segments(vec![ + LocalSegment::Number(123), + LocalSegment::String("abc".to_string()), + ]) + ); + assert_eq!( + p("5+18446744073709551615.abc"), + Version::new([5]).with_local_segments(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_segments(vec![ + LocalSegment::String("18446744073709551616".to_string()), + LocalSegment::String("abc".to_string()), + ]) + ); + assert_eq!( + p("5+ABC.123"), + Version::new([5]).with_local_segments(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + ]) + ); + assert_eq!( + p("5+ABC-123.4_5_xyz-MNO"), + Version::new([5]).with_local_segments(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_segments(vec![ + LocalSegment::String("abc".to_string()), + LocalSegment::Number(123), + ]) + ); + assert_eq!( + p("5.6.7+abc-foo00123"), + Version::new([5, 6, 7]).with_local_segments(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_segments(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_segments(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 local_sentinel_version() { + let sentinel = Version::new([1, 0]).with_local(LocalVersion::Max); + + // Ensure that the "max local version" sentinel is less than the following versions. + let versions = &["1.0.post0", "1.1"]; + + for greater in versions { + let greater = greater.parse::().unwrap(); + assert_eq!( + sentinel.cmp(&greater), + Ordering::Less, + "less: {:?}\ngreater: {:?}", + greater.as_bloated_debug(), + sentinel.as_bloated_debug(), + ); + } + + // Ensure that the "max local version" sentinel is greater than the following versions. + let versions = &["1.0", "1.0.a0", "1.0+local"]; + + for less in versions { + let less = less.parse::().unwrap(); + assert_eq!( + sentinel.cmp(&less), + Ordering::Greater, + "less: {:?}\ngreater: {:?}", + sentinel.as_bloated_debug(), + less.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/tests.rs b/crates/uv-pep440/src/version/tests.rs deleted file mode 100644 index 78b38a0a2f78..000000000000 --- a/crates/uv-pep440/src/version/tests.rs +++ /dev/null @@ -1,1380 +0,0 @@ -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_segments(vec![LocalSegment::String("123abc".to_string())]), - ), - ( - "1.2+123abc456", - Version::new([1, 2]) - .with_local_segments(vec![LocalSegment::String("123abc456".to_string())]), - ), - ( - "1.2+abc", - Version::new([1, 2]).with_local_segments(vec![LocalSegment::String("abc".to_string())]), - ), - ( - "1.2+abc123", - Version::new([1, 2]) - .with_local_segments(vec![LocalSegment::String("abc123".to_string())]), - ), - ( - "1.2+abc123def", - Version::new([1, 2]) - .with_local_segments(vec![LocalSegment::String("abc123def".to_string())]), - ), - ( - "1.2+1234.abc", - Version::new([1, 2]).with_local_segments(vec![ - LocalSegment::Number(1234), - LocalSegment::String("abc".to_string()), - ]), - ), - ( - "1.2+123456", - Version::new([1, 2]).with_local_segments(vec![LocalSegment::Number(123_456)]), - ), - ( - "1.2.r32+123456", - Version::new([1, 2]) - .with_post(Some(32)) - .with_local_segments(vec![LocalSegment::Number(123_456)]), - ), - ( - "1.2.rev33+123456", - Version::new([1, 2]) - .with_post(Some(33)) - .with_local_segments(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_segments(vec![LocalSegment::String("123abc".to_string())]), - ), - ( - "1!1.2+123abc456", - Version::new([1, 2]) - .with_epoch(1) - .with_local_segments(vec![LocalSegment::String("123abc456".to_string())]), - ), - ( - "1!1.2+abc", - Version::new([1, 2]) - .with_epoch(1) - .with_local_segments(vec![LocalSegment::String("abc".to_string())]), - ), - ( - "1!1.2+abc123", - Version::new([1, 2]) - .with_epoch(1) - .with_local_segments(vec![LocalSegment::String("abc123".to_string())]), - ), - ( - "1!1.2+abc123def", - Version::new([1, 2]) - .with_epoch(1) - .with_local_segments(vec![LocalSegment::String("abc123def".to_string())]), - ), - ( - "1!1.2+1234.abc", - Version::new([1, 2]).with_epoch(1).with_local_segments(vec![ - LocalSegment::Number(1234), - LocalSegment::String("abc".to_string()), - ]), - ), - ( - "1!1.2+123456", - Version::new([1, 2]) - .with_epoch(1) - .with_local_segments(vec![LocalSegment::Number(123_456)]), - ), - ( - "1!1.2.r32+123456", - Version::new([1, 2]) - .with_epoch(1) - .with_post(Some(32)) - .with_local_segments(vec![LocalSegment::Number(123_456)]), - ), - ( - "1!1.2.rev33+123456", - Version::new([1, 2]) - .with_epoch(1) - .with_post(Some(33)) - .with_local_segments(vec![LocalSegment::Number(123_456)]), - ), - ( - "98765!1.2.rev33+123456", - Version::new([1, 2]) - .with_epoch(98765) - .with_post(Some(33)) - .with_local_segments(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_segments(vec![LocalSegment::Number(2)]) - ); - assert_eq!( - p("5+a"), - Version::new([5]).with_local_segments(vec![LocalSegment::String("a".to_string())]) - ); - assert_eq!( - p("5+abc.123"), - Version::new([5]).with_local_segments(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - ]) - ); - assert_eq!( - p("5+123.abc"), - Version::new([5]).with_local_segments(vec![ - LocalSegment::Number(123), - LocalSegment::String("abc".to_string()), - ]) - ); - assert_eq!( - p("5+18446744073709551615.abc"), - Version::new([5]).with_local_segments(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_segments(vec![ - LocalSegment::String("18446744073709551616".to_string()), - LocalSegment::String("abc".to_string()), - ]) - ); - assert_eq!( - p("5+ABC.123"), - Version::new([5]).with_local_segments(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - ]) - ); - assert_eq!( - p("5+ABC-123.4_5_xyz-MNO"), - Version::new([5]).with_local_segments(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_segments(vec![ - LocalSegment::String("abc".to_string()), - LocalSegment::Number(123), - ]) - ); - assert_eq!( - p("5.6.7+abc-foo00123"), - Version::new([5, 6, 7]).with_local_segments(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_segments(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_segments(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 local_sentinel_version() { - let sentinel = Version::new([1, 0]).with_local(LocalVersion::Max); - - // Ensure that the "max local version" sentinel is less than the following versions. - let versions = &["1.0.post0", "1.1"]; - - for greater in versions { - let greater = greater.parse::().unwrap(); - assert_eq!( - sentinel.cmp(&greater), - Ordering::Less, - "less: {:?}\ngreater: {:?}", - greater.as_bloated_debug(), - sentinel.as_bloated_debug(), - ); - } - - // Ensure that the "max local version" sentinel is greater than the following versions. - let versions = &["1.0", "1.0.a0", "1.0+local"]; - - for less in versions { - let less = less.parse::().unwrap(); - assert_eq!( - sentinel.cmp(&less), - Ordering::Greater, - "less: {:?}\ngreater: {:?}", - sentinel.as_bloated_debug(), - less.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 9ab5b41b1a34..122f7a3e3699 100644 --- a/crates/uv-pep440/src/version_specifier.rs +++ b/crates/uv-pep440/src/version_specifier.rs @@ -794,4 +794,963 @@ pub(crate) fn parse_version_specifiers( } #[cfg(test)] -mod tests; +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_segments(vec![LocalSegment::Number(5)]), + } + .into(), + ) + .into(), + ), + ( + ">=1.0+deadbeef", + ParseErrorKind::InvalidSpecifier( + BuildErrorKind::OperatorLocalCombo { + operator: Operator::GreaterThanEqual, + version: Version::new([1, 0]).with_local_segments(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_segments(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_segments(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_segments(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-pep440/src/version_specifier/tests.rs b/crates/uv-pep440/src/version_specifier/tests.rs deleted file mode 100644 index ea0bdecc1c49..000000000000 --- a/crates/uv-pep440/src/version_specifier/tests.rs +++ /dev/null @@ -1,951 +0,0 @@ -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_segments(vec![LocalSegment::Number(5)]), - } - .into(), - ) - .into(), - ), - ( - ">=1.0+deadbeef", - ParseErrorKind::InvalidSpecifier( - BuildErrorKind::OperatorLocalCombo { - operator: Operator::GreaterThanEqual, - version: Version::new([1, 0]) - .with_local_segments(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_segments(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_segments(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_segments(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/src/lib.rs b/crates/uv-pep508/src/lib.rs index d9683c5e759b..ce1ad3f041bd 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -985,4 +985,805 @@ fn parse_pep508_requirement( } #[cfg(test)] -mod tests; +mod tests { + //! 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 leading_whitespace() { + let numpy = Requirement::::from_str(" numpy").unwrap(); + assert_eq!(numpy.name.as_ref(), "numpy"); + } + + #[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_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] + #[cfg(feature = "non-pep508-extensions")] + 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] + #[cfg(feature = "non-pep508-extensions")] + 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/marker/algebra.rs b/crates/uv-pep508/src/marker/algebra.rs index 2730058c333a..773d822c3f7c 100644 --- a/crates/uv-pep508/src/marker/algebra.rs +++ b/crates/uv-pep508/src/marker/algebra.rs @@ -1233,4 +1233,89 @@ impl fmt::Debug for NodeId { } #[cfg(test)] -mod tests; +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); + } +} diff --git a/crates/uv-pep508/src/marker/algebra/tests.rs b/crates/uv-pep508/src/marker/algebra/tests.rs deleted file mode 100644 index 00559d4cd90d..000000000000 --- a/crates/uv-pep508/src/marker/algebra/tests.rs +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index f07553bf612f..000000000000 --- a/crates/uv-pep508/src/tests.rs +++ /dev/null @@ -1,797 +0,0 @@ -//! 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 leading_whitespace() { - let numpy = Requirement::::from_str(" numpy").unwrap(); - assert_eq!(numpy.name.as_ref(), "numpy"); -} - -#[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_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] -#[cfg(feature = "non-pep508-extensions")] -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] -#[cfg(feature = "non-pep508-extensions")] -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 467bea6a0545..3c98b33128c1 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -525,4 +525,61 @@ impl std::fmt::Display for Scheme { } #[cfg(test)] -mod tests; +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) + ); + } +} diff --git a/crates/uv-pep508/src/verbatim_url/tests.rs b/crates/uv-pep508/src/verbatim_url/tests.rs deleted file mode 100644 index 5706d0394bf2..000000000000 --- a/crates/uv-pep508/src/verbatim_url/tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -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-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 31f6289e7067..b3b31f43fcc7 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -580,4 +580,1535 @@ fn get_mac_binary_formats(arch: Arch) -> Vec { } #[cfg(test)] -mod tests; +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 + "### + ); + } +} diff --git a/crates/uv-platform-tags/src/tags/tests.rs b/crates/uv-platform-tags/src/tags/tests.rs deleted file mode 100644 index fca79b94899c..000000000000 --- a/crates/uv-platform-tags/src/tags/tests.rs +++ /dev/null @@ -1,1530 +0,0 @@ -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-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index dc45d613f30b..338bb1ef4b54 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -805,4 +805,278 @@ async fn handle_response(registry: &Url, response: Response) -> Result<(), Publi } #[cfg(test)] -mod tests; +mod tests { + 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 raw_filename = "tqdm-999.0.0.tar.gz"; + let file = PathBuf::from("../../scripts/links/").join(raw_filename); + let filename = DistFilename::try_from_normalized_filename(raw_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, + raw_filename, + &filename, + &Url::parse("https://example.org/upload").unwrap(), + &BaseClientBuilder::new().build(), + 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 raw_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(raw_filename); + let filename = DistFilename::try_from_normalized_filename(raw_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, + raw_filename, + &filename, + &Url::parse("https://example.org/upload").unwrap(), + &BaseClientBuilder::new().build(), + 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-publish/src/tests.rs b/crates/uv-publish/src/tests.rs deleted file mode 100644 index 99a675f105e0..000000000000 --- a/crates/uv-publish/src/tests.rs +++ /dev/null @@ -1,273 +0,0 @@ -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 raw_filename = "tqdm-999.0.0.tar.gz"; - let file = PathBuf::from("../../scripts/links/").join(raw_filename); - let filename = DistFilename::try_from_normalized_filename(raw_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, - raw_filename, - &filename, - &Url::parse("https://example.org/upload").unwrap(), - &BaseClientBuilder::new().build(), - 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 raw_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(raw_filename); - let filename = DistFilename::try_from_normalized_filename(raw_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, - raw_filename, - &filename, - &Url::parse("https://example.org/upload").unwrap(), - &BaseClientBuilder::new().build(), - 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/src/lenient_requirement.rs b/crates/uv-pypi-types/src/lenient_requirement.rs index 9dafcbce8a42..8baaf96ed757 100644 --- a/crates/uv-pypi-types/src/lenient_requirement.rs +++ b/crates/uv-pypi-types/src/lenient_requirement.rs @@ -163,4 +163,261 @@ impl<'de> Deserialize<'de> for LenientVersionSpecifiers { } #[cfg(test)] -mod tests; +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); + } +} diff --git a/crates/uv-pypi-types/src/lenient_requirement/tests.rs b/crates/uv-pypi-types/src/lenient_requirement/tests.rs deleted file mode 100644 index 64b8b58cf380..000000000000 --- a/crates/uv-pypi-types/src/lenient_requirement/tests.rs +++ /dev/null @@ -1,251 +0,0 @@ -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 8a1bfd9cc66f..f10a73968832 100644 --- a/crates/uv-pypi-types/src/metadata/metadata23.rs +++ b/crates/uv-pypi-types/src/metadata/metadata23.rs @@ -275,4 +275,37 @@ impl FromStr for Metadata23 { } #[cfg(test)] -mod tests; +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 包")); + } +} diff --git a/crates/uv-pypi-types/src/metadata/metadata23/tests.rs b/crates/uv-pypi-types/src/metadata/metadata23/tests.rs deleted file mode 100644 index be2f358cf1b9..000000000000 --- a/crates/uv-pypi-types/src/metadata/metadata23/tests.rs +++ /dev/null @@ -1,32 +0,0 @@ -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 9d854f7c7a5d..9699a247e9e6 100644 --- a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs +++ b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs @@ -157,4 +157,75 @@ impl ResolutionMetadata { } #[cfg(test)] -mod tests; +mod tests { + use std::str::FromStr; + + use uv_normalize::PackageName; + use uv_pep440::Version; + + use super::*; + use crate::MetadataError; + + #[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/metadata_resolver/tests.rs b/crates/uv-pypi-types/src/metadata/metadata_resolver/tests.rs deleted file mode 100644 index e4a0e90e50b4..000000000000 --- a/crates/uv-pypi-types/src/metadata/metadata_resolver/tests.rs +++ /dev/null @@ -1,68 +0,0 @@ -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 0cf74e9fb18c..4abbd7bff2ac 100644 --- a/crates/uv-pypi-types/src/metadata/pyproject_toml.rs +++ b/crates/uv-pypi-types/src/metadata/pyproject_toml.rs @@ -225,4 +225,90 @@ impl RequiresDist { } #[cfg(test)] -mod tests; +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()]); + } +} diff --git a/crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs b/crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs deleted file mode 100644 index c137fb99bf49..000000000000 --- a/crates/uv-pypi-types/src/metadata/pyproject_toml/tests.rs +++ /dev/null @@ -1,85 +0,0 @@ -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 f84d36c89cfe..681e9c3ceca0 100644 --- a/crates/uv-pypi-types/src/metadata/requires_txt.rs +++ b/crates/uv-pypi-types/src/metadata/requires_txt.rs @@ -109,4 +109,61 @@ impl RequiresTxt { } #[cfg(test)] -mod tests; +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(), + ] + ); + } +} diff --git a/crates/uv-pypi-types/src/metadata/requires_txt/tests.rs b/crates/uv-pypi-types/src/metadata/requires_txt/tests.rs deleted file mode 100644 index ee20fd62e9b8..000000000000 --- a/crates/uv-pypi-types/src/metadata/requires_txt/tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -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 3afcadd709d2..ecc1031b5655 100644 --- a/crates/uv-pypi-types/src/parsed_url.rs +++ b/crates/uv-pypi-types/src/parsed_url.rs @@ -507,4 +507,46 @@ impl From for Url { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-pypi-types/src/parsed_url/tests.rs b/crates/uv-pypi-types/src/parsed_url/tests.rs deleted file mode 100644 index b5f606bbc4eb..000000000000 --- a/crates/uv-pypi-types/src/parsed_url/tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -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 e7823cda499a..2cfd65ac597f 100644 --- a/crates/uv-pypi-types/src/requirement.rs +++ b/crates/uv-pypi-types/src/requirement.rs @@ -798,4 +798,50 @@ pub fn redact_credentials(url: &mut Url) { } #[cfg(test)] -mod tests; +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); + } +} diff --git a/crates/uv-pypi-types/src/requirement/tests.rs b/crates/uv-pypi-types/src/requirement/tests.rs deleted file mode 100644 index 24aa27aa306b..000000000000 --- a/crates/uv-pypi-types/src/requirement/tests.rs +++ /dev/null @@ -1,45 +0,0 @@ -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 efba207deab1..5d6868e8d4ef 100644 --- a/crates/uv-pypi-types/src/simple_json.rs +++ b/crates/uv-pypi-types/src/simple_json.rs @@ -425,4 +425,75 @@ pub enum HashError { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-pypi-types/src/simple_json/tests.rs b/crates/uv-pypi-types/src/simple_json/tests.rs deleted file mode 100644 index cbf94b52bbd1..000000000000 --- a/crates/uv-pypi-types/src/simple_json/tests.rs +++ /dev/null @@ -1,62 +0,0 @@ -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/src/discovery.rs b/crates/uv-python/src/discovery.rs index 7b039aee7b29..51151f3feb74 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -2522,4 +2522,537 @@ fn split_wheel_tag_release_version(version: Version) -> Version { } #[cfg(test)] -mod tests; +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", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3", + ], + ); + + case("default", &["python", "python3"]); + + case("3", &["python3", "python"]); + + case("4", &["python4", "python"]); + + case("3.13", &["python3.13", "python3", "python"]); + + case("pypy", &["pypy", "pypy3", "python", "python3"]); + + case( + "pypy@3.10", + &[ + "pypy3.10", + "pypy3", + "pypy", + "python3.10", + "python3", + "python", + ], + ); + + case( + "3.13t", + &[ + "python3.13t", + "python3.13", + "python3t", + "python3", + "pythont", + "python", + ], + ); + case("3t", &["python3t", "python3", "pythont", "python"]); + + case( + "3.13.2", + &["python3.13.2", "python3.13", "python3", "python"], + ); + + case( + "3.13rc2", + &["python3.13rc2", "python3.13", "python3", "python"], + ); + } +} diff --git a/crates/uv-python/src/discovery/tests.rs b/crates/uv-python/src/discovery/tests.rs deleted file mode 100644 index 97057a3816ad..000000000000 --- a/crates/uv-python/src/discovery/tests.rs +++ /dev/null @@ -1,528 +0,0 @@ -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", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3", - ], - ); - - case("default", &["python", "python3"]); - - case("3", &["python3", "python"]); - - case("4", &["python4", "python"]); - - case("3.13", &["python3.13", "python3", "python"]); - - case("pypy", &["pypy", "pypy3", "python", "python3"]); - - case( - "pypy@3.10", - &[ - "pypy3.10", - "pypy3", - "pypy", - "python3.10", - "python3", - "python", - ], - ); - - case( - "3.13t", - &[ - "python3.13t", - "python3.13", - "python3t", - "python3", - "pythont", - "python", - ], - ); - case("3t", &["python3t", "python3", "pythont", "python"]); - - case( - "3.13.2", - &["python3.13.2", "python3.13", "python3", "python"], - ); - - case( - "3.13rc2", - &["python3.13rc2", "python3.13", "python3", "python"], - ); -} diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index c7bd1740a360..9dd540e3928d 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -823,4 +823,108 @@ impl InterpreterInfo { #[cfg(unix)] #[cfg(test)] -mod tests; +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() + ); + } +} diff --git a/crates/uv-python/src/interpreter/tests.rs b/crates/uv-python/src/interpreter/tests.rs deleted file mode 100644 index 100d0d1a1940..000000000000 --- a/crates/uv-python/src/interpreter/tests.rs +++ /dev/null @@ -1,103 +0,0 @@ -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 3c293a17381b..703e8a033fb7 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -90,4 +90,2404 @@ 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; +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_static::EnvVars; + + 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 + (EnvVars::UV_TEST_PYTHON_PATH, None), + // Ignore active virtual environments (i.e. that the dev is using) + (EnvVars::VIRTUAL_ENV, None), + (EnvVars::PATH, path.as_deref()), + // Use the temporary python directory + ( + EnvVars::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(&[(EnvVars::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(&[(EnvVars::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(&[(EnvVars::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(&[(EnvVars::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( + &[(EnvVars::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" + ); + + let baseenv = context.tempdir.child("base"); + TestContext::mock_conda_prefix(&baseenv, "3.12.1")?; + + // But not if it's a base environment + let result = context.run_with_vars( + &[ + ("CONDA_PREFIX", Some(baseenv.as_os_str())), + ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )?; + + assert!( + matches!(result, Err(PythonNotFound { .. })), + "We should not allow the non-virtual environment; got {result:?}" + ); + + // Unless, system interpreters are included... + let python = context.run_with_vars( + &[ + ("CONDA_PREFIX", Some(baseenv.as_os_str())), + ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )??; + + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.12.1", + "We should find the base conda environment" + ); + + // If the environment name doesn't match the default, we should not treat it as system + let python = context.run_with_vars( + &[ + ("CONDA_PREFIX", Some(condaenv.as_os_str())), + ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), + ], + || { + 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 find the conda environment" + ); + + 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( + &[ + (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())), + (EnvVars::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( + &[(EnvVars::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(&[(EnvVars::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( + &[( + EnvVars::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( + &[ + ( + EnvVars::UV_INTERNAL__PARENT_INTERPRETER, + Some(parent.as_os_str()), + ), + (EnvVars::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( + &[ + ( + EnvVars::UV_INTERNAL__PARENT_INTERPRETER, + Some(parent.as_os_str()), + ), + (EnvVars::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( + &[ + ( + EnvVars::UV_INTERNAL__PARENT_INTERPRETER, + Some(parent.as_os_str()), + ), + (EnvVars::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( + &[ + ( + EnvVars::UV_INTERNAL__PARENT_INTERPRETER, + Some(parent.as_os_str()), + ), + (EnvVars::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( + &[( + EnvVars::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(&[(EnvVars::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(&[(EnvVars::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(&[(EnvVars::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( + &[(EnvVars::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( + &[(EnvVars::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_executable_name_preference() -> 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, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.0", + "We should prefer the versioned one when a version is requested" + ); + + let python = context + .run(|| { + find_python_installation( + &PythonRequest::parse("pypy"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should prefer the generic one when no version is requested" + ); + + 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, + )?; + TestContext::create_mock_interpreter( + &context.tempdir.join("python"), + &PythonVersion::from_str("3.10.2").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, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + "We should prefer the implementation name over the generic name" + ); + + let python = context + .run(|| { + find_python_installation( + &PythonRequest::parse("default"), + EnvironmentPreference::Any, + PythonPreference::OnlySystem, + &context.cache, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.2", + "We should prefer the generic name over the implementation name, but not the versioned name" + ); + + // We prefer `python` executables over `graalpy` executables in the same directory + // if they are both GraalPy + let mut context = TestContext::new()?; + 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, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.1", + ); + + // 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, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.2", + ); + + // And `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, + ) + }) + .unwrap() + .unwrap(); + assert_eq!( + python.interpreter().python_full_version().to_string(), + "3.10.3", + ); + + 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-python/src/libc.rs b/crates/uv-python/src/libc.rs index 15f058999b78..179c818a980e 100644 --- a/crates/uv-python/src/libc.rs +++ b/crates/uv-python/src/libc.rs @@ -235,4 +235,45 @@ fn find_ld_path_at(path: impl AsRef) -> Option { } #[cfg(test)] -mod tests; +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 }); + } +} diff --git a/crates/uv-python/src/libc/tests.rs b/crates/uv-python/src/libc/tests.rs deleted file mode 100644 index f85e97237009..000000000000 --- a/crates/uv-python/src/libc/tests.rs +++ /dev/null @@ -1,40 +0,0 @@ -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 0e5920b05110..b64f6ba6f7b7 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -168,4 +168,37 @@ impl PythonVersion { } #[cfg(test)] -mod tests; +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"); + } +} diff --git a/crates/uv-python/src/python_version/tests.rs b/crates/uv-python/src/python_version/tests.rs deleted file mode 100644 index 7065b1ae0124..000000000000 --- a/crates/uv-python/src/python_version/tests.rs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index df92db2a89f0..000000000000 --- a/crates/uv-python/src/tests.rs +++ /dev/null @@ -1,2384 +0,0 @@ -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_static::EnvVars; - -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 - (EnvVars::UV_TEST_PYTHON_PATH, None), - // Ignore active virtual environments (i.e. that the dev is using) - (EnvVars::VIRTUAL_ENV, None), - (EnvVars::PATH, path.as_deref()), - // Use the temporary python directory - ( - EnvVars::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(&[(EnvVars::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(&[(EnvVars::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(&[(EnvVars::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(&[(EnvVars::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( - &[(EnvVars::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" - ); - - let baseenv = context.tempdir.child("base"); - TestContext::mock_conda_prefix(&baseenv, "3.12.1")?; - - // But not if it's a base environment - let result = context.run_with_vars( - &[ - ("CONDA_PREFIX", Some(baseenv.as_os_str())), - ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlyVirtual, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )?; - - assert!( - matches!(result, Err(PythonNotFound { .. })), - "We should not allow the non-virtual environment; got {result:?}" - ); - - // Unless, system interpreters are included... - let python = context.run_with_vars( - &[ - ("CONDA_PREFIX", Some(baseenv.as_os_str())), - ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), - ], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlySystem, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )??; - - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.1", - "We should find the base conda environment" - ); - - // If the environment name doesn't match the default, we should not treat it as system - let python = context.run_with_vars( - &[ - ("CONDA_PREFIX", Some(condaenv.as_os_str())), - ("CONDA_DEFAULT_ENV", Some(&OsString::from("base"))), - ], - || { - 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 find the conda environment" - ); - - 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( - &[ - (EnvVars::VIRTUAL_ENV, Some(venv.as_os_str())), - (EnvVars::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( - &[(EnvVars::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(&[(EnvVars::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( - &[( - EnvVars::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( - &[ - ( - EnvVars::UV_INTERNAL__PARENT_INTERPRETER, - Some(parent.as_os_str()), - ), - (EnvVars::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( - &[ - ( - EnvVars::UV_INTERNAL__PARENT_INTERPRETER, - Some(parent.as_os_str()), - ), - (EnvVars::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( - &[ - ( - EnvVars::UV_INTERNAL__PARENT_INTERPRETER, - Some(parent.as_os_str()), - ), - (EnvVars::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( - &[ - ( - EnvVars::UV_INTERNAL__PARENT_INTERPRETER, - Some(parent.as_os_str()), - ), - (EnvVars::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( - &[( - EnvVars::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(&[(EnvVars::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(&[(EnvVars::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(&[(EnvVars::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( - &[(EnvVars::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( - &[(EnvVars::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_executable_name_preference() -> 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, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.0", - "We should prefer the versioned one when a version is requested" - ); - - let python = context - .run(|| { - find_python_installation( - &PythonRequest::parse("pypy"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should prefer the generic one when no version is requested" - ); - - 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, - )?; - TestContext::create_mock_interpreter( - &context.tempdir.join("python"), - &PythonVersion::from_str("3.10.2").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, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - "We should prefer the implementation name over the generic name" - ); - - let python = context - .run(|| { - find_python_installation( - &PythonRequest::parse("default"), - EnvironmentPreference::Any, - PythonPreference::OnlySystem, - &context.cache, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.2", - "We should prefer the generic name over the implementation name, but not the versioned name" - ); - - // We prefer `python` executables over `graalpy` executables in the same directory - // if they are both GraalPy - let mut context = TestContext::new()?; - 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, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.1", - ); - - // 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, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.2", - ); - - // And `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, - ) - }) - .unwrap() - .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.10.3", - ); - - 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-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 90d2a5b5ac05..069e2687732b 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -4166,4 +4166,286 @@ 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); + } +} diff --git a/crates/uv-resolver/src/lock/tests.rs b/crates/uv-resolver/src/lock/tests.rs deleted file mode 100644 index 40e03a027eda..000000000000 --- a/crates/uv-resolver/src/lock/tests.rs +++ /dev/null @@ -1,281 +0,0 @@ -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 3010fcedf8f8..af9cb64dd1c7 100644 --- a/crates/uv-resolver/src/redirect.rs +++ b/crates/uv-resolver/src/redirect.rs @@ -84,4 +84,66 @@ fn apply_redirect(url: &VerbatimUrl, redirect: Url) -> VerbatimUrl { } #[cfg(test)] -mod tests; +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(()) + } +} diff --git a/crates/uv-resolver/src/redirect/tests.rs b/crates/uv-resolver/src/redirect/tests.rs deleted file mode 100644 index 6a151adc893a..000000000000 --- a/crates/uv-resolver/src/redirect/tests.rs +++ /dev/null @@ -1,61 +0,0 @@ -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 56e51cb532cb..682c040acc0f 100644 --- a/crates/uv-resolver/src/requires_python.rs +++ b/crates/uv-resolver/src/requires_python.rs @@ -808,4 +808,163 @@ impl From for Bound { } #[cfg(test)] -mod tests; +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); + 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); + 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}" + ); + } + + let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); + let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.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").unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); + let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.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); + 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); + 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:?}"); + } + } + } + + #[test] + fn is_exact_without_patch() { + let test_cases = [ + ("==3.12", true), + ("==3.10, <3.11", true), + ("==3.10, <=3.11", true), + ("==3.12.1", false), + ("==3.12.*", false), + ("==3.*", false), + (">=3.10", false), + (">3.9", false), + ("<4.0", false), + (">=3.10, <3.11", false), + ("", false), + ]; + for (version, expected) in test_cases { + let version_specifiers = VersionSpecifiers::from_str(version).unwrap(); + let requires_python = RequiresPython::from_specifiers(&version_specifiers); + assert_eq!(requires_python.is_exact_without_patch(), expected); + } + } +} diff --git a/crates/uv-resolver/src/requires_python/tests.rs b/crates/uv-resolver/src/requires_python/tests.rs deleted file mode 100644 index dc8e97564c61..000000000000 --- a/crates/uv-resolver/src/requires_python/tests.rs +++ /dev/null @@ -1,158 +0,0 @@ -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); - 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); - 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}" - ); - } - - let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers); - let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.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").unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers); - let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.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); - 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); - 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:?}"); - } - } -} - -#[test] -fn is_exact_without_patch() { - let test_cases = [ - ("==3.12", true), - ("==3.10, <3.11", true), - ("==3.10, <=3.11", true), - ("==3.12.1", false), - ("==3.12.*", false), - ("==3.*", false), - (">=3.10", false), - (">3.9", false), - ("<4.0", false), - (">=3.10, <3.11", false), - ("", false), - ]; - for (version, expected) in test_cases { - let version_specifiers = VersionSpecifiers::from_str(version).unwrap(); - let requires_python = RequiresPython::from_specifiers(&version_specifiers); - assert_eq!(requires_python.is_exact_without_patch(), expected); - } -} diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 02b26b88dc3c..32c6e932753e 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -494,4 +494,233 @@ fn serialize_metadata(metadata: &str) -> String { } #[cfg(test)] -mod tests; +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); + } +} diff --git a/crates/uv-scripts/src/tests.rs b/crates/uv-scripts/src/tests.rs deleted file mode 100644 index caf508792902..000000000000 --- a/crates/uv-scripts/src/tests.rs +++ /dev/null @@ -1,228 +0,0 @@ -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-version/src/lib.rs b/crates/uv-version/src/lib.rs index bf17cf1e83d8..20a8940a1216 100644 --- a/crates/uv-version/src/lib.rs +++ b/crates/uv-version/src/lib.rs @@ -6,4 +6,11 @@ pub fn version() -> &'static str { } #[cfg(test)] -mod tests; +mod tests { + use super::*; + + #[test] + fn test_get_version() { + assert_eq!(version().to_string(), env!("CARGO_PKG_VERSION").to_string()); + } +} diff --git a/crates/uv-version/src/tests.rs b/crates/uv-version/src/tests.rs deleted file mode 100644 index 7887964c548b..000000000000 --- a/crates/uv-version/src/tests.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::*; - -#[test] -fn test_get_version() { - assert_eq!(version().to_string(), env!("CARGO_PKG_VERSION").to_string()); -} diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index bb7107da0746..9ea28cc35632 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1446,4 +1446,1024 @@ impl VirtualProject { #[cfg(test)] #[cfg(unix)] // Avoid path escaping for the unit tests -mod tests; +mod tests { + use std::env; + use std::path::Path; + use std::str::FromStr; + + use anyhow::Result; + use assert_fs::fixture::ChildPath; + use assert_fs::prelude::*; + use insta::{assert_json_snapshot, assert_snapshot}; + + use uv_normalize::GroupName; + + use crate::pyproject::{DependencyGroupSpecifier, PyProjectToml}; + use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; + use crate::WorkspaceError; + + 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, + ) -> Result<(ProjectWorkspace, String), (WorkspaceError, String)> { + let root_escaped = regex::escape(folder.to_string_lossy().as_ref()); + let project = ProjectWorkspace::discover(folder, &DiscoveryOptions::default()) + .await + .map_err(|error| (error, root_escaped.clone()))?; + + Ok((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": {}, + "indexes": [], + "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, + "dependency-groups": 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": {}, + "indexes": [], + "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, + "dependency-groups": 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 + } + ] + }, + "indexes": [], + "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 + } + ] + }, + "index": null, + "workspace": { + "members": [ + "packages/*" + ], + "exclude": null + }, + "managed": null, + "package": null, + "default-groups": null, + "dev-dependencies": null, + "override-dependencies": null, + "constraint-dependencies": null, + "environments": null, + "conflicts": null + } + }, + "dependency-groups": 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": {}, + "indexes": [], + "pyproject_toml": { + "project": null, + "tool": { + "uv": { + "sources": null, + "index": null, + "workspace": { + "members": [ + "packages/*" + ], + "exclude": null + }, + "managed": null, + "package": null, + "default-groups": null, + "dev-dependencies": null, + "override-dependencies": null, + "constraint-dependencies": null, + "environments": null, + "conflicts": null + } + }, + "dependency-groups": 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": {}, + "indexes": [], + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": null, + "dependency-groups": 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.unwrap(); + 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": {}, + "indexes": [], + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "index": null, + "workspace": { + "members": [ + "packages/*" + ], + "exclude": [ + "packages/bird-feeder" + ] + }, + "managed": null, + "package": null, + "default-groups": null, + "dev-dependencies": null, + "override-dependencies": null, + "constraint-dependencies": null, + "environments": null, + "conflicts": null + } + }, + "dependency-groups": 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.unwrap(); + 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": {}, + "indexes": [], + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "index": null, + "workspace": { + "members": [ + "packages/seeds", + "packages/bird-feeder" + ], + "exclude": [ + "packages/bird-feeder" + ] + }, + "managed": null, + "package": null, + "default-groups": null, + "dev-dependencies": null, + "override-dependencies": null, + "constraint-dependencies": null, + "environments": null, + "conflicts": null + } + }, + "dependency-groups": 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.unwrap(); + 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": {}, + "indexes": [], + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "index": null, + "workspace": { + "members": [ + "packages/seeds", + "packages/bird-feeder" + ], + "exclude": [ + "packages" + ] + }, + "managed": null, + "package": null, + "default-groups": null, + "dev-dependencies": null, + "override-dependencies": null, + "constraint-dependencies": null, + "environments": null, + "conflicts": null + } + }, + "dependency-groups": 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.unwrap(); + 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": {}, + "indexes": [], + "pyproject_toml": { + "project": { + "name": "albatross", + "version": "0.1.0", + "requires-python": ">=3.12", + "dependencies": [ + "tqdm>=4,<5" + ], + "optional-dependencies": null + }, + "tool": { + "uv": { + "sources": null, + "index": null, + "workspace": { + "members": [ + "packages/seeds", + "packages/bird-feeder" + ], + "exclude": [ + "packages/*" + ] + }, + "managed": null, + "package": null, + "default-groups": null, + "dev-dependencies": null, + "override-dependencies": null, + "constraint-dependencies": null, + "environments": null, + "conflicts": null + } + }, + "dependency-groups": null + } + } + } + "###); + }); + + Ok(()) + } + + #[test] + fn read_dependency_groups() { + let toml = r#" +[dependency-groups] +foo = ["a", {include-group = "bar"}] +bar = ["b"] +"#; + + let result = + PyProjectToml::from_string(toml.to_string()).expect("Deserialization should succeed"); + + let groups = result + .dependency_groups + .expect("`dependency-groups` should be present"); + let foo = groups + .get(&GroupName::from_str("foo").unwrap()) + .expect("Group `foo` should be present"); + assert_eq!( + foo, + &[ + DependencyGroupSpecifier::Requirement("a".to_string()), + DependencyGroupSpecifier::IncludeGroup { + include_group: GroupName::from_str("bar").unwrap(), + } + ] + ); + + let bar = groups + .get(&GroupName::from_str("bar").unwrap()) + .expect("Group `bar` should be present"); + assert_eq!( + bar, + &[DependencyGroupSpecifier::Requirement("b".to_string())] + ); + } + + #[tokio::test] + async fn nested_workspace() -> 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/*"] + "#, + )?; + + // 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"] + + [tool.uv.workspace] + members = ["nested_packages/*"] + "#, + )?; + + let (error, root_escaped) = temporary_test(root.as_ref()).await.unwrap_err(); + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_snapshot!( + error, + @"Nested workspaces are not supported, but workspace member (`[ROOT]/packages/seeds`) has a `uv.workspace` table"); + }); + + Ok(()) + } + + #[tokio::test] + async fn duplicate_names() -> 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/*"] + "#, + )?; + + // 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"] + + [tool.uv.workspace] + members = ["nested_packages/*"] + "#, + )?; + + // Create an included package (`seeds2`). + root.child("packages") + .child("seeds2") + .child("pyproject.toml") + .write_str( + r#" + [project] + name = "seeds" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = ["idna==3.6"] + + [tool.uv.workspace] + members = ["nested_packages/*"] + "#, + )?; + + let (error, root_escaped) = temporary_test(root.as_ref()).await.unwrap_err(); + let filters = vec![(root_escaped.as_str(), "[ROOT]")]; + insta::with_settings!({filters => filters}, { + assert_snapshot!( + error, + @"Two workspace members are both named: `seeds`: `[ROOT]/packages/seeds` and `[ROOT]/packages/seeds2`"); + }); + + Ok(()) + } +} diff --git a/crates/uv-workspace/src/workspace/tests.rs b/crates/uv-workspace/src/workspace/tests.rs deleted file mode 100644 index dab98ba7bff4..000000000000 --- a/crates/uv-workspace/src/workspace/tests.rs +++ /dev/null @@ -1,1017 +0,0 @@ -use std::env; -use std::path::Path; -use std::str::FromStr; - -use anyhow::Result; -use assert_fs::fixture::ChildPath; -use assert_fs::prelude::*; -use insta::{assert_json_snapshot, assert_snapshot}; - -use uv_normalize::GroupName; - -use crate::pyproject::{DependencyGroupSpecifier, PyProjectToml}; -use crate::workspace::{DiscoveryOptions, ProjectWorkspace}; -use crate::WorkspaceError; - -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, -) -> Result<(ProjectWorkspace, String), (WorkspaceError, String)> { - let root_escaped = regex::escape(folder.to_string_lossy().as_ref()); - let project = ProjectWorkspace::discover(folder, &DiscoveryOptions::default()) - .await - .map_err(|error| (error, root_escaped.clone()))?; - - Ok((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": {}, - "indexes": [], - "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, - "dependency-groups": 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": {}, - "indexes": [], - "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, - "dependency-groups": 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 - } - ] - }, - "indexes": [], - "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 - } - ] - }, - "index": null, - "workspace": { - "members": [ - "packages/*" - ], - "exclude": null - }, - "managed": null, - "package": null, - "default-groups": null, - "dev-dependencies": null, - "override-dependencies": null, - "constraint-dependencies": null, - "environments": null, - "conflicts": null - } - }, - "dependency-groups": 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": {}, - "indexes": [], - "pyproject_toml": { - "project": null, - "tool": { - "uv": { - "sources": null, - "index": null, - "workspace": { - "members": [ - "packages/*" - ], - "exclude": null - }, - "managed": null, - "package": null, - "default-groups": null, - "dev-dependencies": null, - "override-dependencies": null, - "constraint-dependencies": null, - "environments": null, - "conflicts": null - } - }, - "dependency-groups": 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": {}, - "indexes": [], - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": null, - "dependency-groups": 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.unwrap(); - 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": {}, - "indexes": [], - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "index": null, - "workspace": { - "members": [ - "packages/*" - ], - "exclude": [ - "packages/bird-feeder" - ] - }, - "managed": null, - "package": null, - "default-groups": null, - "dev-dependencies": null, - "override-dependencies": null, - "constraint-dependencies": null, - "environments": null, - "conflicts": null - } - }, - "dependency-groups": 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.unwrap(); - 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": {}, - "indexes": [], - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "index": null, - "workspace": { - "members": [ - "packages/seeds", - "packages/bird-feeder" - ], - "exclude": [ - "packages/bird-feeder" - ] - }, - "managed": null, - "package": null, - "default-groups": null, - "dev-dependencies": null, - "override-dependencies": null, - "constraint-dependencies": null, - "environments": null, - "conflicts": null - } - }, - "dependency-groups": 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.unwrap(); - 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": {}, - "indexes": [], - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "index": null, - "workspace": { - "members": [ - "packages/seeds", - "packages/bird-feeder" - ], - "exclude": [ - "packages" - ] - }, - "managed": null, - "package": null, - "default-groups": null, - "dev-dependencies": null, - "override-dependencies": null, - "constraint-dependencies": null, - "environments": null, - "conflicts": null - } - }, - "dependency-groups": 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.unwrap(); - 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": {}, - "indexes": [], - "pyproject_toml": { - "project": { - "name": "albatross", - "version": "0.1.0", - "requires-python": ">=3.12", - "dependencies": [ - "tqdm>=4,<5" - ], - "optional-dependencies": null - }, - "tool": { - "uv": { - "sources": null, - "index": null, - "workspace": { - "members": [ - "packages/seeds", - "packages/bird-feeder" - ], - "exclude": [ - "packages/*" - ] - }, - "managed": null, - "package": null, - "default-groups": null, - "dev-dependencies": null, - "override-dependencies": null, - "constraint-dependencies": null, - "environments": null, - "conflicts": null - } - }, - "dependency-groups": null - } - } - } - "###); - }); - - Ok(()) -} - -#[test] -fn read_dependency_groups() { - let toml = r#" -[dependency-groups] -foo = ["a", {include-group = "bar"}] -bar = ["b"] -"#; - - let result = - PyProjectToml::from_string(toml.to_string()).expect("Deserialization should succeed"); - - let groups = result - .dependency_groups - .expect("`dependency-groups` should be present"); - let foo = groups - .get(&GroupName::from_str("foo").unwrap()) - .expect("Group `foo` should be present"); - assert_eq!( - foo, - &[ - DependencyGroupSpecifier::Requirement("a".to_string()), - DependencyGroupSpecifier::IncludeGroup { - include_group: GroupName::from_str("bar").unwrap(), - } - ] - ); - - let bar = groups - .get(&GroupName::from_str("bar").unwrap()) - .expect("Group `bar` should be present"); - assert_eq!( - bar, - &[DependencyGroupSpecifier::Requirement("b".to_string())] - ); -} - -#[tokio::test] -async fn nested_workspace() -> 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/*"] - "#, - )?; - - // 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"] - - [tool.uv.workspace] - members = ["nested_packages/*"] - "#, - )?; - - let (error, root_escaped) = temporary_test(root.as_ref()).await.unwrap_err(); - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_snapshot!( - error, - @"Nested workspaces are not supported, but workspace member (`[ROOT]/packages/seeds`) has a `uv.workspace` table"); - }); - - Ok(()) -} - -#[tokio::test] -async fn duplicate_names() -> 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/*"] - "#, - )?; - - // 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"] - - [tool.uv.workspace] - members = ["nested_packages/*"] - "#, - )?; - - // Create an included package (`seeds2`). - root.child("packages") - .child("seeds2") - .child("pyproject.toml") - .write_str( - r#" - [project] - name = "seeds" - version = "1.0.0" - requires-python = ">=3.12" - dependencies = ["idna==3.6"] - - [tool.uv.workspace] - members = ["nested_packages/*"] - "#, - )?; - - let (error, root_escaped) = temporary_test(root.as_ref()).await.unwrap_err(); - let filters = vec![(root_escaped.as_str(), "[ROOT]")]; - insta::with_settings!({filters => filters}, { - assert_snapshot!( - error, - @"Two workspace members are both named: `seeds`: `[ROOT]/packages/seeds` and `[ROOT]/packages/seeds2`"); - }); - - Ok(()) -} diff --git a/crates/uv/src/version/tests.rs b/crates/uv/src/version/tests.rs deleted file mode 100644 index 1627cb7d8ee5..000000000000 --- a/crates/uv/src/version/tests.rs +++ /dev/null @@ -1,68 +0,0 @@ -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 - } - } - "###); -}