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