use std::collections::{BTreeMap, BTreeSet};
use ridl_ir::catalog_hash::reachable_decls;
use ridl_ir::v2::{Package, unit_of};
use crate::{DiffReport, Verdict};
pub fn unit_verdict(report: &DiffReport, unit: &str, old: &[Package], new: &[Package]) -> Verdict {
UnitVerdicts::new(report, old, new).verdict(unit)
}
pub struct UnitVerdicts<'a> {
old: Vec<&'a Package>,
new: Vec<&'a Package>,
names_by_unit: BTreeMap<&'a str, BTreeSet<&'a str>>,
changes: Vec<(&'a str, Option<String>, Verdict)>,
}
impl<'a> UnitVerdicts<'a> {
pub fn new(report: &'a DiffReport, old: &'a [Package], new: &'a [Package]) -> Self {
let old: Vec<&Package> = old.iter().collect();
let new: Vec<&Package> = new.iter().collect();
Self::from_refs(report, &old, &new)
}
pub fn from_refs(report: &'a DiffReport, old: &[&'a Package], new: &[&'a Package]) -> Self {
let mut names_by_unit: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for package in old.iter().chain(new) {
names_by_unit
.entry(unit_of(package))
.or_default()
.insert(package.name.as_str());
}
Self {
old: old.to_vec(),
new: new.to_vec(),
names_by_unit,
changes: report
.changes
.iter()
.map(|change| {
let package = change.package();
let canonical = change.declaration().map(|name| format!("{package}.{name}"));
(package, canonical, change.verdict)
})
.collect(),
}
}
pub fn verdict(&self, unit: &str) -> Verdict {
let no_packages = BTreeSet::new();
let packages = self.names_by_unit.get(unit).unwrap_or(&no_packages);
let reached_old = reachable_decls(unit, &self.old);
let reached_new = reachable_decls(unit, &self.new);
self.changes
.iter()
.filter(|(package, canonical, _)| {
packages.contains(package)
|| canonical.as_ref().is_some_and(|canonical| {
reached_old.contains_key(canonical) || reached_new.contains_key(canonical)
})
})
.map(|(_, _, verdict)| *verdict)
.max()
.unwrap_or(Verdict::Identical)
}
}
#[cfg(test)]
mod tests {
use ridl_ir::v2::{
Decl, EnumDef, EnumValue, EventDef, Field, FieldType, IntWidth, Interface, Package,
Reserved, SignalDef, StructDef, StructMember, TypeDef, Visibility, backing, decl,
field_type, struct_member, type_def,
};
use super::unit_verdict;
use crate::{Verdict, diff_sets};
fn int(width: IntWidth) -> FieldType {
FieldType {
optional: false,
kind: Some(field_type::Kind::InlineScalar(Box::new(TypeDef {
backing: Some(ridl_ir::v2::Backing {
kind: Some(backing::Kind::Primitive(
ridl_ir::v2::PrimitiveType::Integer as i32,
)),
}),
width: Some(type_def::Width::IntWidth(width as i32)),
..Default::default()
}))),
}
}
fn one_field_struct(name: &str, field: &str, width: IntWidth) -> Decl {
Decl {
name: name.to_owned(),
visibility: Visibility::Public as i32,
kind: Some(decl::Kind::StructDef(StructDef {
members: vec![StructMember {
member: Some(struct_member::Member::Field(Box::new(Field {
name: field.to_owned(),
ordinal: 1,
r#type: Some(int(width)),
..Default::default()
}))),
}],
fixed_layout: false,
})),
..Default::default()
}
}
fn package(name: &str, decls: Vec<Decl>, interfaces: Vec<Interface>) -> Package {
Package {
name: name.to_owned(),
decls,
interfaces,
unit: name.to_owned(),
..Default::default()
}
}
fn unit_a(payload: &str) -> Package {
let signal = Decl {
name: "status".to_owned(),
ordinal: 1,
kind: Some(decl::Kind::SignalDef(SignalDef {
payload: payload.to_owned(),
..Default::default()
})),
..Default::default()
};
package(
"a",
Vec::new(),
vec![Interface {
name: "Status".to_owned(),
visibility: Visibility::Public as i32,
number: 1,
provisional: false,
interactions: vec![signal],
..Default::default()
}],
)
}
fn unit_b(width: IntWidth) -> Package {
package("b", vec![one_field_struct("S", "x", width)], Vec::new())
}
fn unit_c(width: IntWidth) -> Package {
package("c", vec![one_field_struct("T", "y", width)], Vec::new())
}
fn baseline() -> Vec<Package> {
vec![unit_a("b.S"), unit_b(IntWidth::I32), unit_c(IntWidth::I32)]
}
#[test]
fn a_break_in_a_reached_declaration_of_another_unit_concerns_this_unit() {
let old = baseline();
let new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Identical);
}
#[test]
fn a_break_in_an_unreached_unit_does_not_concern_this_unit() {
let old = baseline();
let new = vec![unit_a("b.S"), unit_b(IntWidth::I32), unit_c(IntWidth::I64)];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Identical);
assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Breaking);
}
#[test]
fn an_appended_interaction_is_compatible_for_its_own_unit() {
let old = baseline();
let mut a = unit_a("b.S");
a.interfaces[0].interactions.push(appended_event("b.S"));
let new = vec![a, unit_b(IntWidth::I32), unit_c(IntWidth::I32)];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Compatible);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
#[test]
fn a_package_added_to_a_unit_concerns_that_unit() {
let old = baseline();
let mut sub = package(
"a.sub",
vec![one_field_struct("U", "z", IntWidth::I32)],
Vec::new(),
);
sub.unit = "a".to_owned();
let mut new = baseline();
new.push(sub);
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Compatible);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
fn appended_event(payload: &str) -> Decl {
Decl {
name: "alarm".to_owned(),
ordinal: 2,
kind: Some(decl::Kind::EventDef(EventDef {
payload: payload.to_owned(),
timing: None,
family: ridl_ir::v2::Family::Dispatch as i32,
})),
..Default::default()
}
}
#[test]
fn a_declaration_reached_only_on_the_new_side_concerns_the_unit() {
let old = baseline();
let mut a = unit_a("b.S");
a.interfaces[0].interactions.push(appended_event("c.T"));
let new = vec![a, unit_b(IntWidth::I32), unit_c(IntWidth::I64)];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
#[test]
fn a_break_reached_only_through_a_retired_interaction_concerns_the_unit() {
let mut a_old = unit_a("b.S");
a_old.interfaces[0].interactions.push(appended_event("c.T"));
let old = vec![a_old, unit_b(IntWidth::I32), unit_c(IntWidth::I32)];
let mut a_new = unit_a("b.S");
a_new.interfaces[0].interactions.push(Decl {
ordinal: 2,
kind: Some(decl::Kind::ReservedSlot(Reserved {
ordinal: 2,
name: Some("alarm".to_owned()),
value: None,
})),
..Default::default()
});
let new = vec![a_new, unit_b(IntWidth::I32), unit_c(IntWidth::I64)];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
fn unit_b_referencing_c() -> Package {
let mut s = one_field_struct("S", "t", IntWidth::I32);
if let Some(decl::Kind::StructDef(def)) = &mut s.kind
&& let Some(struct_member::Member::Field(field)) = &mut def.members[0].member
{
field.r#type = Some(FieldType {
optional: false,
kind: Some(field_type::Kind::Named("c.T".to_owned())),
});
}
package("b", vec![s], Vec::new())
}
#[test]
fn a_break_reached_through_two_hops_concerns_the_unit() {
let old = vec![
unit_a("b.S"),
unit_b_referencing_c(),
unit_c(IntWidth::I32),
package("d", Vec::new(), Vec::new()),
];
let mut new = old.clone();
new[2] = unit_c(IntWidth::I64);
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "d", &old, &new), Verdict::Identical);
}
fn sub_package() -> Package {
let mut sub = package(
"a.sub",
vec![one_field_struct("U", "z", IntWidth::I32)],
Vec::new(),
);
sub.unit = "a".to_owned();
sub
}
#[test]
fn a_package_removed_from_a_unit_concerns_that_unit() {
let mut old = baseline();
old.push(sub_package());
let new = baseline();
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
fn numbered(number: u32, provisional: bool) -> Vec<Package> {
let mut a = unit_a("b.S");
a.interfaces[0].number = number;
a.interfaces[0].provisional = provisional;
vec![a, unit_b(IntWidth::I32), unit_c(IntWidth::I32)]
}
#[test]
fn a_number_frozen_in_place_is_compatible_for_its_unit() {
let old = numbered(1, true);
let new = numbered(1, false);
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Compatible);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
#[test]
fn a_moved_number_is_breaking_for_its_unit() {
let old = numbered(1, true);
let new = numbered(2, true);
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
}
fn unit_b_with_enum(values: &[(&str, i64)], retired: &[i64]) -> Package {
let mode = Decl {
name: "Mode".to_owned(),
visibility: Visibility::Public as i32,
kind: Some(decl::Kind::EnumDef(EnumDef {
values: values
.iter()
.map(|(name, value)| EnumValue {
name: (*name).to_owned(),
value: *value,
..Default::default()
})
.collect(),
reserved: retired
.iter()
.map(|value| Reserved {
value: Some(*value),
..Default::default()
})
.collect(),
})),
..Default::default()
};
package("b", vec![mode], Vec::new())
}
#[test]
fn an_enum_value_reorder_is_compatible_for_its_unit() {
let old = vec![unit_b_with_enum(&[("OFF", 0), ("ON", 1)], &[])];
let new = vec![unit_b_with_enum(&[("ON", 1), ("OFF", 0)], &[])];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Compatible);
}
#[test]
fn an_enum_reserved_list_reorder_is_compatible_for_its_unit() {
let old = vec![unit_b_with_enum(&[("OFF", 0)], &[3, 4])];
let new = vec![unit_b_with_enum(&[("OFF", 0)], &[4, 3])];
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Compatible);
}
#[test]
fn the_unit_verdict_is_the_maximum_over_its_changes() {
let old = baseline();
let mut new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
new.push(sub_package());
let report = diff_sets(&old, &new);
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Identical);
}
#[test]
fn a_walked_change_names_its_package_and_declaration() {
let old = baseline();
let new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
let report = diff_sets(&old, &new);
let change = report
.changes
.iter()
.find(|change| change.package() == "b")
.expect("the width change in b.S is reported");
assert_eq!(change.declaration(), Some("S"));
}
#[test]
fn a_change_matches_by_its_structured_package_and_declaration() {
let old = baseline();
let new = baseline();
let mut report = diff_sets(&old, &new);
report.changes.push(crate::Change {
path: "b/S/x".to_owned(),
category: crate::Category::DeclRemoved,
verdict: Verdict::Breaking,
before: None,
after: None,
});
assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Identical);
}
#[test]
fn one_prepared_value_answers_several_units_in_any_order() {
let old = baseline();
let new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
let report = diff_sets(&old, &new);
let prepared = super::UnitVerdicts::new(&report, &old, &new);
assert_eq!(prepared.verdict("c"), Verdict::Identical);
assert_eq!(prepared.verdict("a"), Verdict::Breaking);
assert_eq!(prepared.verdict("c"), Verdict::Identical);
assert_eq!(prepared.verdict("b"), Verdict::Breaking);
assert_eq!(prepared.verdict("absent"), Verdict::Identical);
assert_eq!(prepared.verdict("a"), Verdict::Breaking);
}
#[test]
fn a_change_to_a_package_has_no_declaration() {
let change = |path: &str| crate::Change {
path: path.to_owned(),
category: crate::Category::DeclRemoved,
verdict: Verdict::Breaking,
before: None,
after: None,
};
assert_eq!(change("veh.cluster").package(), "veh.cluster");
assert_eq!(change("veh.cluster").declaration(), None);
assert_eq!(
change("veh.cluster/Status/doorOpened").package(),
"veh.cluster"
);
assert_eq!(
change("veh.cluster/Status/doorOpened").declaration(),
Some("Status")
);
}
}