atagen

monolith

public
Star0
Watch0
39d2db731e01b9bbb3abf16321ba589ccce85009
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/planner/src/tests.rsRust268 lines8.0 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 interrupts_replan_cleanly(seed in 1u64.., n in 2usize..9, at in 0.0f64..1.0, s in 0.0f64..1.0) {
240
        let mut rng = Rng(seed);
241
        let c = cfg(Style::Orthogonal);
242
        let (a, b, d) = (random_layout(&mut rng, n), random_layout(&mut rng, n), random_layout(&mut rng, n));
243
        let first = plan(&a, &b, &c);
244
        if first.phases() == 0 {
245
            return Ok(());
246
        }
247
        let phase = ((at * first.phases() as f64) as usize).min(first.phases() - 1);
248
        let mid = first.sample(phase, s);
249
        let second = plan(&mid, &d, &c);
250
        assert_clean(&second, &c);
251
        prop_assert_eq!(&second.keyframes[0], &mid);
252
    }
253
}
254
255
/// A hidden window is a zero-size seed. One sitting inside a visible window
256
/// used to join its group and make every strategy fail, so the plan pinched.
257
#[test]
258
fn hidden_seeds_stay_out_of_the_plan() {
259
    let c = cfg(Style::Orthogonal);
260
    let a = cols(region(), 2);
261
    let seed = |r: Rect| Rect::new(r.center_x(), r.center_y(), 0.0, 0.0);
262
    let from = vec![a[0], a[1], seed(a[0])];
263
    let to = vec![a[1], a[0], seed(a[1])];
264
    let p = plan(&from, &to, &c);
265
    assert_clean(&p, &c);
266
    assert!(p.groups.iter().all(|g| !g.members.contains(&2)));
267
    assert!(p.keyframes.iter().all(|k| k[2] == to[2]));
268
}