use crate::model::Node;
use anyhow::{Context, Result};
use serde::Serialize;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::path::Path;
#[derive(Serialize)]
pub struct DiffNode {
pub label: String,
pub path: String,
pub base_text: u64,
pub head_text: u64,
pub base_data: u64,
pub head_data: u64,
pub base_gzip: u64,
pub head_gzip: u64,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<DiffNode>,
}
impl DiffNode {
pub fn delta_text(&self) -> i64 {
self.head_text as i64 - self.base_text as i64
}
pub fn delta_data(&self) -> i64 {
self.head_data as i64 - self.base_data as i64
}
pub fn delta_gzip(&self) -> i64 {
self.head_gzip as i64 - self.base_gzip as i64
}
}
pub fn load_base_tree(path: &Path) -> Result<Node> {
let text = std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
let value: serde_json::Value = serde_json::from_str(&text).context("parsing baseline JSON")?;
let tree = value
.get("tree")
.context("baseline JSON has no `tree` field (save it with `--format json`)")?;
serde_json::from_value(tree.clone()).context("reading baseline tree")
}
pub fn diff_trees(base: &Node, head: &Node) -> DiffNode {
let mut base_children: HashMap<&str, &Node> =
base.groups.iter().map(|c| (c.label.as_str(), c)).collect();
let mut groups = Vec::new();
for hc in &head.groups {
match base_children.remove(hc.label.as_str()) {
Some(bc) => groups.push(diff_trees(bc, hc)),
None => groups.push(one_sided(hc, false)),
}
}
for bc in base_children.into_values() {
groups.push(one_sided(bc, true));
}
groups.sort_by_key(|n| Reverse(n.delta_text().abs()));
DiffNode {
label: head.label.clone(),
path: head.path.clone(),
base_text: base.text,
head_text: head.text,
base_data: base.data,
head_data: head.data,
base_gzip: base.gzip,
head_gzip: head.gzip,
groups,
}
}
fn one_sided(node: &Node, removed: bool) -> DiffNode {
let pick = |v: u64| if removed { (v, 0) } else { (0, v) };
let (base_text, head_text) = pick(node.text);
let (base_data, head_data) = pick(node.data);
let (base_gzip, head_gzip) = pick(node.gzip);
DiffNode {
label: node.label.clone(),
path: node.path.clone(),
base_text,
head_text,
base_data,
head_data,
base_gzip,
head_gzip,
groups: node.groups.iter().map(|c| one_sided(c, removed)).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn node(label: &str, text: u64, groups: Vec<Node>) -> Node {
Node {
label: label.to_string(),
path: label.to_string(),
text: text + groups.iter().map(|g| g.text).sum::<u64>(),
data: 0,
gzip: 0,
count: 1,
conf: String::new(),
stat: None,
loc: None,
unsafe_n: None,
dep_kind: None,
groups,
}
}
#[test]
fn added_removed_changed() {
let base = node("bin", 0, vec![node("a", 100, vec![]), node("b", 50, vec![])]);
let head = node("bin", 0, vec![node("a", 120, vec![]), node("c", 30, vec![])]);
let d = diff_trees(&base, &head);
let by: HashMap<_, _> = d.groups.iter().map(|g| (g.label.as_str(), g)).collect();
assert_eq!((by["a"].base_text, by["a"].head_text), (100, 120));
assert_eq!(by["a"].delta_text(), 20);
assert_eq!((by["b"].base_text, by["b"].head_text), (50, 0)); assert_eq!((by["c"].base_text, by["c"].head_text), (0, 30)); assert_eq!((d.base_text, d.head_text), (150, 150));
assert_eq!(d.delta_text(), 0);
}
#[test]
fn identical_trees_have_zero_delta() {
let t = node("bin", 0, vec![node("a", 100, vec![node("m", 40, vec![])])]);
let same = node("bin", 0, vec![node("a", 100, vec![node("m", 40, vec![])])]);
let d = diff_trees(&t, &same);
assert_eq!(d.delta_text(), 0);
assert!(d.groups.iter().all(|g| g.delta_text() == 0));
}
}