diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index ffda0711..890bd55b 100644 --- a/docs/window-animations-plan.md +++ b/docs/window-animations-plan.md @@ -248,6 +248,9 @@ Current pure planner status: - Planner tests now include a deterministic split-tree generator. It builds valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them through supported transitions, and runs the real planner plus exact validator. +- The generated tests also include a bounded corpus of supported split-tree + transitions across both axes and directions. Each case is planned twice and + compared exactly to catch nondeterministic planner output. Tests: @@ -258,6 +261,7 @@ Tests: - interruption restarts only affected phase groups - reversing direction produces equivalent motion in reverse - accepted and rejected plans expose deterministic strategy explanations +- bounded generated split-tree corpus produces identical plans on repeated runs - child waits for parent/container-space phases when moving upward into a toplevel peer position - mono-mode tab switches do not animate, while entering/exiting mono can animate diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 2addc788..4c37e676 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -1473,11 +1473,47 @@ mod tests { } fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) { + assert_generated_case_plans_deterministically(old, new, bounds); + } + + fn assert_generated_case_plans_deterministically( + old: &TestTree, + new: &TestTree, + bounds: Rect, + ) -> MultiphasePlanned { let req = generated_request(old, new, bounds); assert!(!overlaps(req.windows.iter().map(|window| window.from))); assert!(!overlaps(req.windows.iter().map(|window| window.to))); - let plan = plan_no_overlap(&req).unwrap(); - assert!(validate_plan_continuous(&req, &plan)); + let first = plan_no_overlap_explained(&req).unwrap(); + let second = plan_no_overlap_explained(&req).unwrap(); + assert_eq!(first, second); + assert_eq!(first.plan.phases.len(), first.explanation.phases.len()); + assert_eq!( + first.explanation.validation, + ValidationExplanation::passed() + ); + for phase in &first.explanation.phases { + assert!(phase.nodes.windows(2).all(|pair| pair[0].0 < pair[1].0)); + } + assert!(validate_plan_continuous(&req, &first.plan)); + first + } + + fn bounds_for_axis(axis: PhaseAxis) -> Rect { + match axis { + PhaseAxis::Horizontal => rect(0, 0, 400, 100), + PhaseAxis::Vertical => rect(0, 0, 100, 400), + } + } + + fn push_generated_case_bidirectional( + cases: &mut Vec<(TestTree, TestTree, Rect)>, + old: TestTree, + new: TestTree, + bounds: Rect, + ) { + cases.push((old.clone(), new.clone(), bounds)); + cases.push((new, old, bounds)); } fn request(windows: Vec) -> MultiphaseRequest { @@ -1798,6 +1834,105 @@ mod tests { assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400)); } + #[test] + fn bounded_generated_supported_split_tree_corpus_is_deterministic() { + let mut cases = vec![]; + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + let child_axis = axis.other(); + let bounds = bounds_for_axis(axis); + + push_generated_case_bidirectional( + &mut cases, + split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]), + split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split(10, axis, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)]), + split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + leaf(1), + split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ), + split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), + leaf(3), + ], + ), + split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + leaf(1), + split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), + ], + ), + split( + 10, + axis, + &[1, 3], + vec![ + leaf(1), + split(11, child_axis, &[3, 1], vec![leaf(2), leaf(3)]), + ], + ), + bounds, + ); + push_generated_case_bidirectional( + &mut cases, + split( + 10, + axis, + &[1, 1], + vec![ + split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), + leaf(3), + ], + ), + split( + 10, + axis, + &[3, 1], + vec![ + split(11, child_axis, &[1, 3], vec![leaf(1), leaf(2)]), + leaf(3), + ], + ), + bounds, + ); + } + + assert_eq!(cases.len(), 24); + for (old, new, bounds) in cases { + assert_generated_case_plans_deterministically(&old, &new, bounds); + } + } + #[test] fn stack_extraction_creates_space_before_moving_child() { let req = request(vec![