diff --git a/package.json b/package.json
index 1b7b848bde2..d5aec49f29b 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"eslint-plugin-no-for-of-loops": "^1.0.0",
"eslint-plugin-react": "^6.7.1",
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
+ "fast-check": "^1.24.1",
"fbjs-scripts": "0.8.3",
"filesize": "^6.0.1",
"flow-bin": "0.97",
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
index eabcb41be0e..f388ade94a6 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js
@@ -3,9 +3,8 @@ let Suspense;
let ReactNoop;
let Scheduler;
let ReactFeatureFlags;
-let Random;
-const SEED = process.env.FUZZ_TEST_SEED || 'default';
+const fc = require('fast-check');
const prettyFormatPkg = require('pretty-format');
function prettyFormat(thing) {
@@ -27,7 +26,6 @@ describe('ReactSuspenseFuzz', () => {
Suspense = React.Suspense;
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
- Random = require('random-seed');
});
function createFuzzer() {
@@ -185,123 +183,101 @@ describe('ReactSuspenseFuzz', () => {
expect(concurrentOutput).toEqual(expectedOutput);
}
- function pickRandomWeighted(rand, options) {
- let totalWeight = 0;
- for (let i = 0; i < options.length; i++) {
- totalWeight += options[i].weight;
- }
- let remainingWeight = rand.floatBetween(0, totalWeight);
- for (let i = 0; i < options.length; i++) {
- const {value, weight} = options[i];
- remainingWeight -= weight;
- if (remainingWeight <= 0) {
- return value;
- }
- }
- }
-
- function generateTestCase(rand, numberOfElements) {
- let remainingElements = numberOfElements;
-
- function createRandomChild(hasSibling) {
- const possibleActions = [
- {value: 'return', weight: 1},
- {value: 'text', weight: 1},
- ];
-
- if (hasSibling) {
- possibleActions.push({value: 'container', weight: 1});
- possibleActions.push({value: 'suspense', weight: 1});
- }
-
- const action = pickRandomWeighted(rand, possibleActions);
-
- switch (action) {
- case 'text': {
- remainingElements--;
-
- const numberOfUpdates = pickRandomWeighted(rand, [
- {value: 0, weight: 8},
- {value: 1, weight: 4},
- {value: 2, weight: 1},
- ]);
-
- const updates = [];
- for (let i = 0; i < numberOfUpdates; i++) {
- updates.push({
- beginAfter: rand.intBetween(0, 10000),
- suspendFor: rand.intBetween(0, 10000),
- });
- }
-
- return (
-
- );
- }
- case 'container': {
- const numberOfUpdates = pickRandomWeighted(rand, [
- {value: 0, weight: 8},
- {value: 1, weight: 4},
- {value: 2, weight: 1},
- ]);
-
- const updates = [];
- for (let i = 0; i < numberOfUpdates; i++) {
- updates.push({
- remountAfter: rand.intBetween(0, 10000),
- });
- }
-
- remainingElements--;
- const children = createRandomChildren(3);
- return React.createElement(Container, {updates}, ...children);
- }
- case 'suspense': {
- remainingElements--;
- const children = createRandomChildren(3);
-
- const fallbackType = pickRandomWeighted(rand, [
- {value: 'none', weight: 1},
- {value: 'normal', weight: 1},
- {value: 'nested suspense', weight: 1},
- ]);
-
- let fallback;
- if (fallbackType === 'normal') {
- fallback = 'Loading...';
- } else if (fallbackType === 'nested suspense') {
- fallback = React.createElement(
- React.Fragment,
- null,
- ...createRandomChildren(3),
- );
- }
-
- return React.createElement(Suspense, {fallback}, ...children);
- }
- case 'return':
- default:
- return null;
- }
- }
-
- function createRandomChildren(limit) {
- const children = [];
- while (remainingElements > 0 && children.length < limit) {
- children.push(createRandomChild(children.length > 0));
- }
- return children;
- }
-
- const children = createRandomChildren(Infinity);
- return React.createElement(React.Fragment, null, ...children);
+ function testCaseArbitrary() {
+ const updatesArbitrary = arb =>
+ fc.frequency(
+ // Remark: Using a frequency to build an array
+ // Remove the ability to shrink it automatically
+ // But its content remains shrinkable
+ {arbitrary: fc.constant([]), weight: 8},
+ {arbitrary: fc.array(arb, 1, 1), weight: 4},
+ {arbitrary: fc.array(arb, 2, 2), weight: 1},
+ );
+
+ const {rootChildrenArbitrary} = fc.letrec(tie => ({
+ // Produce one specific type of child
+ returnChildArbitrary: fc.constant(null),
+ textChildArbitrary: fc
+ .tuple(
+ fc.hexaString().noShrink(),
+ updatesArbitrary(
+ fc.record({
+ beginAfter: fc.nat(10000),
+ suspendFor: fc.nat(10000),
+ }),
+ ),
+ fc.nat(10000),
+ )
+ .map(([text, updates, initialDelay]) => (
+
+ )),
+ containerChildArbitrary: fc
+ .tuple(
+ updatesArbitrary(fc.record({remountAfter: fc.nat(10000)})),
+ tie('subChildrenArbitrary'),
+ )
+ .map(([updates, children]) =>
+ React.createElement(Container, {updates}, ...children),
+ ),
+ suspenseChildArbitrary: fc
+ .tuple(
+ fc.oneof(
+ // fallback = none
+ fc.constant(undefined),
+ // fallback = loading
+ fc.constant('Loading...'),
+ // fallback = nested suspense
+ tie('subChildrenArbitrary').map(children =>
+ React.createElement(React.Fragment, null, ...children),
+ ),
+ ),
+ tie('subChildrenArbitrary'),
+ )
+ .map(([fallback, children]) =>
+ React.createElement(Suspense, {fallback}, ...children),
+ ),
+ // Produce the first child
+ childArbitrary: fc.oneof(
+ tie('returnChildArbitrary'),
+ tie('textChildArbitrary'),
+ ),
+ // Produce a child with sibling
+ childWithSiblingArbitrary: fc.oneof(
+ tie('returnChildArbitrary'),
+ tie('textChildArbitrary'),
+ tie('containerChildArbitrary'),
+ tie('suspenseChildArbitrary'),
+ ),
+ // Produce sub children
+ subChildrenArbitrary: fc
+ .tuple(
+ tie('childArbitrary'),
+ fc.array(tie('childWithSiblingArbitrary'), 0, 2),
+ )
+ .map(([firstChild, others]) => [firstChild, ...others]),
+ // Produce the root children
+ rootChildrenArbitrary: fc
+ .tuple(
+ tie('childArbitrary'),
+ fc.array(tie('childWithSiblingArbitrary')),
+ )
+ .map(([firstChild, others]) => [firstChild, ...others]),
+ }));
+
+ return rootChildrenArbitrary.map(children => {
+ const el = React.createElement(React.Fragment, null, ...children);
+ return {
+ randomTestCase: React.createElement(
+ React.Fragment,
+ null,
+ ...children,
+ ),
+ toString: () => prettyFormat(el),
+ };
+ });
}
- return {Container, Text, testResolvedOutput, generateTestCase};
+ return {Container, Text, testResolvedOutput, testCaseArbitrary};
}
it('basic cases', () => {
@@ -318,30 +294,13 @@ describe('ReactSuspenseFuzz', () => {
);
});
- it(`generative tests (random seed: ${SEED})`, () => {
- const {generateTestCase, testResolvedOutput} = createFuzzer();
-
- const rand = Random.create(SEED);
-
- const NUMBER_OF_TEST_CASES = 500;
- const ELEMENTS_PER_CASE = 12;
-
- for (let i = 0; i < NUMBER_OF_TEST_CASES; i++) {
- const randomTestCase = generateTestCase(rand, ELEMENTS_PER_CASE);
- try {
- testResolvedOutput(randomTestCase);
- } catch (e) {
- console.log(`
-Failed fuzzy test case:
-
-${prettyFormat(randomTestCase)}
-
-Random seed is ${SEED}
-`);
-
- throw e;
- }
- }
+ it(`generative tests`, () => {
+ const {testCaseArbitrary, testResolvedOutput} = createFuzzer();
+ fc.assert(
+ fc.property(testCaseArbitrary(), ({randomTestCase}) =>
+ testResolvedOutput(randomTestCase),
+ ),
+ );
});
describe('hard-coded cases', () => {
diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js
index 34f5633ea37..a9786002a8b 100644
--- a/scripts/jest/setupTests.js
+++ b/scripts/jest/setupTests.js
@@ -1,6 +1,7 @@
'use strict';
const chalk = require('chalk');
+const fc = require('fast-check');
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
const {getTestFlags} = require('./TestFlags');
@@ -313,3 +314,10 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
require('jasmine-check').install();
}
+
+// Configure fuzzer based on environment variables if any
+// Do not require fast-check in beforeEach if you want to benefit from this configuration
+fc.configureGlobal({
+ numRuns: 500, // default is 100
+ seed: +process.env.FUZZ_TEST_SEED || undefined,
+});
diff --git a/yarn.lock b/yarn.lock
index 65ede48b120..9b25b81dec2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5158,6 +5158,7 @@ eslint-plugin-no-unsafe-innerhtml@1.0.16:
"eslint-plugin-react-internal@link:./scripts/eslint-rules":
version "0.0.0"
+ uid ""
eslint-plugin-react@^6.7.1:
version "6.10.3"
@@ -5701,6 +5702,14 @@ fancy-log@^1.3.2:
color-support "^1.1.3"
time-stamp "^1.0.0"
+fast-check@^1.24.1:
+ version "1.24.1"
+ resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-1.24.1.tgz#42a153e664122b1a2defdaea4e9b8311635fa7e7"
+ integrity sha512-ECF5LDbt4F8sJyTDI62fRLn0BdHDAdBacxlEsxaYbtqwbsdWofoYZUSaUp9tJrLsqCQ8jG28SkNvPZpDfNo3tw==
+ dependencies:
+ pure-rand "^2.0.0"
+ tslib "^1.10.0"
+
fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@@ -10746,6 +10755,11 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+pure-rand@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-2.0.0.tgz#3324633545207907fe964c2f0ebf05d8e9a7f129"
+ integrity sha512-mk98aayyd00xbfHgE3uEmAUGzz3jCdm8Mkf5DUXUhc7egmOaGG2D7qhVlynGenNe9VaNJZvzO9hkc8myuTkDgw==
+
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"