Skip to content

Commit

Permalink
add end-to-end tests via packse
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv committed Jan 25, 2024
1 parent 4e4e3df commit 1605437
Show file tree
Hide file tree
Showing 12 changed files with 3,908 additions and 33 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# GitHub syntax highlighting
pixi.lock linguist-language=YAML

12 changes: 10 additions & 2 deletions .github/workflows/rust-compile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v4
with:
submodules: ${{ contains(matrix.name, 'Linux' ) }}

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
Expand Down Expand Up @@ -121,7 +123,6 @@ jobs:
${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
--
--nocapture
# test if venv works properly on windows
# using old and newest python version
Expand All @@ -130,7 +131,14 @@ jobs:
- name: Install pixi
uses: prefix-dev/setup-pixi@v0.4.3
with:
run-install: false
run-install: ${{ contains(matrix.name, 'Linux') }}

- name: Run end-to-end tests
if: contains(matrix.name, 'Linux')
shell: bash
run: |
pixi run install_packse
pixi run end_to_end_tests -v -s
- name: Run pixi python==3.7.5 test
if: contains(matrix.name, 'Windows')
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
*.sqlite3
**/__pycache__/**
.DS_STORE
# pixi environments
.pixi

3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test-data/packse"]
path = test-data/packse
url = https://github.com/zanieb/packse
64 changes: 41 additions & 23 deletions crates/rattler_installs_packages/src/resolve/dependency_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,22 @@ use url::Url;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
/// This is a wrapper around [`Specifiers`] that implements [`VersionSet`]
pub(crate) struct PypiVersionSet {
/// The spec to match against
spec: Option<VersionOrUrl>,
/// If the VersionOrUrl is a Version specifier and any of the specifiers contains a
/// prerelease, then pre-releases are allowed. For example,
/// `jupyterlab==3.0.0a1` allows pre-releases, but `jupyterlab==3.0.0` does not.
///
/// We pre-compute if any of the items in the specifiers contains a pre-release and store
/// this as a boolean which is later used during matching.
allows_prerelease: bool,
}

impl PypiVersionSet {
pub fn from_spec(spec: Option<VersionOrUrl>, prerelease_option: &PreReleaseResolution) -> Self {
let allows_prerelease = match prerelease_option {
PreReleaseResolution::Disallow => false,
PreReleaseResolution::AllowIfNoOtherVersions => match spec.as_ref() {
PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { .. } => match spec.as_ref() {
Some(VersionOrUrl::VersionSpecifier(v)) => {
v.iter().any(|s| s.version().any_prerelease())
}
Expand Down Expand Up @@ -70,11 +77,17 @@ pub(crate) enum PypiVersion {
Version {
version: Version,

/// This is true if there are only pre-releases available for this package
/// For example, if the package `foo` has only versions `foo-1.0.0a1` and `foo-1.0.0a2`
/// then this will be true. This allows us later to match against this version and
/// allow the selection of pre-releases.
only_prerelease: bool,
/// Given that the [`PreReleaseResolution`] is
/// AllowIfNoOtherVersionsOrEnabled, this field is true if there are
/// only pre-releases available for this package or if a spec explicitly
/// enabled pre-releases for this package. For example, if the package
/// `foo` has only versions `foo-1.0.0a1` and `foo-1.0.0a2` then this
/// will be true. This allows us later to match against this version and
/// allow the selection of pre-releases. Additionally, this is also true
/// if any of the explicitly mentioned specs (by the user) contains a
/// prerelease (for example c>0.0.0b0) contains the `b0` which signifies
/// a pre-release.
package_allows_prerelease: bool,
},
#[allow(dead_code)]
Url(Url),
Expand All @@ -90,22 +103,22 @@ impl VersionSet for PypiVersionSet {
Some(VersionOrUrl::VersionSpecifier(spec)),
PypiVersion::Version {
version,
only_prerelease,
package_allows_prerelease,
},
) => {
spec.contains(version)
// pre-releases are allowed only when the versionset allows them (jupyterlab==3.0.0a1)
// or there are no other versions available (foo-1.0.0a1, foo-1.0.0a2)
// or alternatively if the user has enabled all pre-releases (this is encoded in the allows_prerelease field)
&& (self.allows_prerelease || *only_prerelease || !version.any_prerelease())
// or alternatively if the user has enabled all pre-releases or this specific (this is encoded in the allows_prerelease field)
&& (self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease())
}
(
None,
PypiVersion::Version {
version,
only_prerelease,
package_allows_prerelease,
},
) => self.allows_prerelease || *only_prerelease || !version.any_prerelease(),
) => self.allows_prerelease || *package_allows_prerelease || !version.any_prerelease(),
(None, PypiVersion::Url(_)) => true,
_ => false,
}
Expand Down Expand Up @@ -403,15 +416,20 @@ impl<'p> DependencyProvider<PypiVersionSet, PypiPackageName>
let locked_package = self.locked_packages.get(package_name.base());
let favored_package = self.favored_packages.get(package_name.base());

let all_pre_release = artifacts
.iter()
.all(|(version, _)| version.any_prerelease());

let allow_prerelease = all_pre_release
&& !matches!(
self.options.pre_release_resolution,
PreReleaseResolution::Disallow
);
let package_allows_prerelease = match &self.options.pre_release_resolution {
PreReleaseResolution::Disallow => false,
PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { allow_names } => {
if allow_names.contains(&package_name.base().to_string()) {
true
} else {
// check if we _only_ have prereleases for this name (if yes, also allow them)
artifacts
.iter()
.all(|(version, _)| version.any_prerelease())
}
}
PreReleaseResolution::Allow => true,
};

for (version, artifacts) in artifacts.iter() {
// Skip this version if a locked or favored version exists for this version. It will be
Expand All @@ -427,7 +445,7 @@ impl<'p> DependencyProvider<PypiVersionSet, PypiPackageName>
name,
PypiVersion::Version {
version: version.clone(),
only_prerelease: allow_prerelease,
package_allows_prerelease,
},
);
candidates.candidates.push(solvable_id);
Expand All @@ -451,7 +469,7 @@ impl<'p> DependencyProvider<PypiVersionSet, PypiPackageName>
name,
PypiVersion::Version {
version: locked.version.clone(),
only_prerelease: locked.version.any_prerelease(),
package_allows_prerelease: locked.version.any_prerelease(),
},
);
candidates.candidates.push(solvable_id);
Expand All @@ -466,7 +484,7 @@ impl<'p> DependencyProvider<PypiVersionSet, PypiPackageName>
name,
PypiVersion::Version {
version: favored.version.clone(),
only_prerelease: favored.version.any_prerelease(),
package_allows_prerelease: favored.version.any_prerelease(),
},
);
candidates.candidates.push(solvable_id);
Expand Down
5 changes: 4 additions & 1 deletion crates/rattler_installs_packages/src/resolve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@
mod dependency_provider;
mod solve;

pub use solve::{resolve, OnWheelBuildFailure, PinnedPackage, PreReleaseResolution, ResolveOptions, SDistResolution};
pub use solve::{
resolve, OnWheelBuildFailure, PinnedPackage, PreReleaseResolution, ResolveOptions,
SDistResolution,
};
41 changes: 37 additions & 4 deletions crates/rattler_installs_packages/src/resolve/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::python_env::{PythonLocation, WheelTags};
use crate::resolve::dependency_provider::{PypiDependencyProvider, PypiVersion};
use crate::types::PackageName;
use crate::{types::ArtifactInfo, types::Extra, types::NormalizedPackageName, types::Version};
use pep508_rs::{MarkerEnvironment, Requirement};
use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl};
use resolvo::{DefaultSolvableDisplay, Solver, UnsolvableOrCancelled};
use std::collections::HashMap;
use std::str::FromStr;
Expand Down Expand Up @@ -127,7 +127,7 @@ pub enum SDistResolution {
}

/// Defines how to pre-releases are handled during package resolution.
#[derive(Default, Debug, Clone, Copy, Eq, PartialOrd, PartialEq)]
#[derive(Debug, Clone, Eq, PartialOrd, PartialEq)]
pub enum PreReleaseResolution {
/// Don't allow pre-releases to be selected during resolution
Disallow,
Expand All @@ -147,13 +147,46 @@ pub enum PreReleaseResolution {
/// the package `supernew` only contains `supernew-1.0.0b0` and
/// `supernew-1.0.0b1` then we allow `supernew==1.0.0` to select
/// `supernew-1.0.0b1` during resolution.
#[default]
AllowIfNoOtherVersions,
/// - Any name that is mentioned in the `allow` list will allow pre-releases (this
/// is usually derived from the specs given by the user). For example, if the user
/// asks for `foo>0.0.0b0`, pre-releases are globally enabled for package foo (also as
/// transitive dependency).
AllowIfNoOtherVersionsOrEnabled {
/// A list of package names that will allow pre-releases to be selected
allow_names: Vec<String>,
},

/// Allow any pre-releases to be selected during resolution
Allow,
}

impl Default for PreReleaseResolution {
fn default() -> Self {
PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled {
allow_names: Vec::new(),
}
}
}

impl PreReleaseResolution {
/// Return a AllowIfNoOtherVersionsOrEnabled variant from a list of requirements
pub fn from_specs(specs: &[Requirement]) -> Self {
let mut allow_names = Vec::new();
for spec in specs {
match &spec.version_or_url {
Some(VersionOrUrl::VersionSpecifier(v)) => {
if v.iter().any(|s| s.version().any_prerelease()) {
let name = PackageName::from_str(&spec.name).expect("invalid package name");
allow_names.push(name.as_str().to_string());
}
}
_ => continue,
};
}
PreReleaseResolution::AllowIfNoOtherVersionsOrEnabled { allow_names }
}
}

impl SDistResolution {
/// Returns true if sdists are allowed to be selected during resolution
pub fn allow_sdists(&self) -> bool {
Expand Down
45 changes: 42 additions & 3 deletions crates/rip_bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use fs_err as fs;
use rattler_installs_packages::resolve::PreReleaseResolution;
use rip_bin::{global_multi_progress, IndicatifWriter};
use serde::Serialize;
use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
Expand All @@ -22,6 +23,13 @@ use rattler_installs_packages::{
resolve::ResolveOptions, types::Requirement,
};

#[derive(Serialize, Debug)]
struct Solution {
resolved: bool,
packages: HashMap<String, String>,
error: Option<String>,
}

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
Expand Down Expand Up @@ -61,6 +69,9 @@ struct Args {
/// Prefer pre-releases over normal releases
#[clap(long)]
pre: bool,

#[clap(long)]
json: bool,
}

#[derive(Parser)]
Expand Down Expand Up @@ -177,7 +188,7 @@ async fn actual_main() -> miette::Result<()> {
let pre_release_resolution = if args.pre {
PreReleaseResolution::Allow
} else {
PreReleaseResolution::AllowIfNoOtherVersions
PreReleaseResolution::from_specs(&args.specs)
};

let resolve_opts = ResolveOptions {
Expand All @@ -202,7 +213,19 @@ async fn actual_main() -> miette::Result<()> {
.await
{
Ok(blueprint) => blueprint,
Err(err) => miette::bail!("Could not solve for the requested requirements:\n{err}"),
Err(err) => {
if args.json {
let solution = Solution {
resolved: false,
packages: HashMap::default(),
error: Some(format!("{}", err)),
};
println!("{}", serde_json::to_string_pretty(&solution).unwrap());
return Ok(());
} else {
miette::bail!("Could not solve for the requested requirements:\n{err}")
}
}
};

// Output the selected versions
Expand Down Expand Up @@ -260,7 +283,11 @@ async fn actual_main() -> miette::Result<()> {
)
.into_diagnostic()?;

for pinned_package in blueprint.into_iter().sorted_by(|a, b| a.name.cmp(&b.name)) {
for pinned_package in blueprint
.clone()
.into_iter()
.sorted_by(|a, b| a.name.cmp(&b.name))
{
println!(
"\ninstalling: {} - {}",
console::style(pinned_package.name).bold().green(),
Expand All @@ -281,6 +308,18 @@ async fn actual_main() -> miette::Result<()> {
console::style("Successfully installed environment!").bold()
);

if args.json {
let solution = Solution {
resolved: true,
packages: blueprint
.into_iter()
.map(|p| (p.name.to_string(), p.version.to_string()))
.collect(),
error: None,
};
println!("{}", serde_json::to_string_pretty(&solution).unwrap());
}

Ok(())
}

Expand Down
Loading

0 comments on commit 1605437

Please sign in to comment.