From 90995ef8b0733ca46b1d9a187828d63ff78852bb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 29 Mar 2023 16:48:02 -0400 Subject: [PATCH] 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. --- .../ReactIncrementalTriangle-test.js | 592 ------------------ 1 file changed, 592 deletions(-) delete mode 100644 packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js deleted file mode 100644 index b3f70d6e25da6..0000000000000 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.js +++ /dev/null @@ -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 => ( - - {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 ; - } - - return ( - - - - - - - - ); - }} - - )} - - ); - } - } - - 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 ( - - ); - } - } - - let keyCounter = 0; - function reset(nextStep = 0) { - triangles = []; - leafTriangles = []; - // Remounts the whole tree by changing the key - if (rootID) { - ReactNoop.renderToRootWithID( - , - rootID, - ); - } else { - ReactNoop.render(); - } - 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; - } - } - }); - }); -});