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.
This commit is contained in:
Charlie Marsh
2024-11-18 15:11:46 -05:00
committed by GitHub
parent 747d69dc96
commit d08bfee718
114 changed files with 15321 additions and 15344 deletions
+74 -1
View File
@@ -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);
}
}
-72
View File
@@ -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);
}
+108 -1
View File
@@ -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));
}
}
-106
View File
@@ -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));
}
+130 -1
View File
@@ -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);
}
}
-128
View File
@@ -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);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+86 -1
View File
@@ -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(())
}
}
-84
View File
@@ -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(())
}
+244 -1
View File
@@ -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
");
}
}
+405 -1
View File
@@ -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 <ferris@rustacean.net>
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 <konstin@mailbox.org>
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`");
}
}
@@ -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 <ferris@rustacean.net>
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 <konstin@mailbox.org>
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`");
}
-242
View File
@@ -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
");
}
+141 -1
View File
@@ -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(())
}
}
@@ -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(())
}
+63 -1
View File
@@ -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()])
);
}
}
-61
View File
@@ -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()])
);
}
+70 -1
View File
@@ -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
}
}
"#);
}
}
-68
View File
@@ -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
}
}
"#);
}
File diff suppressed because it is too large Load Diff
-995
View File
@@ -1,995 +0,0 @@
use super::*;
#[test]
fn parse_sha256() {
let text = r#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#md5=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<head>
<base href="https://index.python.org/">
</head>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2&#43;233fca715f49-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2+233fca715f49-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#sha256%3D4095ada29e51070f7d199a0a5bdf5c8d8e238e03f0bf4dcc02571e78c9ae800d">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl">cpu/torchtext-0.17.0%2Bcpu-cp39-cp39-win_amd64.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a>Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
";
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl?project=legacy">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#sha256">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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#"
<!DOCTYPE html>
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>
<body>
<a href="https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl">cuda100/jaxlib-0.1.52+cuda100-cp36-none-manylinux2010_x86_64.whl</a><br>
<a href="https://storage.googleapis.com/jax-releases/cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl">cuda100/jaxlib-0.1.52+cuda100-cp37-none-manylinux2010_x86_64.whl</a><br>
</body>
</html>
"#;
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: <https://github.com/astral-sh/uv/issues/1388#issuecomment-1947659088>
#[test]
fn parse_code_artifact_index_html() {
let text = r#"
<!DOCTYPE html>
<html>
<head>
<title>Links for flask</title>
</head>
<body>
<h1>Links for flask</h1>
<a href="0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237" data-gpg-sig="false" >Flask-0.1.tar.gz</a>
<br/>
<a href="0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373" data-gpg-sig="false" >Flask-0.10.1.tar.gz</a>
<br/>
<a href="3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403" data-requires-python="&gt;=3.8" data-gpg-sig="false" >flask-3.0.1.tar.gz</a>
<br/>
</body>
</html>
"#;
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#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#sha256=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" data-requires-python="&gt;=3.8,">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
"#;
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: <https://peps.python.org/pep-0714/>).
#[test]
fn parse_core_metadata() {
let text = r#"
<!DOCTYPE html>
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl" data-dist-info-metadata="true">Jinja2-3.1.2-py3-none-any.whl</a><br/>
<a href="/whl/Jinja2-3.1.3-py3-none-any.whl" data-core-metadata="true">Jinja2-3.1.3-py3-none-any.whl</a><br/>
<a href="/whl/Jinja2-3.1.4-py3-none-any.whl" data-dist-info-metadata="false">Jinja2-3.1.4-py3-none-any.whl</a><br/>
<a href="/whl/Jinja2-3.1.5-py3-none-any.whl" data-core-metadata="false">Jinja2-3.1.5-py3-none-any.whl</a><br/>
<a href="/whl/Jinja2-3.1.6-py3-none-any.whl" data-core-metadata="true" data-dist-info-metadata="false">Jinja2-3.1.6-py3-none-any.whl</a><br/>
</body>
</html>
"#;
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,
},
],
}
"###);
}
+323 -1
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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![]
},
]
);
}
}
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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![]
},
]
);
}
+104 -1
View File
@@ -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<String> = 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: <https://github.com/astral-sh/uv/issues/1388>
#[test]
fn relative_urls_code_artifact() -> Result<(), JoinRelativeError> {
let text = r#"
<!DOCTYPE html>
<html>
<head>
<title>Links for flask</title>
</head>
<body>
<h1>Links for flask</h1>
<a href="0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237" data-gpg-sig="false" >Flask-0.1.tar.gz</a>
<br/>
<a href="0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373" data-gpg-sig="false" >Flask-0.10.1.tar.gz</a>
<br/>
<a href="3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403" data-requires-python="&gt;=3.8" data-gpg-sig="false" >flask-3.0.1.tar.gz</a>
<br/>
</body>
</html>
"#;
// 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::<Result<Vec<_>, JoinRelativeError>>()?;
let urls = urls.iter().map(reqwest::Url::as_str).collect::<Vec<_>>();
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(())
}
}
@@ -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<String> = 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: <https://github.com/astral-sh/uv/issues/1388>
#[test]
fn relative_urls_code_artifact() -> Result<(), JoinRelativeError> {
let text = r#"
<!DOCTYPE html>
<html>
<head>
<title>Links for flask</title>
</head>
<body>
<h1>Links for flask</h1>
<a href="0.1/Flask-0.1.tar.gz#sha256=9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237" data-gpg-sig="false" >Flask-0.1.tar.gz</a>
<br/>
<a href="0.10.1/Flask-0.10.1.tar.gz#sha256=4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373" data-gpg-sig="false" >Flask-0.10.1.tar.gz</a>
<br/>
<a href="3.0.1/flask-3.0.1.tar.gz#sha256=6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403" data-requires-python="&gt;=3.8" data-gpg-sig="false" >flask-3.0.1.tar.gz</a>
<br/>
</body>
</html>
"#;
// 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::<Result<Vec<_>, JoinRelativeError>>()?;
let urls = urls.iter().map(reqwest::Url::as_str).collect::<Vec<_>>();
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(())
}
+63 -1
View File
@@ -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(())
}
}
@@ -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(())
}
+79 -1
View File
@@ -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"}"#);
}
}
@@ -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"}"#);
}
+47 -1
View File
@@ -161,4 +161,50 @@ impl schemars::JsonSchema for TrustedHost {
}
#[cfg(test)]
mod tests;
mod tests {
#[test]
fn parse() {
assert_eq!(
"*".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Wildcard
);
assert_eq!(
"example.com".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Host {
scheme: None,
host: "example.com".to_string(),
port: None
}
);
assert_eq!(
"example.com:8080".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Host {
scheme: None,
host: "example.com".to_string(),
port: Some(8080)
}
);
assert_eq!(
"https://example.com".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Host {
scheme: Some("https".to_string()),
host: "example.com".to_string(),
port: None
}
);
assert_eq!(
"https://example.com/hello/world"
.parse::<super::TrustedHost>()
.unwrap(),
super::TrustedHost::Host {
scheme: Some("https".to_string()),
host: "example.com".to_string(),
port: None
}
);
}
}
@@ -1,45 +0,0 @@
#[test]
fn parse() {
assert_eq!(
"*".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Wildcard
);
assert_eq!(
"example.com".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Host {
scheme: None,
host: "example.com".to_string(),
port: None
}
);
assert_eq!(
"example.com:8080".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Host {
scheme: None,
host: "example.com".to_string(),
port: Some(8080)
}
);
assert_eq!(
"https://example.com".parse::<super::TrustedHost>().unwrap(),
super::TrustedHost::Host {
scheme: Some("https".to_string()),
host: "example.com".to_string(),
port: None
}
);
assert_eq!(
"https://example.com/hello/world"
.parse::<super::TrustedHost>()
.unwrap(),
super::TrustedHost::Host {
scheme: Some("https".to_string()),
host: "example.com".to_string(),
port: None
}
);
}
+21 -1
View File
@@ -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 })
}
}
@@ -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 })
}
@@ -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 })
}
}
@@ -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 })
}
+21 -1
View File
@@ -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 })
}
}
@@ -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 })
}
@@ -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 })
}
}
@@ -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 })
}
+35 -1
View File
@@ -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());
}
}
@@ -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());
}
@@ -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
///
/// <https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-name>
/// <https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode>
#[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());
}
}
@@ -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
///
/// <https://packaging.python.org/en/latest/specifications/source-distribution-format/#source-distribution-file-name>
/// <https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode>
#[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());
}
+98 -1
View File
@@ -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
);
}
}
}
@@ -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",
],
},
)
@@ -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",
],
},
)
@@ -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",
],
},
)
@@ -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
);
}
}
+105 -1
View File
@@ -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));
}
}
}
-103
View File
@@ -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));
}
}
+16 -1
View File
@@ -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)
);
}
}
-14
View File
@@ -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)
);
}
+20 -1
View File
@@ -86,4 +86,23 @@ impl AsRef<str> 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");
}
}
}
@@ -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");
}
}
+76 -1
View File
@@ -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());
}
}
}
-74
View File
@@ -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());
}
}
+155 -1
View File
@@ -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);
}
}
-153
View File
@@ -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);
}
+15 -2
View File
@@ -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));
}
}
-11
View File
@@ -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));
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+960 -1
View File
@@ -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::*;
/// <https://peps.python.org/pep-0440/#version-matching>
#[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",
];
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L666-L707>
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L709-L750>
///
/// 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<Version> = 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::<Vec<_>>(),
// Verify that the equal (==) operator works correctly
versions
.iter()
.map(move |x| (x, x, Ordering::Equal))
.collect::<Vec<_>>(),
// 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::<Vec<_>>(),
]
.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 <https://github.com/pypa/packaging/issues/617>
#[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::<Version>().unwrap();
let spec = s_spec.parse::<VersionSpecifier>().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(),
);
}
/// <https://github.com/pypa/packaging/blob/e184feef1a28a5c574ec41f5c263a3a573861f5a/tests/test_specifiers.py#L44-L84>
#[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.
/// <https://pypi.org/simple/geopandas/?format=application/vnd.pypi.simple.v1+json>
#[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::<VersionSpecifiers>().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::<VersionSpecifiers>().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::<VersionSpecifiers>().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"
);
}
}
@@ -1,951 +0,0 @@
use std::{cmp::Ordering, str::FromStr};
use indoc::indoc;
use crate::LocalSegment;
use super::*;
/// <https://peps.python.org/pep-0440/#version-matching>
#[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",
];
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L666-L707>
/// <https://github.com/pypa/packaging/blob/237ff3aa348486cf835a980592af3a59fccd6101/tests/test_version.py#L709-L750>
///
/// 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<Version> = 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::<Vec<_>>(),
// Verify that the equal (==) operator works correctly
versions
.iter()
.map(move |x| (x, x, Ordering::Equal))
.collect::<Vec<_>>(),
// 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::<Vec<_>>(),
]
.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 <https://github.com/pypa/packaging/issues/617>
#[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::<Version>().unwrap();
let spec = s_spec.parse::<VersionSpecifier>().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(),
);
}
/// <https://github.com/pypa/packaging/blob/e184feef1a28a5c574ec41f5c263a3a573861f5a/tests/test_specifiers.py#L44-L84>
#[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.
/// <https://pypi.org/simple/geopandas/?format=application/vnd.pypi.simple.v1+json>
#[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::<VersionSpecifiers>().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::<VersionSpecifiers>().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::<VersionSpecifiers>().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"
);
}
+802 -1
View File
@@ -985,4 +985,805 @@ fn parse_pep508_requirement<T: Pep508Url>(
}
#[cfg(test)]
mod tests;
mod tests {
//! Half of these tests are copied from <https://github.com/pypa/packaging/pull/624>
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::<VerbatimUrl>::from_str(input)
.unwrap_err()
.to_string()
}
#[cfg(feature = "non-pep508-extensions")]
fn parse_unnamed_err(input: &str) -> String {
crate::UnnamedRequirement::<VerbatimUrl>::from_str(input)
.unwrap_err()
.to_string()
}
#[cfg(windows)]
#[test]
fn test_preprocess_url_windows() {
use std::path::PathBuf;
let actual = crate::parse_url::<VerbatimUrl>(
&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::<Url>::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::<Url>::from_str(" numpy").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn parenthesized_single() {
let numpy = Requirement::<Url>::from_str("numpy ( >=1.19 )").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn parenthesized_double() {
let numpy = Requirement::<Url>::from_str("numpy ( >=1.19, <2.0 )").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn versions_single() {
let numpy = Requirement::<Url>::from_str("numpy >=1.19 ").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn versions_double() {
let numpy = Requirement::<Url>::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::<VerbatimUrl>::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::<VerbatimUrl>::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::<VerbatimUrl>::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::<Url>::from_str("black[d]").unwrap();
assert_eq!(numpy.extras, vec![ExtraName::from_str("d").unwrap()]);
}
#[test]
fn error_extras2() {
let numpy = Requirement::<Url>::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::<Url>::from_str("black[]").unwrap();
assert_eq!(black.extras, vec![]);
}
#[test]
fn empty_extras_with_spaces() {
let black = Requirement::<Url>::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::<VerbatimUrl>(
&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::<Url>::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::<VerbatimUrl>::parse(requirement, &cwd).is_ok(),
cfg!(feature = "non-pep508-extensions"),
"{}: {:?}",
requirement,
Requirement::<VerbatimUrl>::parse(requirement, &cwd)
);
}
}
#[test]
fn no_space_after_operator() {
let requirement = Requirement::<Url>::from_str("pytest;python_version<='4.0'").unwrap();
assert_eq!(
requirement.to_string(),
"pytest ; python_full_version < '4.1'"
);
let requirement = Requirement::<Url>::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::<VerbatimUrl>::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::<Url>::from_str("pytest").unwrap();
let expected = Requirement::<Url>::from_str("pytest; extra == 'dotenv'").unwrap();
let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
assert_eq!(actual, expected);
let requirement = Requirement::<Url>::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::<Url>::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(())
}
}
+86 -1
View File
@@ -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);
}
}
@@ -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);
}
-797
View File
@@ -1,797 +0,0 @@
//! Half of these tests are copied from <https://github.com/pypa/packaging/pull/624>
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::<VerbatimUrl>::from_str(input)
.unwrap_err()
.to_string()
}
#[cfg(feature = "non-pep508-extensions")]
fn parse_unnamed_err(input: &str) -> String {
crate::UnnamedRequirement::<VerbatimUrl>::from_str(input)
.unwrap_err()
.to_string()
}
#[cfg(windows)]
#[test]
fn test_preprocess_url_windows() {
use std::path::PathBuf;
let actual = crate::parse_url::<VerbatimUrl>(
&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::<Url>::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::<Url>::from_str(" numpy").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn parenthesized_single() {
let numpy = Requirement::<Url>::from_str("numpy ( >=1.19 )").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn parenthesized_double() {
let numpy = Requirement::<Url>::from_str("numpy ( >=1.19, <2.0 )").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn versions_single() {
let numpy = Requirement::<Url>::from_str("numpy >=1.19 ").unwrap();
assert_eq!(numpy.name.as_ref(), "numpy");
}
#[test]
fn versions_double() {
let numpy = Requirement::<Url>::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::<VerbatimUrl>::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::<VerbatimUrl>::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::<VerbatimUrl>::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::<Url>::from_str("black[d]").unwrap();
assert_eq!(numpy.extras, vec![ExtraName::from_str("d").unwrap()]);
}
#[test]
fn error_extras2() {
let numpy = Requirement::<Url>::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::<Url>::from_str("black[]").unwrap();
assert_eq!(black.extras, vec![]);
}
#[test]
fn empty_extras_with_spaces() {
let black = Requirement::<Url>::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::<VerbatimUrl>(&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::<Url>::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::<VerbatimUrl>::parse(requirement, &cwd).is_ok(),
cfg!(feature = "non-pep508-extensions"),
"{}: {:?}",
requirement,
Requirement::<VerbatimUrl>::parse(requirement, &cwd)
);
}
}
#[test]
fn no_space_after_operator() {
let requirement = Requirement::<Url>::from_str("pytest;python_version<='4.0'").unwrap();
assert_eq!(
requirement.to_string(),
"pytest ; python_full_version < '4.1'"
);
let requirement = Requirement::<Url>::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::<VerbatimUrl>::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::<Url>::from_str("pytest").unwrap();
let expected = Requirement::<Url>::from_str("pytest; extra == 'dotenv'").unwrap();
let actual = requirement.with_extra_marker(&ExtraName::from_str("dotenv")?);
assert_eq!(actual, expected);
let requirement = Requirement::<Url>::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::<Url>::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(())
}
+58 -1
View File
@@ -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)
);
}
}
@@ -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)
);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+275 -1
View File
@@ -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<u64>) -> 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 <charlie.r.marsh@gmail.com>
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 <devs@tqdm.ml>
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==",
},
},
..
}
"###);
});
}
}
-273
View File
@@ -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<u64>) -> 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 <charlie.r.marsh@gmail.com>
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 <devs@tqdm.ml>
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==",
},
},
..
}
"###);
});
}
+258 -1
View File
@@ -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);
}
/// <https://pypi.org/simple/shellingham/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/tensorflowonspark/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/celery/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/wincertstore/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/algoliasearch/?format=application/vnd.pypi.simple.v1+json>
/// <https://pypi.org/simple/okta/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://files.pythonhosted.org/packages/74/49/7349527cea7f708e7d3253ab6b32c9b5bdf84a57dde8fc265a33e6a4e662/boto3-1.2.0-py2.py3-none-any.whl>
#[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);
}
/// <https://github.com/celery/celery/blob/6215f34d2675441ef2177bd850bf5f4b442e944c/requirements/default.txt#L1>
#[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);
}
/// <https://github.com/astral-sh/uv/issues/1798>
#[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);
}
/// <https://github.com/astral-sh/uv/issues/2551>
#[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);
}
}
@@ -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);
}
/// <https://pypi.org/simple/shellingham/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/tensorflowonspark/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/celery/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/wincertstore/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://pypi.org/simple/algoliasearch/?format=application/vnd.pypi.simple.v1+json>
/// <https://pypi.org/simple/okta/?format=application/vnd.pypi.simple.v1+json>
#[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);
}
/// <https://files.pythonhosted.org/packages/74/49/7349527cea7f708e7d3253ab6b32c9b5bdf84a57dde8fc265a33e6a4e662/boto3-1.2.0-py2.py3-none-any.whl>
#[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);
}
/// <https://github.com/celery/celery/blob/6215f34d2675441ef2177bd850bf5f4b442e944c/requirements/default.txt#L1>
#[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);
}
/// <https://github.com/astral-sh/uv/issues/1798>
#[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);
}
/// <https://github.com/astral-sh/uv/issues/2551>
#[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);
}
@@ -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<Metadata23, MetadataError> = 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 包"));
}
}
@@ -1,32 +0,0 @@
use super::*;
use crate::MetadataError;
#[test]
fn test_parse_from_str() {
let s = "Metadata-Version: 1.0";
let meta: Result<Metadata23, MetadataError> = 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 包"));
}
@@ -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?= <x@y.org>\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()]);
}
}
@@ -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?= <x@y.org>\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()]);
}
@@ -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()]);
}
}
@@ -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()]);
}
@@ -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(),
]
);
}
}
@@ -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(),
]
);
}
+43 -1
View File
@@ -507,4 +507,46 @@ impl From<ParsedGitUrl> 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(())
}
}
@@ -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(())
}
+47 -1
View File
@@ -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);
}
}
@@ -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);
}
+72 -1
View File
@@ -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::<Hashes>();
assert!(result.is_err());
let result = "blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"
.parse::<Hashes>();
assert!(result.is_err());
Ok(())
}
}
@@ -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::<Hashes>();
assert!(result.is_err());
let result =
"blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619".parse::<Hashes>();
assert!(result.is_err());
Ok(())
}
+534 -1
View File
@@ -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"],
);
}
}
-528
View File
@@ -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"],
);
}
+105 -1
View File
@@ -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()
);
}
}
-103
View File
@@ -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()
);
}
File diff suppressed because it is too large Load Diff
+42 -1
View File
@@ -235,4 +235,45 @@ fn find_ld_path_at(path: impl AsRef<Path>) -> Option<PathBuf> {
}
#[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 });
}
}
-40
View File
@@ -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 });
}
+34 -1
View File
@@ -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");
}
}
@@ -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");
}

Some files were not shown because too many files have changed in this diff Show More