atagen

monolith

public
Star0
Watch0
d2817bc6c3eb4960441dad08847fa7b055105cf6
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/layout/src/tests.rsRust472 lines15.7 KB
1
use super::*;
2
use proptest::prelude::*;
3
4
const AREA: Rect = Rect::new(0.0, 0.0, 1000.0, 600.0);
5
6
#[derive(Clone, Debug)]
7
enum Op {
8
    Add(u8, bool),
9
    Remove(u8),
10
    Focus(u8),
11
    Move(Dir),
12
    Block(Dir),
13
    Merge(Dir),
14
    View(u8),
15
    ToggleView(u8),
16
    Retag(u8, u8),
17
    Float(u8),
18
    Expand(u8),
19
    Flip,
20
    Resize(bool, i8),
21
    Scratch(u8),
22
    Summon,
23
    Unplug,
24
    Replug,
25
}
26
27
fn dir() -> impl Strategy<Value = Dir> {
28
    prop_oneof![
29
        Just(Dir::Left),
30
        Just(Dir::Right),
31
        Just(Dir::Up),
32
        Just(Dir::Down)
33
    ]
34
}
35
36
fn op() -> impl Strategy<Value = Op> {
37
    prop_oneof![
38
        4 => (0u8..4, prop::bool::weighted(0.15)).prop_map(|(t, f)| Op::Add(t, f)),
39
        1 => any::<u8>().prop_map(Op::Remove),
40
        3 => any::<u8>().prop_map(Op::Focus),
41
        4 => dir().prop_map(Op::Move),
42
        2 => dir().prop_map(Op::Block),
43
        3 => dir().prop_map(Op::Merge),
44
        1 => (0u8..4).prop_map(Op::View),
45
        2 => (0u8..4).prop_map(Op::ToggleView),
46
        1 => (any::<u8>(), 0u8..4).prop_map(|(w, t)| Op::Retag(w, t)),
47
        1 => any::<u8>().prop_map(Op::Float),
48
        1 => (0u8..3).prop_map(Op::Expand),
49
        1 => Just(Op::Flip),
50
        1 => (any::<bool>(), any::<i8>()).prop_map(|(a, d)| Op::Resize(a, d)),
51
        1 => any::<u8>().prop_map(Op::Scratch),
52
        1 => Just(Op::Summon),
53
        1 => Just(Op::Unplug),
54
        1 => Just(Op::Replug),
55
    ]
56
}
57
58
fn nth(l: &Layout, i: u8) -> Option<WindowId> {
59
    let ids: Vec<WindowId> = l.windows().map(|(id, _)| id).collect();
60
    (!ids.is_empty()).then(|| ids[i as usize % ids.len()])
61
}
62
63
fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {
64
    let active = l.active_output();
65
    match *op {
66
        Op::Add(t, f) => {
67
            let w = l.add_window(
68
                None,
69
                Some(tags[t as usize]),
70
                f.then(|| Rect::new(10.0, 10.0, 200.0, 100.0)),
71
            );
72
            if l.visible(w) {
73
                l.focus(w);
74
            }
75
        }
76
        Op::Remove(i) => nth(l, i).into_iter().for_each(|w| l.remove_window(w)),
77
        Op::Focus(i) => {
78
            if let Some(w) = nth(l, i).filter(|&w| l.visible(w)) {
79
                l.focus(w);
80
            }
81
        }
82
        Op::Move(d) => {
83
            l.move_window(d);
84
        }
85
        Op::Block(d) => {
86
            l.move_container(d);
87
        }
88
        Op::Merge(d) => {
89
            l.merge_window(d);
90
        }
91
        Op::View(t) => active.into_iter().for_each(|o| l.view(o, tags[t as usize])),
92
        Op::ToggleView(t) => active
93
            .into_iter()
94
            .for_each(|o| l.toggle_view(o, tags[t as usize])),
95
        Op::Retag(i, t) => nth(l, i)
96
            .into_iter()
97
            .for_each(|w| l.set_tag(w, tags[t as usize])),
98
        Op::Float(i) => nth(l, i).into_iter().for_each(|w| l.toggle_floating(w)),
99
        Op::Expand(s) => {
100
            l.expand(
101
                [
102
                    ExpandScope::Window,
103
                    ExpandScope::Container,
104
                    ExpandScope::All,
105
                ][s as usize],
106
            );
107
        }
108
        Op::Flip => active.into_iter().for_each(|o| l.flip(o)),
109
        Op::Resize(h, d) => {
110
            let axis = if h { Axis::Horizontal } else { Axis::Vertical };
111
            l.resize(axis, f64::from(d) / 200.0);
112
        }
113
        Op::Scratch(i) => nth(l, i).into_iter().for_each(|w| l.scratchpad_add(w, "s")),
114
        Op::Summon => {
115
            l.summon("s");
116
        }
117
        Op::Unplug => {
118
            let b = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o);
119
            if let Some(o) = b {
120
                l.remove_output(o);
121
            }
122
        }
123
        Op::Replug => {
124
            if !l.outputs().any(|(_, out)| out.name == "b") {
125
                l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
126
            }
127
        }
128
    }
