atagen

monolith

public
Star0
Watch0
846bdd33d84f36ceb41b97f3260598f55ef5b864
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/planner/src/tests.rsRust304 lines9.6 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 full_reversal_needs_one_lane_per_window() {
67
    let c = cfg(Style::Orthogonal);
68
    let a = cols(region(), 4);
69
    let b: Vec<Rect> = (0..4).map(|i| a[3 - i]).collect();
70
    let p = plan(&a, &b, &c);
71
    assert_clean(&p, &c);
72
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 4 });
73
}
74
75
#[test]
76
fn growing_both_axes_depends_on_style() {
77
    let grid = |wide: bool| {
78
        let r = if wide { [7.0, 3.0] } else { [1.0, 1.0] };
79
        let rs = split(region(), &r, false);
80
        let mut out = split(rs[0], &r, true);
81
        out.extend(split(rs[1], &[1.0, 1.0], true));
82
        out
83
    };
84
    let o = cfg(Style::Orthogonal);
85
    let d = cfg(Style::Direct);
86
    let po = plan(&grid(false), &grid(true), &o);
87
    let pd = plan(&grid(false), &grid(true), &d);
88
    assert_clean(&po, &o);
89
    assert_clean(&pd, &d);
90
    assert_eq!((po.phases(), pd.phases()), (2, 1));
91
}
92
93
#[test]
94
fn insert_and_split_rewinds() {
95
    let c = cfg(Style::Orthogonal);
96
    let layout = |on: bool| {
97
        let n = if on { 4 } else { 3 };
98
        let ratios: Vec<f64> = (0..n)
99
            .map(|i| if i == 0 && on { 2.0 } else { 1.0 })
100
            .collect();
101
        let cs = split(region(), &ratios, true);
102
        let (a, mut b, c3) = (cs[0], cs[1], cs[n - 1]);
103
        let d = if on {
104
            cs[2]
105
        } else {
106
            Rect::new((b.right() + c3.x) / 2.0, b.y, 0.0, b.h)
107
        };
108
        let e = if on {
109
            let s = rows(b, 2);
110
            b = s[0];
111
            s[1]
112
        } else {
113
            Rect::new(b.x, b.bottom(), b.w, 0.0)
114
        };
115
        vec![a, b, c3, d, e]
116
    };
117
    let on = plan(&layout(false), &layout(true), &c);
118
    let off = plan(&layout(true), &layout(false), &c);
119
    assert_clean(&on, &c);
120
    assert_clean(&off, &c);
121
    assert!(on.groups.iter().all(|g| !g.opens_hole));
122
    let mut rewound = off.keyframes.clone();
123
    rewound.reverse();
124
    assert_eq!(on.keyframes, rewound);
125
}
126
127
#[test]
128
fn tag_rows_swap_rigidly_with_two_lanes() {
129
    let c = cfg(Style::Orthogonal);
130
    let layout = |first_top: bool| {
131
        let rs = rows(region(), 2);
132
        let (t1, t2) = if first_top {
133
            (rs[0], rs[1])
134
        } else {
135
            (rs[1], rs[0])
136
        };
137
        let mut out = cols(t1, 3);
138
        out.extend(cols(t2, 2));
139
        out
140
    };
141
    let blocks = vec![vec![0, 1, 2], vec![3, 4]];
142
    let p = plan_blocks(&layout(true), &layout(false), &blocks, &c);
143
    assert_clean(&p, &c);
144
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 2 });
145
}
146
147
#[test]
148
fn simultaneous_reversals_on_both_axes_use_the_grid() {
149
    let c = cfg(Style::Orthogonal);
150
    let halves = cols(region(), 2);
151
    let (l, r) = (rows(halves[0], 2), rows(halves[1], 2));
152
    let from = vec![l[0], r[1], r[0], l[1]];
153
    let left3 = rows(halves[0], 3);
154
    let to = vec![left3[0], left3[1], left3[2], halves[1]];
155
    let p = plan(&from, &to, &c);
156
    assert_clean(&p, &c);
157
    assert!(matches!(strategy(&p), Strategy::Grid { .. }));
158
}
159
160
struct Rng(u64);
161
162
impl Rng {
163
    fn next(&mut self) -> u64 {
164
        self.0 ^= self.0 << 13;
165
        self.0 ^= self.0 >> 7;
166
        self.0 ^= self.0 << 17;
167
        self.0
168
    }
169
170
    fn below(&mut self, n: usize) -> usize {
171
        (self.next() % n as u64) as usize
172
    }
173
174
    fn unit(&mut self) -> f64 {
175
        (self.next() >> 11) as f64 / (1u64 << 53) as f64
176
    }
177
}
178
179
fn random_layout(rng: &mut Rng, n: usize) -> Vec<Rect> {
180
    let mut ids: Vec<usize> = (0..n).collect();
181
    for i in (1..n).rev() {
182
        ids.swap(i, rng.below(i + 1));
183
    }
184
    let mut out = vec![Rect::default(); n];
185
    let horizontal = rng.below(2) == 0;
186
    build(rng, &ids, region(), horizontal, &mut out);
187
    out
188
}
189
190
fn build(rng: &mut Rng, ids: &[usize], rect: Rect, horizontal: bool, out: &mut [Rect]) {
191
    if let [only] = ids {
192
        out[*only] = rect;
193
        return;
194
    }
195
    let k = ids.len().min(2 + rng.below(2));
196
    let mut cuts = Vec::with_capacity(k);
197
    let mut rest = ids;
198
    for i in 0..k {
199
        let take = if i == k - 1 {
200
            rest.len()
201
        } else {
202
            1 + rng.below(rest.len() - (k - 1 - i))
203
        };
204
        let (head, tail) = rest.split_at(take);
205
        cuts.push(head);
206
        rest = tail;
207
    }
208
    let ratios: Vec<f64> = cuts.iter().map(|_| 0.5 + rng.unit()).collect();
209
    for (part, ids) in split(rect, &ratios, horizontal).into_iter().zip(cuts) {
210
        build(rng, ids, part, !horizontal, out);
211
    }
212
}
213
214
proptest! {
215
    #![proptest_config(ProptestConfig::with_cases(512))]
216
217
    #[test]
218
    fn random_transitions_plan_cleanly(seed in 1u64.., n in 2usize..9, direct: bool) {
219
        let mut rng = Rng(seed);
220
        let c = cfg(if direct { Style::Direct } else { Style::Orthogonal });
221
        let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
222
        let p = plan(&a, &b, &c);
223
        assert_clean(&p, &c);
224
        prop_assert_eq!(&p.keyframes[0], &a);
225
        prop_assert_eq!(p.keyframes.last().unwrap(), &b);
226
    }
227
228
    #[test]
229
    fn planning_back_rewinds(seed in 1u64.., n in 2usize..9) {
230
        let mut rng = Rng(seed);
231
        let c = cfg(Style::Orthogonal);
232
        let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
233
        let mut back = plan_canonical(&b, &a, &c).keys;
234
        back.reverse();
235
        prop_assert_eq!(plan_canonical(&a, &b, &c).keys, back);
236
    }
237
238
    #[test]
239
    fn closing_while_spawning_never_pinches(seed in 1u64.., n in 2usize..9, edge in 0usize..3, direct: bool) {
240
        let mut rng = Rng(seed);
241
        let c = cfg(if direct { Style::Direct } else { Style::Orthogonal });
242
        let a = random_layout(&mut rng, n);
243
        let v = rng.below(n);
244
        let at = [a[v].x - G / 2.0, a[v].center_x(), a[v].right() + G / 2.0][edge];
245
        let line = Rect::new(at, a[v].y, 0.0, a[v].h);
246
        let mut from = a.clone();
247
        from.push(line);
248
        let mut to = a.clone();
249
        to[v] = Rect::new(a[v].center_x(), a[v].y, 0.0, a[v].h);
250
        to.push(a[v]);
251
        let p = plan(&from, &to, &c);
252
        prop_assert!(validate(&p, &c).is_empty(), "{:?}", p);
253
        prop_assert!(p.groups.iter().all(|g| g.strategy != Strategy::Pinch), "{:?}", p);
254
    }
255
256
    #[test]
257
    fn interrupts_replan_cleanly(seed in 1u64.., n in 2usize..9, at in 0.0f64..1.0, s in 0.0f64..1.0) {
258
        let mut rng = Rng(seed);
259
        let c = cfg(Style::Orthogonal);
260
        let (a, b, d) = (random_layout(&mut rng, n), random_layout(&mut rng, n), random_layout(&mut rng, n));
261
        let first = plan(&a, &b, &c);
262
        if first.phases() == 0 {
263
            return Ok(());
264
        }
265
        let phase = ((at * first.phases() as f64) as usize).min(first.phases() - 1);
266
        let mid = first.sample(phase, s);
267
        let second = plan(&mid, &d, &c);
268
        assert_clean(&second, &c);
269
        prop_assert_eq!(&second.keyframes[0], &mid);
270
    }
271
}
272
273
/// A hidden window is a zero-size seed. One sitting inside a visible window
274
/// used to join its group and make every strategy fail, so the plan pinched.
275
#[test]
276
fn hidden_seeds_stay_out_of_the_plan() {
277
    let c = cfg(Style::Orthogonal);
278
    let a = cols(region(), 2);
279
    let seed = |r: Rect| Rect::new(r.center_x(), r.center_y(), 0.0, 0.0);
280
    let from = vec![a[0], a[1], seed(a[0])];
281
    let to = vec![a[1], a[0], seed(a[1])];
282
    let p = plan(&from, &to, &c);
283
    assert_clean(&p, &c);
284
    assert!(p.groups.iter().all(|g| !g.members.contains(&2)));
285
    assert!(p.keyframes.iter().all(|k| k[2] == to[2]));
286
}
287
288
/// A window closes in the slot where another opens. The ghost shrinks to a
289
/// seed inside its own full rect while the newcomer grows from a seed on the
290
/// same line, so no single set of phases keeps them apart and every strategy
291
/// used to fail until the plan pinched the whole output.
292
#[test]
293
fn closing_and_spawning_in_one_slot_plan_in_stages() {
294
    let c = cfg(Style::Orthogonal);
295
    let t = cols(region(), 3);
296
    let seed = Rect::new(600.0, 10.0, 0.0, 680.0);
297
    let from = vec![t[0], t[1], t[2], seed];
298
    let to = vec![t[0], seed, t[2], t[1]];
299
    let p = plan(&from, &to, &c);
300
    assert!(validate(&p, &c).is_empty(), "{p:?}");
301
    assert!(p.groups.iter().all(|g| g.strategy != Strategy::Pinch));
302
    assert_eq!(p.phases(), 2);
303
    assert_eq!(p.keyframes[1], vec![t[0], seed, t[2], seed]);
304
}