Skip to content

Commit f751bd8

Browse files
authored
Fuzz test across multiple roots (#11376)
While refactoring root scheduler, I noticed we have few tests that cover updates across multiple roots. To address, I've added multiple root cases to the fuzz tester. Includes a hard-coded test case that was failing before #11307 landed.
1 parent 5f6159f commit f751bd8

File tree

1 file changed

+200
-87
lines changed

1 file changed

+200
-87
lines changed

packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js

Lines changed: 200 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,77 @@ describe('ReactIncrementalTriangle', () => {
6262
};
6363
}
6464

65-
function TriangleSimulator() {
65+
const STOP = 'STOP';
66+
67+
function randomInteger(min, max) {
68+
min = Math.ceil(min);
69+
max = Math.floor(max);
70+
return Math.floor(Math.random() * (max - min)) + min;
71+
}
72+
73+
function formatAction(action) {
74+
switch (action.type) {
75+
case FLUSH:
76+
return `flush(${action.unitsOfWork})`;
77+
case STEP:
78+
return `step(${action.counter})`;
79+
case INTERRUPT:
80+
return 'interrupt()';
81+
case TOGGLE:
82+
return `toggle(${action.childIndex})`;
83+
case EXPIRE:
84+
return `expire(${action.ms})`;
85+
default:
86+
throw new Error('Switch statement should be exhaustive');
87+
}
88+
}
89+
90+
function formatActions(actions) {
91+
let result = 'simulate(';
92+
for (let i = 0; i < actions.length; i++) {
93+
const action = actions[i];
94+
result += formatAction(action);
95+
if (i !== actions.length - 1) {
96+
result += ', ';
97+
}
98+
}
99+
result += ')';
100+
return result;
101+
}
102+
103+
const MAX_DEPTH = 3;
104+
const TOTAL_CHILDREN = Math.pow(3, MAX_DEPTH);
105+
let TOTAL_TRIANGLES = 0;
106+
for (let i = 0; i <= MAX_DEPTH; i++) {
107+
TOTAL_TRIANGLES += Math.pow(3, i);
108+
}
109+
110+
function randomAction() {
111+
switch (randomInteger(0, 5)) {
112+
case 0:
113+
return flush(randomInteger(0, TOTAL_TRIANGLES * 1.5));
114+
case 1:
115+
return step(randomInteger(0, 10));
116+
case 2:
117+
return interrupt();
118+
case 3:
119+
return toggle(randomInteger(0, TOTAL_CHILDREN));
120+
case 4:
121+
return expire(randomInteger(0, 1500));
122+
default:
123+
throw new Error('Switch statement should be exhaustive');
124+
}
125+
}
126+
127+
function randomActions(n) {
128+
let actions = [];
129+
for (let i = 0; i < n; i++) {
130+
actions.push(randomAction());
131+
}
132+
return actions;
133+
}
134+
135+
function TriangleSimulator(rootID) {
66136
let triangles = [];
67137
let leafTriangles = [];
68138
let yieldAfterEachRender = false;
@@ -132,27 +202,35 @@ describe('ReactIncrementalTriangle', () => {
132202
}
133203
}
134204

135-
const depth = 3;
136-
137205
let keyCounter = 0;
138206
function reset(nextStep = 0) {
139207
triangles = [];
140208
leafTriangles = [];
141209
// Remounts the whole tree by changing the key
142-
ReactNoop.render(<App depth={depth} key={keyCounter++} />);
210+
if (rootID) {
211+
ReactNoop.renderToRootWithID(
212+
<App depth={MAX_DEPTH} key={keyCounter++} />,
213+
rootID,
214+
);
215+
} else {
216+
ReactNoop.render(<App depth={MAX_DEPTH} key={keyCounter++} />);
217+
}
143218
ReactNoop.flush();
144219
assertConsistentTree();
145220
return appInstance;
146221
}
147222

148223
reset();
149-
const totalChildren = leafTriangles.length;
150-
const totalTriangles = triangles.length;
151224

152225
function assertConsistentTree(activeTriangle, counter) {
153226
const activeIndex = activeTriangle ? activeTriangle.leafIndex : -1;
154227

155-
const children = ReactNoop.getChildren();
228+
const children = ReactNoop.getChildren(rootID);
229+
230+
if (children.length !== TOTAL_CHILDREN) {
231+
throw new Error('Wrong number of children.');
232+
}
233+
156234
for (let i = 0; i < children.length; i++) {
157235
let child = children[i];
158236
let num = child.prop;
@@ -183,14 +261,17 @@ describe('ReactIncrementalTriangle', () => {
183261
}
184262
}
185263

186-
function simulate(...actions) {
264+
function* simulateAndYield() {
187265
const app = reset();
188266
let expectedCounterAtEnd = app.state.counter;
189267

190268
let activeTriangle = null;
191-
for (var i = 0; i < actions.length; i++) {
269+
while (true) {
270+
var action = yield;
271+
if (action === STOP) {
272+
break;
273+
}
192274
ReactNoop.flushSync(() => {
193-
const action = actions[i];
194275
switch (action.type) {
195276
case FLUSH:
196277
ReactNoop.flushUnitsOfWork(action.unitsOfWork);
@@ -234,84 +315,83 @@ describe('ReactIncrementalTriangle', () => {
234315
assertConsistentTree(activeTriangle, expectedCounterAtEnd);
235316
}
236317

237-
return {simulate, totalChildren, totalTriangles};
238-
}
318+
function simulate(...actions) {
319+
const gen = simulateAndYield();
320+
for (let action of actions) {
321+
gen.next(action);
322+
}
323+
gen.next(STOP);
324+
}
239325

240-
it('renders the triangle demo without inconsistencies', () => {
241-
const {simulate} = TriangleSimulator();
242-
simulate(step(1));
243-
simulate(toggle(0), step(1), toggle(0));
244-
simulate(step(1), toggle(0), flush(2), step(2), toggle(0));
245-
simulate(step(1), flush(3), toggle(0), step(0));
246-
simulate(step(1), flush(3), toggle(18), step(0));
247-
simulate(step(4), flush(52), expire(1476), flush(17), step(0));
248-
simulate(interrupt(), toggle(10), step(2), expire(990), flush(46));
249-
simulate(interrupt(), step(6), step(7), toggle(6), interrupt());
250-
});
326+
return {
327+
simulateAndYield,
328+
simulate,
329+
randomAction,
330+
randomActions,
331+
};
332+
}
251333

252-
it('fuzz tester', () => {
253-
// This test is not deterministic because the inputs are randomized. It runs
254-
// a limited number of tests on every run. If it fails, it will output the
255-
// case that led to the failure. Add the failing case to the test above
334+
describe('single root', () => {
335+
// These tests are not deterministic because the inputs are randomized. It
336+
// runs a limited number of tests on every run. If it fails, it will output
337+
// the case that led to the failure. Add the failing case to the test above
256338
// to prevent future regressions.
257-
const {simulate, totalTriangles, totalChildren} = TriangleSimulator();
339+
it('hard-coded tests', () => {
340+
const {simulate} = TriangleSimulator();
341+
simulate(step(1));
342+
simulate(toggle(0), step(1), toggle(0));
343+
simulate(step(1), toggle(0), flush(2), step(2), toggle(0));
344+
simulate(step(1), flush(3), toggle(0), step(0));
345+
simulate(step(1), flush(3), toggle(18), step(0));
346+
simulate(step(4), flush(52), expire(1476), flush(17), step(0));
347+
simulate(interrupt(), toggle(10), step(2), expire(990), flush(46));
348+
simulate(interrupt(), step(6), step(7), toggle(6), interrupt());
349+
});
258350

259-
const limit = 1000;
351+
it('generative tests', () => {
352+
const {simulate} = TriangleSimulator();
260353

261-
function randomInteger(min, max) {
262-
min = Math.ceil(min);
263-
max = Math.floor(max);
264-
return Math.floor(Math.random() * (max - min)) + min;
265-
}
354+
const limit = 1000;
355+
356+
for (let i = 0; i < limit; i++) {
357+
const actions = randomActions(5);
358+
try {
359+
simulate(...actions);
360+
} catch (e) {
361+
console.error(
362+
`Triangle fuzz tester error! Copy and paste the following line into the test suite:
363+
${formatActions(actions)}
364+
`,
365+
);
366+
throw e;
367+
}
368+
}
369+
});
370+
});
266371

267-
function randomAction() {
268-
switch (randomInteger(0, 5)) {
269-
case 0:
270-
return flush(randomInteger(0, totalTriangles * 1.5));
271-
case 1:
272-
return step(randomInteger(0, 10));
273-
case 2:
274-
return interrupt();
275-
case 3:
276-
return toggle(randomInteger(0, totalChildren));
277-
case 4:
278-
return expire(randomInteger(0, 1500));
279-
default:
280-
throw new Error('Switch statement should be exhaustive');
372+
describe('multiple roots', () => {
373+
const rootIDs = ['a', 'b', 'c'];
374+
375+
function randomActionsPerRoot() {
376+
function randomRootID() {
377+
const index = randomInteger(0, rootIDs.length);
378+
return rootIDs[index];
281379
}
282-
}
283380

284-
function randomActions(n) {
285-
let actions = [];
286-
for (let i = 0; i < n; i++) {
287-
actions.push(randomAction());
381+
const actions = [];
382+
for (let i = 0; i < 10; i++) {
383+
const rootID = randomRootID();
384+
const action = randomAction();
385+
actions.push([rootID, action]);
288386
}
289387
return actions;
290388
}
291389

292-
function formatActions(actions) {
293-
let result = 'simulate(';
390+
function formatActionsPerRoot(actions) {
391+
let result = 'simulateMultipleRoots(';
294392
for (let i = 0; i < actions.length; i++) {
295-
const action = actions[i];
296-
switch (action.type) {
297-
case FLUSH:
298-
result += `flush(${action.unitsOfWork})`;
299-
break;
300-
case STEP:
301-
result += `step(${action.counter})`;
302-
break;
303-
case INTERRUPT:
304-
result += 'interrupt()';
305-
break;
306-
case TOGGLE:
307-
result += `toggle(${action.childIndex})`;
308-
break;
309-
case EXPIRE:
310-
result += `expire(${action.ms})`;
311-
break;
312-
default:
313-
throw new Error('Switch statement should be exhaustive');
314-
}
393+
const [rootID, action] = actions[i];
394+
result += `['${rootID}', ${formatAction(action)}]`;
315395
if (i !== actions.length - 1) {
316396
result += ', ';
317397
}
@@ -320,19 +400,52 @@ describe('ReactIncrementalTriangle', () => {
320400
return result;
321401
}
322402

323-
for (let i = 0; i < limit; i++) {
324-
const actions = randomActions(5);
325-
try {
326-
simulate(...actions);
327-
} catch (e) {
328-
console.error(
329-
`
330-
Triangle fuzz tester error! Copy and paste the following line into the test suite:
331-
${formatActions(actions)}
332-
`,
333-
);
334-
throw e;
403+
function simulateMultipleRoots(...actions) {
404+
const roots = new Map();
405+
for (let rootID of rootIDs) {
406+
const simulator = TriangleSimulator(rootID);
407+
const generator = simulator.simulateAndYield();
408+
// Call this once to prepare the generator
409+
generator.next();
410+
roots.set(rootID, generator);
335411
}
412+
413+
actions.forEach(([rootID, action]) => {
414+
const generator = roots.get(rootID);
415+
generator.next(action);
416+
});
417+
roots.forEach(generator => {
418+
generator.next(STOP);
419+
});
336420
}
421+
422+
it('hard-coded tests', () => {
423+
simulateMultipleRoots(
424+
['b', interrupt()],
425+
['a', toggle(22)],
426+
['c', step(4)],
427+
['a', expire(10)],
428+
['a', interrupt()],
429+
['c', step(2)],
430+
['b', interrupt()],
431+
);
432+
});
433+
434+
it('generative tests', () => {
435+
const limit = 100;
436+
for (let i = 0; i < limit; i++) {
437+
const actions = randomActionsPerRoot();
438+
try {
439+
simulateMultipleRoots(...actions);
440+
} catch (e) {
441+
console.error(
442+
`Triangle fuzz tester error! Copy and paste the following line into the test suite:
443+
${formatActionsPerRoot(actions)}
444+
`,
445+
);
446+
throw e;
447+
}
448+
}
449+
});
337450
});
338451
});

0 commit comments

Comments
 (0)