//! The tree a seed describes, as a value rather than as widgets. //! //! Two things have to hold for a plan to be worth having. Editing a plan has //! to mean what growing with those edits means, or a scenario reads one thing //! and the oracle another. And reducing a plan has to end, or a shrinker //! searching for the smallest counterexample never returns. use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan}; use std::collections::HashMap; fn some_edits(seed: u64, of: &Plan) -> Edits { let mut rng = Rng::new(seed); let (mut sized, mut aligned, mut nodes, mut spans) = (0, 0, 0, 0); let mut of = of.clone(); of.walk_mut(&mut |p| { if matches!(p.kind, Kind::Span { .. }) { spans += 1; } sized += p.size.is_some() as usize; aligned += p.align.is_some() as usize; nodes += p.region_node.is_some() as usize; }); let pick = |n: usize, rng: &mut Rng| -> Vec { (0..n).filter(|_| rng.chance()).collect() }; Edits { sizes: pick(sized, &mut rng) .into_iter() .map(|i| (i, [Some(LayoutLen::LEFTOVER), None])) .collect(), aligns: pick(aligned, &mut rng) .into_iter() .map(|i| (i, [Some(AxisAlign::POS), None])) .collect(), nodes: pick(nodes, &mut rng) .into_iter() .map(|i| (i, true)) .collect(), spans: pick(spans, &mut rng) .into_iter() .map(|i| { ( i, SpanEdit { detach: vec![0], attach: 2, }, ) }) .collect::>(), fixed_branches: false, } } use iris::prelude::*; /// The two routes to an edited tree are one tree. `plan` resolves edits out /// of the random stream as it draws; `edited` puts them on a tree that /// already exists, which is the only route a shrunk plan has, since no seed /// grows one. A scenario written against either has to read the same. #[test] fn editing_a_plan_is_growing_one_with_those_edits() { for seed in 1..=60 { let bare = plan(seed, 5, &Edits::default()); let edits = some_edits(seed, &bare); assert_eq!( bare.edited(&edits), plan(seed, 5, &edits), "seed {seed}: edited and grown-with-edits disagree" ); } } /// No simplification is larger, which is the half of "the shrinker stops" a /// widget count can see. Most are not smaller either -- a dropped alignment /// and a simpler leaf both keep the count -- so what rules out circling is /// that those are one-way too: a `Some` becomes a `None`, and a kind steps /// down a ladder with no way back up. #[test] fn no_simplification_of_a_plan_is_larger_than_it() { for seed in 1..=60 { let tree = plan(seed, 4, &Edits::default()); let mut queue = vec![tree]; let mut seen = 0; while let Some(node) = queue.pop() { seen += 1; if seen > 400 { break; } for small in node.smaller() { assert!( small.size() <= node.size(), "seed {seed}: a simplification grew from {} to {}", node.size(), small.size() ); if small.size() < node.size() { queue.push(small); } } } } } /// Reducing until nothing reduces ends, and ends at something small enough to /// read rather than at the tree it started from. #[test] fn reducing_a_plan_all_the_way_ends() { for seed in 1..=30 { let mut node = plan(seed, 5, &Edits::default()); let grown = node.size(); let mut steps = 0; while let Some(next) = node.smaller().into_iter().next() { node = next; steps += 1; assert!(steps < 10_000, "seed {seed}: reducing did not end"); } assert!( node.size() < grown.max(2), "seed {seed}: reduced {grown} widgets to {}", node.size() ); } }