uv/crates/uv-resolver/tests/resolver.rs
konsti 081f20c53e
Add support for tool.uv into distribution building (#3904)
With the change, we remove the special casing of workspace dependencies
and resolve `tool.uv` for all git and directory distributions. This
gives us support for non-editable workspace dependencies and path
dependencies in other workspaces. It removes a lot of special casing
around workspaces. These changes are the groundwork for supporting
`tool.uv` with dynamic metadata.

The basis for this change is moving `Requirement` from
`distribution-types` to `pypi-types` and the lowering logic from
`uv-requirements` to `uv-distribution`. This changes should be split out
in separate PRs.

I've included an example workspace `albatross-root-workspace2` where
`bird-feeder` depends on `a` from another workspace `ab`. There's a
bunch of failing tests and regressed error messages that still need
fixing. It does fix the audited package count for the workspace tests.
2024-05-31 02:42:03 +00:00

789 lines
22 KiB
Rust

#![cfg(feature = "pypi")]
//! Integration tests for the resolver. These tests rely on a live network connection, and hit
//! `PyPI` directly.
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Result;
use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use distribution_types::{CachedDist, IndexLocations, Resolution, SourceDist};
use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder};
use platform_tags::{Arch, Os, Platform, Tags};
use pypi_types::Requirement;
use uv_cache::Cache;
use uv_client::RegistryClientBuilder;
use uv_configuration::{
BuildKind, Concurrency, Constraints, NoBinary, NoBuild, Overrides, PreviewMode, SetupPyStrategy,
};
use uv_distribution::DistributionDatabase;
use uv_interpreter::{find_default_interpreter, Interpreter, PythonEnvironment};
use uv_normalize::PackageName;
use uv_resolver::{
DisplayResolutionGraph, ExcludeNewer, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options,
OptionsBuilder, PreReleaseMode, Preference, PythonRequirement, ResolutionGraph, ResolutionMode,
Resolver,
};
use uv_types::{
BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceBuildTrait,
};
// Exclude any packages uploaded after this date.
static EXCLUDE_NEWER: Lazy<ExcludeNewer> = Lazy::new(|| {
ExcludeNewer::from(
DateTime::parse_from_rfc3339("2023-11-18T12:00:00Z")
.unwrap()
.with_timezone(&Utc),
)
});
struct DummyContext {
cache: Cache,
interpreter: Interpreter,
index_locations: IndexLocations,
}
impl DummyContext {
fn new(cache: Cache, interpreter: Interpreter) -> Self {
Self {
cache,
interpreter,
index_locations: IndexLocations::default(),
}
}
}
impl BuildContext for DummyContext {
type SourceDistBuilder = DummyBuilder;
fn cache(&self) -> &Cache {
&self.cache
}
fn interpreter(&self) -> &Interpreter {
&self.interpreter
}
fn build_isolation(&self) -> BuildIsolation {
BuildIsolation::Isolated
}
fn no_build(&self) -> &NoBuild {
&NoBuild::None
}
fn no_binary(&self) -> &NoBinary {
&NoBinary::None
}
fn setup_py_strategy(&self) -> SetupPyStrategy {
SetupPyStrategy::default()
}
fn index_locations(&self) -> &IndexLocations {
&self.index_locations
}
async fn resolve<'a>(&'a self, _: &'a [Requirement]) -> Result<Resolution> {
panic!("The test should not need to build source distributions")
}
async fn install<'a>(
&'a self,
_: &'a Resolution,
_: &'a PythonEnvironment,
) -> Result<Vec<CachedDist>> {
panic!("The test should not need to build source distributions")
}
async fn setup_build<'a>(
&'a self,
_: &'a Path,
_: Option<&'a Path>,
_: &'a str,
_: Option<&'a SourceDist>,
_: BuildKind,
) -> Result<Self::SourceDistBuilder> {
Ok(DummyBuilder)
}
}
struct DummyBuilder;
impl SourceBuildTrait for DummyBuilder {
async fn metadata(&mut self) -> Result<Option<PathBuf>> {
panic!("The test should not need to build source distributions")
}
async fn wheel<'a>(&'a self, _: &'a Path) -> Result<String> {
panic!("The test should not need to build source distributions")
}
}
async fn resolve(
manifest: Manifest,
options: Options,
markers: &'static MarkerEnvironment,
tags: &Tags,
) -> Result<ResolutionGraph> {
let cache = Cache::temp().unwrap().init().unwrap();
let client = RegistryClientBuilder::new(cache).build();
let flat_index = FlatIndex::default();
let index = InMemoryIndex::default();
let real_interpreter = find_default_interpreter(PreviewMode::Disabled, &Cache::temp().unwrap())
.unwrap()
.expect("Python should be installed")
.into_interpreter();
let interpreter = Interpreter::artificial(real_interpreter.platform().clone(), markers.clone());
let python_requirement = PythonRequirement::from_marker_environment(&interpreter, markers);
let cache = Cache::temp().unwrap().init().unwrap();
let build_context = DummyContext::new(cache, interpreter.clone());
let hashes = HashStrategy::None;
let installed_packages = EmptyInstalledPackages;
let concurrency = Concurrency::default();
let resolver = Resolver::new(
manifest,
options,
&python_requirement,
Some(markers),
tags,
&flat_index,
&index,
&hashes,
&build_context,
installed_packages,
DistributionDatabase::new(
&client,
&build_context,
concurrency.downloads,
PreviewMode::Disabled,
),
)?;
Ok(resolver.resolve().await?)
}
macro_rules! assert_snapshot {
($value:expr, @$snapshot:literal) => {
let snapshot = anstream::adapter::strip_str(&format!("{}", $value)).to_string();
insta::assert_snapshot!(&snapshot, @$snapshot)
};
}
#[tokio::test]
async fn black() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(),
)]);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_colorama() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black[colorama]<=23.9.1").unwrap(),
)]);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
colorama==0.4.6
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
/// Resolve Black with an invalid extra. The resolver should ignore the extra.
#[tokio::test]
async fn black_tensorboard() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black[tensorboard]<=23.9.1").unwrap(),
)]);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_python_310() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(),
)]);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_310, &TAGS_310).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
tomli==2.0.1
# via black
typing-extensions==4.8.0
# via black
"###);
Ok(())
}
/// Resolve `black` with a constraint on `mypy-extensions`, to ensure that constraints are
/// respected.
#[tokio::test]
async fn black_mypy_extensions() -> Result<()> {
let manifest = Manifest::new(
vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(),
)],
Constraints::from_requirements(vec![Requirement::from(
pep508_rs::Requirement::from_str("mypy-extensions<0.4.4").unwrap(),
)]),
Overrides::default(),
vec![],
None,
Exclusions::default(),
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==0.4.3
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
/// Resolve `black` with a constraint on `mypy-extensions[extra]`, to ensure that extras are
/// ignored when resolving constraints.
#[tokio::test]
async fn black_mypy_extensions_extra() -> Result<()> {
let manifest = Manifest::new(
vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(),
)],
Constraints::from_requirements(vec![Requirement::from(
pep508_rs::Requirement::from_str("mypy-extensions[extra]<0.4.4").unwrap(),
)]),
Overrides::default(),
vec![],
None,
Exclusions::default(),
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==0.4.3
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
/// Resolve `black` with a redundant constraint on `flake8`, to ensure that constraints don't
/// introduce new dependencies.
#[tokio::test]
async fn black_flake8() -> Result<()> {
let manifest = Manifest::new(
vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=23.9.1").unwrap(),
)],
Constraints::from_requirements(vec![Requirement::from(
pep508_rs::Requirement::from_str("flake8<1").unwrap(),
)]),
Overrides::default(),
vec![],
None,
Exclusions::default(),
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_lowest() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black>21").unwrap(),
)]);
let options = OptionsBuilder::new()
.resolution_mode(ResolutionMode::Lowest)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==22.1.0
click==8.0.0
# via black
mypy-extensions==0.4.3
# via black
pathspec==0.9.0
# via black
platformdirs==2.0.0
# via black
tomli==1.1.0
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_lowest_direct() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black>21").unwrap(),
)]);
let options = OptionsBuilder::new()
.resolution_mode(ResolutionMode::LowestDirect)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==22.1.0
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
tomli==2.0.1
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_respect_preference() -> Result<()> {
let manifest = Manifest::new(
vec![Requirement::from(pep508_rs::Requirement::from_str(
"black<=23.9.1",
)?)],
Constraints::default(),
Overrides::default(),
vec![Preference::simple(
PackageName::from_str("black")?,
Version::from_str("23.9.0")?,
)],
None,
Exclusions::default(),
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.0
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_ignore_preference() -> Result<()> {
let manifest = Manifest::new(
vec![Requirement::from(pep508_rs::Requirement::from_str(
"black<=23.9.1",
)?)],
Constraints::default(),
Overrides::default(),
vec![Preference::simple(
PackageName::from_str("black")?,
Version::from_str("23.9.2")?,
)],
None,
Exclusions::default(),
vec![],
);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
black==23.9.1
click==8.1.7
# via black
mypy-extensions==1.0.0
# via black
packaging==23.2
# via black
pathspec==0.11.2
# via black
platformdirs==4.0.0
# via black
"###);
Ok(())
}
#[tokio::test]
async fn black_disallow_prerelease() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=20.0").unwrap(),
)]);
let options = OptionsBuilder::new()
.prerelease_mode(PreReleaseMode::Disallow)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let err = resolve(manifest, options, &MARKERS_311, &TAGS_311)
.await
.unwrap_err();
assert_snapshot!(err, @r###"
Because only black>20.0 is available and you require black<=20.0, we can conclude that the requirements are unsatisfiable.
hint: Pre-releases are available for black in the requested range (e.g., 19.10b0), but pre-releases weren't enabled (try: `--prerelease=allow`)
"###);
Ok(())
}
#[tokio::test]
async fn black_allow_prerelease_if_necessary() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("black<=20.0").unwrap(),
)]);
let options = OptionsBuilder::new()
.prerelease_mode(PreReleaseMode::IfNecessary)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let err = resolve(manifest, options, &MARKERS_311, &TAGS_311)
.await
.unwrap_err();
assert_snapshot!(err, @r###"
Because only black>20.0 is available and you require black<=20.0, we can conclude that the requirements are unsatisfiable.
hint: Pre-releases are available for black in the requested range (e.g., 19.10b0), but pre-releases weren't enabled (try: `--prerelease=allow`)
"###);
Ok(())
}
#[tokio::test]
async fn pylint_disallow_prerelease() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap(),
)]);
let options = OptionsBuilder::new()
.prerelease_mode(PreReleaseMode::Disallow)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1
# via pylint
isort==5.12.0
# via pylint
mccabe==0.7.0
# via pylint
pylint==2.3.0
"###);
Ok(())
}
#[tokio::test]
async fn pylint_allow_prerelease() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap(),
)]);
let options = OptionsBuilder::new()
.prerelease_mode(PreReleaseMode::Allow)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1
# via pylint
isort==6.0.0b2
# via pylint
mccabe==0.7.0
# via pylint
pylint==2.3.0
"###);
Ok(())
}
#[tokio::test]
async fn pylint_allow_explicit_prerelease_without_marker() -> Result<()> {
let manifest = Manifest::simple(vec![
Requirement::from(pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap()),
Requirement::from(pep508_rs::Requirement::from_str("isort>=5.0.0").unwrap()),
]);
let options = OptionsBuilder::new()
.prerelease_mode(PreReleaseMode::Explicit)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1
# via pylint
isort==5.12.0
# via pylint
mccabe==0.7.0
# via pylint
pylint==2.3.0
"###);
Ok(())
}
#[tokio::test]
async fn pylint_allow_explicit_prerelease_with_marker() -> Result<()> {
let manifest = Manifest::simple(vec![
Requirement::from(pep508_rs::Requirement::from_str("pylint==2.3.0").unwrap()),
Requirement::from(pep508_rs::Requirement::from_str("isort>=5.0.0b").unwrap()),
]);
let options = OptionsBuilder::new()
.prerelease_mode(PreReleaseMode::Explicit)
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let resolution = resolve(manifest, options, &MARKERS_311, &TAGS_311).await?;
assert_snapshot!(DisplayResolutionGraph::from(&resolution), @r###"
astroid==3.0.1
# via pylint
isort==6.0.0b2
# via pylint
mccabe==0.7.0
# via pylint
pylint==2.3.0
"###);
Ok(())
}
/// Resolve `msgraph-sdk==1.0.0`, which depends on `msgraph-core>=1.0.0a2`. The resolver should
/// fail with a pre-release-centric hint.
#[tokio::test]
async fn msgraph_sdk() -> Result<()> {
let manifest = Manifest::simple(vec![Requirement::from(
pep508_rs::Requirement::from_str("msgraph-sdk==1.0.0").unwrap(),
)]);
let options = OptionsBuilder::new()
.exclude_newer(Some(*EXCLUDE_NEWER))
.build();
let err = resolve(manifest, options, &MARKERS_311, &TAGS_311)
.await
.unwrap_err();
assert_snapshot!(err, @r###"
Because only msgraph-core<1.0.0a2 is available and msgraph-sdk==1.0.0 depends on msgraph-core>=1.0.0a2, we can conclude that msgraph-sdk==1.0.0 cannot be used.
And because you require msgraph-sdk==1.0.0, we can conclude that the requirements are unsatisfiable.
hint: msgraph-core was requested with a pre-release marker (e.g., msgraph-core>=1.0.0a2), but pre-releases weren't enabled (try: `--prerelease=allow`)
"###);
Ok(())
}
static MARKERS_311: Lazy<MarkerEnvironment> = Lazy::new(|| {
MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
implementation_name: "cpython",
implementation_version: "3.11.5",
os_name: "posix",
platform_machine: "arm64",
platform_python_implementation: "CPython",
platform_release: "21.6.0",
platform_system: "Darwin",
platform_version: "Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000",
python_full_version: "3.11.5",
python_version: "3.11",
sys_platform: "darwin",
}).unwrap()
});
static TAGS_311: Lazy<Tags> = Lazy::new(|| {
Tags::from_env(
&Platform::new(
Os::Macos {
major: 21,
minor: 6,
},
Arch::Aarch64,
),
(3, 11),
"cpython",
(3, 11),
false,
)
.unwrap()
});
static MARKERS_310: Lazy<MarkerEnvironment> = Lazy::new(|| {
MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
implementation_name: "cpython",
implementation_version: "3.10.5",
os_name: "posix",
platform_machine: "arm64",
platform_python_implementation: "CPython",
platform_release: "21.6.0",
platform_system: "Darwin",
platform_version: "Darwin Kernel Version 21.6.0: Mon Aug 22 20:19:52 PDT 2022; root:xnu-8020.140.49~2/RELEASE_ARM64_T6000",
python_full_version: "3.10.5",
python_version: "3.10",
sys_platform: "darwin",
}).unwrap()
});
static TAGS_310: Lazy<Tags> = Lazy::new(|| {
Tags::from_env(
&Platform::new(
Os::Macos {
major: 21,
minor: 6,
},
Arch::Aarch64,
),
(3, 10),
"cpython",
(3, 10),
false,
)
.unwrap()
});