1crate::ix!();
3
4impl TryFrom<&Skeleton> for StringSkeleton {
5 type Error = StringSkeletonError;
6
7 #[instrument(level="trace", skip(num_skel))]
8 fn try_from(num_skel: &Skeleton) -> Result<Self, Self::Error> {
9 trace!("Converting numeric Skeleton => StringSkeleton, including capstone fields.");
10 let mut out_map = HashMap::<String, StringSkeletonNode>::new();
11
12 for node in num_skel.nodes() {
13 let is_root_id = Some(node.id()) == *num_skel.root_id();
14 let is_original_key_root = node.original_key() == "root";
15
16 let map_key = if is_root_id && is_original_key_root {
21 "root".to_string()
22 } else if !node.original_key().is_empty() && node.original_key() != "root" {
23 node.original_key().to_string()
24 } else {
25 node.name().to_string()
26 };
27
28 trace!(
29 "Processing node id={} => map_key='{}' original_key='{}' name='{}'",
30 node.id(),
31 map_key,
32 node.original_key(),
33 node.name()
34 );
35
36 if out_map.contains_key(&map_key) {
37 warn!("Name collision in final map => '{}'", map_key);
38 return Err(StringSkeletonError::NameCollision);
39 }
40
41 let mut child_names = Vec::new();
43 for &cid in node.child_ids() {
44 if let Some(ch) = num_skel.nodes().iter().find(|n| n.id() == cid) {
45 let child_is_root_id = Some(ch.id()) == *num_skel.root_id();
46 let child_is_original_key_root = ch.original_key() == "root";
47
48 let cname = if child_is_root_id && child_is_original_key_root {
55 "root".to_string()
56 } else if !ch.original_key().is_empty() && ch.original_key() != "root" {
57 ch.original_key().to_string()
58 } else {
59 ch.name().to_string()
60 };
61 child_names.push(cname);
62 } else {
63 trace!("Skipping invalid child ID={} for node='{}'", cid, map_key);
64 }
65 }
66
67 let ordering = node.ordering().cloned();
69 let sn = match node {
70 SkeletonNode::Dispatch { .. } => {
71 let mut child_map = HashMap::new();
72 let child_spec1 = DispatchChildSpecBuilder::default()
73 .branch_selection_likelihood(100)
74 .build()
75 .unwrap();
76 for cname in child_names {
77 child_map.insert(cname, child_spec1.clone());
78 }
79 StringSkeletonNode::Dispatch {
80 name: node.name().to_string(),
81 ordering: ordering.clone(),
82 children: child_map,
83 }
84 }
85 SkeletonNode::Aggregate { .. } => {
86 let mut child_map = HashMap::new();
87 let child_spec1 = AggregateChildSpecBuilder::default()
88 .psome_likelihood(100)
89 .optional(false)
90 .build()
91 .unwrap();
92 for cname in child_names {
93 child_map.insert(cname, child_spec1.clone());
94 }
95 StringSkeletonNode::Aggregate {
96 name: node.name().to_string(),
97 ordering: ordering.clone(),
98 children: child_map,
99 }
100 }
101 SkeletonNode::LeafHolder { leaf_count, capstone, .. } => {
102 StringSkeletonNode::LeafHolder {
103 name: node.name().to_string(),
104 ordering: ordering.clone(),
105 n_leaves: *leaf_count as u8,
106 capstone: *capstone,
107 }
108 }
109 };
110
111 out_map.insert(map_key, sn);
112 }
113
114 let result = StringSkeletonBuilder::default()
115 .target_name(num_skel.target_name())
116 .map(out_map)
117 .build()
118 .map_err(|e| {
119 error!("Failed building StringSkeleton: {:?}", e);
120 StringSkeletonError::NameCollision
121 })?;
122
123 info!(
124 "Numeric Skeleton => StringSkeleton succeeded with {} nodes",
125 result.map().len()
126 );
127 Ok(result)
128 }
129}
130
131impl TryFrom<&StringSkeleton> for Skeleton {
132 type Error = StringSkeletonError;
133
134 #[instrument(level="trace", skip(ss))]
135 fn try_from(ss: &StringSkeleton) -> Result<Self, Self::Error> {
136 trace!("Converting StringSkeleton => Skeleton with possible capstone fields.");
137 let has_explicit_root = ss.map().contains_key("root");
138
139 let mut name_to_id = HashMap::<String, u16>::new();
141 let mut next_id: u16 = 0;
142
143 if has_explicit_root {
144 name_to_id.insert("root".to_string(), 0);
145 }
146
147 for map_key in ss.map().keys() {
148 if map_key == "root" {
149 name_to_id.entry("root".to_string()).or_insert(0);
150 continue;
151 }
152 if name_to_id.contains_key(map_key) {
153 error!("Name collision for key: '{}'", map_key);
154 return Err(StringSkeletonError::NameCollision);
155 }
156 while name_to_id.values().any(|&v| v == next_id) {
158 next_id = next_id.checked_add(1)
159 .ok_or(StringSkeletonError::NameCollision)?;
160 }
161 name_to_id.insert(map_key.clone(), next_id);
162 next_id = next_id.checked_add(1)
163 .ok_or(StringSkeletonError::NameCollision)?;
164 }
165
166 let mut used_as_child = HashSet::new();
168 let mut final_nodes = Vec::new();
169
170 for (map_key, node_data) in ss.map().iter() {
171 let assigned_id = *name_to_id.get(map_key).unwrap_or(&0);
172
173 let (child_keys, n_leaves, cap_flag) = match node_data {
174 StringSkeletonNode::Dispatch { children, .. } => {
175 let ckeys = children.keys().cloned().collect::<Vec<_>>();
176 (ckeys, 0u16, false)
177 }
178 StringSkeletonNode::Aggregate { children, .. } => {
179 let ckeys = children.keys().cloned().collect::<Vec<_>>();
180 (ckeys, 0u16, false)
181 }
182 StringSkeletonNode::LeafHolder { n_leaves, capstone, .. } => {
183 (Vec::new(), *n_leaves as u16, *capstone)
184 }
185 };
186
187 let mut child_ids = Vec::new();
189 match node_data {
190 StringSkeletonNode::Dispatch { children, .. } => {
191 for ck in children.keys() {
192 if let Some(&cid) = name_to_id.get(ck) {
193 child_ids.push(cid);
194 used_as_child.insert(cid);
195 } else {
196 warn!("Missing child name='{}' for node='{}'", ck, map_key);
197 return Err(StringSkeletonError::MissingChildName);
198 }
199 }
200 }
201 StringSkeletonNode::Aggregate { children, .. } => {
202 for ck in children.keys() {
203 if let Some(&cid) = name_to_id.get(ck) {
204 child_ids.push(cid);
205 used_as_child.insert(cid);
206 } else {
207 warn!("Missing child name='{}' for node='{}'", ck, map_key);
208 return Err(StringSkeletonError::MissingChildName);
209 }
210 }
211 }
212 StringSkeletonNode::LeafHolder { .. } => {
213 }
215 }
216
217 let (name_for_debug, ordering_for_debug) = match node_data {
218 StringSkeletonNode::Dispatch { name, ordering, .. } => (name.clone(), ordering.clone()),
219 StringSkeletonNode::Aggregate { name, ordering, .. } => (name.clone(), ordering.clone()),
220 StringSkeletonNode::LeafHolder { name, ordering, .. } => (name.clone(), ordering.clone()),
221 };
222 let final_name = if name_for_debug.is_empty() {
223 format!("(unnamed_{})", map_key)
224 } else {
225 name_for_debug
226 };
227
228 let kind = match node_data {
230 StringSkeletonNode::Dispatch { .. } => NodeKind::Dispatch,
231 StringSkeletonNode::Aggregate { .. } => NodeKind::Aggregate,
232 StringSkeletonNode::LeafHolder { .. } => NodeKind::LeafHolder,
233 };
234
235 let mut builder = SkeletonNodeBuilder::default()
237 .id(assigned_id)
238 .name(final_name)
239 .original_key(map_key.clone())
240 .ordering(ordering_for_debug.clone());
241
242 if let StringSkeletonNode::Dispatch { .. } | StringSkeletonNode::Aggregate { .. } = node_data {
243 builder = builder.child_ids(child_ids);
244 } else if let StringSkeletonNode::LeafHolder { .. } = node_data {
245 builder = builder.leaf_count(n_leaves).capstone(cap_flag);
246 }
247
248 let built_node = builder.build(kind).map_err(|_| {
249 error!("SkeletonNodeBuilder failed for node='{}'", map_key);
250 StringSkeletonError::NameCollision
251 })?;
252
253 debug!(
254 "Built numeric node => id={}, variant={:?}, capstone={} from map_key='{}'",
255 built_node.id(),
256 built_node,
257 cap_flag,
258 map_key
259 );
260 final_nodes.push(built_node);
261 }
262
263 let root_id = if has_explicit_root {
265 Some(0_u16)
266 } else {
267 final_nodes
268 .iter()
269 .map(|nd| nd.id())
270 .find(|nid| !used_as_child.contains(nid))
271 };
272
273 let sk = SkeletonBuilder::default()
275 .target_name(ss.target_name().to_string())
276 .nodes(final_nodes)
277 .root_id(root_id)
278 .build()
279 .map_err(|_| {
280 error!("SkeletonBuilder failed; possibly ID collision");
281 StringSkeletonError::NameCollision
282 })?;
283
284 info!(
285 "StringSkeleton => Skeleton succeeded. node_count={}, root_id={:?}",
286 sk.node_count(),
287 sk.root_id(),
288 );
289 Ok(sk)
290 }
291}
292
293#[cfg(test)]
294mod conversion_tests {
295 use super::*;
296 use std::collections::{HashMap, HashSet};
297
298 #[traced_test]
299 fn test_leafholder_capstone_round_trip() {
300 trace!("Creating a StringSkeleton with one LeafHolder that has capstone=true.");
301
302 let mut map = HashMap::new();
304 let leaf = StringSkeletonNode::LeafHolder {
305 name: "CapLeaf".to_string(),
306 ordering: Some(SubBranchOrdering::None),
307 n_leaves: 5,
308 capstone: true,
309 };
310 map.insert("SingleLeaf".to_string(), leaf);
311
312 let str_skel = StringSkeletonBuilder::default()
313 .map(map)
314 .build()
315 .unwrap();
316
317 trace!("Converting StringSkeleton => numeric Skeleton.");
318 let numeric = Skeleton::try_from(&str_skel).expect("string->numeric ok");
319 assert_eq!(numeric.node_count(), 1);
320
321 let node = &numeric.nodes()[0];
322 match node {
323 SkeletonNode::LeafHolder {
324 leaf_count,
325 capstone,
326 ..
327 } => {
328 info!("Numeric LeafHolder => leaf_count={}, capstone={}", leaf_count, capstone);
329 assert_eq!(*leaf_count, 5);
330 assert_eq!(*capstone, true);
331 }
332 _ => panic!("Expected LeafHolder variant with capstone=true"),
333 }
334
335 trace!("Converting numeric Skeleton => StringSkeleton again.");
336 let round_trip_str = StringSkeleton::try_from(&numeric).expect("numeric->string ok");
337 assert_eq!(round_trip_str.map().len(), 1);
338
339 let node_data = round_trip_str.map().get("SingleLeaf").expect("Missing SingleLeaf key");
340 match node_data {
341 StringSkeletonNode::LeafHolder {
342 n_leaves,
343 capstone,
344 ..
345 } => {
346 info!("Round-trip => n_leaves={}, capstone={}", n_leaves, capstone);
347 assert_eq!(*n_leaves, 5);
348 assert_eq!(*capstone, true);
349 }
350 _ => panic!("Expected LeafHolder variant with capstone=true"),
351 }
352 }
353
354 fn round_trip_string_skeleton(
356 skel_str: &StringSkeleton
357 ) -> Result<StringSkeleton, StringSkeletonError> {
358 let numeric = Skeleton::try_from(skel_str)?;
359 StringSkeleton::try_from(&numeric)
360 }
361
362 #[traced_test]
363 fn test_string_to_numeric_empty() {
364 let empty_str_skel = StringSkeletonBuilder::default()
365 .map(HashMap::new())
366 .build()
367 .unwrap();
368
369 let numeric_res = Skeleton::try_from(&empty_str_skel);
370 assert!(numeric_res.is_ok());
371 let numeric = numeric_res.unwrap();
372 assert_eq!(numeric.node_count(), 0);
373 assert_eq!(numeric.measure_tree_depth(), 0);
374 }
375
376 #[traced_test]
377 fn test_string_to_numeric_single_leaf() {
378 let mut map = HashMap::new();
379 let leaf = StringSkeletonNode::LeafHolder {
380 name: "SoloLeaf".to_string(),
381 ordering: Some(SubBranchOrdering::None),
382 n_leaves: 2,
383 capstone: false,
384 };
385 map.insert("Alpha".to_string(), leaf);
386
387 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
388 let numeric = Skeleton::try_from(&str_skel).unwrap();
389 assert_eq!(numeric.node_count(), 1);
390 assert_eq!(numeric.measure_tree_depth(), 1);
391 }
392
393 #[traced_test]
394 fn test_string_to_numeric_single_dispatch() {
395 let mut map = HashMap::new();
396 let dispatch = StringSkeletonNode::Dispatch {
397 name: "SoloDispatch".to_string(),
398 ordering: None,
399 children: HashMap::new(),
400 };
401 map.insert("Alpha".to_string(), dispatch);
402
403 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
404 let numeric = Skeleton::try_from(&str_skel).unwrap();
405 assert_eq!(numeric.node_count(), 1);
406 assert_eq!(numeric.measure_tree_depth(), 1);
407 }
408
409 #[traced_test]
410 fn test_string_to_numeric_single_aggregate() {
411 let mut map = HashMap::new();
412 let agg = StringSkeletonNode::Aggregate {
413 name: "SoloAgg".to_string(),
414 ordering: Some(SubBranchOrdering::Alphabetical),
415 children: HashMap::new(),
416 };
417 map.insert("Alpha".to_string(), agg);
418
419 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
420 let numeric = Skeleton::try_from(&str_skel).unwrap();
421 assert_eq!(numeric.node_count(), 1);
422 assert_eq!(numeric.measure_tree_depth(), 1);
423 }
424
425 #[traced_test]
426 fn test_string_to_numeric_dispatch_with_child() {
427 let mut map = HashMap::new();
428 let mut child_map = HashMap::new();
429
430 let cspec = DispatchChildSpecBuilder::default()
432 .branch_selection_likelihood(100)
433 .build()
434 .unwrap();
435
436 child_map.insert("B".to_string(), cspec);
437
438 let nodeA = StringSkeletonNode::Dispatch {
439 name: "NodeA".to_string(),
440 ordering: Some(SubBranchOrdering::DifficultyDescending),
441 children: child_map,
442 };
443 map.insert("A".to_string(), nodeA);
444
445 let nodeB = StringSkeletonNode::LeafHolder {
446 name: "NodeB".to_string(),
447 ordering: Some(SubBranchOrdering::DifficultyAscending),
448 n_leaves: 3,
449 capstone: false,
450 };
451 map.insert("B".to_string(), nodeB);
452
453 let str_skel = StringSkeletonBuilder::default()
454 .map(map)
455 .build()
456 .unwrap();
457 let numeric = Skeleton::try_from(&str_skel).unwrap();
458 assert_eq!(numeric.node_count(), 2);
459 let depth = numeric.measure_tree_depth();
460 assert_eq!(depth, 2);
461 }
462
463 #[traced_test]
464 fn test_string_to_numeric_aggregate_with_two_children() {
465 let mut map = HashMap::new();
466
467 let cspec = AggregateChildSpecBuilder::default()
469 .psome_likelihood(100)
470 .optional(false)
471 .build()
472 .unwrap();
473
474 let mut root_kids = HashMap::new();
475 root_kids.insert("X".to_string(), cspec);
476 root_kids.insert("Y".to_string(), cspec);
477
478 let root_agg = StringSkeletonNode::Aggregate {
479 name: "RootAgg".to_string(),
480 ordering: Some(SubBranchOrdering::Alphabetical),
481 children: root_kids,
482 };
483 map.insert("root".to_string(), root_agg);
484
485 let node_x = StringSkeletonNode::LeafHolder {
486 name: "Xnode".to_string(),
487 ordering: None,
488 n_leaves: 2,
489 capstone: false,
490 };
491 map.insert("X".to_string(), node_x);
492
493 let node_y = StringSkeletonNode::Dispatch {
494 name: "Ynode".to_string(),
495 ordering: None,
496 children: HashMap::new(),
497 };
498 map.insert("Y".to_string(), node_y);
499
500 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
501 let numeric = Skeleton::try_from(&str_skel).unwrap();
502 assert_eq!(numeric.node_count(), 3);
503 assert_eq!(numeric.measure_tree_depth(), 2);
504 assert_eq!(*numeric.root_id(), Some(0u16));
505 }
506
507 #[traced_test]
508 fn test_string_to_numeric_explicit_root_label() {
509 let mut map = HashMap::new();
510 let mut kids = HashMap::new();
511
512 let csp = DispatchChildSpecBuilder::default()
513 .branch_selection_likelihood(100)
514 .build()
515 .unwrap();
516 kids.insert("Child".to_string(), csp);
517
518 let root_dispatch = StringSkeletonNode::Dispatch {
519 name: "MyRootDispatch".to_string(),
520 ordering: Some(SubBranchOrdering::DifficultyAscending),
521 children: kids,
522 };
523 map.insert("root".to_string(), root_dispatch);
524
525 let child_leaf = StringSkeletonNode::LeafHolder {
526 name: "ChildLeaf".to_string(),
527 ordering: None,
528 n_leaves: 1,
529 capstone: false,
530 };
531 map.insert("Child".to_string(), child_leaf);
532
533 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
534 let numeric = Skeleton::try_from(&str_skel).unwrap();
535 assert_eq!(numeric.node_count(), 2);
536 assert_eq!(*numeric.root_id(), Some(0u16));
537 let depth = numeric.measure_tree_depth();
538 assert_eq!(depth, 2);
539 }
540
541 #[traced_test]
542 fn test_string_to_numeric_missing_child() {
543 let mut map = HashMap::new();
544 let mut kids_a = HashMap::new();
545
546 let csp = DispatchChildSpecBuilder::default()
547 .branch_selection_likelihood(100)
548 .build()
549 .unwrap();
550 kids_a.insert("B".to_string(), csp);
551
552 let node_a = StringSkeletonNode::Dispatch {
553 name: "A".to_string(),
554 ordering: None,
555 children: kids_a,
556 };
557 map.insert("A".to_string(), node_a);
558
559 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
560 let res = Skeleton::try_from(&str_skel);
561 assert!(res.is_err());
562 matches::assert_matches!(res.err().unwrap(), StringSkeletonError::MissingChildName);
563 }
564
565 #[traced_test]
566 fn test_string_to_numeric_name_collision() {
567 let mut map = HashMap::new();
568 let nd1 = StringSkeletonNode::LeafHolder {
569 name: "Same".to_string(),
570 ordering: None,
571 n_leaves: 1,
572 capstone: false,
573 };
574 map.insert("Dup1".to_string(), nd1);
575
576 let nd2 = StringSkeletonNode::LeafHolder {
577 name: "Same".to_string(),
578 ordering: None,
579 n_leaves: 2,
580 capstone: false,
581 };
582 map.insert("Dup2".to_string(), nd2);
583
584 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
585 let numeric = Skeleton::try_from(&str_skel);
586 if numeric.is_ok() {
587 let sk = numeric.unwrap();
588 assert_eq!(sk.node_count(), 2);
589 } else {
590 info!("NameCollision triggered, acceptable outcome.");
591 }
592 }
593
594 #[traced_test]
595 fn test_string_to_numeric_round_trip() {
596 let mut map = HashMap::new();
597
598 let csp = AggregateChildSpecBuilder::default()
599 .psome_likelihood(100)
600 .optional(false)
601 .build()
602 .unwrap();
603
604 let mut kids = HashMap::new();
605 kids.insert("Side".to_string(), csp);
606
607 let node_main = StringSkeletonNode::Aggregate {
608 name: "MainAggregate".to_string(),
609 ordering: Some(SubBranchOrdering::Random),
610 children: kids,
611 };
612 map.insert("Main".to_string(), node_main);
613
614 let node_side = StringSkeletonNode::LeafHolder {
615 name: "SideLeaf".to_string(),
616 ordering: None,
617 n_leaves: 2,
618 capstone: false,
619 };
620 map.insert("Side".to_string(), node_side);
621
622 let skel_str = StringSkeletonBuilder::default().map(map).build().unwrap();
623 let numeric = Skeleton::try_from(&skel_str).expect("string->numeric ok");
624 let back_str = StringSkeleton::try_from(&numeric).expect("numeric->string ok");
625 assert_eq!(back_str.map().len(), 2);
626 }
627
628 #[traced_test]
629 fn test_string_to_numeric_multi_unreferenced() {
630 let mut map = HashMap::new();
631 let a = StringSkeletonNode::Dispatch {
632 name: "A".to_string(),
633 ordering: None,
634 children: HashMap::new(),
635 };
636 map.insert("A".to_string(), a);
637
638 let b = StringSkeletonNode::LeafHolder {
639 name: "Bleaf".to_string(),
640 ordering: None,
641 n_leaves: 1,
642 capstone: false,
643 };
644 map.insert("B".to_string(), b);
645
646 let c = StringSkeletonNode::Aggregate {
647 name: "Cagg".to_string(),
648 ordering: None,
649 children: HashMap::new(),
650 };
651 map.insert("C".to_string(), c);
652
653 let str_skel = StringSkeletonBuilder::default().map(map).build().unwrap();
654 let numeric = Skeleton::try_from(&str_skel).unwrap();
655 assert_eq!(numeric.node_count(), 3);
656 assert_eq!(numeric.measure_tree_depth(), 1);
657 }
658
659 #[traced_test]
660 fn test_numeric_to_string_empty() {
661 let numeric = SkeletonBuilder::default().build().unwrap();
662 let str_skel = StringSkeleton::try_from(&numeric).expect("empty => ok");
663 assert_eq!(str_skel.map().len(), 0);
664 }
665
666 #[traced_test]
667 fn test_numeric_to_string_dispatch_only() {
668 let disp_node = SkeletonNodeBuilder::default()
669 .id(0)
670 .name("Dispatch0")
671 .build(NodeKind::Dispatch)
672 .unwrap();
673
674 let numeric = SkeletonBuilder::default()
675 .nodes(vec![disp_node])
676 .root_id(Some(0))
677 .build()
678 .unwrap();
679
680 let str_skel = StringSkeleton::try_from(&numeric).unwrap();
681 let map = str_skel.map();
682 assert_eq!(map.len(), 1);
683
684 let node_data = map.get("Dispatch0").expect("expected key=Dispatch0");
685 match node_data {
686 StringSkeletonNode::Dispatch { children, .. } => {
687 assert!(children.is_empty());
688 }
689 _ => panic!("expected Dispatch variant"),
690 }
691 }
692
693 #[traced_test]
694 fn test_numeric_to_string_aggregate_only() {
695 let agg_node = SkeletonNodeBuilder::default()
696 .id(0)
697 .name("Agg0")
698 .build(NodeKind::Aggregate)
699 .unwrap();
700
701 let numeric = SkeletonBuilder::default()
702 .nodes(vec![agg_node])
703 .root_id(Some(0))
704 .build()
705 .unwrap();
706
707 let str_skel = StringSkeleton::try_from(&numeric).unwrap();
708 assert_eq!(str_skel.map().len(), 1);
709 let node_data = str_skel.map().get("Agg0").unwrap();
710 match node_data {
711 StringSkeletonNode::Aggregate { children, .. } => {
712 assert!(children.is_empty());
713 }
714 _ => panic!("expected Aggregate variant"),
715 }
716 }
717
718 #[traced_test]
719 fn test_numeric_to_string_leafholder_only() {
720 let leaf_node = SkeletonNodeBuilder::default()
721 .id(0)
722 .name("Leaf0")
723 .leaf_count(3)
724 .capstone(true)
725 .build(NodeKind::LeafHolder)
726 .unwrap();
727
728 let numeric = SkeletonBuilder::default()
729 .nodes(vec![leaf_node])
730 .root_id(Some(0))
731 .build()
732 .unwrap();
733
734 let str_skel = StringSkeleton::try_from(&numeric).unwrap();
735 let map = str_skel.map();
736 assert_eq!(map.len(), 1);
737 let node_data = map.get("Leaf0").expect("key=Leaf0");
738 match node_data {
739 StringSkeletonNode::LeafHolder { n_leaves, .. } => {
740 assert_eq!(*n_leaves, 3);
741 }
742 _ => panic!("expected LeafHolder variant"),
743 }
744 }
745
746 #[traced_test]
747 fn test_numeric_to_string_children() {
748 let node0 = SkeletonNodeBuilder::default()
752 .id(0)
753 .child_ids(vec![1])
754 .name("Dispatch0")
755 .build(NodeKind::Dispatch)
756 .unwrap();
757
758 let node1 = SkeletonNodeBuilder::default()
759 .id(1)
760 .child_ids(vec![2])
761 .name("Agg1")
762 .build(NodeKind::Aggregate)
763 .unwrap();
764
765 let node2 = SkeletonNodeBuilder::default()
766 .id(2)
767 .leaf_count(2)
768 .name("Leaf2")
769 .build(NodeKind::LeafHolder)
770 .unwrap();
771
772 let numeric = SkeletonBuilder::default()
773 .nodes(vec![node0, node1, node2])
774 .root_id(Some(0))
775 .build()
776 .unwrap();
777
778 let str_skel = StringSkeleton::try_from(&numeric).unwrap();
779 let map = str_skel.map();
780 assert_eq!(map.len(), 3);
781
782 let n0_data = map.get("Dispatch0").expect("key=Dispatch0");
783 match n0_data {
784 StringSkeletonNode::Dispatch { children, .. } => {
785 assert_eq!(children.len(), 1);
786 assert!(children.contains_key("Agg1"));
787 }
788 _ => panic!("expected Dispatch variant"),
789 }
790
791 let n1_data = map.get("Agg1").expect("key=Agg1");
792 match n1_data {
793 StringSkeletonNode::Aggregate { children, .. } => {
794 assert_eq!(children.len(), 1);
795 assert!(children.contains_key("Leaf2"));
796 }
797 _ => panic!("expected Aggregate variant"),
798 }
799
800 let n2_data = map.get("Leaf2").expect("key=Leaf2");
801 match n2_data {
802 StringSkeletonNode::LeafHolder { n_leaves, .. } => {
803 assert_eq!(*n_leaves, 2);
804 }
805 _ => panic!("expected LeafHolder variant"),
806 }
807 }
808
809 #[traced_test]
810 fn test_numeric_to_string_duplicate_node_names() {
811 let n0 = SkeletonNodeBuilder::default()
812 .id(0)
813 .name("Same")
814 .build(NodeKind::Dispatch)
815 .unwrap();
816
817 let n1 = SkeletonNodeBuilder::default()
818 .id(1)
819 .name("Same")
820 .build(NodeKind::LeafHolder)
821 .unwrap();
822
823 let numeric = SkeletonBuilder::default()
824 .nodes(vec![n0, n1])
825 .build()
826 .unwrap();
827
828 let res = StringSkeleton::try_from(&numeric);
829 assert!(res.is_err());
830 matches::assert_matches!(res.err().unwrap(), StringSkeletonError::NameCollision);
831 }
832
833 #[traced_test]
834 fn test_numeric_to_string_round_trip() {
835 let n0 = SkeletonNodeBuilder::default()
836 .id(0)
837 .child_ids(vec![1])
838 .name("RootDispatch")
839 .build(NodeKind::Dispatch)
840 .unwrap();
841
842 let n1 = SkeletonNodeBuilder::default()
843 .id(1)
844 .leaf_count(2)
845 .name("Leaf2")
846 .build(NodeKind::LeafHolder)
847 .unwrap();
848
849 let numeric_skel = SkeletonBuilder::default()
850 .nodes(vec![n0,n1])
851 .root_id(Some(0))
852 .build()
853 .unwrap();
854
855 let str_skel = StringSkeleton::try_from(&numeric_skel).expect("to string ok");
856 let numeric2 = Skeleton::try_from(&str_skel).expect("back to numeric ok");
857 assert_eq!(numeric2.node_count(), 2);
858 assert_eq!(numeric2.measure_tree_depth(), 2);
859 }
860
861 #[traced_test]
862 fn test_self_reference_dispatch() {
863 let mut map = HashMap::new();
864 let mut kids = HashMap::new();
865 let csp = DispatchChildSpecBuilder::default()
866 .branch_selection_likelihood(100)
867 .build()
868 .unwrap();
869 kids.insert("root".to_string(), csp);
870
871 let node_root = StringSkeletonNode::Dispatch {
872 name: "SelfRefRoot".to_string(),
873 ordering: Some(SubBranchOrdering::None),
874 children: kids,
875 };
876 map.insert("root".to_string(), node_root);
877
878 let skel_str = StringSkeletonBuilder::default().map(map).build().unwrap();
879 let numeric = Skeleton::try_from(&skel_str).unwrap();
880 let depth = numeric.measure_tree_depth();
881 assert_eq!(depth, 1);
882 }
883
884 #[traced_test]
885 fn test_repeated_child_reference_in_aggregate() {
886 let mut map = HashMap::new();
888 let mut parent_kids = HashMap::new();
889
890 let csp1 = AggregateChildSpecBuilder::default()
891 .psome_likelihood(100)
892 .optional(false)
893 .build()
894 .unwrap();
895 let csp2 = AggregateChildSpecBuilder::default()
896 .psome_likelihood(50)
897 .optional(true)
898 .build()
899 .unwrap();
900
901 parent_kids.insert("Kid".to_string(), csp1);
902 parent_kids.insert("Kid".to_string(), csp2);
903
904 let node_parent = StringSkeletonNode::Aggregate {
905 name: "ParentAgg".to_string(),
906 ordering: None,
907 children: parent_kids,
908 };
909 map.insert("Parent".to_string(), node_parent);
910
911 let node_kid = StringSkeletonNode::LeafHolder {
912 name: "KidLeaf".to_string(),
913 ordering: None,
914 n_leaves: 2,
915 capstone: false,
916 };
917 map.insert("Kid".to_string(), node_kid);
918
919 let skel_str = StringSkeletonBuilder::default().map(map).build().unwrap();
920 let numeric = Skeleton::try_from(&skel_str).unwrap();
921 assert_eq!(numeric.node_count(), 2);
922 let depth = numeric.measure_tree_depth();
923 assert_eq!(depth, 2);
924 }
925
926 #[traced_test]
927 fn test_string_skeleton_complex_round_trip() {
928 trace!("Creating a moderately complex skeleton with root => Dispatch => kids=[A, B], plus other unreferenced node D, etc.");
929
930 let mut map = HashMap::new();
936
937 let mut r_kids = HashMap::new();
939 let csp = DispatchChildSpecBuilder::default()
940 .branch_selection_likelihood(100)
941 .build()
942 .unwrap();
943 r_kids.insert("A".to_string(), csp.clone());
944 r_kids.insert("B".to_string(), csp);
945
946 let root = StringSkeletonNode::Dispatch {
947 name: "RootDispatch".to_string(),
948 ordering: Some(SubBranchOrdering::DifficultyAscending),
949 children: r_kids,
950 };
951 map.insert("root".to_string(), root);
952
953 let mut a_kids = HashMap::new();
955 let csp_agg = AggregateChildSpecBuilder::default()
956 .psome_likelihood(100)
957 .optional(false)
958 .build()
959 .unwrap();
960 a_kids.insert("C".to_string(), csp_agg);
961
962 let nodeA = StringSkeletonNode::Aggregate {
963 name: "Aagg".to_string(),
964 ordering: Some(SubBranchOrdering::AlphabeticalReverse),
965 children: a_kids, };
967 map.insert("A".to_string(), nodeA);
968
969 let nodeB = StringSkeletonNode::LeafHolder {
971 name: "Bleaf".to_string(),
972 ordering: None,
973 n_leaves: 1,
974 capstone: false,
975 };
976 map.insert("B".to_string(), nodeB);
977
978 let nodeC = StringSkeletonNode::LeafHolder {
980 name: "Cleaf".to_string(),
981 ordering: None,
982 n_leaves: 2,
983 capstone: false,
984 };
985 map.insert("C".to_string(), nodeC);
986
987 let nodeD = StringSkeletonNode::LeafHolder {
989 name: "Dleaf".to_string(),
990 ordering: None,
991 n_leaves: 1,
992 capstone: false,
993 };
994 map.insert("D".to_string(), nodeD);
995
996 let user_skel1 = StringSkeletonBuilder::default().map(map).build().unwrap();
997
998 let numeric1 = Skeleton::try_from(&user_skel1).expect("string->numeric ok");
999 assert_eq!(numeric1.node_count(), 5);
1000
1001 let depth1 = numeric1.measure_tree_depth();
1002 assert_eq!(depth1, 3); let user_skel2 = StringSkeleton::try_from(&numeric1).expect("numeric->string ok");
1005 assert_eq!(user_skel2.map().len(), 5);
1006
1007 let numeric2 = Skeleton::try_from(&user_skel2).expect("final numeric ok");
1008 let depth2 = numeric2.measure_tree_depth();
1009 assert_eq!(depth2, 3);
1010 }
1011
1012 #[traced_test]
1013 fn test_string_to_numeric_all_used_as_child_cycle() {
1014 trace!("Testing scenario: A->B, B->C, C->A cycle => no unreferenced nodes => BFS depth=0, root_id=None.");
1015
1016 let mut map = HashMap::new();
1017
1018 let mut a_kids = HashMap::new();
1020 let dsp = DispatchChildSpecBuilder::default()
1021 .branch_selection_likelihood(100)
1022 .build()
1023 .unwrap();
1024 a_kids.insert("B".to_string(), dsp);
1025 let node_a = StringSkeletonNode::Dispatch {
1026 name: "A".to_string(),
1027 ordering: None,
1028 children: a_kids,
1029 };
1030 map.insert("A".to_string(), node_a);
1031
1032 let mut b_kids = HashMap::new();
1034 let agg_spec = AggregateChildSpecBuilder::default()
1035 .psome_likelihood(100)
1036 .optional(false)
1037 .build()
1038 .unwrap();
1039 b_kids.insert("C".to_string(), agg_spec);
1040 let node_b = StringSkeletonNode::Aggregate {
1041 name: "B".to_string(),
1042 ordering: None,
1043 children: b_kids,
1044 };
1045 map.insert("B".to_string(), node_b);
1046
1047 let mut c_kids = HashMap::new();
1049 let dsp2 = DispatchChildSpecBuilder::default()
1050 .branch_selection_likelihood(100)
1051 .build()
1052 .unwrap();
1053 c_kids.insert("A".to_string(), dsp2);
1054 let node_c = StringSkeletonNode::Dispatch {
1055 name: "C".to_string(),
1056 ordering: None,
1057 children: c_kids,
1058 };
1059 map.insert("C".to_string(), node_c);
1060
1061 let skel_str = StringSkeletonBuilder::default().map(map).build().unwrap();
1062 let numeric = Skeleton::try_from(&skel_str).unwrap();
1063
1064 assert_eq!(numeric.node_count(), 3);
1067 let depth = numeric.measure_tree_depth();
1068 assert_eq!(depth, 0);
1069 }
1070}