Skip to content

Commit b1361dc

Browse files
Merge pull request #85 from RtlZeroMemory/integration-tests
test(core): add deterministic integration, resize/reflow, and stress-fuzz expansion
2 parents 0a6552f + fe5faf7 commit b1361dc

9 files changed

Lines changed: 6220 additions & 0 deletions

packages/core/src/__tests__/integration/integration.dashboard.test.ts

Lines changed: 965 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/__tests__/integration/integration.file-manager.test.ts

Lines changed: 1052 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/__tests__/integration/integration.form-editor.test.ts

Lines changed: 1018 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/__tests__/integration/integration.reflow.test.ts

Lines changed: 712 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/__tests__/integration/integration.resize.test.ts

Lines changed: 551 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/__tests__/stress/fuzz.random-events.test.ts

Lines changed: 705 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { assert, createRng, describe, test } from "@rezi-ui/testkit";
2+
import { type VNode, createDrawlistBuilderV1 } from "../../index.js";
3+
import { layout } from "../../layout/layout.js";
4+
import { renderToDrawlist } from "../../renderer/renderToDrawlist.js";
5+
import { commitVNodeTree } from "../../runtime/commit.js";
6+
import { createInstanceIdAllocator } from "../../runtime/instance.js";
7+
import { ui } from "../../widgets/ui.js";
8+
9+
const ITERATIONS = 1024;
10+
const AXES = ["row", "column"] as const;
11+
const BOX_BORDERS = [
12+
"none",
13+
"single",
14+
"double",
15+
"rounded",
16+
"heavy",
17+
"dashed",
18+
"heavy-dashed",
19+
] as const;
20+
21+
type Axis = (typeof AXES)[number];
22+
type Rng = ReturnType<typeof createRng>;
23+
type TreeProfile = Readonly<{
24+
minNodes: number;
25+
maxNodes: number;
26+
maxDepth: number;
27+
maxChildren: number;
28+
leafChance: number;
29+
boxChance: number;
30+
viewportCols: readonly [number, number];
31+
viewportRows: readonly [number, number];
32+
rootAxis: Axis | "random";
33+
}>;
34+
35+
function hexSeed(seed: number): string {
36+
return `0x${(seed >>> 0).toString(16).padStart(8, "0")}`;
37+
}
38+
39+
function failCtx(seed: number, iter: number): string {
40+
return `seed=${hexSeed(seed)} iter=${String(iter)}`;
41+
}
42+
43+
function describeErr(err: unknown): string {
44+
return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
45+
}
46+
47+
function randomInt(rng: Rng, min: number, max: number): number {
48+
return min + (rng.u32() % (max - min + 1));
49+
}
50+
51+
function chance(rng: Rng, percent: number): boolean {
52+
return rng.u32() % 100 < percent;
53+
}
54+
55+
function pick<T>(rng: Rng, values: readonly T[]): T {
56+
return values[rng.u32() % values.length] as T;
57+
}
58+
59+
function randomText(rng: Rng): string {
60+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789 -_:/";
61+
const len = 1 + (rng.u32() % 24);
62+
let out = "";
63+
for (let i = 0; i < len; i++) {
64+
out += alphabet[rng.u32() % alphabet.length] ?? "x";
65+
}
66+
return out;
67+
}
68+
69+
function randomLeaf(rng: Rng): VNode {
70+
if ((rng.u32() & 3) !== 0) {
71+
return ui.text(randomText(rng));
72+
}
73+
74+
const useFlex = chance(rng, 50);
75+
return ui.spacer({
76+
size: 1 + (rng.u32() % 4),
77+
...(useFlex ? { flex: 1 + (rng.u32() % 3) } : {}),
78+
});
79+
}
80+
81+
function randomStackProps(rng: Rng): Readonly<{
82+
gap?: number;
83+
p?: number;
84+
wrap?: boolean;
85+
}> {
86+
return {
87+
...(chance(rng, 65) ? { gap: rng.u32() % 3 } : {}),
88+
...(chance(rng, 35) ? { p: rng.u32() % 3 } : {}),
89+
...(chance(rng, 15) ? { wrap: chance(rng, 50) } : {}),
90+
};
91+
}
92+
93+
function randomBoxProps(rng: Rng): Readonly<{
94+
border?: (typeof BOX_BORDERS)[number];
95+
p?: number;
96+
title?: string;
97+
titleAlign?: "left" | "center" | "right";
98+
}> {
99+
const withTitle = chance(rng, 20);
100+
return {
101+
...(chance(rng, 85) ? { border: pick(rng, BOX_BORDERS) } : {}),
102+
...(chance(rng, 40) ? { p: rng.u32() % 3 } : {}),
103+
...(withTitle ? { title: randomText(rng).slice(0, 10) } : {}),
104+
...(withTitle && chance(rng, 50)
105+
? { titleAlign: pick(rng, ["left", "center", "right"] as const) }
106+
: {}),
107+
};
108+
}
109+
110+
function randomTree(rng: Rng, profile: TreeProfile): VNode {
111+
const budget = { left: randomInt(rng, profile.minNodes, profile.maxNodes) };
112+
113+
function nextNode(depth: number): VNode {
114+
if (budget.left <= 0) return randomLeaf(rng);
115+
budget.left--;
116+
117+
const canBranch = depth < profile.maxDepth && budget.left > 0;
118+
if (!canBranch || chance(rng, profile.leafChance)) return randomLeaf(rng);
119+
120+
const maxChildren = Math.max(1, Math.min(profile.maxChildren, budget.left));
121+
const childCount = 1 + (rng.u32() % maxChildren);
122+
const children: VNode[] = [];
123+
for (let i = 0; i < childCount && budget.left > 0; i++) {
124+
children.push(nextNode(depth + 1));
125+
}
126+
if (children.length === 0) children.push(randomLeaf(rng));
127+
128+
const kindRoll = rng.u32() % 100;
129+
if (kindRoll < profile.boxChance) {
130+
return ui.box(randomBoxProps(rng), children);
131+
}
132+
if ((kindRoll & 1) === 0) {
133+
return ui.row(randomStackProps(rng), children);
134+
}
135+
return ui.column(randomStackProps(rng), children);
136+
}
137+
138+
const rootChildren = [nextNode(1), nextNode(1)];
139+
const rootKind = rng.u32() % 3;
140+
if (rootKind === 0) return ui.box(randomBoxProps(rng), rootChildren);
141+
if (rootKind === 1) return ui.row(randomStackProps(rng), rootChildren);
142+
return ui.column(randomStackProps(rng), rootChildren);
143+
}
144+
145+
function runTreeFuzz(seed: number, profile: TreeProfile): void {
146+
const rng = createRng(seed);
147+
for (let iter = 0; iter < ITERATIONS; iter++) {
148+
const axis: Axis = profile.rootAxis === "random" ? pick(rng, AXES) : profile.rootAxis;
149+
const cols = randomInt(rng, profile.viewportCols[0], profile.viewportCols[1]);
150+
const rows = randomInt(rng, profile.viewportRows[0], profile.viewportRows[1]);
151+
const vnode = randomTree(rng, profile);
152+
const ctx = failCtx(seed, iter);
153+
154+
try {
155+
const commitRes = commitVNodeTree(null, vnode, { allocator: createInstanceIdAllocator(1) });
156+
if (!commitRes.ok) {
157+
assert.fail(`commit failed (${ctx}): ${commitRes.fatal.code}: ${commitRes.fatal.detail}`);
158+
continue;
159+
}
160+
161+
const layoutRes = layout(commitRes.value.root.vnode, 0, 0, cols, rows, axis);
162+
if (!layoutRes.ok) {
163+
assert.fail(`layout failed (${ctx}): ${layoutRes.fatal.code}: ${layoutRes.fatal.detail}`);
164+
continue;
165+
}
166+
167+
const builder = createDrawlistBuilderV1();
168+
renderToDrawlist({
169+
tree: commitRes.value.root,
170+
layout: layoutRes.value,
171+
viewport: { cols, rows },
172+
focusState: Object.freeze({ focusedId: null }),
173+
builder,
174+
});
175+
176+
const built = builder.build();
177+
if (!built.ok) {
178+
assert.fail(`drawlist build failed (${ctx}): ${built.error.code}: ${built.error.detail}`);
179+
}
180+
} catch (err: unknown) {
181+
assert.fail(`pipeline threw (${ctx}): ${describeErr(err)}`);
182+
}
183+
}
184+
}
185+
186+
describe("seeded random vnode tree fuzz (commit/layout/render)", () => {
187+
test("balanced trees (1024 iters, seed 0x7a11c001)", () => {
188+
runTreeFuzz(0x7a11_c001, {
189+
minNodes: 12,
190+
maxNodes: 64,
191+
maxDepth: 6,
192+
maxChildren: 5,
193+
leafChance: 38,
194+
boxChance: 28,
195+
viewportCols: [40, 120],
196+
viewportRows: [10, 40],
197+
rootAxis: "random",
198+
});
199+
});
200+
201+
test("deep nesting bias (1024 iters, seed 0x7a11c002)", () => {
202+
runTreeFuzz(0x7a11_c002, {
203+
minNodes: 16,
204+
maxNodes: 72,
205+
maxDepth: 11,
206+
maxChildren: 3,
207+
leafChance: 24,
208+
boxChance: 36,
209+
viewportCols: [30, 100],
210+
viewportRows: [8, 32],
211+
rootAxis: "column",
212+
});
213+
});
214+
215+
test("wide sibling fanout (1024 iters, seed 0x7a11c003)", () => {
216+
runTreeFuzz(0x7a11_c003, {
217+
minNodes: 20,
218+
maxNodes: 90,
219+
maxDepth: 5,
220+
maxChildren: 8,
221+
leafChance: 44,
222+
boxChance: 20,
223+
viewportCols: [60, 160],
224+
viewportRows: [10, 36],
225+
rootAxis: "row",
226+
});
227+
});
228+
229+
test("box-heavy container mix (1024 iters, seed 0x7a11c004)", () => {
230+
runTreeFuzz(0x7a11_c004, {
231+
minNodes: 10,
232+
maxNodes: 56,
233+
maxDepth: 8,
234+
maxChildren: 4,
235+
leafChance: 34,
236+
boxChance: 58,
237+
viewportCols: [36, 110],
238+
viewportRows: [8, 30],
239+
rootAxis: "random",
240+
});
241+
});
242+
243+
test("text-dense leaves (1024 iters, seed 0x7a11c005)", () => {
244+
runTreeFuzz(0x7a11_c005, {
245+
minNodes: 14,
246+
maxNodes: 68,
247+
maxDepth: 7,
248+
maxChildren: 6,
249+
leafChance: 52,
250+
boxChance: 24,
251+
viewportCols: [48, 120],
252+
viewportRows: [12, 28],
253+
rootAxis: "column",
254+
});
255+
});
256+
257+
test("small viewport pressure (1024 iters, seed 0x7a11c006)", () => {
258+
runTreeFuzz(0x7a11_c006, {
259+
minNodes: 8,
260+
maxNodes: 48,
261+
maxDepth: 7,
262+
maxChildren: 5,
263+
leafChance: 40,
264+
boxChance: 30,
265+
viewportCols: [12, 48],
266+
viewportRows: [4, 16],
267+
rootAxis: "random",
268+
});
269+
});
270+
});

0 commit comments

Comments
 (0)