129
}
130
131
/// The area a tiled window gets, zero for floating or unplaced ones.
132
fn tiled_area(l: &Layout, w: WindowId) -> f64 {
133
    let Some(o) = l.window(w).and_then(|x| x.output) else {
134
        return 0.0;
135
    };
136
    l.arrange(o)
137
        .tiled
138
        .iter()
139
        .find(|p| p.window == w && p.visible)
140
        .map_or(0.0, |p| p.rect.w * p.rect.h)
141
}
142
143
fn check_geometry(l: &Layout) {
144
    for (o, out) in l.outputs() {
145
        let arr = l.arrange(o);
146
        let mut drawn: Vec<WindowId> = arr
147
            .tiled
148
            .iter()
149
            .filter(|p| p.visible)
150
            .map(|p| p.window)
151
            .chain(arr.floating.iter().map(|&(w, _)| w))
152
            .chain(arr.fullscreen)
153
            .collect();
154
        drawn.sort();
155
        drawn.dedup();
156
        let mut visible: Vec<WindowId> = l
157
            .windows()
158
            .filter(|&(w, win)| win.output == Some(o) && l.visible(w))
159
            .map(|(w, _)| w)
160
            .collect();
161
        visible.sort();
162
        assert_eq!(drawn, visible, "arrangement disagrees with visible()");
163
        let shown: Vec<Rect> = arr
164
            .tiled
165
            .iter()
166
            .filter(|p| p.visible)
167
            .map(|p| p.rect)
168
            .collect();
169
        for p in arr.tiled.iter().filter(|p| !p.visible) {
170
            assert!(p.rect.w * p.rect.h <= 1e-6, "hidden {:?} has area", p.rect);
171
        }
172
        for r in &shown {
173
            assert!(r.w >= 0.0 && r.h >= 0.0);
174
            assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6);
175
            assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6);
176
        }
177
        for (i, a) in shown.iter().enumerate() {
178
            for b in &shown[i + 1..] {
179
                let ox = a.right().min(b.right()) - a.x.max(b.x);
180
                let oy = a.bottom().min(b.bottom()) - a.y.max(b.y);
181
                assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}");
182
            }
183
        }
184
    }
