// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Gleam contributors use std::collections::HashMap; use camino::{Utf8Path, Utf8PathBuf}; use ecow::EcoString; use hexpm::version::Version; use pretty_assertions::assert_eq; use gleam_core::{ Error, build::Runtime, config::{DenoConfig, DenoFlag, Docs, ErlangConfig, JavaScriptConfig}, manifest::{Base16Checksum, Manifest, ManifestPackage, ManifestPackageSource}, paths::ProjectPaths, requirement::Requirement, }; use crate::dependencies::*; #[test] fn list_manifest_format() { let mut buffer = vec![]; let manifest = Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "root".into(), version: Version::parse("1.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, }, ManifestPackage { name: "aaa".into(), version: Version::new(0, 4, 2), build_tools: ["rebar3".into(), "make".into()].into(), otp_app: Some("aaa_app".into()), requirements: vec!["zzz".into(), "gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ManifestPackage { name: "zzz".into(), version: Version::new(0, 4, 0), build_tools: ["mix".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ], }; list_manifest_packages(&mut buffer, manifest).unwrap(); assert_eq!( std::str::from_utf8(&buffer).unwrap(), "Package Version ------- ------- root 1.0.0 aaa 0.4.2 zzz 0.4.0 " ) } #[test] fn tree_format() { let mut buffer = vec![]; let manifest = Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "deps_proj".into(), version: Version::parse("1.0.0").unwrap(), build_tools: [].into(), otp_app: None, requirements: vec!["gleam_regexp".into(), "gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, }, ManifestPackage { name: "gleam_stdlib".into(), version: Version::new(0, 52, 0), build_tools: ["rebar3".into(), "make".into()].into(), otp_app: Some("aaa_app".into()), requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ManifestPackage { name: "gleam_regexp".into(), version: Version::new(1, 0, 0), build_tools: ["mix".into()].into(), otp_app: None, requirements: vec!["gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ], }; let options = TreeOptions { package: None, invert: None, }; let root_package_name = EcoString::from("deps_proj"); list_package_and_dependencies_tree( &mut buffer, options, manifest.packages.clone(), root_package_name, ) .unwrap(); assert_eq!( std::str::from_utf8(&buffer).unwrap(), r#"deps_proj v1.0.0 ├── gleam_regexp v1.0.0 │ └── gleam_stdlib v0.52.0 └── gleam_stdlib v0.52.0 "# ) } #[test] fn tree_package_format() { let mut buffer = vec![]; let manifest = Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "gleam_stdlib".into(), version: Version::new(0, 52, 0), build_tools: ["rebar3".into(), "make".into()].into(), otp_app: Some("aaa_app".into()), requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ManifestPackage { name: "deps_proj".into(), version: Version::parse("1.0.0").unwrap(), build_tools: [].into(), otp_app: None, requirements: vec!["gleam_stdlib".into(), "gleam_regexp".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, }, ManifestPackage { name: "gleam_regexp".into(), version: Version::new(1, 0, 0), build_tools: ["mix".into()].into(), otp_app: None, requirements: vec!["gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ], }; let options = TreeOptions { package: Some("gleam_regexp".to_string()), invert: None, }; let root_package_name = EcoString::from("deps_proj"); list_package_and_dependencies_tree( &mut buffer, options, manifest.packages.clone(), root_package_name, ) .unwrap(); assert_eq!( std::str::from_utf8(&buffer).unwrap(), r#"gleam_regexp v1.0.0 └── gleam_stdlib v0.52.0 "# ) } #[test] fn tree_invert_format() { let mut buffer = vec![]; let manifest = Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "gleam_stdlib".into(), version: Version::new(0, 52, 0), build_tools: ["rebar3".into(), "make".into()].into(), otp_app: Some("aaa_app".into()), requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ManifestPackage { name: "deps_proj".into(), version: Version::parse("1.0.0").unwrap(), build_tools: [].into(), otp_app: None, requirements: vec!["gleam_stdlib".into(), "gleam_regexp".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, }, ManifestPackage { name: "gleam_regexp".into(), version: Version::new(1, 0, 0), build_tools: ["mix".into()].into(), otp_app: None, requirements: vec!["gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ], }; let options = TreeOptions { package: None, invert: Some("gleam_stdlib".to_string()), }; let root_package_name = EcoString::from("deps_proj"); list_package_and_dependencies_tree( &mut buffer, options, manifest.packages.clone(), root_package_name, ) .unwrap(); assert_eq!( std::str::from_utf8(&buffer).unwrap(), r#"gleam_stdlib v0.52.0 ├── deps_proj v1.0.0 └── gleam_regexp v1.0.0 └── deps_proj v1.0.0 "# ) } #[test] fn list_tree_invalid_package_format() { let mut buffer = vec![]; let manifest = Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "gleam_stdlib".into(), version: Version::new(0, 52, 0), build_tools: ["rebar3".into(), "make".into()].into(), otp_app: Some("aaa_app".into()), requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ManifestPackage { name: "gleam_regexp".into(), version: Version::new(1, 0, 0), build_tools: ["mix".into()].into(), otp_app: None, requirements: vec!["gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![3, 22]), }, }, ManifestPackage { name: "root".into(), version: Version::parse("1.0.0").unwrap(), build_tools: [].into(), otp_app: None, requirements: vec!["gleam_regexp".into(), "gleam_stdlib".into()], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, }, ], }; let options = TreeOptions { package: Some("zzzzzz".to_string()), invert: None, }; let root_package_name = EcoString::from("deps_proj"); list_package_and_dependencies_tree( &mut buffer, options, manifest.packages.clone(), root_package_name, ) .unwrap(); assert_eq!( std::str::from_utf8(&buffer).unwrap(), r#"Package not found. Please check the package name. "# ) } #[test] fn parse_gleam_add_specifier_invalid_semver() { assert!(parse_gleam_add_specifier("some_package@1.2.3.4").is_err()); } #[test] fn parse_gleam_add_specifier_non_numeric_version() { assert!(parse_gleam_add_specifier("some_package@not_a_version").is_err()); } #[test] fn parse_gleam_add_specifier_default() { let provided = "some_package"; let expected = Requirement::hex(">= 0.0.0").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); assert_eq!(version, expected); assert_eq!("some_package", package); } #[test] fn parse_gleam_add_specifier_major_only() { let provided = "wobble@1"; let expected = Requirement::hex(">= 1.0.0 and < 2.0.0").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); assert_eq!(version, expected); assert_eq!("wobble", package); } #[test] fn parse_gleam_add_specifier_major_and_minor() { let provided = "wibble@1.2"; let expected = Requirement::hex(">= 1.2.0 and < 2.0.0").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); assert_eq!(version, expected); assert_eq!("wibble", package); } #[test] fn parse_gleam_add_specifier_major_minor_and_patch() { let provided = "bobble@1.2.3"; let expected = Requirement::hex("1.2.3").unwrap(); let (package, version) = parse_gleam_add_specifier(provided).unwrap(); assert_eq!(version, expected); assert_eq!("bobble", package); } #[test] fn missing_local_packages() { let manifest = Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "root".into(), version: Version::parse("1.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, }, ManifestPackage { name: "local1".into(), version: Version::parse("1.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]), }, }, ManifestPackage { name: "local2".into(), version: Version::parse("3.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]), }, }, ], }; let mut extra = LocalPackages { packages: [ ("local2".into(), Version::parse("2.0.0").unwrap()), ("local3".into(), Version::parse("3.0.0").unwrap()), ] .into(), git: HashMap::new(), } .missing_local_packages(&manifest, "root"); extra.sort(); assert_eq!( extra, [ &ManifestPackage { name: "local1".into(), version: Version::parse("1.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]), }, }, &ManifestPackage { name: "local2".into(), version: Version::parse("3.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]), }, }, ] ) } #[test] fn extra_local_packages() { let mut extra = LocalPackages { packages: [ ("local1".into(), Version::parse("1.0.0").unwrap()), ("local2".into(), Version::parse("2.0.0").unwrap()), ("local3".into(), Version::parse("3.0.0").unwrap()), ] .into(), git: HashMap::new(), } .extra_local_packages(&Manifest { requirements: HashMap::new(), packages: vec![ ManifestPackage { name: "local1".into(), version: Version::parse("1.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4, 5]), }, }, ManifestPackage { name: "local2".into(), version: Version::parse("3.0.0").unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![4, 5]), }, }, ], }); extra.sort(); assert_eq!( extra, [ ("local2".into(), Version::new(2, 0, 0)), ("local3".into(), Version::new(3, 0, 0)), ] ) } #[test] fn provide_wrong_package() { let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let result = provide_local_package( "wrong_name".into(), Utf8Path::new("./test/hello_world"), Utf8Path::new("./"), &project_paths, &mut provided, &mut vec!["root".into(), "subpackage".into()], ); match result { Err(Error::WrongDependencyProvided { expected, found, .. }) => { assert_eq!(expected, "wrong_name"); assert_eq!(found, "hello_world"); } _ => { panic!("Expected WrongDependencyProvided error") } } } #[test] fn provide_existing_package() { let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let result = provide_local_package( "hello_world".into(), Utf8Path::new("./test/hello_world"), Utf8Path::new("./"), &project_paths, &mut provided, &mut vec!["root".into(), "subpackage".into()], ); assert_eq!( result, Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) ); let result = provide_local_package( "hello_world".into(), Utf8Path::new("./test/hello_world"), Utf8Path::new("./"), &project_paths, &mut provided, &mut vec!["root".into(), "subpackage".into()], ); assert_eq!( result, Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) ); } #[test] fn provide_conflicting_package() { let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let result = provide_local_package( "hello_world".into(), Utf8Path::new("./test/hello_world"), Utf8Path::new("./"), &project_paths, &mut provided, &mut vec!["root".into(), "subpackage".into()], ); assert_eq!( result, Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) ); let result = provide_package( "hello_world".into(), Utf8PathBuf::from("./test/other"), SourceContext::Local { path: Utf8Path::new("./test/other").to_path_buf(), }, &project_paths, &mut provided, &mut vec!["root".into(), "subpackage".into()], ); match result { Err(Error::ProvidedDependencyConflict { package, .. }) => { assert_eq!(package, "hello_world"); } _ => { panic!("Expected ProvidedDependencyConflict error") } } } #[test] fn provided_is_absolute() { let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let result = provide_local_package( "hello_world".into(), Utf8Path::new("./test/hello_world"), Utf8Path::new("./"), &project_paths, &mut provided, &mut vec!["root".into(), "subpackage".into()], ); assert_eq!( result, Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) ); let package = provided.get("hello_world").unwrap().clone(); match package.source { ProvidedPackageSource::Local { path } => { assert!(path.is_absolute()) } _ => { panic!("Provide_local_package provided a package that is not local!") } } } #[test] fn provided_recursive() { let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let result = provide_local_package( "hello_world".into(), Utf8Path::new("./test/hello_world"), Utf8Path::new("./"), &project_paths, &mut provided, &mut vec!["root".into(), "hello_world".into(), "subpackage".into()], ); assert_eq!( result, Err(Error::PackageCycle { packages: vec!["subpackage".into(), "hello_world".into()], }) ) } #[test] fn provided_local_to_hex() { let provided_package = ProvidedPackage { version: Version::new(1, 0, 0), source: ProvidedPackageSource::Local { path: "canonical/path/to/package".into(), }, requirements: [ ( "req_1".into(), hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), }; let hex_package = hexpm::Package { name: "package".into(), repository: "local".into(), releases: vec![hexpm::Release { version: Version::new(1, 0, 0), retirement_status: None, outer_checksum: vec![], meta: (), requirements: [ ( "req_1".into(), hexpm::Dependency { requirement: hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, }, ), ( "req_2".into(), hexpm::Dependency { requirement: hexpm::version::Range::new("== 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, }, ), ] .into(), }], }; assert_eq!( provided_package.to_hex_package(&"package".into()), hex_package ); } #[test] fn provided_git_to_hex() { let provided_package = ProvidedPackage { version: Version::new(1, 0, 0), source: ProvidedPackageSource::Git { repo: "https://github.com/gleam-lang/gleam.git".into(), commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), path: None, }, requirements: [ ( "req_1".into(), hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), }; let hex_package = hexpm::Package { name: "package".into(), repository: "local".into(), releases: vec![hexpm::Release { version: Version::new(1, 0, 0), retirement_status: None, outer_checksum: vec![], meta: (), requirements: [ ( "req_1".into(), hexpm::Dependency { requirement: hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, }, ), ( "req_2".into(), hexpm::Dependency { requirement: hexpm::version::Range::new("== 1.0.0".into()).unwrap(), optional: false, app: None, repository: None, }, ), ] .into(), }], }; assert_eq!( provided_package.to_hex_package(&"package".into()), hex_package ); } #[test] fn provided_local_to_manifest() { let provided_package = ProvidedPackage { version: Version::new(1, 0, 0), source: ProvidedPackageSource::Local { path: "canonical/path/to/package".into(), }, requirements: [ ( "req_1".into(), hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), }; let manifest_package = ManifestPackage { name: "package".into(), version: Version::new(1, 0, 0), otp_app: None, build_tools: vec!["gleam".into()], requirements: vec!["req_1".into(), "req_2".into()], source: ManifestPackageSource::Local { path: "canonical/path/to/package".into(), }, }; assert_eq!( provided_package.to_manifest_package("package"), manifest_package ); } #[test] fn provided_git_to_manifest() { let provided_package = ProvidedPackage { version: Version::new(1, 0, 0), source: ProvidedPackageSource::Git { repo: "https://github.com/gleam-lang/gleam.git".into(), commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), path: None, }, requirements: [ ( "req_1".into(), hexpm::version::Range::new("~> 1.0.0".into()).unwrap(), ), ( "req_2".into(), hexpm::version::Range::new("== 1.0.0".into()).unwrap(), ), ] .into(), }; let manifest_package = ManifestPackage { name: "package".into(), version: Version::new(1, 0, 0), otp_app: None, build_tools: vec!["gleam".into()], requirements: vec!["req_1".into(), "req_2".into()], source: ManifestPackageSource::Git { repo: "https://github.com/gleam-lang/gleam.git".into(), commit: "bd9fe02f72250e6a136967917bcb1bdccaffa3c8".into(), path: None, }, }; assert_eq!( provided_package.to_manifest_package("package"), manifest_package ); } #[test] fn provided_git_path_package_resolves_repo_relative_path() { let repo_root = fs::canonicalise(Utf8Path::new("./test")).unwrap(); let result = resolve_git_path_package( &"hello_world".into(), Utf8Path::new("../hello_world"), &"https://github.com/gleam-lang/wibble.git".into(), &repo_root.join("hello_world"), &repo_root, ); assert_eq!( result, Ok((repo_root.join("hello_world"), "hello_world".into())) ); } #[test] fn provided_git_path_package_child_uses_canonical_parent_location() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let repo = base.join("repo"); fs::mkdir(&repo.join("packages").join("package_a")).unwrap(); fs::mkdir(&repo.join("packages").join("package_b")).unwrap(); fs::write( &repo.join("packages").join("package_a").join("gleam.toml"), r#" name = "package_a" version = "0.1.0" [dependencies] package_b = { path = "../package_b" } "#, ) .unwrap(); fs::write( &repo.join("packages").join("package_b").join("gleam.toml"), r#" name = "package_b" version = "0.2.0" "#, ) .unwrap(); let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let repo_root = fs::canonicalise(&repo).unwrap(); let result = provide_package( "package_a".into(), fs::canonicalise(&repo.join("packages").join("package_a")).unwrap(), SourceContext::Git { repo: "https://github.com/gleam-lang/wibble.git".into(), commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), path: Some("packages/package_a".into()), repo_root: &repo_root, }, &project_paths, &mut provided, &mut vec!["root".into()], ); assert_eq!( result, Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) ); let package = provided.get("package_b").unwrap(); assert_eq!( package.source, ProvidedPackageSource::Git { repo: "https://github.com/gleam-lang/wibble.git".into(), commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), path: Some("packages/package_b".into()), } ); } #[test] fn provided_git_path_package_to_repo_root_has_no_path() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let repo = base.join("repo"); fs::mkdir(&repo.join("backends").join("package_a")).unwrap(); fs::write( &repo.join("gleam.toml"), r#" name = "package_b" version = "0.2.0" "#, ) .unwrap(); fs::write( &repo.join("backends").join("package_a").join("gleam.toml"), r#" name = "package_a" version = "0.1.0" [dependencies] package_b = { path = "../.." } "#, ) .unwrap(); let mut provided = HashMap::new(); let project_paths = crate::project_paths_at_current_directory_without_toml(); let repo_root = fs::canonicalise(&repo).unwrap(); let result = provide_package( "package_a".into(), fs::canonicalise(&repo.join("backends").join("package_a")).unwrap(), SourceContext::Git { repo: "https://github.com/gleam-lang/wibble.git".into(), commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), path: Some("backends/package_a".into()), repo_root: &repo_root, }, &project_paths, &mut provided, &mut vec!["root".into()], ); assert_eq!( result, Ok(hexpm::version::Range::new("== 0.1.0".into()).unwrap()) ); // A path dependency resolving to the repository root must match how the // same package is provided when declared directly as a git dependency // with no path (`None`), otherwise it is reported as a conflict. let package = provided.get("package_b").unwrap(); assert_eq!( package.source, ProvidedPackageSource::Git { repo: "https://github.com/gleam-lang/wibble.git".into(), commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), path: None, } ); } #[test] fn provided_git_path_package_rejects_missing_directory() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let repo = base.join("repo"); fs::mkdir(&repo.join("parent")).unwrap(); let repo_root = fs::canonicalise(&repo).unwrap(); let result = resolve_git_path_package( &"missing".into(), Utf8Path::new("../missing"), &"https://github.com/gleam-lang/wibble.git".into(), &repo_root.join("parent"), &repo_root, ); assert_eq!( result, Err(Error::GitDependencyPathNotFound { package: "missing".into(), path: "../missing".into(), repo: "https://github.com/gleam-lang/wibble.git".into(), }) ); } #[test] fn resolve_git_subdir_accepts_directory_inside_repo() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let clone = base.join("clone"); fs::mkdir(&clone.join("sub")).unwrap(); let expected = fs::canonicalise(&clone).unwrap().join("sub"); assert_eq!( resolve_git_subdir(&clone, Utf8Path::new("sub")), Some(expected) ); } #[test] fn resolve_git_subdir_rejects_missing_directory() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let clone = base.join("clone"); fs::mkdir(&clone).unwrap(); assert_eq!(resolve_git_subdir(&clone, Utf8Path::new("missing")), None); } #[cfg(unix)] #[test] fn resolve_git_subdir_rejects_symlink_escaping_repo() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let clone = base.join("clone"); let outside = base.join("outside"); fs::mkdir(&clone).unwrap(); fs::mkdir(&outside).unwrap(); std::os::unix::fs::symlink(outside.as_std_path(), clone.join("escape").as_std_path()).unwrap(); assert_eq!(resolve_git_subdir(&clone, Utf8Path::new("escape")), None); } #[test] fn provided_git_path_package_rejects_escaping_path() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let repo = base.join("repo"); let outside = base.join("outside"); fs::mkdir(&repo.join("parent")).unwrap(); fs::mkdir(&outside).unwrap(); let repo_root = fs::canonicalise(&repo).unwrap(); let result = resolve_git_path_package( &"package_a".into(), Utf8Path::new("../../outside"), &"https://github.com/gleam-lang/wibble.git".into(), &repo_root.join("parent"), &repo_root, ); assert_eq!( result, Err(Error::GitDependencyPathNotFound { package: "package_a".into(), path: "../../outside".into(), repo: "https://github.com/gleam-lang/wibble.git".into(), }) ); } #[cfg(unix)] #[test] fn provided_git_path_package_rejects_symlink_escaping_repo() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let repo = base.join("repo"); let outside = base.join("outside"); fs::mkdir(&repo.join("parent")).unwrap(); fs::mkdir(&outside).unwrap(); std::os::unix::fs::symlink(outside.as_std_path(), repo.join("escape").as_std_path()).unwrap(); let repo_root = fs::canonicalise(&repo).unwrap(); let result = resolve_git_path_package( &"escape".into(), Utf8Path::new("../escape"), &"https://github.com/gleam-lang/wibble.git".into(), &repo_root.join("parent"), &repo_root, ); assert_eq!( result, Err(Error::GitDependencyPathNotFound { package: "escape".into(), path: "../escape".into(), repo: "https://github.com/gleam-lang/wibble.git".into(), }) ); } #[test] fn git_repo_dir_name_uses_repo_basename_and_hash_of_full_url() { assert_eq!( git_repo_dir_name("https://github.com/gleam-lang/gleam"), "gleam-4d40009bf5eb0110" ); assert_eq!( git_repo_dir_name("https://github.com/gleam-lang/gleam.git"), "gleam-ea2aeaa3761e8bc6" ); assert_eq!( git_repo_dir_name("git@github.com:gleam-lang/gleam.git"), "gleam-102c9e1e5cf87965" ); assert_eq!( git_repo_dir_name("C:\\src\\gleam.git"), "gleam-ab03c1f252f639da" ); } #[test] fn git_staging_path_is_clone_path_with_staging_suffix() { let paths = ProjectPaths::new("/app".into()); let repo = "https://github.com/gleam-lang/gleam.git"; assert_eq!( paths.build_git_repo(&git_repo_dir_name(repo)), Utf8PathBuf::from("/app/build/git/gleam-ea2aeaa3761e8bc6") ); assert_eq!( git_staging_path(&paths, repo, "wibble"), Utf8PathBuf::from("/app/build/git/gleam-ea2aeaa3761e8bc6-wibble-staging") ); } #[test] fn git_checkout_cleanup_deletes_staging_directory() { let tmp = tempfile::tempdir().unwrap(); let base = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let staging_path = base.join("clone-staging"); fs::mkdir(&staging_path).unwrap(); fs::write(&staging_path.join("gleam.toml"), "name = \"wibble\"").unwrap(); let checkout = GitCheckout::Staged { commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), staging_path: staging_path.clone(), }; assert_eq!(checkout.cleanup(), Ok(())); assert!(!staging_path.exists()); } #[test] fn git_checkout_cleanup_without_staging_is_noop() { let checkout = GitCheckout::InPlace { commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), }; assert_eq!(checkout.cleanup(), Ok(())); } #[test] fn remove_unused_git_clones_sweeps_unexpected_directories() { let tmp = tempfile::tempdir().unwrap(); let root = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let paths = ProjectPaths::new(root); let repo = "https://github.com/gleam-lang/gleam.git"; let expected_clone = paths.build_git_repo(&git_repo_dir_name(repo)); let stale_clone = paths.build_git_repo("other-0011223344556677"); let stale_staging = paths.build_git_repo(&format!("{}-staging", git_repo_dir_name(repo))); fs::mkdir(&expected_clone).unwrap(); fs::mkdir(&stale_clone).unwrap(); fs::mkdir(&stale_staging).unwrap(); let manifest = Manifest { requirements: HashMap::new(), packages: vec![ManifestPackage { name: "wibble".into(), version: Version::new(1, 0, 0), build_tools: ["gleam".into()].into(), otp_app: None, requirements: vec![], source: ManifestPackageSource::Git { repo: repo.into(), commit: "95cd2c2f45907e5571e9b5fcdfb27ff35cdcdd29".into(), path: Some("wibble".into()), }, }], }; assert_eq!(remove_unused_git_clones(&paths, &manifest), Ok(())); assert!(expected_clone.is_dir()); assert!(!stale_clone.exists()); assert!(!stale_staging.exists()); } #[test] fn remove_unused_git_clones_missing_directory_is_noop() { let tmp = tempfile::tempdir().unwrap(); let root = Utf8PathBuf::from_path_buf(tmp.path().to_path_buf()).unwrap(); let paths = ProjectPaths::new(root); let manifest = Manifest { requirements: HashMap::new(), packages: vec![], }; assert_eq!(remove_unused_git_clones(&paths, &manifest), Ok(())); } #[test] fn verified_requirements_equality_with_canonicalized_paths() { let temp_dir = tempfile::tempdir().expect("Failed to create a temp directory"); let temp_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) .expect("Path should be valid UTF-8"); let sub_dir = temp_path.join("subdir"); std::fs::create_dir(&sub_dir).expect("Failed to create a subdir"); let file_path = sub_dir.join("file.txt"); fs::write(&file_path, "content").expect("Failed to write to file"); let canonical_path = std::fs::canonicalize(&file_path).expect("Failed to canonicalize path"); let relative_path = temp_path.join("./subdir/../subdir/./file.txt"); let requirements1 = HashMap::from([( EcoString::from("dep1"), Requirement::Path { path: Utf8PathBuf::from(canonical_path.to_str().expect("Path should be valid UTF-8")), }, )]); let requirements2 = HashMap::from([( EcoString::from("dep1"), Requirement::Path { path: Utf8PathBuf::from(relative_path.to_string()), }, )]); assert!( is_same_requirements(&requirements1, &requirements2, &temp_path) .expect("Requirements should be the same") ); } #[test] fn test_path_dependency_config_updates() { let temp_dir = tempfile::tempdir().expect("Failed to create a temp directory"); let root_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) .expect("Path should be valid UTF-8"); let paths = ProjectPaths::new(root_path.clone()); let dep_path = root_path.join("dep"); std::fs::create_dir_all(&dep_path).expect("Failed to create dependency directory"); let build_packages_dir = root_path.join("build").join("packages"); std::fs::create_dir_all(&build_packages_dir) .expect("Failed to create build/packages directory"); let config = "name = \"dep\" version = \"1.0.0\" [dependencies] "; let dep_config_path = dep_path.join("gleam.toml"); fs::write(&dep_config_path, config).expect("Failed to write to manifest file"); let requirements = HashMap::from([( EcoString::from("dep"), Requirement::Path { path: Utf8PathBuf::from("dep"), }, )]); // Initial check testing let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap(); assert!(!unchanged, "fresh always needs resolution"); let fingerprint_path = build_packages_dir.join("dep.config_fingerprint"); assert!(fingerprint_path.exists(), "fingerprint must exist"); let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap(); assert!(unchanged, "is unchanged"); // Set fingerprint mtime to some time in the past. This causes the mtime check to fail, // so it moves on to checking the fingerprint itself. let past = std::time::SystemTime::now() - std::time::Duration::from_secs(10); let past = filetime::FileTime::from_system_time(past); filetime::set_file_mtime(&fingerprint_path, past).unwrap(); let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap(); assert!(unchanged, "mtime is outdated, but content has not changed"); // Writing new content means the mtime and the fingerprint checks will fail. let config = "name = \"dep\" version = \"1.0.0\" [dependencies] blah = \">= 1.0.0\" "; fs::write(&dep_config_path, config).unwrap(); let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap(); assert!(!unchanged, "content has changed"); // Run again, to ensure that the fingerprint has been updated. let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap(); assert!(unchanged, "no changes since last run"); // Test that mtime is checked first, and that content is not checked when the mtime // is still valid. We do this by having having content that would fail the fingerprint // check, but having a fresh mtime so it never gets checked. // This can never happen in reality as you can't update the content without updating // the mtime, but we create the situation in this test to verify the short-circuiting // behaviour. let config = "name = \"dep\" version = \"1.0.0\" [dependencies] blah = \">= 1.0.0\" wub = \">= 1.0.0\" "; fs::write(&dep_config_path, config).unwrap(); let future = std::time::SystemTime::now() + std::time::Duration::from_secs(10); let future = filetime::FileTime::from_system_time(future); filetime::set_file_mtime(&fingerprint_path, future).unwrap(); let unchanged = path_dependency_configs_unchanged(&requirements, &paths).unwrap(); assert!(unchanged, "fingerprint is outdated, but mtime is not"); } fn create_testable_unlock_manifest( packages: Vec<(EcoString, Version, Vec)>, requirements: Vec<(EcoString, EcoString)>, ) -> Manifest { let manifest_packages = packages .into_iter() .map(|(name, version, requirements)| ManifestPackage { name, version, build_tools: vec!["gleam".into()], otp_app: None, requirements, source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![]), }, }) .collect(); let root_requirements = requirements .into_iter() .map(|(name, range)| { ( name, Requirement::Hex { version: hexpm::version::Range::new(range.into()).unwrap(), }, ) }) .collect(); Manifest { packages: manifest_packages, requirements: root_requirements, } } #[test] fn test_unlock_package() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ("package_d".into(), Version::new(4, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into()], ), ( "package_b".into(), Version::new(2, 0, 0), vec!["package_c".into()], ), ("package_c".into(), Version::new(3, 0, 0), vec![]), ("package_d".into(), Version::new(4, 0, 0), vec![]), ]; let manifest = create_testable_unlock_manifest(packages, Vec::new()); let packages_to_unlock = vec!["package_a".into()]; unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert!(!locked.contains_key("package_a")); assert!(!locked.contains_key("package_b")); assert!(!locked.contains_key("package_c")); assert!(locked.contains_key("package_d")); } #[test] fn test_unlock_package_without_manifest() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ]); let packages_to_unlock = vec!["package_a".into()]; unlock_packages(&mut locked, &packages_to_unlock, None).unwrap(); assert!(!locked.contains_key("package_a")); assert!(locked.contains_key("package_b")); assert!(locked.contains_key("package_c")); } #[test] fn test_unlock_nonexistent_package() { let initial_locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into()], ), ("package_b".into(), Version::new(2, 0, 0), vec![]), ]; let manifest = create_testable_unlock_manifest(packages, Vec::new()); let packages_to_unlock = vec!["nonexistent_package".into()]; let mut locked = initial_locked.clone(); unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert_eq!( initial_locked, locked, "Locked packages should remain unchanged" ); } #[test] fn test_unlock_multiple_packages() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ("package_d".into(), Version::new(4, 0, 0)), ("package_e".into(), Version::new(5, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into()], ), ( "package_b".into(), Version::new(2, 0, 0), vec!["package_c".into()], ), ("package_c".into(), Version::new(3, 0, 0), vec![]), ( "package_d".into(), Version::new(4, 0, 0), vec!["package_e".into()], ), ("package_e".into(), Version::new(5, 0, 0), vec![]), ]; let manifest = create_testable_unlock_manifest(packages, Vec::new()); let packages_to_unlock = vec!["package_a".into(), "package_d".into()]; unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert!(!locked.contains_key("package_a")); assert!(!locked.contains_key("package_b")); assert!(!locked.contains_key("package_c")); assert!(!locked.contains_key("package_d")); assert!(!locked.contains_key("package_e")); } #[test] fn test_unlock_packages_empty_input() { let initial_locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into()], ), ("package_b".into(), Version::new(2, 0, 0), vec![]), ]; let manifest = create_testable_unlock_manifest(packages, Vec::new()); let packages_to_unlock: Vec = vec![]; let mut locked = initial_locked.clone(); unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert_eq!( initial_locked, locked, "Locked packages should remain unchanged when no packages are specified to unlock" ); } #[test] fn test_unlock_package_preserve_shared_deps() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_c".into()], ), ( "package_b".into(), Version::new(2, 0, 0), vec!["package_c".into()], ), ("package_c".into(), Version::new(3, 0, 0), vec![]), ]; let manifest = create_testable_unlock_manifest(packages, Vec::new()); let packages_to_unlock: Vec = vec!["package_a".into()]; unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert!(!locked.contains_key("package_a")); assert!(locked.contains_key("package_b")); assert!(locked.contains_key("package_c")); } #[test] fn test_unlock_package_with_root_dep() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into()], ), ( "package_b".into(), Version::new(2, 0, 0), vec!["package_c".into()], ), ("package_c".into(), Version::new(3, 0, 0), vec![]), ]; let requirements = vec![("package_b".into(), ">= 2.0.0".into())]; let manifest = create_testable_unlock_manifest(packages, requirements); let packages_to_unlock: Vec = vec!["package_a".into()]; unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert!(!locked.contains_key("package_a")); assert!(locked.contains_key("package_b")); assert!(locked.contains_key("package_c")); } #[test] fn test_unlock_root_dep_package() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into()], ), ("package_b".into(), Version::new(2, 0, 0), vec![]), ("package_c".into(), Version::new(3, 0, 0), vec![]), ]; let requirements = vec![("package_a".into(), ">= 1.0.0".into())]; let manifest = create_testable_unlock_manifest(packages, requirements); let packages_to_unlock: Vec = vec!["package_a".into()]; unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert!(!locked.contains_key("package_a")); assert!(!locked.contains_key("package_b")); assert!(locked.contains_key("package_c")); } #[test] fn test_unlock_package_with_and_without_root_dep() { let mut locked = HashMap::from([ ("package_a".into(), Version::new(1, 0, 0)), ("package_b".into(), Version::new(2, 0, 0)), ("package_c".into(), Version::new(3, 0, 0)), ]); let packages = vec![ ( "package_a".into(), Version::new(1, 0, 0), vec!["package_b".into(), "package_c".into()], ), ("package_b".into(), Version::new(2, 0, 0), vec![]), ("package_c".into(), Version::new(3, 0, 0), vec![]), ]; let requirements = vec![("package_b".into(), ">= 2.0.0".into())]; let manifest = create_testable_unlock_manifest(packages, requirements); let packages_to_unlock: Vec = vec!["package_a".into()]; unlock_packages(&mut locked, &packages_to_unlock, Some(&manifest)).unwrap(); assert!(!locked.contains_key("package_a")); assert!(locked.contains_key("package_b")); assert!(!locked.contains_key("package_c")); } fn manifest_package(name: &str, version: &str, requirements: Vec) -> ManifestPackage { ManifestPackage { name: name.into(), version: Version::parse(version).unwrap(), build_tools: ["gleam".into()].into(), otp_app: None, requirements, source: ManifestPackageSource::Hex { outer_checksum: Base16Checksum(vec![1, 2, 3, 4]), }, } } fn package_config( dependencies: HashMap, dev_dependencies: HashMap, ) -> PackageConfig { PackageConfig { name: "the_package".into(), version: Version::parse("1.0.0").unwrap(), gleam_version: None, licences: vec![], description: "".into(), documentation: Docs { pages: vec![] }, dependencies, dev_dependencies, repository: None, links: vec![], erlang: ErlangConfig { application_start_module: None, application_start_argument: None, extra_applications: vec![], }, javascript: JavaScriptConfig { typescript_declarations: false, source_maps: false, runtime: Runtime::NodeJs, deno: DenoConfig { allow_env: DenoFlag::AllowAll, allow_sys: true, allow_hrtime: true, allow_net: DenoFlag::AllowAll, allow_ffi: true, allow_read: DenoFlag::AllowAll, allow_run: DenoFlag::AllowAll, allow_write: DenoFlag::AllowAll, allow_all: true, unstable: true, location: None, }, }, target: Target::Erlang, internal_modules: None, } } #[test] fn test_remove_do_nothing() { let config = package_config( HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), HashMap::from([("b".into(), Requirement::hex("~>2.0").unwrap())]), ); let mut manifest = Manifest { requirements: HashMap::from([ ("a".into(), Requirement::hex("~>1.0").unwrap()), ("b".into(), Requirement::hex("~>2.0").unwrap()), ]), packages: vec![ manifest_package("a", "1.0.0", vec![]), manifest_package("b", "2.0.8", vec![]), ], }; let manifest_copy = manifest.clone(); remove_extra_requirements(&config, &mut manifest).unwrap(); assert_eq!(manifest.requirements, manifest_copy.requirements); assert_eq!(manifest.packages, manifest_copy.packages); } #[test] fn test_remove_simple() { let config = package_config(HashMap::new(), HashMap::new()); let mut manifest = Manifest { requirements: HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), packages: vec![manifest_package("a", "1.0.0", vec![])], }; remove_extra_requirements(&config, &mut manifest).unwrap(); assert_eq!(manifest.requirements, config.dependencies); assert_eq!(manifest.packages, vec![]); } #[test] fn test_remove_package_with_transitive_dependencies() { let config = package_config(HashMap::new(), HashMap::new()); let mut manifest = Manifest { requirements: HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), packages: vec![ manifest_package("a", "1.0.0", vec!["b".into()]), manifest_package("b", "1.2.3", vec!["c".into()]), manifest_package("c", "2.0.0", vec![]), ], }; remove_extra_requirements(&config, &mut manifest).unwrap(); assert_eq!(manifest.requirements, config.dependencies); assert_eq!(manifest.packages, vec![]); } #[test] fn test_remove_package_with_shared_transitive_dependencies() { let config = package_config( HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), HashMap::new(), ); let mut manifest = Manifest { requirements: HashMap::from([ ("a".into(), Requirement::hex("~>1.0").unwrap()), ("b".into(), Requirement::hex("~>1.0").unwrap()), ]), packages: vec![ manifest_package("a", "1.0.0", vec!["c".into()]), manifest_package("b", "1.2.3", vec!["c".into(), "d".into()]), manifest_package("c", "2.0.0", vec![]), manifest_package("d", "0.1.0", vec![]), ], }; remove_extra_requirements(&config, &mut manifest).unwrap(); assert_eq!(manifest.requirements, config.dependencies); assert_eq!( manifest.packages, vec![ manifest_package("a", "1.0.0", vec!["c".into()]), manifest_package("c", "2.0.0", vec![]), ] ); } #[test] fn test_remove_package_that_is_also_a_transitive_dependency() { let config = package_config( HashMap::from([("a".into(), Requirement::hex("~>1.0").unwrap())]), HashMap::new(), ); let mut manifest = Manifest { requirements: HashMap::from([ ("a".into(), Requirement::hex("~>1.0").unwrap()), ("b".into(), Requirement::hex("~>1.0").unwrap()), ]), packages: vec![ manifest_package("a", "1.0.0", vec!["b".into(), "c".into()]), manifest_package("b", "1.2.3", vec!["c".into(), "d".into()]), manifest_package("c", "2.0.0", vec![]), manifest_package("d", "0.1.0", vec![]), ], }; let manifest_copy = manifest.clone(); remove_extra_requirements(&config, &mut manifest).unwrap(); assert_eq!(manifest.requirements, config.dependencies); assert_eq!(manifest.packages, manifest_copy.packages); } #[test] fn test_pretty_print_major_versions_available() { let versions = vec![ ( "very_long_package_name".to_string(), (Version::new(18, 382, 43), Version::new(19, 0, 38)), ), ( "gleam_stdlib".to_string(), (Version::new(0, 45, 0), Version::new(1, 0, 0)), ), ( "short_name".to_string(), (Version::new(1, 0, 0), Version::new(2, 0, 0)), ), ] .into_iter() .collect(); let output = pretty_print_major_versions_available(versions); insta::assert_snapshot!(output); } #[test] fn test_pretty_print_version_updates() { let versions = vec![ ( "gleam_stdlib".to_string(), (Version::new(0, 45, 0), Version::new(0, 46, 0)), ), ( "wisp".to_string(), (Version::new(2, 1, 0), Version::new(2, 1, 1)), ), ( "very_long_package_name".to_string(), (Version::new(12, 12, 12), Version::new(120, 12, 12)), ), ] .into_iter() .collect(); let output = pretty_print_version_updates(versions); insta::assert_snapshot!(output); } #[test] fn test_pretty_print_outdated_versions() { let versions = vec![ ( "gleam_stdlib".to_string(), (Version::new(0, 45, 0), Version::new(0, 46, 0)), ), ( "wisp".to_string(), (Version::new(2, 1, 0), Version::new(2, 1, 1)), ), ( "very_long_package_name".to_string(), (Version::new(12, 12, 12), Version::new(120, 12, 12)), ), ] .into_iter() .collect(); let output = pretty_print_outdated_versions(12, versions); insta::assert_snapshot!(output); } #[test] fn test_pretty_print_outdated_versions_no_updates() { let versions: HashMap = HashMap::new(); let output = pretty_print_outdated_versions(12, versions); insta::assert_snapshot!(output); } #[test] fn test_ensure_packages_exist_locally_all_present() { let manifest = Manifest { requirements: HashMap::new(), packages: vec![ manifest_package("package_a", "1.0.0", vec![]), manifest_package("package_b", "2.0.0", vec![]), manifest_package("package_c", "3.0.0", vec![]), ], }; let packages_to_check = vec!["package_a".into(), "package_b".into()]; let result = dependency_manager::ensure_packages_exist_locally(&manifest, &packages_to_check); assert!(result.is_ok(), "All packages exist, should return Ok"); } #[test] fn test_ensure_packages_exist_locally_some_missing() { let manifest = Manifest { requirements: HashMap::new(), packages: vec![ manifest_package("package_a", "1.0.0", vec![]), manifest_package("package_b", "2.0.0", vec![]), ], }; let packages_to_check = vec![ "package_a".into(), "package_b".into(), "missing_package".into(), "another_missing".into(), ]; let result = dependency_manager::ensure_packages_exist_locally(&manifest, &packages_to_check); match result { Err(Error::PackagesToUpdateNotExist { packages }) => { assert_eq!(packages.len(), 2); assert!(packages.contains(&"missing_package".into())); assert!(packages.contains(&"another_missing".into())); } _ => panic!("Expected PackagesToUpdateNotExist error"), } }