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.
Andrew Clark committed
Oct 26, 2017 at 11:03 UTC
f751bd8877afac396b7309bab8f8d53c15eb4dea
1 file changed
+200
-87
packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js
+200
-87
@@ -62,7 +62,77 @@ describe('ReactIncrementalTriangle', () => {
62
};
63
}
64
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) {
136
let triangles = [];
137
let leafTriangles = [];
138
let yieldAfterEachRender = false;
@@ -132,27 +202,35 @@ describe('ReactIncrementalTriangle', () => {
202
}
203
}
204
135
- const depth = 3;
136
-
205
let keyCounter = 0;
206
function reset(nextStep = 0) {
207
triangles = [];
208
leafTriangles = [];
209
// 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
+ }
218
ReactNoop.flush();
219
assertConsistentTree();
220
return appInstance;
221
}
222
223
reset();
149
- const totalChildren = leafTriangles.length;
150
- const totalTriangles = triangles.length;
224
225
function assertConsistentTree(activeTriangle, counter) {
226
const activeIndex = activeTriangle ? activeTriangle.leafIndex : -1;
227
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
+
234
for (let i = 0; i < children.length; i++) {
235
let child = children[i];
236
let num = child.prop;
@@ -183,14 +261,17 @@ describe('ReactIncrementalTriangle', () => {
261
}
262
}
263
186
- function simulate(...actions) {
264
+ function* simulateAndYield() {
265
const app = reset();
266
let expectedCounterAtEnd = app.state.counter;
267
268
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
+ }
274
ReactNoop.flushSync(() => {
193
- const action = actions[i];
275
switch (action.type) {
276
case FLUSH:
277
ReactNoop.flushUnitsOfWork(action.unitsOfWork);
@@ -234,84 +315,83 @@ describe('ReactIncrementalTriangle', () => {
315
assertConsistentTree(activeTriangle, expectedCounterAtEnd);
316
}
317
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
+ }
325
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
+ }
333
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
338
// 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
+ });
350
259
- const limit = 1000;
351
+ it('generative tests', () => {
352
+ const {simulate} = TriangleSimulator();
353
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
+ });
371
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];
379
}
282
- }
380
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]);
386
}
387
return actions;
388
}
389
292
- function formatActions(actions) {
293
- let result = 'simulate(';
390
+ function formatActionsPerRoot(actions) {
391
+ let result = 'simulateMultipleRoots(';
392
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)}]`;
395
if (i !== actions.length - 1) {
396
result += ', ';
397
}
@@ -320,19 +400,52 @@ describe('ReactIncrementalTriangle', () => {
400
return result;
401
}
402
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);
411
}
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
+ });
420
}
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
+ });
450
});
451
});