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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-72
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`");
|
||||
}
|
||||
@@ -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
|
||||
");
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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()])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()])
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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+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=">=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=">=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,
|
||||
},
|
||||
],
|
||||
}
|
||||
"###);
|
||||
}
|
||||
@@ -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![]
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -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=">=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=">=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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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"}"#);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-27
@@ -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",
|
||||
],
|
||||
},
|
||||
)
|
||||
-29
@@ -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",
|
||||
],
|
||||
},
|
||||
)
|
||||
-22
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
+1383
-1
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
[](https://pypi.org/project/tqdm)
|
||||
[](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==",
|
||||
},
|
||||
},
|
||||
..
|
||||
}
|
||||
"###);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
[](https://pypi.org/project/tqdm)
|
||||
[](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==",
|
||||
},
|
||||
},
|
||||
..
|
||||
}
|
||||
"###);
|
||||
});
|
||||
}
|
||||
@@ -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(),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
+2401
-1
File diff suppressed because it is too large
Load Diff
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user