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
Dan Abramov committed
Dec 7, 2017 at 20:53 UTC
41f920e43043ce027970cfe3e1ac65bbd2477c7a
4 files changed
+122
scripts/babel/__tests__/transform-prevent-infinite-loops-test.js
new
+38
@@ -0,0 +1,38 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+describe('transform-prevent-infinite-loops', () => {
9
+ // Note: instead of testing the transform by applying it,
10
+ // we assume that it *is* already applied. Since we expect
11
+ // it to be applied to all our tests.
12
+
13
+ it('fails the test for `while` loops', () => {
14
+ expect(global.infiniteLoopError).toBe(null);
15
+ expect(() => {
16
+ while (true) {
17
+ // do nothing
18
+ }
19
+ }).toThrow(RangeError);
20
+ // Make sure this gets set so the test would fail regardless.
21
+ expect(global.infiniteLoopError).not.toBe(null);
22
+ // Clear the flag since otherwise *this* test would fail.
23
+ global.infiniteLoopError = null;
24
+ });
25
+
26
+ it('fails the test for `for` loops', () => {
27
+ expect(global.infiniteLoopError).toBe(null);
28
+ expect(() => {
29
+ for (;;) {
30
+ // do nothing
31
+ }
32
+ }).toThrow(RangeError);
33
+ // Make sure this gets set so the test would fail regardless.
34
+ expect(global.infiniteLoopError).not.toBe(null);
35
+ // Clear the flag since otherwise *this* test would fail.
36
+ global.infiniteLoopError = null;
37
+ });
38
+});
scripts/babel/transform-prevent-infinite-loops.js
new
+66
@@ -0,0 +1,66 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ * Copyright (c) 2017, Amjad Masad
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+'use strict';
10
+
11
+// Based on https://repl.it/site/blog/infinite-loops.
12
+
13
+// This should be reasonable for all loops in the source.
14
+// Note that if the numbers are too large, the tests will take too long to fail
15
+// for this to be useful (each individual test case might hit an infinite loop).
16
+const MAX_SOURCE_ITERATIONS = 1500;
17
+// Code in tests themselves is permitted to run longer.
18
+// For example, in the fuzz tester.
19
+const MAX_TEST_ITERATIONS = 5000;
20
+
21
+module.exports = ({types: t, template}) => {
22
+ // We set a global so that we can later fail the test
23
+ // even if the error ends up being caught by the code.
24
+ const buildGuard = template(`
25
+ if (ITERATOR++ > MAX_ITERATIONS) {
26
+ global.infiniteLoopError = new RangeError(
27
+ 'Potential infinite loop: exceeded ' +
28
+ MAX_ITERATIONS +
29
+ ' iterations.'
30
+ );
31
+ throw global.infiniteLoopError;
32
+ }
33
+ `);
34
+
35
+ return {
36
+ visitor: {
37
+ 'WhileStatement|ForStatement|DoWhileStatement': (path, file) => {
38
+ const filename = file.file.opts.filename;
39
+ const MAX_ITERATIONS =
40
+ filename.indexOf('__tests__') === -1
41
+ ? MAX_SOURCE_ITERATIONS
42
+ : MAX_TEST_ITERATIONS;
43
+
44
+ // An iterator that is incremented with each iteration
45
+ const iterator = path.scope.parent.generateUidIdentifier('loopIt');
46
+ const iteratorInit = t.numericLiteral(0);
47
+ path.scope.parent.push({
48
+ id: iterator,
49
+ init: iteratorInit,
50
+ });
51
+ // If statement and throw error if it matches our criteria
52
+ const guard = buildGuard({
53
+ ITERATOR: iterator,
54
+ MAX_ITERATIONS: t.numericLiteral(MAX_ITERATIONS),
55
+ });
56
+ // No block statment e.g. `while (1) 1;`
57
+ if (!path.get('body').isBlockStatement()) {
58
+ const statement = path.get('body').node;
59
+ path.get('body').replaceWith(t.blockStatement([guard, statement]));
60
+ } else {
61
+ path.get('body').unshiftContainer('body', guard);
62
+ }
63
+ },
64
+ },
65
+ };
66
+};
scripts/jest/preprocessor.js
+2
@@ -34,6 +34,8 @@ var babelOptions = {
34
// into ReactART builds that include JSX.
35
// TODO: I have not verified that this actually works.
36
require.resolve('babel-plugin-transform-react-jsx-source'),
37
+
38
+ require.resolve('../babel/transform-prevent-infinite-loops'),
39
],
40
retainLines: true,
41
};
scripts/jest/setupTests.js
+16
@@ -39,6 +39,22 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
39
global.spyOnDevAndProd = spyOn;
40
}
41
42
+ // We have a Babel transform that inserts guards against infinite loops.
43
+ // If a loop runs for too many iterations, we throw an error and set this
44
+ // global variable. The global lets us detect an infinite loop even if
45
+ // the actual error object ends up being caught and ignored. An infinite
46
+ // loop must always fail the test!
47
+ env.beforeEach(() => {
48
+ global.infiniteLoopError = null;
49
+ });
50
+ env.afterEach(() => {
51
+ const error = global.infiniteLoopError;
52
+ global.infiniteLoopError = null;
53
+ if (error) {
54
+ throw error;
55
+ }
56
+ });
57
+
58
['error', 'warn'].forEach(methodName => {
59
var oldMethod = console[methodName];
60
var newMethod = function() {