#![deny(missing_docs, dead_code)]
use std::{collections::HashMap, io::Read, path::Path, sync::Arc};
use indexmap::IndexSet;
mod builder;
mod channel;
mod conda;
mod file_format_version;
mod hash;
pub mod options;
mod parse;
mod platform;
mod pypi;
mod pypi_indexes;
pub mod source;
mod source_identifier;
mod source_timestamps;
mod url_or_path;
mod utils;
mod verbatim;
pub use builder::{LockFileBuilder, LockedPackage};
pub use channel::Channel;
pub use conda::{
CondaBinaryData, CondaPackageData, CondaSourceData, ConversionError, FullSourceMetadata,
GitShallowSpec, InputHash, PackageBuildSource, PartialSourceMetadata, SourceMetadata,
VariantValue,
};
pub use file_format_version::FileFormatVersion;
pub use hash::PackageHashes;
pub use options::{PypiPrereleaseMode, SolveOptions};
pub use parse::ParseCondaLockError;
pub use platform::{OwnedPlatform, ParsePlatformError, Platform, PlatformData, PlatformName};
pub use pypi::{PypiDistributionData, PypiPackageData, PypiSourceData, PypiSourceTreeHashable};
pub use pypi_indexes::{FindLinksUrlOrPath, PypiIndexes};
pub use rattler_conda_types::{Matches, RepoDataRecord};
pub use source_identifier::{ParseSourceIdentifierError, SourceIdentifier};
pub use source_timestamps::SourceTimestamps;
pub use url_or_path::UrlOrPath;
pub use verbatim::Verbatim;
pub const DEFAULT_ENVIRONMENT_NAME: &str = "default";
#[derive(Clone, Default, Debug)]
pub struct LockFile {
inner: Arc<LockFileInner>,
}
#[derive(Default, Debug)]
struct LockFileInner {
version: FileFormatVersion,
platforms: Vec<PlatformData>,
environments: Vec<EnvironmentData>,
conda_packages: Vec<CondaPackageData>,
pypi_packages: Vec<PypiPackageData>,
environment_lookup: ahash::HashMap<String, usize>,
}
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
enum EnvironmentPackageData {
Conda(usize),
Pypi(usize),
}
#[derive(Clone, Debug)]
struct EnvironmentData {
channels: Vec<Channel>,
indexes: Option<PypiIndexes>,
options: SolveOptions,
packages: ahash::HashMap<usize, IndexSet<EnvironmentPackageData>>,
}
impl LockFile {
pub fn builder() -> LockFileBuilder {
LockFileBuilder::new()
}
pub fn from_reader(
mut reader: impl Read,
base_dir: Option<&Path>,
) -> Result<Self, ParseCondaLockError> {
let mut str = String::new();
reader.read_to_string(&mut str)?;
parse::from_str_with_base_directory(&str, base_dir)
}
pub fn from_path(path: &Path) -> Result<Self, ParseCondaLockError> {
let base_dir = path.parent();
let source = std::fs::read_to_string(path)?;
parse::from_str_with_base_directory(&source, base_dir)
}
pub fn from_str_with_base_directory(
source: &str,
base_dir: Option<&Path>,
) -> Result<Self, ParseCondaLockError> {
parse::from_str_with_base_directory(source, base_dir)
}
pub fn to_path(&self, path: &Path) -> Result<(), std::io::Error> {
let file = std::fs::File::create(path)?;
serde_yaml::to_writer(file, self).map_err(std::io::Error::other)
}
pub fn render_to_string(&self) -> Result<String, std::io::Error> {
serde_yaml::to_string(self).map_err(std::io::Error::other)
}
pub fn platform(&self, name: &str) -> Option<Platform<'_>> {
crate::platform::find_index_by_name(&self.inner.platforms, name)
.map(|index| Platform::new(&self.inner, index))
}
pub fn platforms(&self) -> impl ExactSizeIterator<Item = Platform<'_>> {
self.inner
.platforms
.iter()
.enumerate()
.map(|(index, _)| Platform::new(&self.inner, index))
}
pub fn environment(&self, name: &str) -> Option<Environment<'_>> {
let index = *self.inner.environment_lookup.get(name)?;
Some(Environment {
lock_file: self,
index,
})
}
pub fn default_environment(&self) -> Option<Environment<'_>> {
self.environment(DEFAULT_ENVIRONMENT_NAME)
}
pub fn environments(&self) -> impl ExactSizeIterator<Item = (&str, Environment<'_>)> + '_ {
self.inner
.environment_lookup
.iter()
.map(move |(name, index)| {
(
name.as_str(),
Environment {
lock_file: self,
index: *index,
},
)
})
}
pub fn version(&self) -> FileFormatVersion {
self.inner.version
}
pub fn is_empty(&self) -> bool {
self.inner.conda_packages.is_empty() && self.inner.pypi_packages.is_empty()
}
}
#[derive(Clone, Copy)]
pub struct Environment<'lock> {
lock_file: &'lock LockFile,
index: usize,
}
impl<'lock> Environment<'lock> {
fn data(&self) -> &'lock EnvironmentData {
&self.lock_file.inner.environments[self.index]
}
pub fn lock_file(&self) -> &'lock LockFile {
self.lock_file
}
pub fn platforms(&self) -> impl ExactSizeIterator<Item = Platform<'lock>> + '_ {
let indices = self
.data()
.packages
.keys()
.map(|index| Platform::new(&self.lock_file.inner, *index))
.collect::<Vec<_>>();
crate::platform::PlatformIterator::new(indices)
}
pub fn channels(&self) -> &[Channel] {
&self.data().channels
}
pub fn pypi_indexes(&self) -> Option<&PypiIndexes> {
self.data().indexes.as_ref()
}
pub fn pypi_prerelease_mode(&self) -> PypiPrereleaseMode {
self.data().options.pypi_prerelease_mode
}
pub fn solve_options(&self) -> &SolveOptions {
&self.data().options
}
pub fn packages(
&self,
platform: Platform<'lock>,
) -> Option<impl DoubleEndedIterator<Item = LockedPackageRef<'lock>> + ExactSizeIterator + '_>
{
if std::ptr::from_ref(self.lock_file.inner.as_ref())
!= std::ptr::from_ref(platform.lock_file_inner)
{
return None;
}
Some(
self.data()
.packages
.get(&platform.index)?
.iter()
.map(move |package| match package {
EnvironmentPackageData::Conda(data) => {
LockedPackageRef::Conda(&self.lock_file.inner.conda_packages[*data])
}
EnvironmentPackageData::Pypi(data) => {
LockedPackageRef::Pypi(&self.lock_file.inner.pypi_packages[*data])
}
}),
)
}
pub fn packages_by_platform(
&self,
) -> impl ExactSizeIterator<
Item = (
Platform<'lock>,
impl DoubleEndedIterator<Item = LockedPackageRef<'lock>> + ExactSizeIterator + '_,
),
> + '_ {
self.data()
.packages
.iter()
.map(|(platform_index, data)| {
(Platform::new(&self.lock_file.inner, *platform_index), data)
})
.map(move |(platform, data)| {
(
platform,
data.iter().map(move |package| match package {
EnvironmentPackageData::Conda(data) => {
LockedPackageRef::Conda(&self.lock_file.inner.conda_packages[*data])
}
EnvironmentPackageData::Pypi(data) => {
LockedPackageRef::Pypi(&self.lock_file.inner.pypi_packages[*data])
}
}),
)
})
}
pub fn pypi_packages_by_platform(
&self,
) -> impl ExactSizeIterator<
Item = (
Platform<'_>,
impl DoubleEndedIterator<Item = &'_ PypiPackageData>,
),
> + '_ {
self.packages_by_platform()
.map(move |(platform, packages)| {
(platform, packages.filter_map(LockedPackageRef::as_pypi))
})
}
pub fn conda_packages_by_platform(
&self,
) -> impl ExactSizeIterator<
Item = (
Platform<'lock>,
impl DoubleEndedIterator<Item = &'lock CondaPackageData> + '_,
),
> + '_ {
self.packages_by_platform()
.map(move |(platform, packages)| {
(platform, packages.filter_map(LockedPackageRef::as_conda))
})
}
pub fn conda_repodata_records_by_platform(
&self,
) -> Result<HashMap<Platform<'lock>, Vec<RepoDataRecord>>, ConversionError> {
self.conda_packages_by_platform()
.map(|(platform, packages)| {
Ok((
platform,
packages
.filter_map(CondaPackageData::as_binary)
.map(RepoDataRecord::try_from)
.collect::<Result<Vec<_>, ConversionError>>()?,
))
})
.collect()
}
pub fn conda_packages(
&self,
platform: Platform<'lock>,
) -> Option<impl DoubleEndedIterator<Item = &'lock CondaPackageData> + '_> {
self.packages(platform)
.map(|packages| packages.filter_map(LockedPackageRef::as_conda))
}
pub fn conda_repodata_records(
&self,
platform: Platform<'lock>,
) -> Result<Option<Vec<RepoDataRecord>>, ConversionError> {
self.conda_packages(platform)
.map(|packages| {
packages
.filter_map(CondaPackageData::as_binary)
.map(RepoDataRecord::try_from)
.collect()
})
.transpose()
}
pub fn pypi_packages(
&self,
platform: Platform<'lock>,
) -> Option<impl DoubleEndedIterator<Item = &'lock PypiPackageData> + '_> {
self.packages(platform)
.map(|pkgs| pkgs.filter_map(LockedPackageRef::as_pypi))
}
pub fn has_pypi_packages(&self, platform: Platform<'lock>) -> bool {
self.pypi_packages(platform)
.is_some_and(|mut packages| packages.next().is_some())
}
pub fn to_owned(self) -> OwnedEnvironment {
OwnedEnvironment {
lock_file: self.lock_file.clone(),
index: self.index,
}
}
}
#[derive(Clone)]
pub struct OwnedEnvironment {
lock_file: LockFile,
index: usize,
}
impl OwnedEnvironment {
pub fn as_ref(&self) -> Environment<'_> {
Environment {
lock_file: &self.lock_file,
index: self.index,
}
}
pub fn lock_file(&self) -> LockFile {
self.lock_file.clone()
}
}
#[derive(Clone, Copy)]
pub enum LockedPackageRef<'lock> {
Conda(&'lock CondaPackageData),
Pypi(&'lock PypiPackageData),
}
impl<'lock> LockedPackageRef<'lock> {
pub fn name(self) -> &'lock str {
match self {
LockedPackageRef::Conda(data) => data.name().as_source(),
LockedPackageRef::Pypi(data) => data.name().as_ref(),
}
}
pub fn location(self) -> &'lock UrlOrPath {
match self {
LockedPackageRef::Conda(data) => data.location(),
LockedPackageRef::Pypi(data) => data.location().inner(),
}
}
pub fn as_pypi(self) -> Option<&'lock PypiPackageData> {
match self {
LockedPackageRef::Conda(_) => None,
LockedPackageRef::Pypi(data) => Some(data),
}
}
pub fn as_conda(self) -> Option<&'lock CondaPackageData> {
match self {
LockedPackageRef::Conda(data) => Some(data),
LockedPackageRef::Pypi(..) => None,
}
}
pub fn as_binary_conda(self) -> Option<&'lock CondaBinaryData> {
self.as_conda().and_then(CondaPackageData::as_binary)
}
pub fn as_source_conda(self) -> Option<&'lock CondaSourceData> {
self.as_conda().and_then(CondaPackageData::as_source)
}
}
#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
use rattler_conda_types::RepoDataRecord;
use rstest::*;
use crate::platform::PlatformName;
use super::{LockFile, DEFAULT_ENVIRONMENT_NAME};
fn test_path() -> PathBuf {
if cfg!(windows) {
PathBuf::from("C:\\tmp\\some\\test\\path")
} else {
PathBuf::from("/tmp/some/test/path")
}
}
#[rstest]
#[case::v0_numpy("v0/numpy-conda-lock.yml")]
#[case::v0_python("v0/python-conda-lock.yml")]
#[case::v0_pypi_matplotlib("v0/pypi-matplotlib-conda-lock.yml")]
#[case::v3_robostack("v3/robostack-turtlesim-conda-lock.yml")]
#[case::v3_numpy("v4/numpy-lock.yml")]
#[case::v4_python("v4/python-lock.yml")]
#[case::v4_pypi_matplotlib("v4/pypi-matplotlib-lock.yml")]
#[case::v4_turtlesim("v4/turtlesim-lock.yml")]
#[case::v4_pypi_path("v4/path-based-lock.yml")]
#[case::v4_pypi_absolute_path("v4/absolute-path-lock.yml")]
#[case::v5_pypi_flat_index("v5/flat-index-lock.yml")]
#[case::v5_with_and_without_purl("v5/similar-with-and-without-purl.yml")]
#[case::v6_conda_source_path("v6/conda-path-lock.yml")]
#[case::v6_derived_channel("v6/derived-channel-lock.yml")]
#[case::v6_sources("v6/sources-lock.yml")]
#[case::v6_options("v6/options-lock.yml")]
#[case::v6_pixi_build_pinned_source("v6/pixi-build-pinned-source-lock.yml")]
#[case::v6_pixi_build_url_source("v6/pixi-build-url-source-lock.yml")]
#[case::v6_pixi_build_git_tag_source("v6/pixi-build-git-tag-source-lock.yml")]
#[case::v6_pixi_build_git_rev_only_source("v6/pixi-build-git-rev-only-source-lock.yml")]
#[case::v6_source_package_with_variants("v6/source-package-with-variants-lock.yml")]
#[case::v6_multiple_source_packages_with_variants(
"v6/multiple-source-packages-with-variants-lock.yml"
)]
#[case::v7_conda_source_path("v7/conda-path-lock.yml")]
#[case::v7_derived_channel("v7/derived-channel-lock.yml")]
#[case::v7_sources("v7/sources-lock.yml")]
#[case::v7_source_timestamps_map("v7/source-timestamps-map-lock.yml")]
#[case::v7_options("v7/options-lock.yml")]
#[case::v7_pixi_build_pinned_source("v7/pixi-build-pinned-source-lock.yml")]
#[case::v7_pixi_build_url_source("v7/pixi-build-url-source-lock.yml")]
#[case::v7_pixi_build_git_tag_source("v7/pixi-build-git-tag-source-lock.yml")]
#[case::v7_pixi_build_git_rev_only_source("v7/pixi-build-git-rev-only-source-lock.yml")]
#[case::v7_source_package_with_variants("v7/source-package-with-variants-lock.yml")]
#[case::v7_multiple_source_packages_with_variants(
"v7/multiple-source-packages-with-variants-lock.yml"
)]
#[case::v7_pypi_absolute_url("v7/pypi_absolute_url.yml")]
#[case::v7_pypi_relative_url("v7/pypi_relative_url.yml")]
#[case::v7_pypi_relative_outside_url("v7/pypi_relative_outside_url.yml")]
#[case::v7_pypi_custom_index("v7/pypi_custom_index.yml")]
fn test_parse(#[case] file_name: &str) {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join(file_name);
let conda_lock = LockFile::from_path(&path).unwrap();
insta::assert_yaml_snapshot!(file_name, conda_lock);
}
#[rstest]
#[case::v7_invalid_platform("v7/invalid_platform_name.yml")]
#[case::v7_invalid_platform("v7/invalid_platform_subdir.yml")]
#[case::v7_missing_platform("v7/missing_platform.yml")]
#[case::v7_missing_platform("v7/duplicate_platform_definition.yml")]
#[case::v7_missing_platform("v7/duplicate_platform_use.yml")]
fn test_parse_fail(#[case] file_name: &str) {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/lock_parse_fails")
.join(file_name);
let error_message = if let Err(error) = LockFile::from_path(&path) {
format!("{error}")
} else {
"Lockfile was read fine, this is unexpected in a test for error cases".to_string()
};
insta::assert_yaml_snapshot!(file_name, error_message);
}
#[rstest]
fn test_roundtrip(
#[files("../../test-data/conda-lock/**/*.yml")]
#[exclude("forward-compatible-lock")]
path: PathBuf,
) {
let conda_lock = LockFile::from_path(&path).unwrap();
let rendered_lock_file = conda_lock.render_to_string().unwrap();
let source_path = test_path();
let parsed_lock_file =
LockFile::from_str_with_base_directory(&rendered_lock_file, Some(&source_path))
.unwrap();
let rerendered_lock_file = parsed_lock_file.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered_lock_file, rerendered_lock_file);
}
#[test]
fn test_issue_615() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v4/absolute-path-lock.yml");
let conda_lock = LockFile::from_path(&path);
assert!(conda_lock.is_ok());
}
#[test]
fn packages_for_platform() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v0/numpy-conda-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let linux = conda_lock.platform("linux-64").unwrap();
let osx = conda_lock.platform("osx-64").unwrap();
insta::assert_yaml_snapshot!(conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.map(|p| p.location().to_string())
.collect::<Vec<_>>());
insta::assert_yaml_snapshot!(conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(osx)
.unwrap()
.map(|p| p.location().to_string())
.collect::<Vec<_>>());
}
#[test]
fn test_has_pypi_packages() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v4/pypi-matplotlib-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let linux = conda_lock.platform("linux-64").unwrap();
assert!(conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.has_pypi_packages(linux));
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/numpy-as-pypi-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let osx_arm64 = conda_lock.platform("osx-arm64").unwrap();
assert!(conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.has_pypi_packages(osx_arm64));
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/python-from-conda-only-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
let osx_arm64 = conda_lock.platform("osx-arm64").unwrap();
assert!(!conda_lock
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.has_pypi_packages(osx_arm64));
}
#[test]
fn test_pypi_index_url() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v7/pypi_custom_index.yml");
let lock_file = LockFile::from_path(&path).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let pypi_packages: Vec<_> = env
.packages(linux)
.unwrap()
.filter_map(super::LockedPackageRef::as_pypi)
.collect();
let requests = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "requests")
.expect("requests package");
assert_eq!(
requests
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://my-custom-index.example.com/simple")
);
let numpy = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "numpy")
.expect("numpy package");
assert_eq!(
numpy
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://my-custom-index.example.com/simple")
);
let local_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "local-pkg")
.expect("local-pkg package");
assert!(local_pkg.as_source().is_some());
}
#[test]
fn v7_pypi_default_index_from_environment() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://first-index.example.com/simple
- https://second-index.example.com/simple
packages:
linux-64:
- pypi: https://first-index.example.com/packages/requests-2.31.0-py3-none-any.whl
- pypi: https://second-index.example.com/packages/numpy-1.26.0-cp311-linux_x86_64.whl
packages:
- pypi: https://first-index.example.com/packages/requests-2.31.0-py3-none-any.whl
name: requests
version: 2.31.0
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- pypi: https://second-index.example.com/packages/numpy-1.26.0-cp311-linux_x86_64.whl
name: numpy
version: 1.26.0
index: https://second-index.example.com/simple
sha256: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let pypi_packages: Vec<_> = env
.packages(linux)
.unwrap()
.filter_map(super::LockedPackageRef::as_pypi)
.collect();
let requests = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "requests")
.expect("requests package");
assert_eq!(
requests
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://first-index.example.com/simple"),
);
let numpy = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "numpy")
.expect("numpy package");
assert_eq!(
numpy
.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://second-index.example.com/simple"),
);
}
#[test]
fn v5_pypi_index_url_is_none() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v5/flat-index-lock.yml");
let lock_file = LockFile::from_path(&path).unwrap();
let platform = lock_file.platform("osx-arm64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
for pkg in env
.packages(platform)
.unwrap()
.filter_map(super::LockedPackageRef::as_pypi)
{
if let Some(wheel) = pkg.as_wheel() {
assert!(
wheel.index_url.is_none(),
"v5 package {:?} should have no index_url",
wheel.name
);
}
}
}
#[test]
fn v6_pypi_index_url_is_none() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock/v6/numpy-as-pypi-lock.yml");
let lock_file = LockFile::from_path(&path).unwrap();
let platform = lock_file.platform("osx-arm64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let numpy = env
.packages(platform)
.unwrap()
.filter_map(super::LockedPackageRef::as_pypi)
.find(|p| p.name().as_ref() == "numpy")
.expect("numpy package");
assert!(numpy.as_wheel().unwrap().index_url.is_none());
}
#[test]
fn v3_pypi_index_url_is_none() {
let lock_file_str = "\
version: 3
metadata:
content_hash:
linux-64: abc123
channels:
- url: https://conda.anaconda.org/conda-forge/
used_env_vars: []
platforms:
- linux-64
sources: []
package:
- platform: linux-64
name: requests
version: '2.31.0'
category: main
manager: pip
dependencies: []
url: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
hash:
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
assert!(pkg.as_wheel().unwrap().index_url.is_none());
}
#[test]
fn v7_pypi_no_indexes_falls_back_to_pypi() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- pypi: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
packages:
- pypi: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
name: requests
version: 2.31.0
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
assert_eq!(
pkg.as_wheel()
.unwrap()
.index_url
.as_ref()
.map(url::Url::as_str),
Some("https://pypi.org/simple"),
);
}
#[test]
fn test_is_empty() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/empty-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
assert!(conda_lock.is_empty());
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/conda-lock")
.join("v6/python-from-conda-only-lock.yml");
let conda_lock = LockFile::from_path(&path).unwrap();
assert!(!conda_lock.is_empty());
}
#[test]
fn solve_roundtrip() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test-data/repodata-records/_libgcc_mutex-0.1-conda_forge.json");
let content = std::fs::read_to_string(&path).unwrap();
let repodata_record: RepoDataRecord = serde_json::from_str(&content).unwrap();
assert_eq!(repodata_record.package_record.arch, None);
assert_eq!(repodata_record.package_record.platform, None);
let lock_file = LockFile::builder()
.with_platforms(vec![crate::PlatformData {
name: PlatformName::try_from("linux-64").unwrap(),
subdir: rattler_conda_types::Platform::Linux64,
virtual_packages: Vec::new(),
}])
.unwrap()
.with_conda_package(
DEFAULT_ENVIRONMENT_NAME,
"linux-64",
repodata_record.clone().into(),
)
.unwrap()
.finish();
let rendered_lock_file = lock_file.render_to_string().unwrap();
let parsed_lock_file =
LockFile::from_str_with_base_directory(&rendered_lock_file, None).unwrap();
let linux = parsed_lock_file.platform("linux-64").unwrap();
let repodata_records = parsed_lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.conda_repodata_records(linux)
.unwrap()
.unwrap();
let repodata_record_two = repodata_records[0].clone();
assert_eq!(
repodata_record_two.package_record.arch,
Some("x86_64".to_string())
);
assert_eq!(
repodata_record_two.package_record.platform,
Some("linux".to_string())
);
let rerendered_lock_file_two = parsed_lock_file.render_to_string().unwrap();
assert_eq!(rendered_lock_file, rerendered_lock_file_two);
}
#[test]
fn test_partial_metadata_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- conda_source: \"my-partial-pkg[abcd1234] @ my-partial-pkg\"
packages:
- conda_source: \"my-partial-pkg[abcd1234] @ my-partial-pkg\"
depends:
- python >=3.10
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let source_data = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(platform)
.unwrap()
.find_map(super::LockedPackageRef::as_source_conda)
.expect("expected a source package");
assert!(source_data.record().is_none());
assert_eq!(source_data.name().as_source(), "my-partial-pkg");
assert_eq!(source_data.depends(), &["python >=3.10".to_string()]);
let rendered = lock_file.render_to_string().unwrap();
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn test_source_identifier_hash_is_preserved() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- conda_source: \"my-package[deadbeef] @ my-package\"
packages:
- conda_source: \"my-package[deadbeef] @ my-package\"
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let source_data = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(platform)
.unwrap()
.find_map(super::LockedPackageRef::as_source_conda)
.expect("expected a source package");
assert_eq!(source_data.identifier_hash.as_deref(), Some("deadbeef"));
let rendered = lock_file.render_to_string().unwrap();
assert!(rendered.contains("my-package[deadbeef]"));
}
#[test]
fn test_identical_source_packages_different_hashes_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
packages:
linux-64:
- conda_source: \"my-package[aaaaaaaa] @ my-package\"
- conda_source: \"my-package[bbbbbbbb] @ my-package\"
packages:
- conda_source: \"my-package[aaaaaaaa] @ my-package\"
- conda_source: \"my-package[bbbbbbbb] @ my-package\"
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let source_packages: Vec<_> = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(platform)
.unwrap()
.filter_map(super::LockedPackageRef::as_source_conda)
.collect();
assert_eq!(source_packages.len(), 2);
let hashes: Vec<_> = source_packages
.iter()
.map(|s| s.identifier_hash.as_deref())
.collect();
assert!(hashes.contains(&Some("aaaaaaaa")));
assert!(hashes.contains(&Some("bbbbbbbb")));
let rendered = lock_file.render_to_string().unwrap();
assert!(rendered.contains("my-package[aaaaaaaa]"));
assert!(rendered.contains("my-package[bbbbbbbb]"));
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn test_pypi_relative_source_packages_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./
- pypi: ./my_subdir
- pypi: ../external_pkg
packages:
- pypi: ../external_pkg
name: external-pkg
- pypi: ./my_subdir
name: my-subdir
- pypi: ./
name: my-project
requires_dist:
- my-subdir @ file:my_subdir
- external-pkg @ file:../external_pkg
";
let base_dir = test_path();
let lock_file =
LockFile::from_str_with_base_directory(lock_file_str, Some(&base_dir)).unwrap();
let platform = lock_file.platform("linux-64").unwrap();
let env = lock_file.environment(DEFAULT_ENVIRONMENT_NAME).unwrap();
let pypi_packages: Vec<_> = env
.packages(platform)
.unwrap()
.filter_map(super::LockedPackageRef::as_pypi)
.collect();
assert_eq!(pypi_packages.len(), 3);
let root_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "my-project")
.expect("my-project package");
let root_source = root_pkg.as_source().unwrap();
assert_eq!(
root_source.location.given(),
Some("./"),
"verbatim relative path for root should be preserved"
);
assert!(
root_pkg.as_source().is_some(),
"local-path pypi packages must be the Source variant"
);
assert_eq!(root_source.requires_dist.len(), 2);
let subdir_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "my-subdir")
.expect("my-subdir package");
let subdir_source = subdir_pkg.as_source().unwrap();
assert_eq!(subdir_source.location.given(), Some("./my_subdir"));
assert!(
subdir_pkg.as_source().is_some(),
"local-path pypi packages must be the Source variant"
);
let external_pkg = pypi_packages
.iter()
.find(|p| p.name().as_ref() == "external-pkg")
.expect("external-pkg package");
let external_source = external_pkg.as_source().unwrap();
assert_eq!(external_source.location.given(), Some("../external_pkg"));
assert!(
external_pkg.as_source().is_some(),
"local-path pypi packages must be the Source variant"
);
let rendered = lock_file.render_to_string().unwrap();
let yaml: serde_yaml::Value = serde_yaml::from_str(&rendered).unwrap();
let package_pypi_keys: std::collections::HashSet<&str> = yaml["packages"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|pkg| pkg["pypi"].as_str())
.collect();
let selector_pypi_keys: Vec<&str> = yaml["environments"]["default"]["packages"]["linux-64"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|sel| sel["pypi"].as_str())
.collect();
assert_eq!(
selector_pypi_keys.len(),
3,
"expected 3 pypi selectors in environment"
);
for selector_key in &selector_pypi_keys {
assert!(
package_pypi_keys.contains(selector_key),
"environment selector `pypi: {selector_key}` has no matching \
entry in the packages section (available: {package_pypi_keys:?})"
);
}
let reparsed = LockFile::from_str_with_base_directory(&rendered, Some(&base_dir)).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn test_pypi_file_url_selector_matches_package() {
let base_dir = test_path();
let lock_file_str = format!(
r#"version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: file://{0}/my_pkg
packages:
- pypi: file://{0}/my_pkg
name: my-pkg
version: 1.0.0
sha256: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
"#,
base_dir.display()
);
eprintln!("Lockfile:\n{lock_file_str}");
let lock_file =
LockFile::from_str_with_base_directory(&lock_file_str, Some(&base_dir)).unwrap();
let rendered = lock_file.render_to_string().unwrap();
let yaml: serde_yaml::Value = serde_yaml::from_str(&rendered).unwrap();
let package_pypi_keys: std::collections::HashSet<&str> = yaml["packages"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|pkg| pkg["pypi"].as_str())
.collect();
let selector_pypi_keys: Vec<&str> = yaml["environments"]["default"]["packages"]["linux-64"]
.as_sequence()
.unwrap()
.iter()
.filter_map(|sel| sel["pypi"].as_str())
.collect();
assert_eq!(selector_pypi_keys.len(), 1);
for selector_key in &selector_pypi_keys {
assert!(
package_pypi_keys.contains(selector_key),
"environment selector `pypi: {selector_key}` has no matching \
entry in the packages section (available: {package_pypi_keys:?})"
);
}
}
#[test]
fn v5_local_path_pypi_package_hash_is_stripped() {
let lock_file_str = "\
version: 5
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./local-pkg
packages:
- kind: pypi
name: local-pkg
version: '0.1.0'
path: ./local-pkg
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let base_dir = test_path();
let lock_file =
LockFile::from_str_with_base_directory(lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("local-path pypi package must be the Source variant");
assert_eq!(source.name.as_ref(), "local-pkg");
}
#[test]
fn v6_local_path_pypi_package_hash_is_stripped() {
let lock_file_str = "\
version: 6
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./local-pkg
packages:
- pypi: ./local-pkg
name: local-pkg
version: '0.1.0'
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("local-path pypi package must be the Source variant");
assert_eq!(source.name.as_ref(), "local-pkg");
}
#[test]
fn v7_local_path_pypi_package_version_stripped_even_when_present() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./local-pkg
packages:
- pypi: ./local-pkg
name: local-pkg
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("local-path pypi package must be the Source variant");
assert_eq!(source.name.as_ref(), "local-pkg");
}
#[test]
fn v7_url_pypi_package_hash_is_preserved() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: https://files.pythonhosted.org/packages/numpy-1.26.0-cp311-cp311-linux_x86_64.whl
packages:
- pypi: https://files.pythonhosted.org/packages/numpy-1.26.0-cp311-cp311-linux_x86_64.whl
name: numpy
version: 1.26.0
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("URL-based pypi package must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "numpy");
assert!(
wheel.hash.is_some(),
"hash must be preserved for URL-based pypi packages"
);
}
#[test]
fn v5_local_path_wheel_preserves_version_and_hash() {
let lock_file_str = "\
version: 5
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- kind: pypi
name: my-pkg
version: '1.0.0'
path: ./dist/my_pkg-1.0.0-py3-none-any.whl
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let base_dir = test_path();
let lock_file =
LockFile::from_str_with_base_directory(lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("local wheel file must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert_eq!(wheel.version.to_string(), "1.0.0");
assert!(
wheel.hash.is_some(),
"hash must be preserved for local wheel files, got None"
);
}
#[test]
fn v6_local_path_wheel_preserves_version_and_hash() {
let lock_file_str = "\
version: 6
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
name: my-pkg
version: '1.0.0'
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("local wheel file must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert_eq!(wheel.version.to_string(), "1.0.0");
assert!(
wheel.hash.is_some(),
"hash must be preserved for local wheel files, got None"
);
}
#[test]
fn v7_local_path_wheel_preserves_version_and_hash() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- pypi: ./dist/my_pkg-1.0.0-py3-none-any.whl
name: my-pkg
version: '1.0.0'
sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("local wheel file must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert_eq!(wheel.version.to_string(), "1.0.0");
assert!(
wheel.hash.is_some(),
"hash must be preserved for local wheel files, got None"
);
}
#[test]
fn v7_file_url_wheel_preserves_version_and_hash() {
let base_dir = test_path();
let lock_file_str = format!(
"\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: file://{0}/dist/my_pkg-1.0.0-py3-none-any.whl
packages:
- pypi: file://{0}/dist/my_pkg-1.0.0-py3-none-any.whl
name: my-pkg
version: 1.0.0
sha256: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
",
base_dir.display()
);
let lock_file =
LockFile::from_str_with_base_directory(&lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let wheel = pkg
.as_wheel()
.expect("file:// wheel URL must be the Wheel variant");
assert_eq!(wheel.name.as_ref(), "my-pkg");
assert!(
wheel.hash.is_some(),
"hash must be preserved for file:// wheel URLs, got None"
);
}
#[test]
fn v7_file_url_pypi_package_version_is_stripped() {
let base_dir = test_path();
let lock_file_str = format!(
"\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: file://{0}/my_pkg
packages:
- pypi: file://{0}/my_pkg
name: my-pkg
",
base_dir.display()
);
let lock_file =
LockFile::from_str_with_base_directory(&lock_file_str, Some(&base_dir)).unwrap();
let linux = lock_file.platform("linux-64").unwrap();
let pkg = lock_file
.environment(DEFAULT_ENVIRONMENT_NAME)
.unwrap()
.packages(linux)
.unwrap()
.find_map(super::LockedPackageRef::as_pypi)
.expect("expected a pypi package");
let source = pkg
.as_source()
.expect("file:// pypi package (local path) must be the Source variant");
assert_eq!(source.name.as_ref(), "my-pkg");
}
#[test]
fn builder_deduplicates_conda_source_packages() {
use std::collections::BTreeMap;
use rattler_conda_types::{PackageName, PackageRecord};
use crate::{
CondaPackageData, CondaSourceData, FullSourceMetadata, PlatformData, SourceMetadata,
UrlOrPath,
};
let source_pkg = CondaPackageData::Source(Box::new(CondaSourceData {
location: UrlOrPath::Path("my-source-pkg".into()),
package_build_source: None,
variants: BTreeMap::from([(
"target_platform".to_string(),
crate::VariantValue::String("noarch".to_string()),
)]),
timestamp: None,
identifier_hash: None,
metadata: SourceMetadata::Full(Box::new(FullSourceMetadata {
package_record: {
let version: rattler_conda_types::Version = "1.0.0".parse().unwrap();
let mut r = PackageRecord::new(
PackageName::new_unchecked("my-source-pkg"),
version,
"py_0".to_string(),
);
r.subdir = "noarch".to_string();
r
},
sources: BTreeMap::new(),
})),
}));
let lock_file = LockFile::builder()
.with_platforms(vec![PlatformData {
name: PlatformName::try_from("linux-64").unwrap(),
subdir: rattler_conda_types::Platform::Linux64,
virtual_packages: Vec::new(),
}])
.unwrap()
.with_conda_package("default", "linux-64", source_pkg.clone())
.unwrap()
.with_conda_package("dev", "linux-64", source_pkg)
.unwrap()
.finish();
let rendered = lock_file.render_to_string().unwrap();
let count = rendered.matches("conda_source:").count();
assert_eq!(
count, 3,
"expected 3 conda_source occurrences (2 selectors + 1 package) \
but found {count}:\n{rendered}"
);
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn duplicate_pypi_git_entries_collapsed_on_roundtrip() {
let lock_file_str = "\
version: 7
platforms:
- name: linux-64
environments:
default:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
dev:
channels:
- url: https://conda.anaconda.org/conda-forge/
indexes:
- https://pypi.org/simple
packages:
linux-64:
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
packages:
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
name: minimalloc
version: 0.1.0
requires_dist:
- numpy>=1.0
requires_python: '>=3.7'
- pypi: git+https://github.com/example/minimalloc.git?rev=abc123#abc123
name: minimalloc
version: 0.1.0
requires_dist:
- numpy>=1.0
- scipy>=1.0 ; extra == 'test'
requires_python: '>=3.7'
";
let lock_file = LockFile::from_str_with_base_directory(lock_file_str, None).unwrap();
let rendered = lock_file.render_to_string().unwrap();
let url = "git+https://github.com/example/minimalloc.git";
let count = rendered.matches(url).count();
assert_eq!(
count, 3,
"expected 3 occurrences of git URL (2 selectors + 1 package) \
but found {count}:\n{rendered}"
);
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
#[test]
fn builder_deduplicates_pypi_git_packages() {
use crate::{PlatformData, PypiPackageData, UrlOrPath, Verbatim};
let url: url::Url = "git+https://github.com/example/minimalloc.git?rev=abc123#abc123"
.parse()
.unwrap();
let pkg_a = PypiPackageData::Distribution(Box::new(crate::PypiDistributionData {
name: "minimalloc".parse().unwrap(),
version: "0.1.0".parse().unwrap(),
location: Verbatim::new(UrlOrPath::Url(url.clone())),
index_url: None,
hash: None,
requires_dist: vec!["numpy>=1.0".parse().unwrap()],
requires_python: Some(">=3.7".parse().unwrap()),
}));
let pkg_b = PypiPackageData::Distribution(Box::new(crate::PypiDistributionData {
name: "minimalloc".parse().unwrap(),
version: "0.1.0".parse().unwrap(),
location: Verbatim::new(UrlOrPath::Url(url)),
index_url: None,
hash: None,
requires_dist: vec![
"numpy>=1.0".parse().unwrap(),
"scipy>=1.0 ; extra == 'test'".parse().unwrap(),
],
requires_python: Some(">=3.7".parse().unwrap()),
}));
let lock_file = LockFile::builder()
.with_platforms(vec![PlatformData {
name: PlatformName::try_from("linux-64").unwrap(),
subdir: rattler_conda_types::Platform::Linux64,
virtual_packages: Vec::new(),
}])
.unwrap()
.with_pypi_package("default", "linux-64", pkg_a)
.unwrap()
.with_pypi_package("dev", "linux-64", pkg_b)
.unwrap()
.finish();
let rendered = lock_file.render_to_string().unwrap();
let url_str = "git+https://github.com/example/minimalloc.git";
let count = rendered.matches(url_str).count();
assert_eq!(
count, 3,
"expected 3 occurrences of git URL (2 selectors + 1 package) \
but found {count}:\n{rendered}"
);
let reparsed = LockFile::from_str_with_base_directory(&rendered, None).unwrap();
let rerendered = reparsed.render_to_string().unwrap();
similar_asserts::assert_eq!(rendered, rerendered);
}
}