Skip to content

Commit 744c6d1

Browse files
committed
Fuzz tester for context
1 parent 269f263 commit 744c6d1

File tree

3 files changed

+218
-2
lines changed

3 files changed

+218
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"platform": "^1.1.0",
8181
"prettier": "1.8.1",
8282
"prop-types": "^15.6.0",
83+
"random-seed": "^0.3.0",
8384
"rimraf": "^2.6.1",
8485
"rollup": "^0.52.1",
8586
"rollup-plugin-babel": "^3.0.1",

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

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99

1010
'use strict';
1111

12-
let React;
12+
let React = require('React');
1313
let ReactNoop;
14+
let gen;
1415

1516
describe('ReactNewContext', () => {
1617
beforeEach(() => {
1718
jest.resetModules();
1819
React = require('react');
1920
ReactNoop = require('react-noop-renderer');
21+
gen = require('random-seed');
2022
});
2123

2224
// function div(...children) {
@@ -303,4 +305,211 @@ describe('ReactNewContext', () => {
303305
span('Result: 4'),
304306
]);
305307
});
308+
309+
describe('fuzz test', () => {
310+
const contextKeys = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
311+
const contexts = new Map(
312+
contextKeys.map(key => {
313+
const Context = React.createContext(0);
314+
Context.displayName = 'Context' + key;
315+
return [key, Context];
316+
}),
317+
);
318+
const Fragment = React.Fragment;
319+
320+
const FLUSH_ALL = 'FLUSH_ALL';
321+
function flushAll() {
322+
return {
323+
type: FLUSH_ALL,
324+
toString() {
325+
return `flushAll()`;
326+
},
327+
};
328+
}
329+
330+
const FLUSH = 'FLUSH';
331+
function flush(unitsOfWork) {
332+
return {
333+
type: FLUSH,
334+
unitsOfWork,
335+
toString() {
336+
return `flush(${unitsOfWork})`;
337+
},
338+
};
339+
}
340+
341+
const UPDATE = 'UPDATE';
342+
function update(key, value) {
343+
return {
344+
type: UPDATE,
345+
key,
346+
value,
347+
toString() {
348+
return `update('${key}', ${value})`;
349+
},
350+
};
351+
}
352+
353+
function randomInteger(min, max) {
354+
min = Math.ceil(min);
355+
max = Math.floor(max);
356+
return Math.floor(Math.random() * (max - min)) + min;
357+
}
358+
359+
function randomAction() {
360+
switch (randomInteger(0, 3)) {
361+
case 0:
362+
return flushAll();
363+
case 1:
364+
return flush(randomInteger(0, 500));
365+
case 2:
366+
const key = contextKeys[randomInteger(0, contextKeys.length)];
367+
const value = randomInteger(1, 10);
368+
return update(key, value);
369+
default:
370+
throw new Error('Switch statement should be exhaustive');
371+
}
372+
}
373+
374+
function randomActions(n) {
375+
let actions = [];
376+
for (let i = 0; i < n; i++) {
377+
actions.push(randomAction());
378+
}
379+
return actions;
380+
}
381+
382+
class ConsumerTree extends React.Component {
383+
shouldComponentUpdate() {
384+
return false;
385+
}
386+
render() {
387+
if (this.props.depth >= this.props.maxDepth) {
388+
return null;
389+
}
390+
const consumers = [0, 1, 2].map(i => {
391+
const randomKey =
392+
contextKeys[this.props.rand.intBetween(0, contextKeys.length - 1)];
393+
const Context = contexts.get(randomKey);
394+
return Context.consume(
395+
value => (
396+
<Fragment>
397+
<span prop={`${randomKey}:${value}`} />
398+
<ConsumerTree
399+
rand={this.props.rand}
400+
depth={this.props.depth + 1}
401+
maxDepth={this.props.maxDepth}
402+
/>
403+
</Fragment>
404+
),
405+
i,
406+
);
407+
});
408+
return consumers;
409+
}
410+
}
411+
412+
function Root(props) {
413+
return contextKeys.reduceRight((children, key) => {
414+
const Context = contexts.get(key);
415+
const value = props.values[key];
416+
return Context.provide(value, children);
417+
}, <ConsumerTree rand={props.rand} depth={0} maxDepth={props.maxDepth} />);
418+
}
419+
420+
const initialValues = contextKeys.reduce(
421+
(result, key, i) => ({...result, [key]: i + 1}),
422+
{},
423+
);
424+
425+
function assertConsistentTree(expectedValues = {}) {
426+
const children = ReactNoop.getChildren();
427+
children.forEach(child => {
428+
const text = child.prop;
429+
const key = text[0];
430+
const value = parseInt(text[2], 10);
431+
const expectedValue = expectedValues[key];
432+
if (expectedValue === undefined) {
433+
// If an expected value was not explicitly passed to this function,
434+
// use the first occurrence.
435+
expectedValues[key] = value;
436+
} else if (value !== expectedValue) {
437+
throw new Error(
438+
`Inconsistent value! Expected: ${key}:${expectedValue}. Actual: ${
439+
text
440+
}`,
441+
);
442+
}
443+
});
444+
}
445+
446+
function ContextSimulator(maxDepth) {
447+
function simulate(seed, actions) {
448+
const rand = gen.create(seed);
449+
let finalExpectedValues = initialValues;
450+
function updateRoot() {
451+
ReactNoop.render(
452+
<Root
453+
maxDepth={maxDepth}
454+
rand={rand}
455+
values={finalExpectedValues}
456+
/>,
457+
);
458+
}
459+
updateRoot();
460+
461+
actions.forEach(action => {
462+
switch (action.type) {
463+
case FLUSH_ALL:
464+
ReactNoop.flush();
465+
break;
466+
case FLUSH:
467+
ReactNoop.flushUnitsOfWork(action.unitsOfWork);
468+
break;
469+
case UPDATE:
470+
finalExpectedValues = {
471+
...finalExpectedValues,
472+
[action.key]: action.value,
473+
};
474+
updateRoot();
475+
break;
476+
default:
477+
throw new Error('Switch statement should be exhaustive');
478+
}
479+
assertConsistentTree();
480+
});
481+
482+
ReactNoop.flush();
483+
assertConsistentTree(finalExpectedValues);
484+
}
485+
486+
return {simulate};
487+
}
488+
489+
it('hard-coded tests', () => {
490+
const {simulate} = ContextSimulator(5);
491+
simulate('randomSeed', [flush(3), update('A', 4)]);
492+
});
493+
494+
it('generated tests', () => {
495+
const {simulate} = ContextSimulator(5);
496+
497+
const LIMIT = 100;
498+
for (let i = 0; i < LIMIT; i++) {
499+
const seed = Math.random()
500+
.toString(36)
501+
.substr(2, 5);
502+
const actions = randomActions(5);
503+
try {
504+
simulate(seed, actions);
505+
} catch (error) {
506+
console.error(`
507+
Context fuzz tester error! Copy and paste the following line into the test suite:
508+
simulate('${seed}', ${actions.join(', ')});
509+
`);
510+
throw error;
511+
}
512+
}
513+
});
514+
});
306515
});

yarn.lock

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3045,7 +3045,7 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
30453045
dependencies:
30463046
jsonify "~0.0.0"
30473047

3048-
json-stringify-safe@~5.0.1:
3048+
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
30493049
version "5.0.1"
30503050
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
30513051

@@ -3799,6 +3799,12 @@ qs@~6.4.0:
37993799
version "6.4.0"
38003800
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
38013801

3802+
random-seed@^0.3.0:
3803+
version "0.3.0"
3804+
resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd"
3805+
dependencies:
3806+
json-stringify-safe "^5.0.1"
3807+
38023808
randomatic@^1.1.3:
38033809
version "1.1.6"
38043810
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"

0 commit comments

Comments
 (0)