1use ridl_ir::v2::{self, Component, Deployment, InterfaceRef, System};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum SystemHeading {
33 PlacementChanged,
35 CompositionChanged,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct SystemChange {
42 pub heading: SystemHeading,
43 pub path: String,
46 pub before: Option<String>,
48 pub after: Option<String>,
50}
51
52pub fn heading_text(heading: SystemHeading) -> &'static str {
54 match heading {
55 SystemHeading::PlacementChanged => "placement changed",
56 SystemHeading::CompositionChanged => "composition changed",
57 }
58}
59
60pub fn diff_systems(old: &System, new: &System) -> Vec<SystemChange> {
70 let mut changes = Vec::new();
71 let system_path = format!("{}.{}", new.package, new.name);
72 declaration_attrs(
73 &mut changes,
74 SystemHeading::CompositionChanged,
75 &system_path,
76 (&old.labels, old.deprecated.as_ref(), &old.attributes),
77 (&new.labels, new.deprecated.as_ref(), &new.attributes),
78 );
79 for line in &new.members {
80 if let Some(before) = old
81 .members
82 .iter()
83 .find(|before| before.component == line.component)
84 {
85 line_attrs(
86 &mut changes,
87 SystemHeading::CompositionChanged,
88 &format!("{system_path}/{}", line.component),
89 &before.attributes,
90 &line.attributes,
91 );
92 }
93 }
94 for component in &new.components {
95 let name = component.qualified_name();
96 match old
97 .components
98 .iter()
99 .find(|before| before.qualified_name() == name)
100 {
101 Some(before) => compare_components(before, component, &mut changes),
102 None => composition(&mut changes, name, None, Some("in the closure")),
103 }
104 }
105 for component in &old.components {
106 let name = component.qualified_name();
107 if !new
108 .components
109 .iter()
110 .any(|after| after.qualified_name() == name)
111 {
112 composition(&mut changes, name, Some("in the closure"), None);
113 }
114 }
115 let empty_distribution = v2::Distribution::default();
116 for distribution in &new.distributions {
117 let name = distribution.qualified_name();
118 let before = old
119 .distributions
120 .iter()
121 .find(|before| before.qualified_name() == name);
122 if before.is_none() {
123 composition(&mut changes, name.clone(), None, Some("distribution"));
124 }
125 compare_distribution(
126 before.unwrap_or(&empty_distribution),
127 distribution,
128 &name,
129 &mut changes,
130 );
131 }
132 for distribution in &old.distributions {
133 let name = distribution.qualified_name();
134 if !new
135 .distributions
136 .iter()
137 .any(|after| after.qualified_name() == name)
138 {
139 composition(&mut changes, name.clone(), Some("distribution"), None);
140 compare_distribution(distribution, &empty_distribution, &name, &mut changes);
141 }
142 }
143 for deployment in &new.deployments {
144 match old
145 .deployments
146 .iter()
147 .find(|before| before.name == deployment.name)
148 {
149 Some(before) => compare_deployments(before, deployment, &mut changes),
150 None => placement(
151 &mut changes,
152 deployment.name.clone(),
153 None,
154 Some("deployment"),
155 ),
156 }
157 }
158 for deployment in &old.deployments {
159 if !new
160 .deployments
161 .iter()
162 .any(|after| after.name == deployment.name)
163 {
164 placement(
165 &mut changes,
166 deployment.name.clone(),
167 Some("deployment"),
168 None,
169 );
170 }
171 }
172 changes
173}
174
175fn compare_components(old: &Component, new: &Component, changes: &mut Vec<SystemChange>) {
178 let name = new.qualified_name();
179 let old_offers: Vec<&str> = old
180 .offers
181 .iter()
182 .map(|offer| offer.service.as_str())
183 .collect();
184 let new_offers: Vec<&str> = new
185 .offers
186 .iter()
187 .map(|offer| offer.service.as_str())
188 .collect();
189 lines(changes, &name, "offers", &old_offers, &new_offers);
190 let old_requires: Vec<String> = old
191 .requires
192 .iter()
193 .map(|require| interface_text(require.interface.as_ref()))
194 .collect();
195 let new_requires: Vec<String> = new
196 .requires
197 .iter()
198 .map(|require| interface_text(require.interface.as_ref()))
199 .collect();
200 let old_requires: Vec<&str> = old_requires.iter().map(String::as_str).collect();
201 let new_requires: Vec<&str> = new_requires.iter().map(String::as_str).collect();
202 lines(changes, &name, "requires", &old_requires, &new_requires);
203 if old.instances != new.instances {
204 changes.push(SystemChange {
205 heading: SystemHeading::CompositionChanged,
206 path: format!("{name}/instances"),
207 before: Some(instance_list(&old.instances)),
208 after: Some(instance_list(&new.instances)),
209 });
210 }
211 if old.external != new.external {
212 changes.push(SystemChange {
213 heading: SystemHeading::CompositionChanged,
214 path: format!("{name}/external"),
215 before: Some(old.external.to_string()),
216 after: Some(new.external.to_string()),
217 });
218 }
219 declaration_attrs(
220 changes,
221 SystemHeading::CompositionChanged,
222 &name,
223 (&old.labels, old.deprecated.as_ref(), &old.attributes),
224 (&new.labels, new.deprecated.as_ref(), &new.attributes),
225 );
226 for offer in &new.offers {
227 if let Some(before) = old
228 .offers
229 .iter()
230 .find(|before| before.service == offer.service)
231 {
232 line_attrs(
233 changes,
234 SystemHeading::CompositionChanged,
235 &format!("{name}/offers/{}", offer.service),
236 &before.attributes,
237 &offer.attributes,
238 );
239 }
240 }
241 for require in &new.requires {
242 let interface = interface_text(require.interface.as_ref());
243 if let Some(before) = old
244 .requires
245 .iter()
246 .find(|before| interface_text(before.interface.as_ref()) == interface)
247 {
248 line_attrs(
249 changes,
250 SystemHeading::CompositionChanged,
251 &format!("{name}/requires/{interface}"),
252 &before.attributes,
253 &require.attributes,
254 );
255 }
256 }
257}
258
259fn lines(
261 changes: &mut Vec<SystemChange>,
262 component: &str,
263 keyword: &str,
264 old: &[&str],
265 new: &[&str],
266) {
267 for added in new.iter().filter(|line| !old.contains(line)) {
268 composition(
269 changes,
270 format!("{component}/{keyword}/{added}"),
271 None,
272 Some(&format!("{keyword} {added}")),
273 );
274 }
275 for removed in old.iter().filter(|line| !new.contains(line)) {
276 composition(
277 changes,
278 format!("{component}/{keyword}/{removed}"),
279 Some(&format!("{keyword} {removed}")),
280 None,
281 );
282 }
283}
284
285fn compare_deployments(old: &Deployment, new: &Deployment, changes: &mut Vec<SystemChange>) {
288 declaration_attrs(
289 changes,
290 SystemHeading::PlacementChanged,
291 &new.name,
292 (&old.labels, old.deprecated.as_ref(), &old.attributes),
293 (&new.labels, new.deprecated.as_ref(), &new.attributes),
294 );
295 for machine in &new.machines {
296 let path = format!("{}/{}", new.name, machine.name);
297 match old
298 .machines
299 .iter()
300 .find(|before| before.name == machine.name)
301 {
302 Some(before) if before.external != machine.external => changes.push(SystemChange {
303 heading: SystemHeading::PlacementChanged,
304 path: format!("{path}/external"),
305 before: Some(before.external.to_string()),
306 after: Some(machine.external.to_string()),
307 }),
308 Some(_) => {}
309 None => placement(changes, path, None, Some("machine")),
310 }
311 if let Some(before) = old
312 .machines
313 .iter()
314 .find(|before| before.name == machine.name)
315 {
316 declaration_attrs(
317 changes,
318 SystemHeading::PlacementChanged,
319 &format!("{}/{}", new.name, machine.name),
320 (
321 &before.labels,
322 before.deprecated.as_ref(),
323 &before.attributes,
324 ),
325 (
326 &machine.labels,
327 machine.deprecated.as_ref(),
328 &machine.attributes,
329 ),
330 );
331 }
332 }
333 for machine in &old.machines {
334 if !new.machines.iter().any(|after| after.name == machine.name) {
335 placement(
336 changes,
337 format!("{}/{}", new.name, machine.name),
338 Some("machine"),
339 None,
340 );
341 }
342 }
343 for placed in &new.placements {
344 let paired = old.placements.iter().find(|before| {
345 before.component == placed.component && before.instance == placed.instance
346 });
347 let Some(before) = paired else {
348 continue;
349 };
350 if before.machine != placed.machine {
351 changes.push(SystemChange {
352 heading: SystemHeading::PlacementChanged,
353 path: format!("{}/{}.{}", new.name, placed.component, placed.instance),
354 before: Some(before.machine.clone()),
355 after: Some(placed.machine.clone()),
356 });
357 }
358 line_attrs(
359 changes,
360 SystemHeading::PlacementChanged,
361 &format!("{}/{}.{}", new.name, placed.component, placed.instance),
362 &before.attributes,
363 &placed.attributes,
364 );
365 }
366}
367
368fn compare_distribution(
369 before: &v2::Distribution,
370 after: &v2::Distribution,
371 name: &str,
372 changes: &mut Vec<SystemChange>,
373) {
374 value_change(
375 changes,
376 SystemHeading::CompositionChanged,
377 format!("{name}/tier"),
378 tier_text(before.tier),
379 tier_text(after.tier),
380 );
381 declaration_attrs(
382 changes,
383 SystemHeading::CompositionChanged,
384 name,
385 (
386 &before.labels,
387 before.deprecated.as_ref(),
388 &before.attributes,
389 ),
390 (&after.labels, after.deprecated.as_ref(), &after.attributes),
391 );
392 for line in &after.members {
395 if let Some(before) = before
396 .members
397 .iter()
398 .find(|earlier| earlier.component == line.component)
399 {
400 line_attrs(
401 changes,
402 SystemHeading::CompositionChanged,
403 &format!("{name}/{}", line.component),
404 &before.attributes,
405 &line.attributes,
406 );
407 }
408 }
409}
410
411fn composition(
412 changes: &mut Vec<SystemChange>,
413 path: String,
414 before: Option<&str>,
415 after: Option<&str>,
416) {
417 changes.push(SystemChange {
418 heading: SystemHeading::CompositionChanged,
419 path,
420 before: before.map(str::to_string),
421 after: after.map(str::to_string),
422 });
423}
424
425fn placement(
426 changes: &mut Vec<SystemChange>,
427 path: String,
428 before: Option<&str>,
429 after: Option<&str>,
430) {
431 changes.push(SystemChange {
432 heading: SystemHeading::PlacementChanged,
433 path,
434 before: before.map(str::to_string),
435 after: after.map(str::to_string),
436 });
437}
438
439fn label_list(labels: &[String]) -> String {
441 format!("({})", labels.join(", "))
442}
443
444fn attribute_list(attributes: &[v2::Attribute]) -> String {
447 if attributes.is_empty() {
448 return "[]".to_string();
449 }
450 let items: Vec<String> = attributes
451 .iter()
452 .map(|attribute| match &attribute.value {
453 Some(value) => format!(
454 "{}.{} = {}",
455 attribute.namespace,
456 attribute.key,
457 value_text(value)
458 ),
459 None => format!("{}.{}", attribute.namespace, attribute.key),
460 })
461 .collect();
462 format!("[ {} ]", items.join(", "))
463}
464
465fn value_text(value: &v2::AttributeValue) -> String {
467 match &value.kind {
468 Some(v2::attribute_value::Kind::Scalar(text)) => text.clone(),
469 Some(v2::attribute_value::Kind::List(list)) => {
470 let items: Vec<String> = list.items.iter().map(value_text).collect();
471 format!("({})", items.join(", "))
472 }
473 None => String::new(),
474 }
475}
476
477fn tier_text(tier: i32) -> Option<String> {
481 match v2::Tier::try_from(tier) {
482 Ok(v2::Tier::Platform) => Some("PLATFORM".to_string()),
483 Ok(v2::Tier::Application) => Some("APPLICATION".to_string()),
484 Ok(v2::Tier::Unspecified) => None,
485 Err(_) => Some(format!("tier({tier})")),
486 }
487}
488
489fn value_change(
491 changes: &mut Vec<SystemChange>,
492 heading: SystemHeading,
493 path: String,
494 before: Option<String>,
495 after: Option<String>,
496) {
497 if before != after {
498 changes.push(SystemChange {
499 heading,
500 path,
501 before,
502 after,
503 });
504 }
505}
506
507fn declaration_attrs(
510 changes: &mut Vec<SystemChange>,
511 heading: SystemHeading,
512 path: &str,
513 old: (&[String], Option<&String>, &[v2::Attribute]),
514 new: (&[String], Option<&String>, &[v2::Attribute]),
515) {
516 value_change(
517 changes,
518 heading,
519 format!("{path}/labels"),
520 Some(label_list(old.0)),
521 Some(label_list(new.0)),
522 );
523 value_change(
524 changes,
525 heading,
526 format!("{path}/deprecated"),
527 old.1.map(|reason| format!("{reason:?}")),
528 new.1.map(|reason| format!("{reason:?}")),
529 );
530 line_attrs(changes, heading, path, old.2, new.2);
531}
532
533fn line_attrs(
535 changes: &mut Vec<SystemChange>,
536 heading: SystemHeading,
537 path: &str,
538 old: &[v2::Attribute],
539 new: &[v2::Attribute],
540) {
541 value_change(
542 changes,
543 heading,
544 format!("{path}/attributes"),
545 Some(attribute_list(old)),
546 Some(attribute_list(new)),
547 );
548}
549
550fn interface_text(interface: Option<&InterfaceRef>) -> String {
553 match interface {
554 Some(interface) if interface.inline => interface.name.clone(),
555 Some(interface) => format!("{}.{}", interface.catalog, interface.name),
556 None => String::new(),
557 }
558}
559
560fn instance_list(instances: &[String]) -> String {
562 format!("({})", instances.join(", "))
563}
564
565#[cfg(test)]
566mod tests {
567 use ridl_ir::v2;
568
569 use super::*;
570
571 fn component(name: &str, instances: &[&str], offers: &[&str], requires: &[&str]) -> Component {
572 Component {
573 name: name.to_string(),
574 package: "veh.topology".to_string(),
575 implicit: false,
576 external: false,
577 instances: instances
578 .iter()
579 .map(|instance| instance.to_string())
580 .collect(),
581 offers: offers
582 .iter()
583 .map(|service| v2::Offer {
584 service: service.to_string(),
585 attributes: Vec::new(),
586 doc: String::new(),
587 links: Vec::new(),
588 see: Vec::new(),
589 since: Vec::new(),
590 })
591 .collect(),
592 requires: requires
593 .iter()
594 .map(|interface| v2::Require {
595 interface: Some(v2::InterfaceRef {
596 catalog: "veh.adas".to_string(),
597 name: interface.to_string(),
598 inline: false,
599 }),
600 service: String::new(),
601 producer: String::new(),
602 attributes: Vec::new(),
603 doc: String::new(),
604 links: Vec::new(),
605 see: Vec::new(),
606 since: Vec::new(),
607 })
608 .collect(),
609 labels: Vec::new(),
610 attributes: Vec::new(),
611 doc: String::new(),
612 links: Vec::new(),
613 see: Vec::new(),
614 since: Vec::new(),
615 deprecated: None,
616 }
617 }
618
619 fn machine(name: &str, external: bool) -> v2::Machine {
620 v2::Machine {
621 name: name.to_string(),
622 external,
623 labels: Vec::new(),
624 attributes: Vec::new(),
625 doc: String::new(),
626 links: Vec::new(),
627 see: Vec::new(),
628 since: Vec::new(),
629 deprecated: None,
630 }
631 }
632
633 fn placed(component: &str, instance: &str, machine: &str) -> v2::Placement {
634 v2::Placement {
635 component: format!("veh.topology.{component}"),
636 instance: instance.to_string(),
637 machine: machine.to_string(),
638 attributes: Vec::new(),
639 sizing: None,
640 }
641 }
642
643 fn base() -> System {
645 System {
646 name: "Vehicle".to_string(),
647 package: "veh.topology".to_string(),
648 components: vec![
649 component(
650 "Cruise",
651 &["primary", "backup"],
652 &["veh.adas.cruise"],
653 &["LaneAssist"],
654 ),
655 component("Panel", &["Unit"], &[], &["CruiseControl"]),
656 ],
657 deployments: vec![Deployment {
658 name: "Production".to_string(),
659 package: "veh.topology".to_string(),
660 machines: vec![machine("AdasHpc", false), machine("Cockpit", false)],
661 placements: vec![
662 placed("Cruise", "primary", "AdasHpc"),
663 placed("Cruise", "backup", "Cockpit"),
664 placed("Panel", "Unit", "Cockpit"),
665 ],
666 ..Deployment::default()
667 }],
668 ..System::default()
669 }
670 }
671
672 fn lines(changes: &[SystemChange]) -> Vec<String> {
674 changes
675 .iter()
676 .map(|change| {
677 let mut line = format!("{}: {}", heading_text(change.heading), change.path);
678 crate::push_values(&mut line, change.before.as_ref(), change.after.as_ref());
679 line
680 })
681 .collect()
682 }
683
684 fn backend(namespace: &str, key: &str, value: Option<&str>) -> v2::Attribute {
685 v2::Attribute {
686 namespace: namespace.to_string(),
687 key: key.to_string(),
688 value: value.map(|text| v2::AttributeValue {
689 kind: Some(v2::attribute_value::Kind::Scalar(text.to_string())),
690 }),
691 }
692 }
693
694 fn with_distribution() -> System {
697 let mut system = base();
698 system.members = vec![
699 v2::MemberLine {
700 component: "veh.topology.Cruise".to_string(),
701 ..Default::default()
702 },
703 v2::MemberLine {
704 component: "veh.topology.Panel".to_string(),
705 ..Default::default()
706 },
707 ];
708 system.distributions.push(v2::Distribution {
709 name: "Adas".to_string(),
710 package: "veh.topology".to_string(),
711 members: vec![v2::MemberLine {
712 component: "veh.topology.Cruise".to_string(),
713 ..Default::default()
714 }],
715 ..Default::default()
716 });
717 system
718 }
719
720 #[test]
721 fn bare_distributions_are_reported_in_both_directions() {
722 let present = with_distribution();
723 let mut absent = present.clone();
724 absent.distributions.clear();
725 let change = SystemChange {
726 heading: SystemHeading::CompositionChanged,
727 path: "veh.topology.Adas".to_string(),
728 before: None,
729 after: Some("distribution".to_string()),
730 };
731 assert_eq!(
732 diff_systems(&absent, &present).as_slice(),
733 std::slice::from_ref(&change),
734 );
735 assert_eq!(
736 diff_systems(&present, &absent),
737 [SystemChange {
738 before: change.after,
739 after: change.before,
740 ..change
741 }]
742 );
743 }
744
745 #[test]
746 fn added_and_removed_distributions_include_their_attributes() {
747 let mut old = with_distribution();
748 old.distributions.push(v2::Distribution {
751 name: "Adas".to_string(),
752 package: "veh.other".to_string(),
753 ..Default::default()
754 });
755 let mut new = old.clone();
756 let distribution = &mut new.distributions[0];
757 distribution.tier = v2::Tier::Platform as i32;
758 distribution.labels = vec!["QM".to_string()];
759 distribution.deprecated = Some("use Next".to_string());
760 distribution.attributes = vec![backend("sign", "key", Some("release"))];
761 let mut absent = old.clone();
762 absent
763 .distributions
764 .retain(|distribution| distribution.package != "veh.topology");
765 let added = diff_systems(&absent, &new);
766 let expected = [
767 ("veh.topology.Adas", None, Some("distribution")),
768 ("veh.topology.Adas/tier", None, Some("PLATFORM")),
769 ("veh.topology.Adas/labels", Some("()"), Some("(QM)")),
770 ("veh.topology.Adas/deprecated", None, Some("\"use Next\"")),
771 (
772 "veh.topology.Adas/attributes",
773 Some("[]"),
774 Some("[ sign.key = release ]"),
775 ),
776 ];
777 assert_eq!(added.len(), expected.len(), "{added:?}");
778 for (change, (path, before, after)) in added.iter().zip(expected) {
779 assert_eq!(change.heading, SystemHeading::CompositionChanged);
780 assert_eq!(change.path, path);
781 assert_eq!(change.before.as_deref(), before);
782 assert_eq!(change.after.as_deref(), after);
783 }
784 assert_eq!(diff_systems(&old, &new), added[1..]);
785 let removed = diff_systems(&new, &absent);
786 assert_eq!(removed.len(), added.len());
787 for (removed, added) in removed.iter().zip(&added) {
788 assert_eq!(removed.heading, added.heading);
789 assert_eq!(removed.path, added.path);
790 assert_eq!(removed.before, added.after);
791 assert_eq!(removed.after, added.before);
792 }
793 }
794
795 #[test]
800 fn the_registry_rows_are_reported_as_recorded() {
801 use ridl_core::attributes::{ATTRIBUTES, AttrName, Diff, Site};
802 for row in ATTRIBUTES {
803 if !row
804 .sites
805 .iter()
806 .any(|site| !matches!(site, Site::Command | Site::Query))
807 {
808 continue;
809 }
810 let old = with_distribution();
811 let mut new = with_distribution();
812 let key = match row.name {
813 AttrName::Key(key) => key,
814 AttrName::Namespaced => "ns.key",
815 };
816 match key {
817 "instances" => new.components[0].instances = vec!["Unit".to_string()],
818 "external" => new.components[0].external = true,
819 "tier" => new.distributions[0].tier = v2::Tier::Application as i32,
820 "labels" => new.distributions[0].labels = vec!["QM".to_string()],
821 "deprecated" => new.deployments[0].deprecated = Some("use v2".to_string()),
822 "depth" => {
823 new.deployments[0].sizing = Some(v2::Sizing {
824 depth: Some(4),
825 slots: None,
826 budget: None,
827 })
828 }
829 "slots" => {
830 new.deployments[0].placements[0].sizing = Some(v2::Sizing {
831 depth: None,
832 slots: Some(4),
833 budget: None,
834 })
835 }
836 "budget" => {
837 new.deployments[0].sizing = Some(v2::Sizing {
838 depth: None,
839 slots: None,
840 budget: Some(4096),
841 })
842 }
843 "ns.key" => new.deployments[0].machines[0].attributes.push(backend(
844 "linux",
845 "cpuset",
846 Some("(2)"),
847 )),
848 other => panic!("registry row `{other}` has no mutation in this test"),
849 }
850 let changes = diff_systems(&old, &new);
851 match row.diff {
852 Diff::Compared(_) => assert!(!changes.is_empty(), "`{key}` must be reported"),
853 Diff::NotReported(reason) => assert!(
854 changes.is_empty(),
855 "`{key}` is recorded as not reported ({reason:?}) but was: {changes:?}"
856 ),
857 }
858 }
859 }
860
861 #[test]
868 fn attribute_changes_are_listed_under_the_heading_of_their_site() {
869 let old = with_distribution();
870 let mut new = with_distribution();
871 new.labels = vec!["QM".to_string()];
872 new.components[0].deprecated = Some("use Cruise2".to_string());
873 new.distributions[0].tier = v2::Tier::Platform as i32;
874 new.members[0]
875 .attributes
876 .push(backend("deb", "section", Some("net")));
877 new.components[0].requires[0]
878 .attributes
879 .push(backend("someip", "clientId", Some("17")));
880 new.distributions[0].members[0]
881 .attributes
882 .push(backend("sign", "key", None));
883 new.components[0].offers[0]
884 .attributes
885 .push(backend("someip", "serviceId", Some("4660")));
886 new.deployments[0]
887 .attributes
888 .push(backend("rust", "crate", Some("\"s\"")));
889 new.deployments[0].machines[0].labels = vec!["ASIL_B".to_string()];
890 new.deployments[0].placements[0]
891 .attributes
892 .push(backend("linux", "cpuset", Some("(2)")));
893 assert_eq!(
894 lines(&diff_systems(&old, &new)),
895 [
896 "composition changed: veh.topology.Vehicle/labels: () -> (QM)",
897 "composition changed: veh.topology.Vehicle/veh.topology.Cruise/attributes: \
898 [] -> [ deb.section = net ]",
899 "composition changed: veh.topology.Cruise/deprecated: (absent) -> \"use Cruise2\"",
900 "composition changed: veh.topology.Cruise/offers/veh.adas.cruise/attributes: \
901 [] -> [ someip.serviceId = 4660 ]",
902 "composition changed: veh.topology.Cruise/requires/veh.adas.LaneAssist/attributes: \
903 [] -> [ someip.clientId = 17 ]",
904 "composition changed: veh.topology.Adas/tier: (absent) -> PLATFORM",
905 "composition changed: veh.topology.Adas/veh.topology.Cruise/attributes: \
906 [] -> [ sign.key ]",
907 "placement changed: Production/attributes: [] -> [ rust.crate = \"s\" ]",
908 "placement changed: Production/AdasHpc/labels: () -> (ASIL_B)",
909 "placement changed: Production/veh.topology.Cruise.primary/attributes: \
910 [] -> [ linux.cpuset = (2) ]",
911 ]
912 );
913 }
914
915 #[test]
917 fn an_attribute_removed_is_reported() {
918 let mut old = with_distribution();
919 old.components[0].deprecated = Some("use Cruise2".to_string());
920 old.distributions[0].tier = v2::Tier::Platform as i32;
921 let new = with_distribution();
922 assert_eq!(
923 lines(&diff_systems(&old, &new)),
924 [
925 "composition changed: veh.topology.Cruise/deprecated: \"use Cruise2\" -> (removed)",
926 "composition changed: veh.topology.Adas/tier: PLATFORM -> (removed)",
927 ]
928 );
929 }
930
931 #[test]
932 fn an_unchanged_system_has_no_system_change() {
933 assert!(diff_systems(&base(), &base()).is_empty());
934 }
935
936 #[test]
939 fn a_region_hash_change_is_not_a_system_change() {
940 let region = |hash: u8| v2::Region {
941 catalog: "veh.adas".to_string(),
942 interfaces: Vec::new(),
943 hash: vec![hash; 32],
944 };
945 let old = System {
946 regions: vec![region(1)],
947 ..base()
948 };
949 let new = System {
950 regions: vec![region(2)],
951 ..base()
952 };
953 assert!(diff_systems(&old, &new).is_empty());
954 }
955
956 #[test]
959 fn system_doc_changes_are_not_reported() {
960 let see = v2::DocLink {
961 text: "Other".to_string(),
962 target: "veh.topology.Other".to_string(),
963 ..Default::default()
964 };
965 macro_rules! document {
966 ($carrier:expr) => {{
967 let carrier = &mut $carrier;
968 carrier.doc = "documented".to_string();
969 carrier.links.push(see.clone());
970 carrier.see.push(see.clone());
971 carrier.since.push("1.2".to_string());
972 }};
973 }
974 let mut old = base();
975 old.members.push(v2::MemberLine {
976 component: "veh.topology.Cruise".to_string(),
977 ..Default::default()
978 });
979 old.distributions.push(v2::Distribution {
980 name: "Adas".to_string(),
981 package: "veh.topology".to_string(),
982 members: old.members.clone(),
983 ..Default::default()
984 });
985 let mut new = old.clone();
986 document!(new);
987 document!(new.members[0]);
988 document!(new.components[0]);
989 document!(new.components[0].offers[0]);
990 document!(new.components[0].requires[0]);
991 document!(new.distributions[0]);
992 document!(new.distributions[0].members[0]);
993 let deployment = &mut new.deployments[0];
994 deployment.doc = "documented".to_string();
995 deployment.doc_links.push(see.clone());
996 deployment.see.push(see.clone());
997 deployment.since.push("1.2".to_string());
998 document!(new.deployments[0].machines[0]);
999 assert_ne!(old, new);
1000 assert!(diff_systems(&old, &new).is_empty());
1001 }
1002
1003 #[test]
1006 fn system_sizing_changes_are_not_reported() {
1007 let sized = |depth: u32, slots: u32, budget: u64| v2::Sizing {
1008 depth: Some(depth),
1009 slots: Some(slots),
1010 budget: Some(budget),
1011 };
1012 let mut old = base();
1013 old.deployments[0].sizing = Some(sized(3, 16, 4096));
1014 old.deployments[0].placements[0].sizing = Some(sized(3, 16, 4096));
1015 let mut new = old.clone();
1016 new.deployments[0].sizing = Some(sized(300, 64, 65536));
1017 new.deployments[0].placements[0].sizing = Some(sized(300, 64, 65536));
1018 assert_ne!(old, new);
1019 assert!(diff_systems(&old, &new).is_empty());
1020
1021 assert!(diff_systems(&base(), &new).is_empty());
1024 assert!(diff_systems(&new, &base()).is_empty());
1025 }
1026
1027 #[test]
1031 fn system_changes_render_under_their_headings_with_no_verdict() {
1032 let old = base();
1033 let mut new = base();
1034 new.deployments[0].placements[1].machine = "AdasHpc".to_string();
1035 new.components[1].external = true;
1036
1037 let report = crate::diff_workspaces(&[], Some(&old), &[], Some(&new), &[]);
1038 assert_eq!(report.verdict, crate::Verdict::Identical);
1039 assert_eq!(
1040 crate::render_text(&report),
1041 "identical\n\
1042 placement changed\n\
1043 \x20 Production/veh.topology.Cruise.backup: Cockpit -> AdasHpc\n\
1044 composition changed\n\
1045 \x20 veh.topology.Panel/external: false -> true\n"
1046 );
1047 let json: serde_json::Value =
1048 serde_json::from_str(&crate::render_json(&report)).expect("the report is JSON");
1049 assert_eq!(
1050 json["placement_changed"],
1051 serde_json::json!([{
1052 "path": "Production/veh.topology.Cruise.backup",
1053 "before": "Cockpit",
1054 "after": "AdasHpc"
1055 }])
1056 );
1057 assert_eq!(json["composition_changed"][0]["after"], "true");
1058
1059 let one_side = crate::diff_workspaces(&[], None, &[], Some(&new), &[]);
1060 assert!(one_side.system.is_empty());
1061 let json: serde_json::Value =
1062 serde_json::from_str(&crate::render_json(&one_side)).expect("the report is JSON");
1063 assert_eq!(
1064 json,
1065 serde_json::json!({ "verdict": "identical", "changes": [] })
1066 );
1067 }
1068
1069 #[test]
1073 fn a_deployment_change_is_listed_under_placement_changed() {
1074 let old = base();
1075 let mut new = base();
1076 new.deployments[0].placements[1].machine = "AdasHpc".to_string();
1077 new.deployments[0].machines[1].external = true;
1078 new.deployments[0].machines.push(machine("Cloud", true));
1079 new.deployments.push(Deployment {
1080 name: "Bench".to_string(),
1081 ..Deployment::default()
1082 });
1083 assert_eq!(
1084 lines(&diff_systems(&old, &new)),
1085 [
1086 "placement changed: Production/Cockpit/external: false -> true",
1087 "placement changed: Production/Cloud: (absent) -> machine",
1088 "placement changed: Production/veh.topology.Cruise.backup: Cockpit -> AdasHpc",
1089 "placement changed: Bench: (absent) -> deployment",
1090 ]
1091 );
1092 assert_eq!(
1093 lines(&diff_systems(&new, &old)),
1094 [
1095 "placement changed: Production/Cockpit/external: true -> false",
1096 "placement changed: Production/Cloud: machine -> (removed)",
1097 "placement changed: Production/veh.topology.Cruise.backup: AdasHpc -> Cockpit",
1098 "placement changed: Bench: deployment -> (removed)",
1099 ]
1100 );
1101 }
1102
1103 #[test]
1107 fn a_closure_or_line_change_is_listed_under_composition_changed() {
1108 let old = base();
1109 let mut new = base();
1110 new.components[0].instances = vec!["Unit".to_string()];
1111 new.components[0].requires.clear();
1112 new.components[1].external = true;
1113 new.components[1].offers.push(v2::Offer {
1114 service: "veh.hmi.panel".to_string(),
1115 attributes: Vec::new(),
1116 doc: String::new(),
1117 links: Vec::new(),
1118 see: Vec::new(),
1119 since: Vec::new(),
1120 });
1121 new.components
1122 .push(component("Lane", &["Unit"], &["veh.adas.lane"], &[]));
1123 assert_eq!(
1124 lines(&diff_systems(&old, &new)),
1125 [
1126 "composition changed: veh.topology.Cruise/requires/veh.adas.LaneAssist: \
1127 requires veh.adas.LaneAssist -> (removed)",
1128 "composition changed: veh.topology.Cruise/instances: (primary, backup) -> (Unit)",
1129 "composition changed: veh.topology.Panel/offers/veh.hmi.panel: \
1130 (absent) -> offers veh.hmi.panel",
1131 "composition changed: veh.topology.Panel/external: false -> true",
1132 "composition changed: veh.topology.Lane: (absent) -> in the closure",
1133 ]
1134 );
1135 assert_eq!(
1136 lines(&diff_systems(&new, &old)).last().map(String::as_str),
1137 Some("composition changed: veh.topology.Lane: in the closure -> (removed)")
1138 );
1139 }
1140}