185
}
186
187
proptest! {
188
    #![proptest_config(ProptestConfig::with_cases(2000))]
189
190
    #[test]
191
    fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {
192
        let mut l = Layout::default();
193
        l.add_output("a", AREA);
194
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
195
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
196
        for op in &ops {
197
            let moving = matches!(op, Op::Move(_) | Op::Merge(_) | Op::Block(_));
198
            let shown_before = l.focused().filter(|&w| tiled_area(&l, w) > 1.0);
199
            apply(&mut l, &tags, op);
200
            l.check();
201
            check_geometry(&l);
202
            if moving && let Some(w) = shown_before {
203
                prop_assert!(tiled_area(&l, w) > 1.0, "{w:?} vanished after {op:?}");
204
            }
205
        }
206
    }
207
208
    #[test]
209
    fn replugging_restores_what_unplugging_moved(ops in prop::collection::vec(op(), 1..60)) {
210
        let mut l = Layout::default();
211
        l.add_output("a", AREA);
212
        l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
213
        let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];
214
        for op in &ops {
215
            apply(&mut l, &tags, op);
216
        }
217
        let Some(b) = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o) else {
218
            return Ok(());
219
        };
220
        let before = |l: &Layout, o| {
221
            let out = l.output(o).unwrap();
222
            (out.tree.windows().collect::<Vec<_>>(), out.floating.clone(), out.viewed.clone())
223
        };
224
        let was = before(&l, b);
225
        l.remove_output(b);
226
        let b = l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
227
        l.check();
228
        prop_assert_eq!(before(&l, b), was);
229
    }
230
}
231
232
/// The hidden-slot rules from planner.html, demo 11.
233
#[test]
234
fn hidden_windows_keep_their_slots() {
235
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
236
    let mut t: Tree<u32> = Tree::default();
237
    t.insert_row(a);
238
    t.insert_after(d, a);
239
    t.insert_after(b, d);
240
    t.insert_row(c);
241
    t.insert_after(e, c);
242
    t.check();
243
    let hidden = |w: u32| w != d && w != e;
244
    let all = |_| true;
245
    let rect = |t: &Tree<u32>, w| {
246
        t.arrange(AREA, 0.0, all)
247
            .into_iter()
248
            .find(|p| p.0 == w)
249
            .unwrap()
250
            .1
251
    };
252
253
    assert!(t.move_window(a, Dir::Right, hidden));
254
    assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);
255
256
    assert!(t.move_window(c, Dir::Up, hidden));
257
    t.check();
258
    assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");
