use std::collections::BTreeSet;
use serde::Serialize;
use crate::deps::DepsGraph;
#[derive(Debug, Default, Serialize)]
pub struct DeriveReport {
pub derived: Vec<Derived>,
pub added: Vec<String>,
pub removed: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct Derived {
pub name: String,
pub reason: Option<String>,
}
pub fn derive(
graph: &DepsGraph,
keeps: &BTreeSet<String>,
owned: &BTreeSet<String>,
current: &BTreeSet<String>,
) -> DeriveReport {
let reachable = graph.reachable(keeps);
let mut report = DeriveReport::default();
for name in &reachable {
if keeps.contains(name) || !owned.contains(name) {
continue;
}
report.derived.push(Derived {
name: name.clone(),
reason: graph.witness_reason(name, &reachable),
});
if !current.contains(name) {
report.added.push(name.clone());
}
}
let derived_names: BTreeSet<&str> = report.derived.iter().map(|d| d.name.as_str()).collect();
report.removed = current
.iter()
.filter(|c| !derived_names.contains(c.as_str()))
.cloned()
.collect();
report
}
pub fn format_report(report: &DeriveReport, applied: bool) -> String {
let mut out = String::new();
out.push_str(&format!(
"derived inventory: {} package(s) ({} to add, {} to remove{})\n",
report.derived.len(),
report.added.len(),
report.removed.len(),
if applied {
"; applied"
} else {
"; report only"
},
));
for name in &report.added {
out.push_str(&format!(" + {name}\n"));
}
for name in &report.removed {
out.push_str(&format!(" - {name}\n"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::deps::GraphProvider;
fn graph() -> DepsGraph {
let mut g = DepsGraph {
roots: vec!["app-a".into(), "crate-c".into()],
..Default::default()
};
for (b, s) in [
("app-a", "app-a"),
("crate-c-devel", "crate-c"),
("src:crate-c", "crate-c"),
("tool-t", "tool-t"),
("liba", "lib-l"),
] {
g.binary_sources.insert(b.into(), s.into());
}
let edge = |g: &mut DepsGraph, cap: &str, requirer: &str, binary: &str, source: &str| {
g.requirers
.entry(cap.into())
.or_default()
.insert(requirer.into());
g.providers
.entry(cap.into())
.or_default()
.insert(GraphProvider {
binary: binary.into(),
source: source.into(),
repoid: "rawhide".into(),
});
};
edge(&mut g, "crate(c)", "app-a", "crate-c-devel", "crate-c");
edge(&mut g, "tool-t", "src:crate-c", "tool-t", "tool-t");
g
}
#[test]
fn the_view_is_reachable_owned_non_keeps_with_witnesses() {
let keeps: BTreeSet<String> = ["app-a".to_string()].into();
let owned: BTreeSet<String> = ["app-a", "crate-c", "lib-l"].map(String::from).into();
let current: BTreeSet<String> = ["crate-c", "lib-l"].map(String::from).into();
let report = derive(&graph(), &keeps, &owned, ¤t);
let names: Vec<&str> = report.derived.iter().map(|d| d.name.as_str()).collect();
assert_eq!(names, ["crate-c"]);
assert_eq!(
report.derived[0].reason.as_deref(),
Some("dependency (rawhide): app-a requires crate(c)")
);
assert!(report.added.is_empty());
assert_eq!(report.removed, ["lib-l"]);
let corrected: BTreeSet<String> = ["crate-c".to_string()].into();
let again = derive(&graph(), &keeps, &owned, &corrected);
assert!(again.added.is_empty() && again.removed.is_empty());
let text = format_report(&again, false);
assert!(text.contains("1 package(s) (0 to add, 0 to remove; report only)"));
}
}