Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Dec 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions scripts/babel/__tests__/transform-prevent-infinite-loops-test.js
Original file line number Diff line number Diff line change
@@ -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;
});
});
66 changes: 66 additions & 0 deletions scripts/babel/transform-prevent-infinite-loops.js
Original file line number Diff line number Diff line change
@@ -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);
}
},
},
};
};
2 changes: 2 additions & 0 deletions scripts/jest/preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
16 changes: 16 additions & 0 deletions scripts/jest/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down