259
}
260
261
/// Moving out of a row toward a lane of hidden windows joins that lane, so
262
/// they share it when shown again instead of a new lane opening between.
263
#[test]
264
fn moving_toward_a_hidden_lane_joins_it() {
265
    let (a, b, c, d, e) = (1, 2, 3, 4, 5);
266
    let mut t: Tree<u32> = Tree::default();
267
    t.insert_row(a);
268
    t.insert_after(b, a);
269
    t.insert_after(c, b);
270
    t.insert_row(d);
271
    t.insert_after(e, c);
272
    let tag_two = |w: u32| w != a && w != d;
273
    assert!(t.move_window(e, Dir::Down, tag_two));
274
    t.check();
275
    let all = t.arrange(AREA, 0.0, |_| true);
276
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
277
    assert_eq!(rect(e).y, rect(d).y);
278
    assert!(rect(d).x < rect(e).x);
279
    assert!(rect(a).y < rect(d).y);
280
}
281
282
#[test]
283
fn merging_steps_out_of_a_stack_before_joining_another() {
284
    let (a, b, c) = (1, 2, 3);
285
    let stacked = || {
286
        let mut t: Tree<u32> = Tree::default();
287
        t.insert_row(a);
288
        t.insert_after(b, a);
289
        t.insert_after(c, b);
290
        assert!(t.merge_window(b, Dir::Left, |_| true));
291
        t
292
    };
293
    let rects = |t: &Tree<u32>| {
294
        t.check();
295
        let all = t.arrange(AREA, 0.0, |_| true);
296
        move |w| all.iter().find(|p: &&(u32, Rect)| p.0 == w).unwrap().1
297
    };
298
299
    let mut t = stacked();
300
    assert!(t.merge_window(a, Dir::Right, |_| true));
301
    let rect = rects(&t);
302
    assert!(rect(b).x < rect(a).x && rect(a).x < rect(c).x);
303
    assert_eq!(rect(a).h, AREA.h);
304
    assert!(t.merge_window(a, Dir::Right, |_| true));
305
    let rect = rects(&t);
306
    assert_eq!(rect(a).x, rect(c).x);
307
308
    let mut t = stacked();
309
    assert!(t.merge_window(a, Dir::Up, |_| true));
310
    let rect = rects(&t);
311
    assert_eq!(rect(a).w, AREA.w);
312
    assert!(rect(a).bottom() <= rect(b).y && rect(b).y == rect(c).y);
313
}
314
315
/// A window alone on screen in a container of hidden ones expands among the
316
/// windows it is shown beside, not inside the container nobody can see.
317
#[test]
318
fn expand_skips_containers_of_hidden_windows() {
319
    let (a, b, c) = (1, 2, 3);
320
    let mut t: Tree<u32> = Tree::default();
321
    t.insert_row(a);
322
    t.insert_after(b, a);
323
    t.insert_after(c, b);
324
    assert!(t.merge_window(a, Dir::Right, |_| true));
325
    let shown = |w: u32| w != a;
326
    assert!(t.toggle_expand(b, 0, 1, shown));
327
    assert!(t.maximized(b, shown));
328
    let all = t.arrange(AREA, 0.0, shown);
329
    let rect = |w| all.iter().find(|p| p.0 == w).unwrap().1;
330
    assert_eq!(rect(b).w, AREA.w);
331
}
332
333
/// Going in and out of a stack repeatedly leaves every window the size it was.
334
#[test]
335
fn stacking_in_and_out_does_not_drift() {
336
    for out in [Dir::Up, Dir::Down, Dir::Right] {
337
        let (a, b, c) = (1, 2, 3);
338
        let mut t: Tree<u32> = Tree::default();
339
        t.insert_row(a);
340
        t.insert_after(b, a);
341
        t.insert_after(c, b);
342
        assert!(t.merge_window(c, Dir::Left, |_| true));
343
        assert!(t.merge_window(c, out, |_| true));
344
        let once = t.arrange(AREA, 0.0, |_| true);
345
        for _ in 0..8 {
346
            t.merge_window(c, Dir::Left, |_| true);
347
            t.merge_window(c, out, |_| true);
348
        }
349
        t.check();
350
        assert_eq!(t.arrange(AREA, 0.0, |_| true), once, "stepping out {out:?}");
351
    }
352
}
353
354
/// Windows of other tags in the rows around a window must not change what
355
/// the user sees as it moves through them, nor drift over round trips.
356
#[test]
357
fn moving_past_hidden_windows_does_not_drift() {
358
    let (a, b, h, h2) = (1, 2, 3, 4);
359
    let mut t: Tree<u32> = Tree::default();
360
    t.insert_row(h);
361
    t.insert_row(a);
362
    t.insert_after(h2, a);
363
    t.insert_after(b, h2);
364
    let vis = |w: u32| w != h && w != h2;
365
    let mut once = None;
366
    for _ in 0..4 {
367
        for d in [Dir::Up, Dir::Up, Dir::Down] {
368
            t.move_window(b, d, vis);
369
            let shown = t.arrange(AREA, 0.0, vis);
370
            let r = shown.iter().find(|p| p.0 == b).unwrap().1;
371
            assert!(
372
                (r.w * r.h - AREA.w * AREA.h / 2.0).abs() < 1e-6,
373
                "{d:?} left {r:?}"
374
            );
375
        }
376
        t.check();
377
        let all = t.arrange(AREA, 0.0, |_| true);
378
        assert_eq!(*once.get_or_insert_with(|| all.clone()), all);
379
    }
380
}
381
382
/// Monitors sleeping together unplug one after another, so windows already
383
/// moved off the first must still go home to it, not to the second.
384
#[test]
385
fn windows_return_to_where_they_were_first_evicted_from() {
386
    let mut l = Layout::default();
387
    let a = l.add_output("a", AREA);
388
    l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
389
    let c = l.add_output("c", Rect::new(1800.0, 0.0, 800.0, 600.0));
390
    let w = l.add_window(Some(a), None, None);
391
    l.remove_output(a);
392
    let b = l.outputs().find(|(_, out)| out.name == "b").unwrap().0;
393
    l.remove_output(b);
394
    assert_eq!(l.window(w).unwrap().output, Some(c));
395
    l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));
