Add a test-only transform to catch infinite loops (#11790)

* Add a test-only transform to catch infinite loops

* Only track iteration count, not time

This makes the detection dramatically faster, and is okay in our case because we don't have tests that iterate so much.

* Use clearer naming

* Set different limits for tests

* Fail tests with infinite loops even if the error was caught

* Add a test
This commit is contained in:
Dan Abramov 2017-12-07 20:53:13 +00:00 committed by GitHub
parent 825682390d
commit 41f920e430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 0 deletions

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
describe('transform-prevent-infinite-loops', () => {
// Note: instead of testing the transform by applying it,
// we assume that it *is* already applied. Since we expect
// it to be applied to all our tests.
it('fails the test for `while` loops', () => {
expect(global.infiniteLoopError).toBe(null);
expect(() => {
while (true) {
// do nothing
}
}).toThrow(RangeError);
// Make sure this gets set so the test would fail regardless.
expect(global.infiniteLoopError).not.toBe(null);
// Clear the flag since otherwise *this* test would fail.
global.infiniteLoopError = null;
});
it('fails the test for `for` loops', () => {
expect(global.infiniteLoopError).toBe(null);
expect(() => {
for (;;) {
// do nothing
}
}).toThrow(RangeError);
// Make sure this gets set so the test would fail regardless.
expect(global.infiniteLoopError).not.toBe(null);
// Clear the flag since otherwise *this* test would fail.
global.infiniteLoopError = null;
});
});

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* Copyright (c) 2017, Amjad Masad
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
// Based on https://repl.it/site/blog/infinite-loops.
// This should be reasonable for all loops in the source.
// Note that if the numbers are too large, the tests will take too long to fail
// for this to be useful (each individual test case might hit an infinite loop).
const MAX_SOURCE_ITERATIONS = 1500;
// Code in tests themselves is permitted to run longer.
// For example, in the fuzz tester.
const MAX_TEST_ITERATIONS = 5000;
module.exports = ({types: t, template}) => {
// We set a global so that we can later fail the test
// even if the error ends up being caught by the code.
const buildGuard = template(`
if (ITERATOR++ > MAX_ITERATIONS) {
global.infiniteLoopError = new RangeError(
'Potential infinite loop: exceeded ' +
MAX_ITERATIONS +
' iterations.'
);
throw global.infiniteLoopError;
}
`);
return {
visitor: {
'WhileStatement|ForStatement|DoWhileStatement': (path, file) => {
const filename = file.file.opts.filename;
const MAX_ITERATIONS =
filename.indexOf('__tests__') === -1
? MAX_SOURCE_ITERATIONS
: MAX_TEST_ITERATIONS;
// An iterator that is incremented with each iteration
const iterator = path.scope.parent.generateUidIdentifier('loopIt');
const iteratorInit = t.numericLiteral(0);
path.scope.parent.push({
id: iterator,
init: iteratorInit,
});
// If statement and throw error if it matches our criteria
const guard = buildGuard({
ITERATOR: iterator,
MAX_ITERATIONS: t.numericLiteral(MAX_ITERATIONS),
});
// No block statment e.g. `while (1) 1;`
if (!path.get('body').isBlockStatement()) {
const statement = path.get('body').node;
path.get('body').replaceWith(t.blockStatement([guard, statement]));
} else {
path.get('body').unshiftContainer('body', guard);
}
},
},
};
};

View File

@ -34,6 +34,8 @@ var babelOptions = {
// into ReactART builds that include JSX.
// TODO: I have not verified that this actually works.
require.resolve('babel-plugin-transform-react-jsx-source'),
require.resolve('../babel/transform-prevent-infinite-loops'),
],
retainLines: true,
};

View File

@ -39,6 +39,22 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
global.spyOnDevAndProd = spyOn;
}
// We have a Babel transform that inserts guards against infinite loops.
// If a loop runs for too many iterations, we throw an error and set this
// global variable. The global lets us detect an infinite loop even if
// the actual error object ends up being caught and ignored. An infinite
// loop must always fail the test!
env.beforeEach(() => {
global.infiniteLoopError = null;
});
env.afterEach(() => {
const error = global.infiniteLoopError;
global.infiniteLoopError = null;
if (error) {
throw error;
}
});
['error', 'warn'].forEach(methodName => {
var oldMethod = console[methodName];
var newMethod = function() {