atagen

monolith

public
Star0
Watch0
7de49c2aa70b317e35783f54619e08eff5326c7f
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/planner/src/tests.rsRust331 lines9.8 KB
1
use crate::geom::Rect;
2
use crate::plan::{Config, Plan, Strategy, Style, plan, plan_canonical};
3
use crate::{plan_blocks, validate};
4
use proptest::prelude::*;
5
6
const G: f64 = 10.0;
7
8
fn region() -> Rect {
9
    Rect::new(G, G, 1180.0, 680.0)
10
}
11
12
fn split(rect: Rect, ratios: &[f64], horizontal: bool) -> Vec<Rect> {
13
    let total: f64 = ratios.iter().sum();
14
    let len = if horizontal { rect.w } else { rect.h };
15
    let avail = len - G * (ratios.len() - 1) as f64;
16
    let mut pos = if horizontal { rect.x } else { rect.y };
17
    ratios
18
        .iter()
19
        .map(|r| {
20
            let size = avail * r / total;
21
            let out = if horizontal {
22
                Rect::new(pos, rect.y, size, rect.h)
23
            } else {
24
                Rect::new(rect.x, pos, rect.w, size)
25
            };
26
            pos += size + G;
27
            out
28
        })
29
        .collect()
30
}
31
32
fn cols(rect: Rect, n: usize) -> Vec<Rect> {
33
    split(rect, &vec![1.0; n], true)
34
}
35
36
fn rows(rect: Rect, n: usize) -> Vec<Rect> {
37
    split(rect, &vec![1.0; n], false)
38
}
39
40
fn cfg(style: Style) -> Config {
41
    Config {
42
        style,
43
        ..Config::default()
44
    }
45
}
46
47
fn assert_clean(p: &Plan, c: &Config) {
48
    let v = validate(p, c);
49
    assert!(v.is_empty(), "violations {v:?} in {p:?}");
50
    assert!(p.phases() <= 4, "{} phases", p.phases());
51
    assert!(
52
        p.groups.iter().all(|g| g.strategy != Strategy::Pinch),
53
        "fell back to pinch: {p:?}"
54
    );
55
}
56
57
fn strategy(p: &Plan) -> Strategy {
58
    p.groups
59
        .iter()
60
        .map(|g| g.strategy)
61
        .find(|s| *s != Strategy::Unchanged)
62
        .unwrap_or(Strategy::Unchanged)
63
}
64
65
#[test]
66
fn swap_uses_two_lanes() {
67
    let c = cfg(Style::Orthogonal);
68
    let ab = cols(region(), 2);
69
    let ba = vec![ab[1], ab[0]];
70
    let p = plan(&ab, &ba, &c);
71
    assert_clean(&p, &c);
72
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 2 });
73
}
74
75
#[test]
76
fn full_reversal_needs_one_lane_per_window() {
77
    let c = cfg(Style::Orthogonal);
78
    let a = cols(region(), 4);
79
    let b: Vec<Rect> = (0..4).map(|i| a[3 - i]).collect();
80
    let p = plan(&a, &b, &c);
81
    assert_clean(&p, &c);
82
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 4 });
83
}
84
85
#[test]
86
fn orientation_flip_is_two_phases() {
87
    let c = cfg(Style::Orthogonal);
88
    let p = plan(&cols(region(), 3), &rows(region(), 3), &c);
89
    assert_clean(&p, &c);
90
    assert_eq!(strategy(&p), Strategy::AxisOrder);
91
}
92
93
#[test]
94
fn growing_both_axes_depends_on_style() {
95
    let grid = |wide: bool| {
96
        let r = if wide { [7.0, 3.0] } else { [1.0, 1.0] };
97
        let rs = split(region(), &r, false);
98
        let mut out = split(rs[0], &r, true);
99
        out.extend(split(rs[1], &[1.0, 1.0], true));
100
        out
101
    };
102
    let o = cfg(Style::Orthogonal);
103
    let d = cfg(Style::Direct);
104
    let po = plan(&grid(false), &grid(true), &o);
105
    let pd = plan(&grid(false), &grid(true), &d);
106
    assert_clean(&po, &o);
107
    assert_clean(&pd, &d);
108
    assert_eq!((po.phases(), pd.phases()), (2, 1));
109
}
110
111
#[test]
112
fn insert_and_split_rewinds() {
113
    let c = cfg(Style::Orthogonal);
114
    let layout = |on: bool| {
115
        let n = if on { 4 } else { 3 };
116
        let ratios: Vec<f64> = (0..n)
117
            .map(|i| if i == 0 && on { 2.0 } else { 1.0 })
118
            .collect();
119
        let cs = split(region(), &ratios, true);
120
        let (a, mut b, c3) = (cs[0], cs[1], cs[n - 1]);
121
        let d = if on {
122
            cs[2]
123
        } else {
124
            Rect::new((b.right() + c3.x) / 2.0, b.y, 0.0, b.h)
125
        };
126
        let e = if on {
127
            let s = rows(b, 2);
128
            b = s[0];
129
            s[1]
130
        } else {
131
            Rect::new(b.x, b.bottom(), b.w, 0.0)
132
        };
133
        vec![a, b, c3, d, e]
134
    };
135
    let on = plan(&layout(false), &layout(true), &c);
136
    let off = plan(&layout(true), &layout(false), &c);
137
    assert_clean(&on, &c);
138
    assert_clean(&off, &c);
139
    assert!(on.groups.iter().all(|g| !g.opens_hole));
140
    let mut rewound = off.keyframes.clone();
141
    rewound.reverse();
142
    assert_eq!(on.keyframes, rewound);
143
}
144
145
#[test]
146
fn inserting_a_seed_is_one_phase() {
147
    let c = cfg(Style::Orthogonal);
148
    let three = cols(region(), 3);
149
    let four = cols(region(), 4);
150
    let seed = Rect::new(
151
        (three[1].right() + three[2].x) / 2.0,
152
        three[1].y,
153
        0.0,
154
        three[1].h,
155
    );
156
    let from = vec![three[0], three[1], three[2], seed];
157
    let to = vec![four[0], four[1], four[3], four[2]];
158
    let p = plan(&from, &to, &c);
159
    assert_clean(&p, &c);
160
    assert_eq!(strategy(&p), Strategy::Direct);
161
}
162
163
#[test]
164
fn tag_rows_swap_rigidly_with_two_lanes() {
165
    let c = cfg(Style::Orthogonal);
166
    let layout = |first_top: bool| {
167
        let rs = rows(region(), 2);
168
        let (t1, t2) = if first_top {
169
            (rs[0], rs[1])
170
        } else {
171
            (rs[1], rs[0])
172
        };
173
        let mut out = cols(t1, 3);
174
        out.extend(cols(t2, 2));
175
        out
176
    };
177
    let blocks = vec![vec![0, 1, 2], vec![3, 4]];
178
    let p = plan_blocks(&layout(true), &layout(false), &blocks, &c);
179
    assert_clean(&p, &c);
180
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 2 });
181
}
182
183
#[test]
184
fn simultaneous_reversals_on_both_axes_use_the_grid() {
185
    let c = cfg(Style::Orthogonal);
186
    let halves = cols(region(), 2);
187
    let (l, r) = (rows(halves[0], 2), rows(halves[1], 2));
188
    let from = vec![l[0], r[1], r[0], l[1]];
189
    let left3 = rows(halves[0], 3);
190
    let to = vec![left3[0], left3[1], left3[2], halves[1]];
191
    let p = plan(&from, &to, &c);
192
    assert_clean(&p, &c);
193
    assert!(matches!(strategy(&p), Strategy::Grid { .. }));
194
}
195
196
#[test]
197
fn distant_groups_plan_independently() {
198
    let c = cfg(Style::Orthogonal);
199
    let thirds = cols(region(), 3);
200
    let l = rows(thirds[0], 2);
201
    let from = vec![l[0], l[1], thirds[1], thirds[2]];
202
    let to = vec![l[1], l[0], thirds[1], thirds[2]];
203
    let p = plan(&from, &to, &c);
204
    assert_clean(&p, &c);
205
    assert!(
206
        p.groups
207
            .iter()
208
            .any(|g| g.members == [3] && g.strategy == Strategy::Unchanged)
209
    );
210
}
211
212
struct Rng(u64);
213
214
impl Rng {
215
    fn next(&mut self) -> u64 {
216
        self.0 ^= self.0 << 13;
217
        self.0 ^= self.0 >> 7;
218
        self.0 ^= self.0 << 17;
219
        self.0
220
    }
221
222
    fn below(&mut self, n: usize) -> usize {
223
        (self.next() % n as u64) as usize
224
    }
225
226
    fn unit(&mut self) -> f64 {
227
        (self.next() >> 11) as f64 / (1u64 << 53) as f64
228
    }
229
}
230
231
fn random_layout(rng: &mut Rng, n: usize) -> Vec<Rect> {
232
    let mut ids: Vec<usize> = (0..n).collect();
233
    for i in (1..n).rev() {
234
        ids.swap(i, rng.below(i + 1));
235
    }
236
    let mut out = vec![Rect::default(); n];
237
    let horizontal = rng.below(2) == 0;
238
    build(rng, &ids, region(), horizontal, &mut out);
239
    out
240
}
241
242
fn build(rng: &mut Rng, ids: &[usize], rect: Rect, horizontal: bool, out: &mut [Rect]) {
243
    if let [only] = ids {
244
        out[*only] = rect;
245
        return;
246
    }
247
    let k = ids.len().min(2 + rng.below(2));
248
    let mut cuts = Vec::with_capacity(k);
249
    let mut rest = ids;
250
    for i in 0..k {
251
        let take = if i == k - 1 {
252
            rest.len()
253
        } else {
254
            1 + rng.below(rest.len() - (k - 1 - i))
255
        };
256
        let (head, tail) = rest.split_at(take);
257
        cuts.push(head);
258
        rest = tail;
259
    }
260
    let ratios: Vec<f64> = cuts.iter().map(|_| 0.5 + rng.unit()).collect();
261
    for (part, ids) in split(rect, &ratios, horizontal).into_iter().zip(cuts) {
262
        build(rng, ids, part, !horizontal, out);
263
    }
264
}
265
266
proptest! {
267
    #![proptest_config(ProptestConfig::with_cases(512))]
268
269
    #[test]
270
    fn random_transitions_plan_cleanly(seed in 1u64.., n in 2usize..9, direct: bool) {
271
        let mut rng = Rng(seed);
272
        let c = cfg(if direct { Style::Direct } else { Style::Orthogonal });
273
        let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
274
        let p = plan(&a, &b, &c);
275
        assert_clean(&p, &c);
276
        prop_assert_eq!(&p.keyframes[0], &a);
277
        prop_assert_eq!(p.keyframes.last().unwrap(), &b);
278
    }
279
280
    #[test]
281
    fn planning_back_rewinds(seed in 1u64.., n in 2usize..9) {
282
        let mut rng = Rng(seed);
283
        let c = cfg(Style::Orthogonal);
284
        let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
285
        let mut back = plan_canonical(&b, &a, &c).keys;
286
        back.reverse();
287
        prop_assert_eq!(plan_canonical(&a, &b, &c).keys, back);
288
    }
289
290
    #[test]
291
    fn interrupts_replan_cleanly(seed in 1u64.., n in 2usize..9, at in 0.0f64..1.0, s in 0.0f64..1.0) {
292
        let mut rng = Rng(seed);
293
        let c = cfg(Style::Orthogonal);
294
        let (a, b, d) = (random_layout(&mut rng, n), random_layout(&mut rng, n), random_layout(&mut rng, n));
295
        let first = plan(&a, &b, &c);
296
        if first.phases() == 0 {
297
            return Ok(());
298
        }
299
        let phase = ((at * first.phases() as f64) as usize).min(first.phases() - 1);
300
        let mid = first.sample(phase, s);
301
        let second = plan(&mid, &d, &c);
302
        assert_clean(&second, &c);
303
        prop_assert_eq!(&second.keyframes[0], &mid);
304
    }
305
}
306
307
#[test]
308
#[ignore = "statistics, run with --ignored --nocapture"]
309
fn strategy_distribution() {
310
    use std::collections::BTreeMap;
311
    for style in [Style::Orthogonal, Style::Direct] {
312
        let c = cfg(style);
313
        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
314
        let mut counts: BTreeMap<String, usize> = BTreeMap::new();
315
        let mut holes = 0;
316
        for _ in 0..20_000 {
317
            let n = 2 + rng.below(7);
318
            let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
319
            let p = plan(&a, &b, &c);
320
            assert_clean(&p, &c);
321
            holes += p.groups.iter().filter(|g| g.opens_hole).count();
322
            let key = match strategy(&p) {
323
                Strategy::Lanes { .. } => "Lanes".to_string(),
324
                Strategy::Grid { .. } => "Grid".to_string(),
325
                s => format!("{s:?}"),
326
            };
327
            *counts.entry(key).or_default() += 1;
328
        }
329
        println!("{style:?}: {counts:?}, plans opening a hole: {holes}");
330
    }
331
}