396
    let a = l.add_output("a", AREA);
397
    l.check();
398
    assert_eq!(l.window(w).unwrap().output, Some(a));
399
    assert!(l.output(a).unwrap().tree.contains(w));
400
}
401
402
/// A stack showing one window looks like a lone window, so moving toward it
403
/// swaps places instead of joining a stack the user cannot see.
404
#[test]
405
fn moving_toward_a_stack_showing_one_window_swaps() {
406
    let (a, b, h) = (1, 2, 3);
407
    let mut t: Tree<u32> = Tree::default();
408
    t.insert_row(a);
409
    t.insert_after(b, a);
410
    t.insert_after(h, b);
411
    assert!(t.merge_window(h, Dir::Left, |_| true));
412
    let vis = |w: u32| w != h;
413
    assert!(t.move_window(a, Dir::Right, vis));
414
    t.check();
415
    let shown = t.arrange(AREA, 0.0, vis);
416
    let rect = |w| shown.iter().find(|p| p.0 == w).unwrap().1;
417
    assert!(rect(b).x < rect(a).x);
418
    assert_eq!(rect(a).h, AREA.h);
419
}
420
421
/// A move either changes what is on screen or changes nothing at all, and a
422
/// window move never rearranges hidden windows among themselves. Steps around
423
/// hidden windows used to succeed invisibly and eat the keypress.
424
#[test]
425
fn moves_are_seen_or_undone() {
426
    let dirs = [Dir::Left, Dir::Right, Dir::Up, Dir::Down];
427
    let mut runner = proptest::test_runner::TestRunner::deterministic();
428
    let ops = prop::collection::vec((0u8..4, 0u8..4, 0u8..4, 0u8..32), 1..14);
429
    for _ in 0..2000 {
430
        let seq = proptest::strategy::ValueTree::current(&ops.new_tree(&mut runner).unwrap());
431
        let mut t: Tree<u32> = Tree::default();
432
        t.insert_row(0);
433
        let mut n = 1;
434
        for &(w, d, kind, hide) in &seq {
435
            let vis = |x: u32| hide & (1 << x) == 0;
436
            if kind == 0 && n < 5 {
437
                t.insert_after(n, u32::from(w) % n);
438
                n += 1;
439
                continue;
440
            }
441
            let w = u32::from(w) % n;
442
            let shown = |t: &Tree<u32>| {
443
                let mut v: Vec<_> = t
444
                    .arrange(AREA, 0.0, vis)
445
                    .into_iter()
446
                    .filter(|p| vis(p.0))
447
                    .collect();
448
                v.sort_by_key(|p| p.0);
449
                v
450
            };
451
            let before = shown(&t);
452
            let hidden = |t: &Tree<u32>| {
453
                let mut t = t.clone();
454
                t.retain(|x| !vis(x));
455
                t.shape()
456
            };
457
            let rest = hidden(&t);
458
            let d = dirs[usize::from(d)];
459
            let moved = match kind {
460
                1 => t.merge_window(w, d, vis),
461
                2 => t.move_window(w, d, vis),
462
                3 => t.move_container(w, d, vis),
463
                _ => continue,
464
            };
465
            t.check();
466
            assert_eq!(moved, shown(&t) != before, "{seq:?}");
467
            if kind != 3 {
468
                assert_eq!(hidden(&t), rest, "{seq:?}");
469
            }
470
        }
471
    }
472
}