uv/crates/puffin-resolver/tests/resolver.rs
konsti e41ec12239
Option to resolve at a fixed timestamp with pip-compile --exclude-newer YYYY-MM-DD (#434)
This works by filtering out files with a more recent upload time, so if
the index you use does not provide upload times, the results might be
inaccurate. pypi provides upload times for all files. This is, the field
is non-nullable in the warehouse schema, but the simple API PEP does not
know this field.

If you have only pypi dependencies, this means deterministic,
reproducible(!) resolution. We could try doing the same for git repos
but it doesn't seem worth the effort, i'd recommend pinning commits
since git histories are arbitrarily malleable and also if you care about
reproducibility and such you such not use git dependencies but a custom
index.

Timestamps are given either as RFC 3339 timestamps such as
`2006-12-02T02:07:43Z` or as UTC dates in the same format such as
`2006-12-02`. Dates are interpreted as including this day, i.e. until
midnight UTC that day. Date only is required to make this ergonomic and
midnight seems like an ergonomic choice.

In action for `pandas`:

```console
$ target/debug/puffin pip-compile --exclude-newer 2023-11-16 target/pandas.in
Resolved 6 packages in 679ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    target/debug/puffin pip-compile --exclude-newer 2023-11-16 target/pandas.in
numpy==1.26.2
    # via pandas
pandas==2.1.3
python-dateutil==2.8.2
    # via pandas
pytz==2023.3.post1
    # via pandas
six==1.16.0
    # via python-dateutil
tzdata==2023.3
    # via pandas
$ target/debug/puffin pip-compile --exclude-newer 2022-11-16 target/pandas.in
Resolved 5 packages in 655ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    target/debug/puffin pip-compile --exclude-newer 2022-11-16 target/pandas.in
numpy==1.23.4
    # via pandas
pandas==1.5.1
python-dateutil==2.8.2
    # via pandas
pytz==2022.6
    # via pandas
six==1.16.0
    # via python-dateutil
$ target/debug/puffin pip-compile --exclude-newer 2021-11-16 target/pandas.in
Resolved 5 packages in 594ms
# This file was autogenerated by Puffin v0.0.1 via the following command:
#    target/debug/puffin pip-compile --exclude-newer 2021-11-16 target/pandas.in
numpy==1.21.4
    # via pandas
pandas==1.3.4
python-dateutil==2.8.2
    # via pandas
pytz==2021.3
    # via pandas
six==1.16.0
    # via python-dateutil
```
2023-11-16 19:46:17 +00:00

496 lines
13 KiB
Rust

#![cfg(feature = "pypi")]
//! Integration tests for the resolver. These tests rely on a live network connection, and hit
//! `PyPI` directly.
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::str::FromStr;
use anyhow::Result;
use once_cell::sync::Lazy;
use tempfile::tempdir;
use pep508_rs::{MarkerEnvironment, Requirement, StringVersion};
use platform_host::{Arch, Os, Platform};
use platform_tags::Tags;
use puffin_client::RegistryClientBuilder;
use puffin_interpreter::{InterpreterInfo, Virtualenv};
use puffin_resolver::{Graph, Manifest, PreReleaseMode, ResolutionMode, Resolver};
use puffin_traits::BuildContext;
struct DummyContext {
interpreter_info: InterpreterInfo,
}
impl BuildContext for DummyContext {
fn cache(&self) -> &Path {
panic!("The test should not need to build source distributions")
}
fn interpreter_info(&self) -> &InterpreterInfo {
&self.interpreter_info
}
fn base_python(&self) -> &Path {
panic!("The test should not need to build source distributions")
}
fn resolve<'a>(
&'a self,
_requirements: &'a [Requirement],
) -> Pin<Box<dyn Future<Output = Result<Vec<Requirement>>> + Send + 'a>> {
panic!("The test should not need to build source distributions")
}
fn install<'a>(
&'a self,
_requirements: &'a [Requirement],
_venv: &'a Virtualenv,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
panic!("The test should not need to build source distributions")
}
fn build_source<'a>(
&'a self,
_sdist: &'a Path,
_subdirectory: Option<&'a Path>,
_wheel_dir: &'a Path,
_package_id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
panic!("The test should not need to build source distributions")
}
}
async fn resolve(
manifest: Manifest,
markers: &'static MarkerEnvironment,
tags: &Tags,
) -> Result<Graph> {
let tempdir = tempdir()?;
let client = RegistryClientBuilder::new(tempdir.path()).build();
let build_context = DummyContext {
interpreter_info: InterpreterInfo::artificial(
Platform::current()?,
markers.clone(),
PathBuf::from("/dev/null"),
PathBuf::from("/dev/null"),
PathBuf::from("/dev/null"),
),
};
let resolver = Resolver::new(manifest, markers, tags, &client, &build_context);
Ok(resolver.resolve().await?)
}
#[tokio::test]
async fn black() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_colorama() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black[colorama]<=23.9.1").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_python_310() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_310, &TAGS_310).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
/// Resolve `black` with a constraint on `mypy-extensions`, to ensure that constraints are
/// respected.
#[tokio::test]
async fn black_mypy_extensions() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![Requirement::from_str("mypy-extensions<0.4.4").unwrap()],
vec![],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
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<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![Requirement::from_str("mypy-extensions[extra]<0.4.4").unwrap()],
vec![],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
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<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![Requirement::from_str("flake8<1").unwrap()],
vec![],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_lowest() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black>21").unwrap()],
vec![],
vec![],
ResolutionMode::Lowest,
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_lowest_direct() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black>21").unwrap()],
vec![],
vec![],
ResolutionMode::LowestDirect,
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_respect_preference() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![],
vec![Requirement::from_str("black==23.9.0").unwrap()],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_ignore_preference() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=23.9.1").unwrap()],
vec![],
vec![Requirement::from_str("black==23.9.2").unwrap()],
ResolutionMode::default(),
PreReleaseMode::default(),
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn black_disallow_prerelease() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=20.0").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::Disallow,
None,
None,
);
let err = resolve(manifest, &MARKERS_311, &TAGS_311)
.await
.unwrap_err();
insta::assert_display_snapshot!(err);
Ok(())
}
#[tokio::test]
async fn black_allow_prerelease_if_necessary() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("black<=20.0").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::IfNecessary,
None,
None,
);
let err = resolve(manifest, &MARKERS_311, &TAGS_311)
.await
.unwrap_err();
insta::assert_display_snapshot!(err);
Ok(())
}
#[tokio::test]
async fn pylint_disallow_prerelease() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("pylint==2.3.0").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::Disallow,
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn pylint_allow_prerelease() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![Requirement::from_str("pylint==2.3.0").unwrap()],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::Allow,
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn pylint_allow_explicit_prerelease_without_marker() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![
Requirement::from_str("pylint==2.3.0").unwrap(),
Requirement::from_str("isort>=5.0.0").unwrap(),
],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::Explicit,
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
#[tokio::test]
async fn pylint_allow_explicit_prerelease_with_marker() -> Result<()> {
colored::control::set_override(false);
let manifest = Manifest::new(
vec![
Requirement::from_str("pylint==2.3.0").unwrap(),
Requirement::from_str("isort>=5.0.0b").unwrap(),
],
vec![],
vec![],
ResolutionMode::default(),
PreReleaseMode::Explicit,
None,
None,
);
let resolution = resolve(manifest, &MARKERS_311, &TAGS_311).await?;
insta::assert_display_snapshot!(resolution);
Ok(())
}
static MARKERS_311: Lazy<MarkerEnvironment> = Lazy::new(|| {
MarkerEnvironment {
implementation_name: "cpython".to_string(),
implementation_version: StringVersion::from_str("3.11.5").unwrap(),
os_name: "posix".to_string(),
platform_machine: "arm64".to_string(),
platform_python_implementation: "CPython".to_string(),
platform_release: "21.6.0".to_string(),
platform_system: "Darwin".to_string(),
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".to_string(),
python_full_version: StringVersion::from_str("3.11.5").unwrap(),
python_version: StringVersion::from_str("3.11").unwrap(),
sys_platform: "darwin".to_string(),
}
});
static TAGS_311: Lazy<Tags> = Lazy::new(|| {
Tags::from_env(
&Platform::new(
Os::Macos {
major: 21,
minor: 6,
},
Arch::Aarch64,
),
(3, 11),
)
.unwrap()
});
static MARKERS_310: Lazy<MarkerEnvironment> = Lazy::new(|| {
MarkerEnvironment {
implementation_name: "cpython".to_string(),
implementation_version: StringVersion::from_str("3.10.5").unwrap(),
os_name: "posix".to_string(),
platform_machine: "arm64".to_string(),
platform_python_implementation: "CPython".to_string(),
platform_release: "21.6.0".to_string(),
platform_system: "Darwin".to_string(),
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".to_string(),
python_full_version: StringVersion::from_str("3.10.5").unwrap(),
python_version: StringVersion::from_str("3.10").unwrap(),
sys_platform: "darwin".to_string(),
}
});
static TAGS_310: Lazy<Tags> = Lazy::new(|| {
Tags::from_env(
&Platform::new(
Os::Macos {
major: 21,
minor: 6,
},
Arch::Aarch64,
),
(3, 10),
)
.unwrap()
});