Files
depot/src/planner/tests.rs
T

577 lines
18 KiB
Rust

use super::{
Candidate, CandidateKind, MatchKind, PlannerOptions, build_dependency_install_plan,
dedupe_candidate_packages, prune_replacement_fallback_candidates, sort_candidates,
source_deps_for_install,
};
use crate::config::{BinaryRepo, Config};
use crate::db;
use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
fn mk_spec() -> PackageSpec {
PackageSpec {
package: PackageInfo {
name: "foo".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
},
packages: vec![PackageInfo {
name: "foo-libs".into(),
real_name: None,
version: "1.0".into(),
revision: 1,
description: "d".into(),
homepage: "h".into(),
abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()],
}],
alternatives: Alternatives::default(),
manual_sources: Vec::new(),
source: vec![Source {
url: "https://example.test/foo.tar.gz".into(),
sha256: "skip".into(),
extract_dir: "foo".into(),
patches: Vec::new(),
post_extract: Vec::new(),
cherry_pick: Vec::new(),
}],
build: Build {
build_type: BuildType::Autotools,
flags: BuildFlags::default(),
},
dependencies: Dependencies {
build: vec!["make".into()],
runtime: vec!["foo-libs".into(), "zlib".into()],
test: vec!["bats".into()],
optional: vec!["docs-viewer".into()],
groups: Vec::new(),
lib32: None,
},
package_alternatives: BTreeMap::from([(
"foo-libs".into(),
Alternatives {
provides: vec!["libfoo".into()],
conflicts: Vec::new(),
replaces: Vec::new(),
lib32: None,
},
)]),
package_dependencies: BTreeMap::from([(
"foo".into(),
Dependencies {
build: Vec::new(),
runtime: vec!["foo-libs".into(), "libfoo".into(), "openssl".into()],
test: Vec::new(),
optional: Vec::new(),
groups: Vec::new(),
lib32: None,
},
)]),
spec_dir: PathBuf::from("."),
}
}
fn mk_installed_spec(name: &str, version: &str) -> PackageSpec {
let mut spec = mk_spec();
spec.package.name = name.to_string();
spec.package.version = version.to_string();
spec
}
fn mk_binary_candidate(name: &str, repo_name: &str, priority: i32) -> Candidate {
Candidate {
package: name.to_string(),
kind: CandidateKind::Binary {
repo_name: repo_name.to_string(),
record: Box::new(db::repo::BinaryRepoPackageRecord {
repo_name: repo_name.to_string(),
name: name.to_string(),
real_name: None,
version: "1.0.0".to_string(),
revision: 1,
abi_breaking: false,
built_against: Vec::new(),
completed_at: None,
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
size: 1024,
sha512: "sha512".to_string(),
description: None,
homepage: None,
license: None,
provides: Vec::new(),
conflicts: Vec::new(),
replaces: Vec::new(),
runtime_dependencies: Vec::new(),
optional_dependencies: Vec::new(),
groups: Vec::new(),
}),
},
match_kind: MatchKind::Exact,
sort_repo_priority: priority,
sort_label: format!("binary:{repo_name}"),
}
}
fn mk_source_candidate(name: &str, path: &Path, local_sibling: bool) -> Candidate {
Candidate {
package: name.to_string(),
kind: CandidateKind::Source {
path: path.to_path_buf(),
local_sibling,
},
match_kind: MatchKind::Exact,
sort_repo_priority: if local_sibling { -10 } else { 0 },
sort_label: if local_sibling {
"source:local-sibling".to_string()
} else {
"source:local".to_string()
},
}
}
#[test]
fn source_deps_for_install_excludes_local_runtime_outputs_and_provides() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, false, false);
assert!(deps.contains(&"make".to_string()));
assert!(deps.contains(&"zlib".to_string()));
assert!(deps.contains(&"openssl".to_string()));
assert!(!deps.contains(&"foo-libs".to_string()));
assert!(!deps.contains(&"libfoo".to_string()));
}
#[test]
fn source_deps_for_install_does_not_include_test_deps() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, false, false);
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_includes_test_deps_when_enabled() {
let spec = mk_spec();
let deps = source_deps_for_install(&spec, true, false);
assert!(deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_uses_lib32_only_dependencies_when_requested() {
let mut spec = mk_spec();
spec.dependencies.lib32 = Some(crate::package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: vec!["lib32-bats".into()],
optional: Vec::new(),
groups: Vec::new(),
});
let deps = source_deps_for_install(&spec, true, true);
assert!(deps.contains(&"gcc-multilib".to_string()));
assert!(deps.contains(&"lib32-zlib".to_string()));
assert!(!deps.contains(&"lib32-bats".to_string()));
assert!(!deps.contains(&"make".to_string()));
assert!(!deps.contains(&"zlib".to_string()));
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn source_deps_for_install_uses_lib32_only_dependencies_from_spec_flag() {
let mut spec = mk_spec();
spec.build.flags.lib32_only = true;
spec.dependencies.lib32 = Some(crate::package::DependencyGroup {
build: vec!["gcc-multilib".into()],
runtime: vec!["lib32-zlib".into()],
test: vec!["lib32-bats".into()],
optional: Vec::new(),
groups: Vec::new(),
});
let deps = source_deps_for_install(&spec, true, false);
assert!(deps.contains(&"gcc-multilib".to_string()));
assert!(deps.contains(&"lib32-zlib".to_string()));
assert!(!deps.contains(&"lib32-bats".to_string()));
assert!(!deps.contains(&"make".to_string()));
assert!(!deps.contains(&"zlib".to_string()));
assert!(!deps.contains(&"bats".to_string()));
}
#[test]
fn candidate_dedup_keeps_highest_priority_origin_for_same_package() {
let candidates = vec![
mk_source_candidate("meson", Path::new("packages/core/meson/meson.toml"), false),
mk_binary_candidate("meson", "core", 0),
mk_source_candidate(
"meson",
Path::new("../packages/core/meson/meson.toml"),
true,
),
];
let deduped = dedupe_candidate_packages(sort_candidates(&candidates, true));
assert_eq!(deduped.len(), 1);
assert!(matches!(deduped[0].kind, CandidateKind::Binary { .. }));
}
#[test]
fn candidate_dedup_uses_local_origin_when_binaries_are_not_preferred() {
let candidates = vec![
mk_binary_candidate("ninja", "core", 0),
mk_source_candidate("ninja", Path::new("packages/core/ninja/ninja.toml"), false),
];
let deduped = dedupe_candidate_packages(sort_candidates(&candidates, false));
assert_eq!(deduped.len(), 1);
assert!(matches!(deduped[0].kind, CandidateKind::Source { .. }));
}
#[test]
fn replacement_candidates_are_pruned_when_direct_matches_exist() {
let mut replacement = mk_binary_candidate("vx", "core", 0);
replacement.match_kind = MatchKind::Replaces;
let mut exact = mk_binary_candidate("patch", "core", 0);
exact.match_kind = MatchKind::Exact;
let mut provides = mk_binary_candidate("busybox", "core", 0);
provides.match_kind = MatchKind::Provides;
let pruned = prune_replacement_fallback_candidates(vec![replacement, exact, provides]);
assert_eq!(pruned.len(), 2);
assert!(
pruned
.iter()
.all(|candidate| candidate.match_kind != MatchKind::Replaces)
);
}
#[test]
fn replacement_candidates_remain_when_they_are_the_only_matches() {
let mut replacement = mk_binary_candidate("vx", "core", 0);
replacement.match_kind = MatchKind::Replaces;
let pruned = prune_replacement_fallback_candidates(vec![replacement]);
assert_eq!(pruned.len(), 1);
assert!(matches!(pruned[0].match_kind, MatchKind::Replaces));
assert_eq!(pruned[0].package, "vx");
}
#[test]
fn build_dependency_install_plan_skips_installed_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let db_path = config.db_dir.join("packages.db");
let destdir = rootfs.path().join("dest");
std::fs::create_dir_all(&destdir).unwrap();
db::register_package(&db_path, &mk_installed_spec("meson", "1.0.0"), &destdir).unwrap();
let plan = build_dependency_install_plan(
&config,
rootfs.path(),
&["meson".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
assert!(plan.steps.is_empty());
assert!(plan.actionable_steps().next().is_none());
}
#[test]
fn build_dependency_install_plan_reports_source_cycle_chain() {
let rootfs = tempfile::tempdir().unwrap();
let repo_root = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let alpha_dir = repo_root.path().join("alpha");
let beta_dir = repo_root.path().join("beta");
fs::create_dir_all(&alpha_dir).unwrap();
fs::create_dir_all(&beta_dir).unwrap();
fs::write(
alpha_dir.join("alpha.toml"),
r#"
[build]
type = "meta"
[dependencies]
runtime = ["beta"]
[package]
description = "alpha"
homepage = "https://example.test/alpha"
license = "MIT"
name = "alpha"
version = "1.0.0"
"#,
)
.unwrap();
fs::write(
beta_dir.join("beta.toml"),
r#"
[build]
type = "meta"
[dependencies]
runtime = ["alpha"]
[package]
description = "beta"
homepage = "https://example.test/beta"
license = "MIT"
name = "beta"
version = "1.0.0"
"#,
)
.unwrap();
let err = build_dependency_install_plan(
&config,
rootfs.path(),
&["alpha".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: false,
local_sibling_root: Some(repo_root.path().to_path_buf()),
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap_err();
assert_eq!(
err.to_string(),
"Dependency cycle detected: alpha -> beta -> alpha"
);
}
#[test]
fn build_dependency_install_plan_matches_local_sibling_real_name() {
let rootfs = tempfile::tempdir().unwrap();
let repo_root = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let libressl_dir = repo_root.path().join("libressl43");
fs::create_dir_all(&libressl_dir).unwrap();
fs::write(
libressl_dir.join("libressl43.toml"),
r#"
[build]
type = "meta"
[package]
description = "LibreSSL"
homepage = "https://www.libressl.org/"
license = "ISC"
name = "libressl43"
real_name = "libressl"
version = "4.3.2"
"#,
)
.unwrap();
let plan = build_dependency_install_plan(
&config,
rootfs.path(),
&["libressl".to_string()],
PlannerOptions {
assume_yes: false,
prefer_binary: false,
local_sibling_root: Some(repo_root.path().to_path_buf()),
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].package, "libressl43");
assert!(matches!(
plan.steps[0].origin,
super::PlanOrigin::Source {
local_sibling: true,
..
}
));
}
fn write_compressed_repo_db(db_path: &Path, zst_path: &Path) {
let mut input = fs::File::open(db_path).unwrap();
let output = fs::File::create(zst_path).unwrap();
let mut encoder = zstd::stream::write::Encoder::new(output, 3).unwrap();
std::io::copy(&mut input, &mut encoder).unwrap();
encoder.finish().unwrap();
}
#[test]
fn binary_plan_uses_built_against_concrete_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let mut config = Config::for_rootfs(rootfs.path());
config.package_cache_dir = cache_dir.path().to_path_buf();
config.repo_settings.prefer_binary = true;
config.binary_repos.insert(
"core".into(),
BinaryRepo {
url: url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string(),
allow_unsigned: true,
..BinaryRepo::default()
},
);
let installed_dest = rootfs.path().join("installed");
fs::create_dir_all(&installed_dest).unwrap();
let mut installed = mk_installed_spec("icu79", "79.1");
installed.package.real_name = Some("icu".into());
db::register_package(
&config.installed_db_path(rootfs.path()),
&installed,
&installed_dest,
)
.unwrap();
let db_path = repo_dir.path().join("repo.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"
CREATE TABLE packages (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
real_name TEXT,
version TEXT NOT NULL,
revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER,
description TEXT,
homepage TEXT,
license TEXT,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
sha512 TEXT NOT NULL
);
CREATE TABLE provides (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE replaces (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE conflicts (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE dependencies (package_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL);
CREATE TABLE groups (package_id INTEGER, name TEXT NOT NULL);
",
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, NULL, 'app.pkg', 1, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (1, 'runtime', 'icu')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu78.pkg', 1, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (3, 'icu79', 'icu', '79.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu79.pkg', 1, 'ee', 'ff')",
[],
)
.unwrap();
drop(conn);
write_compressed_repo_db(&db_path, &repo_dir.path().join("repo.db.zst"));
fs::remove_file(&db_path).unwrap();
let plan = super::build_install_plan(
&config,
rootfs.path(),
super::InstallTarget::PackageName("app".into()),
PlannerOptions {
assume_yes: true,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
let actionable: Vec<_> = plan
.actionable_steps()
.map(|step| step.package.as_str())
.collect();
assert_eq!(actionable, vec!["icu78", "app"]);
}
#[test]
fn add_dependency_edge_skips_active_binary_cycle_back_edge() {
let rootfs = tempfile::tempdir().unwrap();
let config = Config::for_rootfs(rootfs.path());
let mut resolver = super::Resolver::new(
&config,
rootfs.path(),
PlannerOptions {
assume_yes: false,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
);
let freetype2 = resolver.graph.add_node(super::NodeData {
step: super::PlannedStep {
package: "freetype2".into(),
action: super::PlanAction::InstallBinary,
origin: super::PlanOrigin::Installed,
requested_by: vec!["requested".into()],
},
});
let harfbuzz = resolver.graph.add_node(super::NodeData {
step: super::PlannedStep {
package: "harfbuzz".into(),
action: super::PlanAction::InstallBinary,
origin: super::PlanOrigin::Installed,
requested_by: vec!["requested".into()],
},
});
resolver.stack = vec!["freetype2".into(), "harfbuzz".into()];
resolver
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
.unwrap();
assert_eq!(resolver.graph.edge_count(), 0);
resolver.stack.clear();
resolver
.add_dependency_edge(freetype2, harfbuzz, "harfbuzz")
.unwrap();
assert_eq!(resolver.graph.edge_count(), 1);
}