Delete "triangle" resuming fuzz tester (#26508)

This deletes the ReactIncrementalTriangle test suite, which I originally
added back in 2017 when I was working on Fiber's "resuming" feature. It
was meant to simulate a similar scenario as Seb's "Sierpinski Triangle"
Fiber demo.

We eventually ended up removing resuming, but we kept this fuzz tester
around since it wasn't really harming anything. However, over the years,
we've had to make many small tweaks to decouple it from implementation
details, to the point that it doesn't test anything useful anymore. And
the thing that it originally tested has long since been removed.

If or when we do add back resuming, we would write a different fuzz
tester from scratch rather than build on this one.

So rather than continue to contrive ways to prevent it from breaking, I
propose we delete it.

We still have other fuzz testers for things like Suspense and context
propagation. Only this particular one has outlived its usefulness.
This commit is contained in:
Andrew Clark
2023-03-29 16:48:02 -04:00
committed by GitHub
parent f118b7cebf
commit 90995ef8b0
@@ -1,592 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
let React;
let ReactNoop;
let Scheduler;
describe('ReactIncrementalTriangle', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
});
function span(prop) {
return {type: 'span', children: [], prop, hidden: false};
}
const FLUSH = 'FLUSH';
function flush(unitsOfWork = Infinity) {
return {
type: FLUSH,
unitsOfWork,
};
}
const FLUSH_ALL = 'FLUSH_ALL';
function flushAll() {
return {
type: FLUSH_ALL,
};
}
const STEP = 'STEP';
function step(counter) {
return {
type: STEP,
counter,
};
}
const INTERRUPT = 'INTERRUPT';
function interrupt(key) {
return {
type: INTERRUPT,
};
}
const TOGGLE = 'TOGGLE';
function toggle(childIndex) {
return {
type: TOGGLE,
childIndex,
};
}
const EXPIRE = 'EXPIRE';
function expire(ms) {
return {
type: EXPIRE,
ms,
};
}
const STOP = 'STOP';
function randomInteger(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
function formatAction(action) {
switch (action.type) {
case FLUSH:
return `flush(${action.unitsOfWork})`;
case FLUSH_ALL:
return 'flushAll()';
case STEP:
return `step(${action.counter})`;
case INTERRUPT:
return 'interrupt()';
case TOGGLE:
return `toggle(${action.childIndex})`;
case EXPIRE:
return `expire(${action.ms})`;
default:
throw new Error('Switch statement should be exhaustive');
}
}
function formatActions(actions) {
let result = 'simulate(';
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
result += formatAction(action);
if (i !== actions.length - 1) {
result += ', ';
}
}
result += ')';
return result;
}
const MAX_DEPTH = 3;
const TOTAL_CHILDREN = Math.pow(3, MAX_DEPTH);
let TOTAL_TRIANGLES = 0;
for (let i = 0; i <= MAX_DEPTH; i++) {
TOTAL_TRIANGLES += Math.pow(3, i);
}
function randomAction() {
const weights = [
[FLUSH, 1],
[FLUSH_ALL, 1],
[STEP, 1],
[INTERRUPT, 1],
[TOGGLE, 1],
[EXPIRE, 1],
];
let totalWeight = 0;
for (let i = 0; i < weights.length; i++) {
totalWeight += weights[i][1];
}
const randomNumber = Math.random() * totalWeight;
let actionType;
let remainingWeight = randomNumber;
for (let i = 0; i < weights.length; i++) {
const [option, weight] = weights[i];
remainingWeight -= weight;
if (remainingWeight <= 0) {
actionType = option;
break;
}
}
switch (actionType) {
case FLUSH:
return flush(randomInteger(0, TOTAL_TRIANGLES * 1.5));
case FLUSH_ALL:
return flushAll();
case STEP:
return step(randomInteger(0, 10));
case INTERRUPT:
return interrupt();
case TOGGLE:
return toggle(randomInteger(0, TOTAL_TRIANGLES));
case EXPIRE:
return expire(randomInteger(0, 1500));
default:
throw new Error('Switch statement should be exhaustive');
}
}
function randomActions(n) {
const actions = [];
for (let i = 0; i < n; i++) {
actions.push(randomAction());
}
return actions;
}
function TriangleSimulator(rootID) {
const CounterContext = React.createContext([]);
const ActiveContext = React.createContext(0);
let triangles = [];
let leafTriangles = [];
const yieldAfterEachRender = false;
class Triangle extends React.Component {
constructor(props) {
super();
this.state = {isActive: false};
this.child = React.createRef(null);
}
activate() {
this.setState({isActive: true});
}
deactivate() {
this.setState({isActive: false});
}
shouldComponentUpdate(nextProps, nextState) {
return (
this.props.counter !== nextProps.counter ||
this.props.activeDepth !== nextProps.activeDepth ||
this.state.isActive !== nextState.isActive
);
}
componentDidMount() {
this.index = triangles.length;
triangles.push(this);
if (this.props.remainingDepth === 0) {
this.leafIndex = leafTriangles.length;
leafTriangles.push(this);
}
}
componentDidUpdate() {
if (this.child.current !== null) {
const {prop: currentCounter} = JSON.parse(this.child.current.prop);
if (this.props.counter !== currentCounter) {
throw new Error('Incorrect props in lifecycle');
}
}
}
render() {
if (yieldAfterEachRender) {
Scheduler.log(this);
}
const {counter, remainingDepth} = this.props;
return (
<ActiveContext.Consumer>
{activeContext => (
<CounterContext.Consumer>
{counterContext => {
const activeDepthProp = this.state.isActive
? this.props.activeDepth + 1
: this.props.activeDepth;
const activeDepthContext = this.state.isActive
? activeContext + 1
: activeContext;
if (remainingDepth === 0) {
// Leaf
const output = JSON.stringify({
prop: counter,
isActive: this.state.isActive,
counterContext: counterContext,
activeDepthProp,
activeDepthContext,
});
return <span ref={this.child} prop={output} />;
}
return (
<ActiveContext.Provider value={activeDepthContext}>
<CounterContext.Provider value={counter}>
<Triangle
counter={counter}
activeDepth={activeDepthProp}
remainingDepth={remainingDepth - 1}
/>
<Triangle
counter={counter}
activeDepth={activeDepthProp}
remainingDepth={remainingDepth - 1}
/>
<Triangle
counter={counter}
activeDepth={activeDepthProp}
remainingDepth={remainingDepth - 1}
/>
</CounterContext.Provider>
</ActiveContext.Provider>
);
}}
</CounterContext.Consumer>
)}
</ActiveContext.Consumer>
);
}
}
let appInstance;
class App extends React.Component {
state = {counter: 0};
interrupt() {
// Triggers a restart from the top.
this.forceUpdate();
}
setCounter(counter) {
const currentCounter = this.state.counter;
this.setState({counter});
return currentCounter;
}
render() {
appInstance = this;
return (
<Triangle
counter={this.state.counter}
activeDepth={0}
remainingDepth={this.props.remainingDepth}
/>
);
}
}
let keyCounter = 0;
function reset(nextStep = 0) {
triangles = [];
leafTriangles = [];
// Remounts the whole tree by changing the key
if (rootID) {
ReactNoop.renderToRootWithID(
<App remainingDepth={MAX_DEPTH} key={keyCounter++} />,
rootID,
);
} else {
ReactNoop.render(<App remainingDepth={MAX_DEPTH} key={keyCounter++} />);
}
Scheduler.unstable_flushAllWithoutAsserting();
assertConsistentTree();
return appInstance;
}
reset();
function assertConsistentTree(activeTriangleIndices = new Set(), counter) {
const childrenJSX = ReactNoop.getPendingChildrenAsJSX(rootID);
const children = childrenJSX === null ? [] : childrenJSX.props.children;
if (children.length !== TOTAL_CHILDREN) {
throw new Error('Wrong number of children.');
}
let expectedCounter = counter;
for (let i = 0; i < children.length; i++) {
const child = children[i];
const output = JSON.parse(child.props.prop);
const prop = output.prop;
const isActive = output.isActive;
const counterContext = output.counterContext;
const activeDepthProp = output.activeDepthProp;
const activeDepthContext = output.activeDepthContext;
// If an expected counter is not specified, use the value of the
// first child.
if (expectedCounter === undefined) {
expectedCounter = prop;
}
const expectedIsActive = activeTriangleIndices.has(i);
if (prop !== expectedCounter) {
throw new Error(
`Triangle ${i} is inconsistent: prop ${prop} instead of ` +
expectedCounter,
);
}
if (isActive !== expectedIsActive) {
throw new Error(
`Triangle ${i} is inconsistent: isActive ${isActive} instead of ` +
expectedIsActive,
);
}
if (counterContext !== prop) {
throw new Error(
`Triangle ${i} is inconsistent: prop ${prop} does not match ` +
`counterContext ${counterContext}`,
);
}
if (activeDepthContext !== activeDepthProp) {
throw new Error(
`Triangle ${i} is inconsistent: activeDepthProp ` +
`${activeDepthProp} does not match activeDepthContext ` +
activeDepthContext,
);
}
}
}
function* simulateAndYield() {
const app = reset();
let expectedCounterAtEnd = app.state.counter;
const activeIndices = new Set();
const activeLeafIndices = new Set();
let action;
while (true) {
action = yield;
if (action === STOP) {
break;
}
ReactNoop.flushSync(() => {
switch (action.type) {
case FLUSH:
Scheduler.unstable_flushNumberOfYields(action.unitsOfWork);
break;
case FLUSH_ALL:
Scheduler.unstable_flushAllWithoutAsserting();
break;
case STEP:
ReactNoop.deferredUpdates(() => {
app.setCounter(action.counter);
expectedCounterAtEnd = action.counter;
});
break;
case INTERRUPT:
app.interrupt();
break;
case TOGGLE:
const targetTriangle = triangles[action.childIndex];
if (targetTriangle === undefined) {
throw new Error('Target index is out of bounds');
}
const index = targetTriangle.index;
const leafIndex = targetTriangle.leafIndex;
if (activeIndices.has(index)) {
activeIndices.delete(index);
if (leafIndex !== undefined) {
activeLeafIndices.delete(leafIndex);
}
targetTriangle.deactivate();
} else {
activeIndices.add(index);
if (leafIndex !== undefined) {
activeLeafIndices.add(leafIndex);
}
targetTriangle.activate();
}
break;
case EXPIRE:
ReactNoop.expire(action.ms);
break;
default:
throw new Error('Switch statement should be exhaustive');
}
});
assertConsistentTree(activeLeafIndices);
}
// Flush remaining work
Scheduler.unstable_flushAllWithoutAsserting();
ReactNoop.flushSync();
assertConsistentTree(activeLeafIndices, expectedCounterAtEnd);
}
function simulate(...actions) {
const gen = simulateAndYield();
// Call this once to prepare the generator
gen.next();
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const action of actions) {
gen.next(action);
}
gen.next(STOP);
}
return {
simulateAndYield,
simulate,
randomAction,
randomActions,
};
}
describe('single root', () => {
// These tests are not deterministic because the inputs are randomized. It
// runs a limited number of tests on every run. If it fails, it will output
// the case that led to the failure. Add the failing case to the test above
// to prevent future regressions.
it('hard-coded tests', () => {
const {simulate} = TriangleSimulator();
simulate(step(1));
simulate(toggle(0), step(1), toggle(0));
simulate(step(1), toggle(0), flush(2), step(2), toggle(0));
simulate(step(1), flush(3), toggle(0), step(0));
simulate(step(1), flush(3), toggle(18), step(0));
simulate(step(4), flush(52), expire(1476), flush(17), step(0));
simulate(interrupt(), toggle(10), step(2), expire(990), flush(46));
simulate(interrupt(), step(6), step(7), toggle(6), interrupt());
simulate(interrupt(), toggle(31), toggle(31), toggle(13), step(1));
});
it('generative tests', () => {
const {simulate} = TriangleSimulator();
const limit = 1000;
for (let i = 0; i < limit; i++) {
const actions = randomActions(5);
try {
simulate(...actions);
} catch (e) {
console.error(
`Triangle fuzz tester error! Copy and paste the following line into the test suite:
${formatActions(actions)}
`,
);
throw e;
}
}
});
});
describe('multiple roots', () => {
const rootIDs = ['a', 'b', 'c'];
function randomActionsPerRoot() {
function randomRootID() {
const index = randomInteger(0, rootIDs.length);
return rootIDs[index];
}
const actions = [];
for (let i = 0; i < 10; i++) {
const rootID = randomRootID();
const action = randomAction();
actions.push([rootID, action]);
}
return actions;
}
function formatActionsPerRoot(actions) {
let result = 'simulateMultipleRoots(';
for (let i = 0; i < actions.length; i++) {
const [rootID, action] = actions[i];
result += `['${rootID}', ${formatAction(action)}]`;
if (i !== actions.length - 1) {
result += ', ';
}
}
result += ')';
return result;
}
function simulateMultipleRoots(...actions) {
const roots = new Map();
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const rootID of rootIDs) {
const simulator = TriangleSimulator(rootID);
const generator = simulator.simulateAndYield();
// Call this once to prepare the generator
generator.next();
roots.set(rootID, generator);
}
actions.forEach(([rootID, action]) => {
const generator = roots.get(rootID);
generator.next(action);
});
roots.forEach(generator => {
generator.next(STOP);
});
}
it('hard-coded tests', () => {
simulateMultipleRoots(
['b', interrupt()],
['a', toggle(22)],
['c', step(4)],
['a', expire(10)],
['a', interrupt()],
['c', step(2)],
['b', interrupt()],
);
simulateMultipleRoots(
['c', toggle(0)],
['c', step(1)],
['b', flush(7)],
['c', toggle(0)],
);
simulateMultipleRoots(
['c', step(1)],
['c', expire(5000)],
['b', toggle(1)],
);
});
it('generative tests', () => {
const limit = 100;
for (let i = 0; i < limit; i++) {
const actions = randomActionsPerRoot();
try {
simulateMultipleRoots(...actions);
} catch (e) {
console.error(
`Triangle fuzz tester error! Copy and paste the following line into the test suite:
${formatActionsPerRoot(actions)}
`,
);
throw e;
}
}
});
});
});