@samitouri / QOS-React-2 / commits / f751bd8877

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 });