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