@@ -985,4 +985,109 @@ mod tests {
985985 assert ! ( pos_3 < pos_2) ;
986986 assert ! ( pos_2 < pos_1) ;
987987 }
988+
989+ // ── Property-based tests ─────────────────────────────────────────
990+ //
991+ // The function takes a slice of `(id, parent_id)` pairs and returns a
992+ // bottom-up ordering. The properties we pin here are the ones the callers
993+ // (`compute_all_aggregates`, the incremental aggregator paths) rely on:
994+ // each id appears at most once, descendants come before ancestors, and
995+ // pathological inputs (cycles, duplicates, large random forests) don't
996+ // panic or hang.
997+
998+ mod proptests {
999+ use super :: * ;
1000+ use proptest:: prelude:: * ;
1001+ use std:: collections:: HashSet ;
1002+
1003+ /// Generate an acyclic forest of `n` nodes where every node's parent
1004+ /// is either `0` (forest root, treated as "out of set") or one of
1005+ /// the already-emitted nodes. Returns `Vec<(id, parent_id)>` with
1006+ /// ids in `1..=n`.
1007+ fn forest_strategy ( max_nodes : usize ) -> impl Strategy < Value = Vec < ( i64 , i64 ) > > {
1008+ ( 1usize ..=max_nodes) . prop_flat_map ( |n| {
1009+ // For each node i (1-indexed), pick a parent index in 0..i.
1010+ // Index 0 maps to parent_id 0 (sentinel for "no parent in set").
1011+ let parent_picks: Vec < _ > = ( 0 ..n) . map ( |i| 0usize ..=i) . collect ( ) ;
1012+ parent_picks. prop_map ( move |picks| {
1013+ picks
1014+ . into_iter ( )
1015+ . enumerate ( )
1016+ . map ( |( i, pick) | {
1017+ let id = ( i as i64 ) + 1 ;
1018+ let parent_id = pick as i64 ; // 0 means "no parent in set"
1019+ ( id, parent_id)
1020+ } )
1021+ . collect :: < Vec < _ > > ( )
1022+ } )
1023+ } )
1024+ }
1025+
1026+ proptest ! {
1027+ /// For any acyclic forest, the sort emits each node exactly once
1028+ /// and places every descendant before its ancestor.
1029+ #[ test]
1030+ fn forest_descendant_before_ancestor( entries in forest_strategy( 40 ) ) {
1031+ let sorted = topological_sort_bottom_up( & entries) ;
1032+
1033+ // Every id appears exactly once.
1034+ let unique_ids: HashSet <i64 > = entries. iter( ) . map( |& ( id, _) | id) . collect( ) ;
1035+ prop_assert_eq!( sorted. len( ) , unique_ids. len( ) , "output length must match unique input ids" ) ;
1036+ let sorted_set: HashSet <i64 > = sorted. iter( ) . copied( ) . collect( ) ;
1037+ prop_assert_eq!( & sorted_set, & unique_ids, "output must be a permutation of the input ids" ) ;
1038+
1039+ // Build position map and parent map.
1040+ let pos: HashMap <i64 , usize > =
1041+ sorted. iter( ) . enumerate( ) . map( |( i, & id) | ( id, i) ) . collect( ) ;
1042+ let parent_of: HashMap <i64 , i64 > =
1043+ entries. iter( ) . copied( ) . collect( ) ;
1044+
1045+ // For every (child, parent_in_set) pair, child must come first.
1046+ for & ( id, pid) in & entries {
1047+ if pid != 0 && unique_ids. contains( & pid) {
1048+ let cp = pos[ & id] ;
1049+ let pp = pos[ & pid] ;
1050+ prop_assert!(
1051+ cp < pp,
1052+ "descendant {} at pos {} must come before ancestor {} at pos {}" ,
1053+ id, cp, pid, pp
1054+ ) ;
1055+ }
1056+ // Transitively the same must hold for any ancestor — but
1057+ // chain through `parent_of` to be sure.
1058+ let mut cursor = pid;
1059+ let mut hops = 0 ;
1060+ while cursor != 0 && unique_ids. contains( & cursor) && hops < entries. len( ) + 1 {
1061+ prop_assert!(
1062+ pos[ & id] < pos[ & cursor] ,
1063+ "descendant {} must come before transitive ancestor {}" ,
1064+ id, cursor
1065+ ) ;
1066+ cursor = * parent_of. get( & cursor) . unwrap_or( & 0 ) ;
1067+ hops += 1 ;
1068+ }
1069+ }
1070+ }
1071+
1072+ /// Robustness: the function must not panic and must produce a
1073+ /// subset of unique input ids, even on arbitrary (possibly
1074+ /// cyclic, duplicate, or detached) (id, parent_id) lists.
1075+ #[ test]
1076+ fn arbitrary_input_is_panic_free_and_subset(
1077+ entries in proptest:: collection:: vec( ( -50i64 ..50i64 , -50i64 ..50i64 ) , 0 ..30 )
1078+ ) {
1079+ let sorted = topological_sort_bottom_up( & entries) ;
1080+ let unique_ids: HashSet <i64 > = entries. iter( ) . map( |& ( id, _) | id) . collect( ) ;
1081+
1082+ // No duplicates in output.
1083+ let sorted_set: HashSet <i64 > = sorted. iter( ) . copied( ) . collect( ) ;
1084+ prop_assert_eq!( sorted. len( ) , sorted_set. len( ) , "output must have no duplicate ids" ) ;
1085+
1086+ // Output is a subset of unique input ids.
1087+ for id in & sorted_set {
1088+ prop_assert!( unique_ids. contains( id) , "output id {} must come from input" , id) ;
1089+ }
1090+ }
1091+ }
1092+ }
9881093}
0 commit comments