#![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, Requirement, Resolution, SourceDist}; use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder}; use platform_tags::{Arch, Os, Platform, Tags}; use uv_cache::Cache; use uv_client::RegistryClientBuilder; use uv_configuration::{ BuildKind, Concurrency, Constraints, NoBinary, NoBuild, Overrides, 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 = 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 { panic!("The test should not need to build source distributions") } async fn install<'a>( &'a self, _: &'a Resolution, _: &'a PythonEnvironment, ) -> Result> { 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 { Ok(DummyBuilder) } } struct DummyBuilder; impl SourceBuildTrait for DummyBuilder { async fn metadata(&mut self) -> Result> { panic!("The test should not need to build source distributions") } async fn wheel<'a>(&'a self, _: &'a Path) -> Result { panic!("The test should not need to build source distributions") } } async fn resolve( manifest: Manifest, options: Options, markers: &'static MarkerEnvironment, tags: &Tags, ) -> Result { 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(&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), )?; 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, vec![], 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, vec![], 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, vec![], 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, vec![], 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, vec![], 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 = 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 = 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 = 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 = Lazy::new(|| { Tags::from_env( &Platform::new( Os::Macos { major: 21, minor: 6, }, Arch::Aarch64, ), (3, 10), "cpython", (3, 10), false, ) .unwrap() });