@samitouri / QOS-React / commits / 41f920e430

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() {