157 lines
5.2 KiB
Rust
157 lines
5.2 KiB
Rust
//! 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::harness::Harness;
|
|
use iris::prelude::*;
|
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, 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<usize> { (0..n).filter(|_| rng.chance()).collect() };
|
|
Edits {
|
|
sizes: pick(sized, &mut rng)
|
|
.into_iter()
|
|
.map(|i| {
|
|
(
|
|
i,
|
|
SizeRules {
|
|
x: SizeRule::from(LayoutLen::LEFTOVER),
|
|
y: SizeRule::FREE,
|
|
},
|
|
)
|
|
})
|
|
.collect(),
|
|
aligns: pick(aligned, &mut rng)
|
|
.into_iter()
|
|
.map(|i| {
|
|
(
|
|
i,
|
|
Align {
|
|
x: Some(AxisAlign::POS),
|
|
y: 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::<HashMap<_, _>>(),
|
|
fixed_branches: false,
|
|
}
|
|
}
|
|
|
|
/// 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()
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Every image in a tree is the same picture, and a handle is a reference to
|
|
/// the texture rather than a copy of it, so one upload and one slot serve all
|
|
/// of them however many a tree grows -- and the trees are grown in hundreds.
|
|
#[test]
|
|
fn a_tree_of_images_uploads_one_texture() {
|
|
let mut images = 0;
|
|
let mut tree = plan(1, 4, &Edits::default());
|
|
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
|
|
assert!(images > 1, "a tree of {images} images tests nothing");
|
|
|
|
let mut h = Harness::new((900, 1200));
|
|
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
|
|
h.state.root = Some(root);
|
|
h.frame();
|
|
assert_eq!(h.rsc.ui().textures.count(), 1);
|
|
}
|