use anyhow::Result;
use bob::PackageStateKind::*;
use bob::Scan;
use bob::scan::ScanSummary;
use pkgsrc::ScanIndex;
use std::fs::File;
use std::io::BufReader;
use std::sync::OnceLock;
const PSCAN_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/pscan.zst");
static SCAN_DATA: OnceLock<(ScanSummary, usize)> = OnceLock::new();
fn get_scan_result() -> &'static (ScanSummary, usize) {
SCAN_DATA.get_or_init(|| {
let scan_data = load_pscan(PSCAN_PATH).expect("failed to load pscan");
let count = scan_data.len();
let mut scan = Scan::default();
let result = scan.resolve(scan_data).expect("failed to resolve");
(result, count)
})
}
fn load_pscan(path: &str) -> Result<Vec<ScanIndex>> {
let file = File::open(path)?;
let decoder = zstd::stream::Decoder::new(file)?;
let reader = BufReader::new(decoder);
let results: Result<Vec<_>, _> = ScanIndex::from_reader(reader).collect();
Ok(results?)
}
#[test]
fn resolve_full_tree() -> Result<()> {
let (result, imported) = get_scan_result();
assert_eq!(*imported, 29022, "expected 29022 packages in pscan");
let c = result.counts();
assert_eq!(c.buildable, 27370);
assert_eq!(c.states[PreSkipped], 1148);
assert_eq!(c.states[PreFailed], 175);
assert_eq!(c.states[IndirectPreSkipped], 277);
assert_eq!(c.states[IndirectPreFailed], 40);
assert_eq!(c.states[Unresolved], 6);
assert_eq!(result.packages.len(), *imported);
Ok(())
}
#[test]
fn resolve_presolve_output() -> Result<()> {
use std::io::BufRead;
let (result, _) = get_scan_result();
let expected_path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/presolve.zst");
let file = File::open(expected_path)?;
let decoder = zstd::stream::Decoder::new(file)?;
let mut expected_lines = BufReader::new(decoder).lines();
let mut line_num = 0;
for pkg in &result.packages {
for actual_line in pkg.to_string().lines() {
line_num += 1;
match expected_lines.next() {
Some(Ok(expected_line)) if expected_line == actual_line => {}
Some(Ok(expected_line)) => {
panic!(
"presolve mismatch at line {}:\n expected: {}\n actual: {}",
line_num, expected_line, actual_line
);
}
Some(Err(e)) => panic!("error reading expected: {}", e),
None => panic!(
"actual output has more lines than expected (line {})",
line_num
),
}
}
}
if let Some(line) = expected_lines.next() {
panic!(
"expected output has more lines than actual (line {}): {}",
line_num + 1,
line?
);
}
Ok(())
}
#[test]
fn resolve_errors_accurate() -> Result<()> {
use std::collections::HashSet;
let (result, _) = get_scan_result();
let unresolved = [
("py311-buildbot-[0-9]*", "py311-buildbot-badges-2.6.0nb1"),
(
"py311-buildbot-[0-9]*",
"py311-buildbot-waterfall-view-2.6.0nb1",
),
("py311-stevedore>=1.20.0", "py311-e3-core-22.10.0nb3"),
("py312-daemon>=2.3.0", "py312-libagent-0.15.0"),
("py313-daemon>=2.3.0", "py313-libagent-0.15.0"),
("py314-daemon>=2.3.0", "py314-libagent-0.15.0"),
];
let expected: HashSet<String> = unresolved
.iter()
.map(|(dep, pkg)| format!("No match found for dependency {dep} of package {pkg}"))
.collect();
let actual: HashSet<String> = result.errors().map(String::from).collect();
assert_eq!(actual, expected);
Ok(())
}
fn parse_scan_data(data: &str) -> Vec<ScanIndex> {
let reader = std::io::BufReader::new(data.as_bytes());
ScanIndex::from_reader(reader)
.map(|r| r.expect("failed to parse scan index"))
.collect()
}
#[test]
fn resolve_circular_dependencies() -> Result<()> {
let data = r#"PKGNAME=a-1.0
PKG_LOCATION=test/a
ALL_DEPENDS=b-[0-9]*:test/b
PKG_SKIP_REASON=
PKG_FAIL_REASON=
PKGNAME=b-1.0
PKG_LOCATION=test/b
ALL_DEPENDS=c-[0-9]*:test/c
PKG_SKIP_REASON=
PKG_FAIL_REASON=
PKGNAME=c-1.0
PKG_LOCATION=test/c
ALL_DEPENDS=a-[0-9]*:test/a
PKG_SKIP_REASON=
PKG_FAIL_REASON=
"#;
let scan_data = parse_scan_data(data);
let mut scan = Scan::default();
let result = scan.resolve(scan_data);
assert!(result.is_err(), "circular dependencies should be detected");
let err = result.unwrap_err().to_string();
assert!(
err.contains("Circular dependencies detected"),
"error should mention circular dependencies: {err}"
);
Ok(())
}