1use std::collections::{BTreeMap, BTreeSet};
4
5use ridl_ir::catalog_hash::reachable_decls;
6use ridl_ir::v2::{Package, unit_of};
7
8use crate::{DiffReport, Verdict};
9
10pub fn unit_verdict(report: &DiffReport, unit: &str, old: &[Package], new: &[Package]) -> Verdict {
42 UnitVerdicts::new(report, old, new).verdict(unit)
43}
44
45pub struct UnitVerdicts<'a> {
50 old: Vec<&'a Package>,
51 new: Vec<&'a Package>,
52 names_by_unit: BTreeMap<&'a str, BTreeSet<&'a str>>,
54 changes: Vec<(&'a str, Option<String>, Verdict)>,
57}
58
59impl<'a> UnitVerdicts<'a> {
60 pub fn new(report: &'a DiffReport, old: &'a [Package], new: &'a [Package]) -> Self {
62 let old: Vec<&Package> = old.iter().collect();
63 let new: Vec<&Package> = new.iter().collect();
64 Self::from_refs(report, &old, &new)
65 }
66
67 pub fn from_refs(report: &'a DiffReport, old: &[&'a Package], new: &[&'a Package]) -> Self {
70 let mut names_by_unit: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
71 for package in old.iter().chain(new) {
72 names_by_unit
73 .entry(unit_of(package))
74 .or_default()
75 .insert(package.name.as_str());
76 }
77 Self {
78 old: old.to_vec(),
79 new: new.to_vec(),
80 names_by_unit,
81 changes: report
82 .changes
83 .iter()
84 .map(|change| {
85 let package = change.package();
86 let canonical = change.declaration().map(|name| format!("{package}.{name}"));
87 (package, canonical, change.verdict)
88 })
89 .collect(),
90 }
91 }
92
93 pub fn verdict(&self, unit: &str) -> Verdict {
95 let no_packages = BTreeSet::new();
96 let packages = self.names_by_unit.get(unit).unwrap_or(&no_packages);
97 let reached_old = reachable_decls(unit, &self.old);
98 let reached_new = reachable_decls(unit, &self.new);
99
100 self.changes
101 .iter()
102 .filter(|(package, canonical, _)| {
103 packages.contains(package)
104 || canonical.as_ref().is_some_and(|canonical| {
105 reached_old.contains_key(canonical) || reached_new.contains_key(canonical)
106 })
107 })
108 .map(|(_, _, verdict)| *verdict)
109 .max()
110 .unwrap_or(Verdict::Identical)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use ridl_ir::v2::{
117 Decl, EnumDef, EnumValue, EventDef, Field, FieldType, IntWidth, Interface, Package,
118 Reserved, SignalDef, StructDef, StructMember, TypeDef, Visibility, backing, decl,
119 field_type, struct_member, type_def,
120 };
121
122 use super::unit_verdict;
123 use crate::{Verdict, diff_sets};
124
125 fn int(width: IntWidth) -> FieldType {
127 FieldType {
128 optional: false,
129 kind: Some(field_type::Kind::InlineScalar(Box::new(TypeDef {
130 backing: Some(ridl_ir::v2::Backing {
131 kind: Some(backing::Kind::Primitive(
132 ridl_ir::v2::PrimitiveType::Integer as i32,
133 )),
134 }),
135 width: Some(type_def::Width::IntWidth(width as i32)),
136 ..Default::default()
137 }))),
138 }
139 }
140
141 fn one_field_struct(name: &str, field: &str, width: IntWidth) -> Decl {
143 Decl {
144 name: name.to_owned(),
145 visibility: Visibility::Public as i32,
146 kind: Some(decl::Kind::StructDef(StructDef {
147 members: vec![StructMember {
148 member: Some(struct_member::Member::Field(Box::new(Field {
149 name: field.to_owned(),
150 ordinal: 1,
151 r#type: Some(int(width)),
152 ..Default::default()
153 }))),
154 }],
155 fixed_layout: false,
156 })),
157 ..Default::default()
158 }
159 }
160
161 fn package(name: &str, decls: Vec<Decl>, interfaces: Vec<Interface>) -> Package {
162 Package {
163 name: name.to_owned(),
164 decls,
165 interfaces,
166 unit: name.to_owned(),
167 ..Default::default()
168 }
169 }
170
171 fn unit_a(payload: &str) -> Package {
173 let signal = Decl {
174 name: "status".to_owned(),
175 ordinal: 1,
176 kind: Some(decl::Kind::SignalDef(SignalDef {
177 payload: payload.to_owned(),
178 ..Default::default()
179 })),
180 ..Default::default()
181 };
182 package(
183 "a",
184 Vec::new(),
185 vec![Interface {
186 name: "Status".to_owned(),
187 visibility: Visibility::Public as i32,
188 number: 1,
189 provisional: false,
190 interactions: vec![signal],
191 ..Default::default()
192 }],
193 )
194 }
195
196 fn unit_b(width: IntWidth) -> Package {
197 package("b", vec![one_field_struct("S", "x", width)], Vec::new())
198 }
199
200 fn unit_c(width: IntWidth) -> Package {
201 package("c", vec![one_field_struct("T", "y", width)], Vec::new())
202 }
203
204 fn baseline() -> Vec<Package> {
205 vec![unit_a("b.S"), unit_b(IntWidth::I32), unit_c(IntWidth::I32)]
206 }
207
208 #[test]
209 fn a_break_in_a_reached_declaration_of_another_unit_concerns_this_unit() {
210 let old = baseline();
211 let new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
212 let report = diff_sets(&old, &new);
213 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
214 assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Identical);
215 }
216
217 #[test]
218 fn a_break_in_an_unreached_unit_does_not_concern_this_unit() {
219 let old = baseline();
220 let new = vec![unit_a("b.S"), unit_b(IntWidth::I32), unit_c(IntWidth::I64)];
221 let report = diff_sets(&old, &new);
222 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Identical);
223 assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Breaking);
224 }
225
226 #[test]
227 fn an_appended_interaction_is_compatible_for_its_own_unit() {
228 let old = baseline();
229 let mut a = unit_a("b.S");
230 a.interfaces[0].interactions.push(appended_event("b.S"));
231 let new = vec![a, unit_b(IntWidth::I32), unit_c(IntWidth::I32)];
232 let report = diff_sets(&old, &new);
233 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Compatible);
234 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
235 }
236
237 #[test]
238 fn a_package_added_to_a_unit_concerns_that_unit() {
239 let old = baseline();
240 let mut sub = package(
241 "a.sub",
242 vec![one_field_struct("U", "z", IntWidth::I32)],
243 Vec::new(),
244 );
245 sub.unit = "a".to_owned();
246 let mut new = baseline();
247 new.push(sub);
248 let report = diff_sets(&old, &new);
249 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Compatible);
250 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
251 }
252
253 fn appended_event(payload: &str) -> Decl {
255 Decl {
256 name: "alarm".to_owned(),
257 ordinal: 2,
258 kind: Some(decl::Kind::EventDef(EventDef {
259 payload: payload.to_owned(),
260 timing: None,
261 family: ridl_ir::v2::Family::Dispatch as i32,
262 })),
263 ..Default::default()
264 }
265 }
266
267 #[test]
270 fn a_declaration_reached_only_on_the_new_side_concerns_the_unit() {
271 let old = baseline();
272 let mut a = unit_a("b.S");
273 a.interfaces[0].interactions.push(appended_event("c.T"));
274 let new = vec![a, unit_b(IntWidth::I32), unit_c(IntWidth::I64)];
275 let report = diff_sets(&old, &new);
276 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
277 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
278 }
279
280 #[test]
285 fn a_break_reached_only_through_a_retired_interaction_concerns_the_unit() {
286 let mut a_old = unit_a("b.S");
287 a_old.interfaces[0].interactions.push(appended_event("c.T"));
288 let old = vec![a_old, unit_b(IntWidth::I32), unit_c(IntWidth::I32)];
289 let mut a_new = unit_a("b.S");
290 a_new.interfaces[0].interactions.push(Decl {
291 ordinal: 2,
292 kind: Some(decl::Kind::ReservedSlot(Reserved {
293 ordinal: 2,
294 name: Some("alarm".to_owned()),
295 value: None,
296 })),
297 ..Default::default()
298 });
299 let new = vec![a_new, unit_b(IntWidth::I32), unit_c(IntWidth::I64)];
300 let report = diff_sets(&old, &new);
301 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
302 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
303 }
304
305 fn unit_b_referencing_c() -> Package {
307 let mut s = one_field_struct("S", "t", IntWidth::I32);
308 if let Some(decl::Kind::StructDef(def)) = &mut s.kind
309 && let Some(struct_member::Member::Field(field)) = &mut def.members[0].member
310 {
311 field.r#type = Some(FieldType {
312 optional: false,
313 kind: Some(field_type::Kind::Named("c.T".to_owned())),
314 });
315 }
316 package("b", vec![s], Vec::new())
317 }
318
319 #[test]
322 fn a_break_reached_through_two_hops_concerns_the_unit() {
323 let old = vec![
324 unit_a("b.S"),
325 unit_b_referencing_c(),
326 unit_c(IntWidth::I32),
327 package("d", Vec::new(), Vec::new()),
328 ];
329 let mut new = old.clone();
330 new[2] = unit_c(IntWidth::I64);
331 let report = diff_sets(&old, &new);
332 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
333 assert_eq!(unit_verdict(&report, "d", &old, &new), Verdict::Identical);
334 }
335
336 fn sub_package() -> Package {
338 let mut sub = package(
339 "a.sub",
340 vec![one_field_struct("U", "z", IntWidth::I32)],
341 Vec::new(),
342 );
343 sub.unit = "a".to_owned();
344 sub
345 }
346
347 #[test]
350 fn a_package_removed_from_a_unit_concerns_that_unit() {
351 let mut old = baseline();
352 old.push(sub_package());
353 let new = baseline();
354 let report = diff_sets(&old, &new);
355 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
356 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
357 }
358
359 fn numbered(number: u32, provisional: bool) -> Vec<Package> {
362 let mut a = unit_a("b.S");
363 a.interfaces[0].number = number;
364 a.interfaces[0].provisional = provisional;
365 vec![a, unit_b(IntWidth::I32), unit_c(IntWidth::I32)]
366 }
367
368 #[test]
372 fn a_number_frozen_in_place_is_compatible_for_its_unit() {
373 let old = numbered(1, true);
374 let new = numbered(1, false);
375 let report = diff_sets(&old, &new);
376 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Compatible);
377 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
378 }
379
380 #[test]
383 fn a_moved_number_is_breaking_for_its_unit() {
384 let old = numbered(1, true);
385 let new = numbered(2, true);
386 let report = diff_sets(&old, &new);
387 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
388 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Identical);
389 }
390
391 fn unit_b_with_enum(values: &[(&str, i64)], retired: &[i64]) -> Package {
394 let mode = Decl {
395 name: "Mode".to_owned(),
396 visibility: Visibility::Public as i32,
397 kind: Some(decl::Kind::EnumDef(EnumDef {
398 values: values
399 .iter()
400 .map(|(name, value)| EnumValue {
401 name: (*name).to_owned(),
402 value: *value,
403 ..Default::default()
404 })
405 .collect(),
406 reserved: retired
407 .iter()
408 .map(|value| Reserved {
409 value: Some(*value),
410 ..Default::default()
411 })
412 .collect(),
413 open: false,
414 })),
415 ..Default::default()
416 };
417 package("b", vec![mode], Vec::new())
418 }
419
420 #[test]
424 fn an_enum_value_reorder_is_compatible_for_its_unit() {
425 let old = vec![unit_b_with_enum(&[("OFF", 0), ("ON", 1)], &[])];
426 let new = vec![unit_b_with_enum(&[("ON", 1), ("OFF", 0)], &[])];
427 let report = diff_sets(&old, &new);
428 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Compatible);
429 }
430
431 #[test]
433 fn an_enum_reserved_list_reorder_is_compatible_for_its_unit() {
434 let old = vec![unit_b_with_enum(&[("OFF", 0)], &[3, 4])];
435 let new = vec![unit_b_with_enum(&[("OFF", 0)], &[4, 3])];
436 let report = diff_sets(&old, &new);
437 assert_eq!(unit_verdict(&report, "b", &old, &new), Verdict::Compatible);
438 }
439
440 #[test]
444 fn the_unit_verdict_is_the_maximum_over_its_changes() {
445 let old = baseline();
446 let mut new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
447 new.push(sub_package());
448 let report = diff_sets(&old, &new);
449 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
450 assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Identical);
451 }
452
453 #[test]
456 fn a_walked_change_names_its_package_and_declaration() {
457 let old = baseline();
458 let new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
459 let report = diff_sets(&old, &new);
460 let change = report
461 .changes
462 .iter()
463 .find(|change| change.package() == "b")
464 .expect("the width change in b.S is reported");
465 assert_eq!(change.declaration(), Some("S"));
466 }
467
468 #[test]
469 fn a_change_matches_by_its_structured_package_and_declaration() {
470 let old = baseline();
471 let new = baseline();
472 let mut report = diff_sets(&old, &new);
473 report.changes.push(crate::Change {
474 path: "b/S/x".to_owned(),
475 category: crate::Category::DeclRemoved,
476 verdict: Verdict::Breaking,
477 before: None,
478 after: None,
479 });
480 assert_eq!(unit_verdict(&report, "a", &old, &new), Verdict::Breaking);
483 assert_eq!(unit_verdict(&report, "c", &old, &new), Verdict::Identical);
484 }
485
486 #[test]
487 fn one_prepared_value_answers_several_units_in_any_order() {
488 let old = baseline();
489 let new = vec![unit_a("b.S"), unit_b(IntWidth::I64), unit_c(IntWidth::I32)];
490 let report = diff_sets(&old, &new);
491 let prepared = super::UnitVerdicts::new(&report, &old, &new);
492 assert_eq!(prepared.verdict("c"), Verdict::Identical);
493 assert_eq!(prepared.verdict("a"), Verdict::Breaking);
494 assert_eq!(prepared.verdict("c"), Verdict::Identical);
495 assert_eq!(prepared.verdict("b"), Verdict::Breaking);
496 assert_eq!(prepared.verdict("absent"), Verdict::Identical);
497 assert_eq!(prepared.verdict("a"), Verdict::Breaking);
498 }
499
500 #[test]
501 fn a_change_to_a_package_has_no_declaration() {
502 let change = |path: &str| crate::Change {
503 path: path.to_owned(),
504 category: crate::Category::DeclRemoved,
505 verdict: Verdict::Breaking,
506 before: None,
507 after: None,
508 };
509 assert_eq!(change("veh.cluster").package(), "veh.cluster");
510 assert_eq!(change("veh.cluster").declaration(), None);
511 assert_eq!(
512 change("veh.cluster/Status/doorOpened").package(),
513 "veh.cluster"
514 );
515 assert_eq!(
516 change("veh.cluster/Status/doorOpened").declaration(),
517 Some("Status")
518 );
519 }
520}