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:
parent
825682390d
commit
41f920e430
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue