Skip to content

Commit

Permalink
Add fuzzing tests to React.Suspense
Browse files Browse the repository at this point in the history
It would actually have detected the issue facebook#18657.

It found the following counterexample:
```
[Scheduler`
      -> [task#2] sequence::Scheduling "8" with priority 3 resolved
      -> [task#1] promise::Request for "447b0ed" resolved with value "resolved 447b0ed!"`,"447b0ed",[{"priority":3,"text":"8"}],<function :: ["447b0ed"] => true, ["8"] => true>]
```
Reproduced by https://codesandbox.io/s/strange-frost-d4ujl?file=/src/App.js

Related to facebook#18669
  • Loading branch information
dubzzz committed Apr 23, 2020
1 parent b58dec9 commit 07b7869
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "1.2.0",
"filesize": "^6.0.1",
"flow-bin": "0.97",
Expand Down
138 changes: 138 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactSuspense-test.property.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
let React;
let ReactFeatureFlags;
let ReactNoop;
let Scheduler;
let Suspense;

const fc = require('fast-check');

const beforeEachAction = () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
ReactFeatureFlags.enableSuspenseServerRenderer = true;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
Suspense = React.Suspense;
};

function flushAndYieldScheduler() {
Scheduler.unstable_flushAllWithoutAsserting();
Scheduler.unstable_clearYields();
}

function Text({text}) {
return <span>{text}</span>;
}

function AsyncText({text, readOrThrow}) {
readOrThrow(text);
return <span>{text}</span>;
}

describe('ReactSuspense', () => {
beforeEach(beforeEachAction);

it('render based on the latest state', async () => {
await fc.assert(
fc
.asyncProperty(
// Scheduler able to re-order operations
fc.scheduler(),
// The initial text defined in the App component
fc.stringOf(fc.hexa()),
// Array of updates with the associated priority
fc.array(
fc.record({
// Priority of the task
priority: fc.constantFrom(
Scheduler.unstable_ImmediatePriority,
Scheduler.unstable_UserBlockingPriority,
Scheduler.unstable_NormalPriority,
Scheduler.unstable_IdlePriority,
Scheduler.unstable_LowPriority,
),
// Value to set for text
text: fc.stringOf(fc.hexa()),
}),
),
// The code under test
async (s, initialText, textUpdates) => {
// We simulate a cache: string -> Promise
// It may contain successes and rejections
const cache = new Map();
const readOrThrow = text => {
if (cache.has(text)) {
// The text has already been queried
const {promise, resolvedWith} = cache.get(text);
// Not resolved yet?
if (resolvedWith === null) throw promise;
// Success
return text;
} else {
// Not yet queried
const promise = s.schedule(
Promise.resolve(),
`Request for ${JSON.stringify(text)}`,
);
const cachedValue = {promise, resolvedWith: null};
promise.then(success => (cachedValue.resolvedWith = {success}));
cache.set(text, cachedValue);
throw promise;
}
};

let setText;
function App() {
const [text, _setText] = React.useState(initialText);
setText = _setText;
return <AsyncText text={text} readOrThrow={readOrThrow} />;
}

// Initial render
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
flushAndYieldScheduler();
expect(ReactNoop).toMatchRenderedOutput(<span>Loading...</span>);

// Schedule updates into the scheduler
// Updates will not be reordered
// BUT promises that they may trigger may be scheduled in-between
s.scheduleSequence(
textUpdates.map(update => {
return {
label: `Scheduling ${JSON.stringify(
update.text,
)} with priority ${update.priority}`,
builder: async () =>
Scheduler.unstable_runWithPriority(update.priority, () => {
setText(update.text);
}),
};
}),
);

// Exhaust the queue of scheduled tasks
while (s.count() !== 0) {
await ReactNoop.act(async () => {
await s.waitOne();
flushAndYieldScheduler();
});
}

// Check the final value is the expected one
const lastText =
textUpdates.length > 0
? textUpdates[textUpdates.length - 1].text
: initialText;
expect(ReactNoop).toMatchRenderedOutput(<span>{lastText}</span>);
},
)
.beforeEach(beforeEachAction),
);
});
});
8 changes: 8 additions & 0 deletions scripts/jest/setupTests.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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,
});
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5513,6 +5513,14 @@ fancy-log@^1.3.2:
parse-node-version "^1.0.0"
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"
Expand Down Expand Up @@ -10550,6 +10558,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"
Expand Down

0 comments on commit 07b7869

Please sign in to comment.