// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 The Gleam contributors use std::{cell::RefCell, cmp::Reverse, collections::HashMap, rc::Rc}; use crate::{Error, Result, manifest}; use ecow::EcoString; use hexpm::{ Dependency, Release, version::{Range, Version}, }; use pubgrub::{Dependencies, Map}; use thiserror::Error; pub type PackageVersions = HashMap; type PubgrubRange = pubgrub::Range; pub fn resolve_versions( package_fetcher: &impl PackageFetcher, provided_packages: HashMap, root_name: EcoString, dependencies: Requirements, locked: &HashMap, ) -> Result where Requirements: Iterator, { tracing::info!("resolving_versions"); let root_version = Version::new(0, 0, 0); let requirements = root_dependencies(dependencies, locked)?; // Creating a map of all the required packages that have exact versions // specified let exact_deps = &requirements .iter() .filter_map(|(name, dependency)| { parse_exact_version(dependency.requirement.as_str()).map(|version| (name, version)) }) .map(|(name, version)| (name.clone(), version)) .collect(); let root = hexpm::Package { name: root_name.as_str().into(), repository: "local".into(), releases: vec![Release { version: root_version.clone(), outer_checksum: vec![], retirement_status: None, requirements, meta: (), }], }; let packages = pubgrub::resolve( &DependencyProvider::new(package_fetcher, provided_packages, root, locked, exact_deps), root_name.as_str().into(), root_version, ) .map_err(|error| Error::dependency_resolution_failed(error, root_name.clone()))? .into_iter() .filter(|(name, _)| name.as_str() != root_name.as_str()) .collect(); Ok(packages) } /** * Used to compare 2 versions of a package. */ pub type PackageVersionDiffs = HashMap; fn resolve_versions_diffs( package_fetcher: &impl PackageFetcher, versions: PackageVersions, check_major_versions: bool, ) -> PackageVersionDiffs { versions .iter() .filter_map(|(package, version)| { let Ok(hex_package) = package_fetcher.get_dependencies(package) else { return None; }; let latest = hex_package .releases .iter() .map(|release| &release.version) .filter(|version| !version.is_pre()) .max()?; // If we're checking for major version updates, only include the // package if a new major version is available. Otherwise, include // the package if there is any new version available. match check_major_versions { true => { if latest.major <= version.major { return None; } } false => { if latest <= version { return None; } } } Some((package.to_string(), (version.clone(), latest.clone()))) }) .collect() } /// Check for major version updates for direct dependencies that are being /// blocked by some version constraints. pub fn check_for_major_version_updates( manifest: &manifest::Manifest, package_fetcher: &impl PackageFetcher, ) -> PackageVersionDiffs { let versions: PackageVersions = manifest .packages .iter() .filter(|manifest_package| { // It makes sense to check for major version upgrades only for // packages that are actually published. // git and path dependencies are skipped. manifest_package.is_hex() && manifest .requirements .iter() .any(|(required_package, _)| manifest_package.name == *required_package) }) .map(|manifest_package| { ( manifest_package.name.to_string(), manifest_package.version.clone(), ) }) .collect(); resolve_versions_diffs(package_fetcher, versions, true) } /// Check for version updates for direct and transitive dependencies that are /// being blocked by some version constraints. pub fn check_for_version_updates( manifest: &manifest::Manifest, package_fetcher: &impl PackageFetcher, ) -> PackageVersionDiffs { let versions = manifest .packages .iter() .filter(|manifest_package| manifest_package.is_hex()) .map(|manifest_package| { ( manifest_package.name.to_string(), manifest_package.version.clone(), ) }) .collect(); resolve_versions_diffs(package_fetcher, versions, false) } // If the string would parse to an exact version then return the version fn parse_exact_version(ver: &str) -> Option { let version = ver.trim(); let first_byte = version.as_bytes().first(); // Version is exact if it starts with an explicit == or a number if version.starts_with("==") || first_byte.is_some_and(|v| v.is_ascii_digit()) { let version = version.replace("==", ""); let version = version.as_str().trim(); Version::parse(version).ok() } else { None } } fn root_dependencies( base_requirements: Requirements, locked: &HashMap, ) -> Result, Error> where Requirements: Iterator, { // Record all of the already locked versions as hard requirements let mut requirements: HashMap<_, _> = locked .iter() .map(|(name, version)| { ( name.to_string(), Dependency { app: None, optional: false, repository: None, requirement: version.clone().into(), }, ) }) .collect(); for (name, range) in base_requirements { match locked.get(&name) { // If the package was not already locked then we can use the // specified version requirement without modification. None => { let _ = requirements.insert( name.into(), Dependency { app: None, optional: false, repository: None, requirement: range, }, ); } // If the version was locked we verify that the requirement is // compatible with the locked version. Some(locked_version) => { let compatible = range.to_pubgrub().contains(locked_version); if !compatible { return Err(Error::IncompatibleLockedVersion { error: format!( "{name} is specified with the requirement `{range}`, \ but it is locked to {locked_version}, which is incompatible.", ), }); } } } } Ok(requirements) } pub trait PackageFetcher { fn get_dependencies(&self, package: &str) -> Result, PackageFetchError>; } #[derive(Debug, Error)] pub enum PackageFetchError { #[error("the package {0} was not found in the package repository")] NotFoundError(String), #[error("{0}")] ApiError(hexpm::ApiError), #[error("{0}")] FetchError(String), } impl PackageFetchError { pub fn fetch_error(err: T) -> Self { Self::FetchError(err.to_string()) } pub fn from_api_error(api_error: hexpm::ApiError, package: &str) -> Self { match &api_error { hexpm::ApiError::NotFound => Self::NotFoundError(package.to_string()), hexpm::ApiError::Json(_) | hexpm::ApiError::IncorrectOneTimePassword | hexpm::ApiError::OAuthRefreshTokenRejected | hexpm::ApiError::OAuthAccessDenied | hexpm::ApiError::OAuthTimeout | hexpm::ApiError::ExpiredToken | hexpm::ApiError::Io(_) | hexpm::ApiError::InvalidProtobuf(_) | hexpm::ApiError::UnexpectedResponse(_, _) | hexpm::ApiError::RateLimited | hexpm::ApiError::InvalidCredentials | hexpm::ApiError::InvalidPackageNameFormat(_) | hexpm::ApiError::IncorrectPayloadSignature | hexpm::ApiError::InvalidVersionFormat(_) | hexpm::ApiError::InvalidVersionRequirementFormat(_) | hexpm::ApiError::IncorrectChecksum | hexpm::ApiError::Forbidden | hexpm::ApiError::NotReplacing | hexpm::ApiError::LateDeletion | hexpm::ApiError::LateModification => Self::ApiError(api_error), } } } #[derive(Debug)] pub struct DependencyProvider<'a, T: PackageFetcher> { packages: RefCell>, remote: &'a T, locked: &'a HashMap, // Map of packages where an exact version was requested // We need this because by default pubgrub checks exact version by checking // if a version is between the exact and the version 1 bump ahead. // That default breaks on prerelease builds since a bump includes the whole // patch. exact_only: &'a HashMap, optional_dependencies: RefCell>>, } impl<'a, T> DependencyProvider<'a, T> where T: PackageFetcher, { fn new( remote: &'a T, mut packages: HashMap, root: hexpm::Package, locked: &'a HashMap, exact_only: &'a HashMap, ) -> Self { let _ = packages.insert(root.name.as_str().into(), root); Self { packages: RefCell::new(packages), locked, remote, exact_only, optional_dependencies: RefCell::new(HashMap::new()), } } /// Download information about the package from the registry into the local /// store. Does nothing if the packages are already known. /// /// Package versions are sorted from newest to oldest, with all pre-releases /// at the end to ensure that a non-prerelease version will be picked first /// if there is one. // fn ensure_package_fetched( // We would like to use `&mut self` but the pubgrub library enforces // `&self` with interop mutability. &self, name: &str, ) -> Result<(), PackageFetchError> { let mut packages = self.packages.borrow_mut(); if packages.get(name).is_none() { let package = self.remote.get_dependencies(name)?; // mut (therefore clone) is required here in order to sort the releases let mut package = (*package).clone(); // Sort the packages from newest to oldest, pres after all others package.releases.sort_by(|a, b| a.version.cmp(&b.version)); package.releases.reverse(); let (pre, mut norm): (_, Vec<_>) = package .releases .into_iter() .partition(|release| release.version.is_pre()); norm.extend(pre); package.releases = norm; let _ = packages.insert(name.into(), package); } Ok(()) } } type PackageName = String; pub type ResolutionError<'a, T> = pubgrub::PubGrubError>; impl pubgrub::DependencyProvider for DependencyProvider<'_, T> where T: PackageFetcher, { fn get_dependencies( &self, package: &Self::P, version: &Self::V, ) -> Result, Self::Err> { self.ensure_package_fetched(package)?; let packages = self.packages.borrow(); let release = match packages .get(package.as_str()) .into_iter() .flat_map(|package| package.releases.iter()) .find(|release| &release.version == version) { Some(release) => release, None => { return Ok(Dependencies::Unavailable(format!( "{package}@{version} is not available" ))); } }; // Only use retired versions if they have been locked if release.is_retired() && self.locked.get(package.as_str()) != Some(version) { return Ok(Dependencies::Unavailable(format!( "{package}@{version} is retired" ))); } let mut deps: Map = Map::default(); for (name, dependency) in &release.requirements { let mut range = dependency.requirement.to_pubgrub().clone(); let mut opt_deps = self.optional_dependencies.borrow_mut(); // if it's optional and it was not provided yet, store and skip if dependency.optional && !packages.contains_key(name.as_str()) { let _ = opt_deps .entry(name.into()) .and_modify(|stored_range| { *stored_range = range.intersection(stored_range); }) .or_insert(range); continue; } // if a now required dep was optional before, add back the constraints if let Some(other_range) = opt_deps.remove(name.as_str()) { range = range.intersection(&other_range); } let _ = deps.insert(name.clone(), range); } Ok(Dependencies::Available(deps)) } fn prioritize( &self, package: &Self::P, range: &Self::VS, _package_conflicts_counts: &pubgrub::PackageResolutionStatistics, ) -> Self::Priority { Reverse( self.packages .borrow() .get(package.as_str()) .cloned() .into_iter() .flat_map(|package| { package .releases .into_iter() .filter(|release| range.contains(&release.version)) }) .count(), ) } fn choose_version( &self, package: &Self::P, range: &Self::VS, ) -> std::result::Result, Self::Err> { self.ensure_package_fetched(package)?; let exact_package = self.exact_only.get(package); let potential_versions = self .packages .borrow() .get(package.as_str()) .cloned() .into_iter() .flat_map(move |package| { package .releases .into_iter() // if an exact version of a package is specified then we // only want to allow that version as available. .filter_map(move |release| match exact_package { Some(version) => (version == &release.version).then_some(release.version), _ => Some(release.version), }) }) .filter(|version| range.contains(version)); match potential_versions .clone() .filter(|version| !version.is_pre()) .max() { // Don't resolve to a pre-releaase package unless we *have* to Some(version) => Ok(Some(version)), None => Ok(potential_versions.max()), } } type P = PackageName; type V = Version; type VS = PubgrubRange; type Priority = Reverse; type M = String; type Err = PackageFetchError; } #[cfg(test)] mod tests { use hexpm::RetirementStatus; use crate::{ manifest::{Base16Checksum, ManifestPackage, ManifestPackageSource}, requirement, }; use super::*; struct Remote { deps: HashMap>, } impl PackageFetcher for Remote { fn get_dependencies(&self, package: &str) -> Result, PackageFetchError> { self.deps .get(package) .map(Rc::clone) .ok_or(PackageFetchError::NotFoundError(package.to_string())) } } fn make_remote() -> Remote { remote(vec![ ( "gleam_stdlib", vec![ release("0.1.0", vec![]), release("0.2.0", vec![]), release("0.2.2", vec![]), release("0.3.0", vec![]), ], ), ( "gleam_otp", vec![ release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0")]), release("0.2.0", vec![("gleam_stdlib", ">= 0.1.0")]), release("0.3.0-rc1", vec![("gleam_stdlib", ">= 0.1.0")]), release("0.3.0-rc2", vec![("gleam_stdlib", ">= 0.1.0")]), ], ), ( "package_with_retired", vec![ release("0.1.0", vec![]), retired_release( "0.2.0", vec![], hexpm::RetirementReason::Security, "it's bad", ), ], ), ( "package_with_optional", vec![release_with_optional( "0.1.0", vec![], vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")], )], ), ( "direct_pkg_with_major_version", vec![ release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]), release("1.0.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]), release("1.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]), ], ), ( "depends_on_old_version_of_direct_pkg", vec![release( "0.1.0", vec![("direct_pkg_with_major_version", ">= 0.1.0 and < 0.3.0")], )], ), ( "this_pkg_depends_on_indirect_pkg", vec![release( "0.1.0", vec![("indirect_pkg_with_major_version", ">= 0.1.0 and < 1.0.0")], )], ), ( "indirect_pkg_with_major_version", vec![ release("0.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]), release("1.0.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]), release("1.1.0", vec![("gleam_stdlib", ">= 0.1.0 and < 0.3.0")]), ], ), ]) } #[test] fn resolution_with_locked() { let locked_stdlib = ("gleam_stdlib".into(), Version::parse("0.1.0").unwrap()); let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![locked_stdlib].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![("gleam_stdlib".into(), Version::parse("0.1.0").unwrap())] .into_iter() .collect() ); } #[test] fn resolution_without_deps() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!(result, vec![].into_iter().collect()) } #[test] fn resolution_1_dep() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap())] .into_iter() .collect() ); } #[test] fn resolution_with_nested_deps() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![ ("gleam_otp".into(), Version::try_from("0.2.0").unwrap()), ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()) ] .into_iter() .collect() ); } #[test] fn resolution_with_optional_deps() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![( "package_with_optional".into(), Range::new("~> 0.1".into()).unwrap(), )] .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![( "package_with_optional".into(), Version::try_from("0.1.0").unwrap() )] .into_iter() .collect() ); } #[test] fn resolution_with_optional_deps_explicitly_provided() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![ ( "package_with_optional".into(), Range::new("~> 0.1".into()).unwrap(), ), ("gleam_stdlib".into(), Range::new("~> 0.1".into()).unwrap()), ] .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![ ("gleam_stdlib".into(), Version::try_from("0.2.2").unwrap()), ( "package_with_optional".into(), Version::try_from("0.1.0").unwrap() ), ] .into_iter() .collect() ); } #[test] fn resolution_with_optional_deps_incompatible() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![ ( "package_with_optional".into(), Range::new("~> 0.1".into()).unwrap(), ), ("gleam_stdlib".into(), Range::new("~> 0.3".into()).unwrap()), ] .into_iter(), &vec![].into_iter().collect(), ); assert!(result.is_err()); } #[test] fn resolution_with_optional_deps_required_by_nested_deps() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![ ( "package_with_optional".into(), Range::new("~> 0.1".into()).unwrap(), ), ("gleam_otp".into(), Range::new("~> 0.1".into()).unwrap()), ] .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![ ("gleam_stdlib".into(), Version::try_from("0.2.2").unwrap()), ("gleam_otp".into(), Version::try_from("0.2.0").unwrap()), ( "package_with_optional".into(), Version::try_from("0.1.0").unwrap() ), ] .into_iter() .collect() ); } #[test] fn resolution_with_optional_deps_keep_constraints() {} #[test] fn resolution_locked_to_older_version() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_otp".into(), Range::new("~> 0.1.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![ ("gleam_otp".into(), Version::try_from("0.1.0").unwrap()), ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()) ] .into_iter() .collect() ); } #[test] fn resolution_retired_versions_not_used_by_default() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![( "package_with_retired".into(), Range::new("> 0.0.0".into()).unwrap(), )] .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![( "package_with_retired".into(), // Uses the older version that hasn't been retired Version::try_from("0.1.0").unwrap() ),] .into_iter() .collect() ); } #[test] fn resolution_retired_versions_can_be_used_if_locked() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![( "package_with_retired".into(), Range::new("> 0.0.0".into()).unwrap(), )] .into_iter(), &vec![("package_with_retired".into(), Version::new(0, 2, 0))] .into_iter() .collect(), ) .unwrap(); assert_eq!( result, vec![( "package_with_retired".into(), // Uses the locked version even though it's retired Version::new(0, 2, 0) ),] .into_iter() .collect() ); } #[test] fn resolution_prerelease_can_be_selected() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![( "gleam_otp".into(), Range::new("~> 0.3.0-rc1".into()).unwrap(), )] .into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![ ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()), ("gleam_otp".into(), Version::try_from("0.3.0-rc2").unwrap()), ] .into_iter() .collect(), ); } #[test] fn resolution_exact_prerelease_can_be_selected() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_otp".into(), Range::new("0.3.0-rc1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![ ("gleam_stdlib".into(), Version::try_from("0.3.0").unwrap()), ("gleam_otp".into(), Version::try_from("0.3.0-rc1").unwrap()), ] .into_iter() .collect(), ); } #[test] fn resolution_not_found_dep() { let err = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("unknown".into(), Range::new("~> 0.1".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap_err(); match err { Error::DependencyResolutionError(error) => assert_eq!( error, "An error occurred while choosing the version of unknown: \ the package unknown was not found in the package repository" ), _ => panic!("wrong error: {err}"), } } #[test] fn resolution_no_matching_version() { let _ = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_stdlib".into(), Range::new("~> 99.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap_err(); } #[test] fn resolution_locked_version_doesnt_satisfy_requirements() { let err = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![( "gleam_stdlib".into(), Range::new("~> 0.1.0".into()).unwrap(), )] .into_iter(), &vec![("gleam_stdlib".into(), Version::new(0, 2, 0))] .into_iter() .collect(), ) .unwrap_err(); match err { Error::IncompatibleLockedVersion { error } => assert_eq!( error, "gleam_stdlib is specified with the requirement `~> 0.1.0`, \ but it is locked to 0.2.0, which is incompatible." ), _ => panic!("wrong error: {err}"), } } #[test] fn resolution_with_exact_dep() { let result = resolve_versions( &make_remote(), HashMap::new(), "app".into(), vec![("gleam_stdlib".into(), Range::new("0.1.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ) .unwrap(); assert_eq!( result, vec![("gleam_stdlib".into(), Version::try_from("0.1.0").unwrap())] .into_iter() .collect() ); } #[test] fn parse_exact_version_test() { assert_eq!( parse_exact_version("1.0.0"), Some(Version::parse("1.0.0").unwrap()) ); assert_eq!( parse_exact_version("==1.0.0"), Some(Version::parse("1.0.0").unwrap()) ); assert_eq!( parse_exact_version("== 1.0.0"), Some(Version::parse("1.0.0").unwrap()) ); assert_eq!(parse_exact_version("~> 1.0.0"), None); assert_eq!(parse_exact_version(">= 1.0.0"), None); } #[test] fn resolve_major_version_upgrades() { let manifest = manifest::Manifest { requirements: vec![ ( EcoString::from("package_depends_on_indirect_pkg"), requirement::Requirement::Hex { version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(), }, ), ( EcoString::from("direct_pkg_with_major_version"), requirement::Requirement::Hex { version: Range::new("> 0.1.0 and <= 2.0.0".into()).unwrap(), }, ), ( EcoString::from("depends_on_old_version_of_direct_pkg"), requirement::Requirement::Hex { version: Range::new("> 0.1.0 and <= 1.0.0".into()).unwrap(), }, ), ] .into_iter() .collect(), packages: vec![ ManifestPackage { name: "direct_pkg_with_major_version".into(), version: Version::parse("0.1.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3]), }, }, ManifestPackage { name: "depends_on_old_version_of_direct_pkg".into(), version: Version::parse("0.1.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec!["direct_pkg_with_major_version".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3]), }, }, ManifestPackage { name: "pkg_depends_on_indirect_pkg".into(), version: Version::parse("0.1.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec!["indirect_pkg_with_major_version".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3]), }, }, ManifestPackage { name: "indirect_pkg_with_major_version".into(), version: Version::parse("0.1.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3]), }, }, ], }; let result = check_for_major_version_updates(&manifest, &make_remote()); // indirect package with major version will not be in the result even though a major // version of it is available assert_eq!( result, vec![( "direct_pkg_with_major_version".into(), ( Version::try_from("0.1.0").unwrap(), Version::try_from("1.1.0").unwrap() ) ),] .into_iter() .collect() ); } #[test] fn git_deps_are_not_checked_on_hex_for_major_version_updates() { let manifest = manifest::Manifest { requirements: vec![( EcoString::from("wibble"), requirement::Requirement::Git { git: "git".into(), ref_: "ref".into(), path: None, }, )] .into_iter() .collect(), packages: vec![ManifestPackage { name: "wibble".into(), version: Version::parse("0.1.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Git { repo: "repo".into(), commit: "commit".into(), path: None, }, }], }; // There's a package on hex with the same name of the git dependency // and a new major version. Those are not the same package though!! let remote = remote(vec![("wibble", vec![release("2.0.0", vec![])])]); let result = check_for_major_version_updates(&manifest, &remote); assert_eq!(result, HashMap::new()); } #[test] fn local_deps_are_not_checked_on_hex_for_major_version_updates() { let manifest = manifest::Manifest { requirements: vec![( EcoString::from("wibble"), requirement::Requirement::Path { path: "path".into(), }, )] .into_iter() .collect(), packages: vec![ManifestPackage { name: "wibble".into(), version: Version::parse("0.1.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Local { path: "path".into(), }, }], }; // There's a package on hex with the same name of the local dependency // and a new major version. Those are not the same package though!! let remote = remote(vec![("wibble", vec![release("2.0.0", vec![])])]); let result = check_for_major_version_updates(&manifest, &remote); assert_eq!(result, HashMap::new()); } fn retired_release( version: &str, requirements: Vec<(&str, &str)>, reason: hexpm::RetirementReason, message: &str, ) -> Release<()> { Release { retirement_status: Some(RetirementStatus { reason, message: message.into(), }), ..release(version, requirements) } } fn release(version: &str, requirements: Vec<(&str, &str)>) -> Release<()> { release_with_optional(version, requirements, vec![]) } fn release_with_optional( version: &str, requirements: Vec<(&str, &str)>, optional_requirements: Vec<(&str, &str)>, ) -> Release<()> { let mut all_requirements = HashMap::new(); for (name, range) in requirements { let requirement = Range::new(range.to_string()).unwrap(); let dependency = Dependency { requirement, optional: false, app: None, repository: None, }; let _ = all_requirements.insert(name.to_string(), dependency); } for (name, range) in optional_requirements { let requirement = Range::new(range.to_string()).unwrap(); let dependency = Dependency { requirement, optional: true, app: None, repository: None, }; let _ = all_requirements.insert(name.to_string(), dependency); } Release { version: Version::try_from(version).unwrap(), requirements: all_requirements, retirement_status: None, outer_checksum: vec![1, 2, 3], meta: (), } } fn remote(dependencies: Vec<(&str, Vec>)>) -> Remote { let mut deps = HashMap::new(); for (package, releases) in dependencies { let _ = deps.insert( package.into(), Rc::new(hexpm::Package { name: package.into(), repository: "hexpm".into(), releases, }), ); } Remote { deps } } #[test] fn resolution_error_message() { let remote = remote(vec![ ( "wibble", vec![ release("1.2.0", vec![("wobble", ">= 1.0.0 and < 2.0.0")]), release("1.3.0", vec![("wobble", ">= 2.0.0 and < 3.0.0")]), ], ), ( "wobble", vec![ release("1.1.0", vec![("woo", ">= 1.0.0 and < 2.0.0")]), release("2.0.0", vec![("waa", ">= 1.0.0 and < 2.0.0")]), ], ), ( "woo", vec![release("1.0.0", vec![]), release("2.0.0", vec![])], ), ( "waa", vec![release("1.0.0", vec![]), release("2.0.0", vec![])], ), ]); let result = resolve_versions( &remote, HashMap::new(), "app".into(), vec![ ( "wibble".into(), Range::new(">= 1.0.0 and < 2.0.0".into()).unwrap(), ), ( "woo".into(), Range::new(">= 2.0.0 and < 3.0.0".into()).unwrap(), ), ( "waa".into(), Range::new(">= 2.0.0 and < 3.0.0".into()).unwrap(), ), ] .into_iter(), &vec![].into_iter().collect(), ); if let Err(error @ Error::DependencyResolutionNoSolution { .. }) = result { let message = error.pretty_string(); insta::assert_snapshot!(message) } else { panic!("expected a resolution error message") } } #[test] fn adding_package_version_that_does_not_exist() { let remote = remote(vec![( "wibble", vec![release("1.2.0", vec![]), release("1.3.0", vec![])], )]); let result = resolve_versions( &remote, HashMap::new(), "app".into(), vec![("wibble".into(), Range::new("1.0.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ); if let Err(error @ Error::DependencyResolutionNoSolution { .. }) = result { let message = error.pretty_string(); insta::assert_snapshot!(message) } else { panic!("expected a resolution error message") } } #[test] fn adding_package_version_that_does_not_exist_2() { let remote = remote(vec![( "wibble", vec![release("1.2.0", vec![]), release("1.3.0", vec![])], )]); let result = resolve_versions( &remote, HashMap::new(), "app".into(), vec![("wibble".into(), Range::new("> 2.0.0".into()).unwrap())].into_iter(), &vec![].into_iter().collect(), ); if let Err(error @ Error::DependencyResolutionNoSolution { .. }) = result { let message = error.pretty_string(); insta::assert_snapshot!(message) } else { panic!("expected a resolution error message") } } }