use crate::result::{DiffNode, DiffResult};
use crate::status::StateStatus;
use ledvar_core::{Error, Node, Snapshot, validate};
use std::collections::{HashMap, HashSet};
pub fn diff(previous: Option<&Snapshot>, current: &Snapshot) -> Result<DiffResult, Error> {
if let Some(p) = previous {
validate(p)?;
}
validate(current)?;
let mut prev_index: HashMap<String, String> = HashMap::new();
if let Some(p) = previous {
for n in &p.tree {
prev_index.insert(n.identity_key()?, n.content_hash()?);
}
}
let mut nodes = Vec::with_capacity(current.tree.len());
let mut current_ids = HashSet::with_capacity(current.tree.len());
for node in ¤t.tree {
let id = node.identity_key()?;
let content_hash = node.content_hash()?;
current_ids.insert(id.clone());
let status = if previous.is_none() {
StateStatus::Baseline
} else if let Some(prev_hash) = prev_index.get(&id) {
if *prev_hash == content_hash {
StateStatus::Unchanged
} else {
StateStatus::Modified
}
} else {
StateStatus::Added
};
nodes.push(processed(node.clone(), id, content_hash, status));
}
if let Some(prev) = previous {
for node in &prev.tree {
let id = node.identity_key()?;
if !current_ids.contains(&id) {
let content_hash = node.content_hash()?;
nodes.push(processed(node.clone(), id, content_hash, StateStatus::Removed));
}
}
}
Ok(DiffResult {
protocol_version: current.protocol_version.clone(),
origin_id: current.origin_id.clone(),
provider_name: current.provider_name.clone(),
timestamp: current.timestamp,
fingerprint: current.fingerprint.clone(),
parent_origin_id: current.parent_origin_id.clone(),
labels: current.labels.clone(),
nodes,
})
}
fn processed(node: Node, identity_key: String, content_hash: String, state_status: StateStatus) -> DiffNode {
DiffNode {
node,
identity_key,
content_hash,
state_status,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeMap, BTreeSet};
fn node(path: &[&str], tags: &[&str]) -> Node {
let mut content = BTreeMap::new();
if !tags.is_empty() {
content.insert(
"tags".into(),
tags.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>(),
);
}
Node {
path: path.iter().map(|s| s.to_string()).collect(),
content,
labels: BTreeMap::new(),
refs: Vec::new(),
}
}
fn snap(nodes: Vec<Node>) -> Snapshot {
Snapshot {
protocol_version: "0.1.0".into(),
origin_id: "o".into(),
provider_name: "p".into(),
timestamp: 0,
fingerprint: None,
parent_origin_id: None,
labels: BTreeMap::new(),
tree: nodes,
}
}
fn status_of<'a>(r: &'a DiffResult, path: &[&str]) -> &'a StateStatus {
let want: Vec<String> = path.iter().map(|s| s.to_string()).collect();
&r.nodes
.iter()
.find(|n| n.node.path == want)
.expect("node present")
.state_status
}
#[test]
fn cold_start_is_all_baseline() {
let r = diff(None, &snap(vec![node(&["a"], &["X"]), node(&["b"], &[])])).unwrap();
assert!(r.nodes.iter().all(|n| n.state_status == StateStatus::Baseline));
assert!(!r.has_drift());
}
#[test]
fn the_four_outcomes() {
let prev = snap(vec![
node(&["vm-1"], &["web"]),
node(&["vm-2"], &["web"]),
node(&["vm-3"], &["edge"]),
]);
let curr = snap(vec![
node(&["vm-1"], &["web", "prod"]),
node(&["vm-3"], &["edge"]),
node(&["vm-4"], &["web"]),
]);
let r = diff(Some(&prev), &curr).unwrap();
assert_eq!(*status_of(&r, &["vm-1"]), StateStatus::Modified);
assert_eq!(*status_of(&r, &["vm-3"]), StateStatus::Unchanged);
assert_eq!(*status_of(&r, &["vm-4"]), StateStatus::Added);
assert_eq!(*status_of(&r, &["vm-2"]), StateStatus::Removed);
assert!(r.has_drift());
}
#[test]
fn a_label_only_change_is_unchanged() {
let labelled = |env: &str| {
let mut n = node(&["vm-1"], &["web"]);
n.labels.insert("env".to_string(), env.to_string());
n
};
let r = diff(
Some(&snap(vec![labelled("prod")])),
&snap(vec![labelled("staging")]),
)
.unwrap();
assert_eq!(*status_of(&r, &["vm-1"]), StateStatus::Unchanged);
assert!(!r.has_drift(), "a label-only change is not drift");
}
#[test]
fn refuses_an_ill_formed_snapshot() {
let mut bad = node(&["x"], &[]);
bad.content.insert("a".into(), BTreeSet::new()); assert!(diff(None, &snap(vec![bad.clone()])).is_err());
assert!(diff(Some(&snap(vec![bad])), &snap(vec![node(&["x"], &[])])).is_err());
}
#[test]
fn output_order_is_deterministic() {
let prev = snap(vec![node(&["a"], &[]), node(&["gone"], &[])]);
let curr = snap(vec![node(&["a"], &[]), node(&["new"], &[])]);
let paths: Vec<_> = diff(Some(&prev), &curr)
.unwrap()
.nodes
.iter()
.map(|n| n.node.path.clone())
.collect();
assert_eq!(
paths,
vec![
vec!["a".to_string()],
vec!["new".to_string()],
vec!["gone".to_string()]
]
);
}
}