Skip to main content

capability_skeleton_string/
conversion.rs

1// ---------------- [ File: capability-skeleton-string/src/conversion.rs ]
2crate::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            // We’ll pick the final map key as follows:
17            //  1) If this is truly the root node AND its original_key is "root", use "root".
18            //  2) Else if original_key is non-empty (and not "root") => use that.
19            //  3) Otherwise => fall back to node.name().
20            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            // Collect child names
42            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                    // Recursively apply the same map-key logic to child?
49                    // But that might cause re-lookup collisions. We typically
50                    // just do a simpler approach: if child’s original_key is "root" & is root => "root"
51                    // else child’s original_key or name. We'll keep it simpler here, but it should
52                    // match what we do for the node itself.
53
54                    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            // Build the node
68            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        // 1) Assign IDs to each map key.
140        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            // assign next free ID
157            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        // 2) Build numeric nodes
167        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            // Convert child map keys => child_ids
188            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                    // no children for a leaf holder
214                }
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            // Decide variant
229            let kind = match node_data {
230                StringSkeletonNode::Dispatch { .. } => NodeKind::Dispatch,
231                StringSkeletonNode::Aggregate { .. } => NodeKind::Aggregate,
232                StringSkeletonNode::LeafHolder { .. } => NodeKind::LeafHolder,
233            };
234
235            // 4) Build
236            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        // 5) root_id
264        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        // 6) Build final
274        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        // Build a small string-based skeleton:
303        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    /// Helper: round-trip string -> skeleton -> string
355    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        // Instead of .probability_percent(100), just set .branch_selection_likelihood(100)
431        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        // Instead of .probability_percent(100), just use .psome_likelihood(100)
468        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        // 0 => Dispatch => child=1
749        // 1 => Aggregate => child=2
750        // 2 => LeafHolder => n_leaves=2
751        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        // "Parent" => Aggregate => children=["Kid","Kid"]
887        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        // "root" => Dispatch => kids=["A","B"]
931        // "A" => Aggregate => kids=["C"]
932        // "B" => LeafHolder => n_leaves=1
933        // "C" => LeafHolder => n_leaves=2
934        // plus D => unreferenced => LeafHolder => n_leaves=1
935        let mut map = HashMap::new();
936
937        // For the root => Dispatch => kids=[A,B]
938        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        // "A" => Aggregate => kids=["C"]
954        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, // <-- Must reference `a_kids` for the test to pass
966        };
967        map.insert("A".to_string(), nodeA);
968
969        // "B" => LeafHolder => n_leaves=1
970        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        // "C" => LeafHolder => n_leaves=2
979        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        // "D" => unreferenced => LeafHolder => n_leaves=1
988        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); // Expect BFS depth = 3
1003
1004        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        // A => Dispatch => child=B
1019        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        // B => Aggregate => child=C
1033        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        // C => Dispatch => child=A
1048        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        // With a perfect cycle of A->B->C->A, all used as children, so there's no unique root => root_id=None.
1065        // BFS/DFS sees a cycle => measure_tree_depth() = 0
1066        assert_eq!(numeric.node_count(), 3);
1067        let depth = numeric.measure_tree_depth();
1068        assert_eq!(depth, 0);
1069    }
1070}