Isolated exec tests (#11531)

* Run exec tests in fresh contexts

* Reevaluate modules in every context

* Cache module code when running tests

* Eliminate weakmap accesses as much as possible

* Remove old multiline usage

* Using bundled polyfill to significantly increase performance

The individual requires for each file were the part that was sooooo slow.

* Drop LRU cache size

* Fixes

* Fix test

Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
This commit is contained in:
Justin Ridgewell 2020-08-10 18:57:48 -04:00 committed by GitHub
parent 3bff1ce35a
commit a5bc48661b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 175 additions and 143 deletions

View File

@ -1,8 +1,8 @@
var code = multiline([ var code = `
"function foo() {", function foo() {
" var a = a ? a : a;", var a = a ? a : a;
"}", }
]); `;
transform(code, { transform(code, {
plugins: [ plugins: [

View File

@ -1,12 +1,12 @@
var code = multiline([ var code = `
"(function() {", (function() {
" var bar = 'lol';", var bar = 'lol';
" function foo(b){", function foo(b){
" b === bar;", b === bar
" foo(b);", foo(b);
" }", }
"})();", })();
]); `;
transform(code, { transform(code, {
plugins: [ plugins: [

View File

@ -23,6 +23,7 @@
"jest": "^24.8.0", "jest": "^24.8.0",
"jest-diff": "^24.8.0", "jest-diff": "^24.8.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"quick-lru": "5.1.0",
"resolve": "^1.3.2", "resolve": "^1.3.2",
"source-map": "^0.5.0" "source-map": "^0.5.0"
} }

View File

@ -14,34 +14,100 @@ import fs from "fs";
import path from "path"; import path from "path";
import vm from "vm"; import vm from "vm";
import checkDuplicatedNodes from "babel-check-duplicated-nodes"; import checkDuplicatedNodes from "babel-check-duplicated-nodes";
import QuickLRU from "quick-lru";
import diff from "jest-diff"; import diff from "jest-diff";
const moduleCache = {}; const cachedScripts = new QuickLRU({ maxSize: 10 });
const testContext = vm.createContext({ const contextModuleCache = new WeakMap();
...helpers, const sharedTestContext = createContext();
process: process,
transform: babel.transform,
setTimeout: setTimeout,
setImmediate: setImmediate,
expect,
});
testContext.global = testContext;
// Initialize the test context with the polyfill, and then freeze the global to prevent implicit function createContext() {
// global creation in tests, which could cause things to bleed between tests. const context = vm.createContext({
runModuleInTestContext("@babel/polyfill", __filename); ...helpers,
process: process,
transform: babel.transform,
setTimeout: setTimeout,
setImmediate: setImmediate,
expect,
});
context.global = context;
// Populate the "babelHelpers" global with Babel's helper utilities. const moduleCache = Object.create(null);
runCodeInTestContext(buildExternalHelpers(), { contextModuleCache.set(context, moduleCache);
filename: path.join(__dirname, "babel-helpers-in-memory.js"),
}); // Initialize the test context with the polyfill, and then freeze the global to prevent implicit
// global creation in tests, which could cause things to bleed between tests.
runModuleInTestContext(
"@babel/polyfill/dist/polyfill.min",
__filename,
context,
moduleCache,
);
// Populate the "babelHelpers" global with Babel's helper utilities.
runCacheableScriptInTestContext(
path.join(__dirname, "babel-helpers-in-memory.js"),
buildExternalHelpers,
context,
moduleCache,
);
return context;
}
function runCacheableScriptInTestContext(
filename: string,
srcFn: () => string,
context: Context,
moduleCache: Object,
) {
let cached = cachedScripts.get(filename);
if (!cached) {
const code = `(function (exports, require, module, __filename, __dirname) {\n${srcFn()}\n});`;
cached = {
code,
cachedData: undefined,
};
cachedScripts.set(filename, cached);
}
const script = new vm.Script(cached.code, {
filename,
displayErrors: true,
lineOffset: -1,
cachedData: cached.cachedData,
produceCachedData: true,
});
if (script.cachedDataProduced) {
cached.cachedData = script.cachedData;
}
const module = {
id: filename,
exports: {},
};
const req = id => runModuleInTestContext(id, filename, context, moduleCache);
const dirname = path.dirname(filename);
script
.runInContext(context)
.call(module.exports, module.exports, req, module, filename, dirname);
return module;
}
/** /**
* A basic implementation of CommonJS so we can execute `@babel/polyfill` inside our test context. * A basic implementation of CommonJS so we can execute `@babel/polyfill` inside our test context.
* This allows us to run our unittests * This allows us to run our unittests
*/ */
function runModuleInTestContext(id: string, relativeFilename: string) { function runModuleInTestContext(
id: string,
relativeFilename: string,
context: Context,
moduleCache: Object,
) {
const filename = resolve.sync(id, { const filename = resolve.sync(id, {
basedir: path.dirname(relativeFilename), basedir: path.dirname(relativeFilename),
}); });
@ -50,23 +116,17 @@ function runModuleInTestContext(id: string, relativeFilename: string) {
// the context's global scope. // the context's global scope.
if (filename === id) return require(id); if (filename === id) return require(id);
// Modules can only evaluate once per context, so the moduleCache is a
// stronger cache guarantee than the LRU's Script cache.
if (moduleCache[filename]) return moduleCache[filename].exports; if (moduleCache[filename]) return moduleCache[filename].exports;
const module = (moduleCache[filename] = { const module = runCacheableScriptInTestContext(
id: filename,
exports: {},
});
const dirname = path.dirname(filename);
const req = id => runModuleInTestContext(id, filename);
const src = fs.readFileSync(filename, "utf8");
const code = `(function (exports, require, module, __filename, __dirname) {\n${src}\n});`;
vm.runInContext(code, testContext, {
filename, filename,
displayErrors: true, () => fs.readFileSync(filename, "utf8"),
lineOffset: -1, context,
}).call(module.exports, module.exports, req, module, filename, dirname); moduleCache,
);
moduleCache[filename] = module;
return module.exports; return module.exports;
} }
@ -76,10 +136,15 @@ function runModuleInTestContext(id: string, relativeFilename: string) {
* *
* Exposed for unit tests, not for use as an API. * Exposed for unit tests, not for use as an API.
*/ */
export function runCodeInTestContext(code: string, opts: { filename: string }) { export function runCodeInTestContext(
code: string,
opts: { filename: string },
context = sharedTestContext,
) {
const filename = opts.filename; const filename = opts.filename;
const dirname = path.dirname(filename); const dirname = path.dirname(filename);
const req = id => runModuleInTestContext(id, filename); const moduleCache = contextModuleCache.get(context);
const req = id => runModuleInTestContext(id, filename, context, moduleCache);
const module = { const module = {
id: filename, id: filename,
@ -94,7 +159,7 @@ export function runCodeInTestContext(code: string, opts: { filename: string }) {
// Note: This isn't doing .call(module.exports, ...) because some of our tests currently // Note: This isn't doing .call(module.exports, ...) because some of our tests currently
// rely on 'this === global'. // rely on 'this === global'.
const src = `(function(exports, require, module, __filename, __dirname, opts) {\n${code}\n});`; const src = `(function(exports, require, module, __filename, __dirname, opts) {\n${code}\n});`;
return vm.runInContext(src, testContext, { return vm.runInContext(src, context, {
filename, filename,
displayErrors: true, displayErrors: true,
lineOffset: -1, lineOffset: -1,
@ -183,13 +248,14 @@ function run(task) {
let resultExec; let resultExec;
if (execCode) { if (execCode) {
const context = createContext();
const execOpts = getOpts(exec); const execOpts = getOpts(exec);
result = babel.transform(execCode, execOpts); result = babel.transform(execCode, execOpts);
checkDuplicatedNodes(babel, result.ast); checkDuplicatedNodes(babel, result.ast);
execCode = result.code; execCode = result.code;
try { try {
resultExec = runCodeInTestContext(execCode, execOpts); resultExec = runCodeInTestContext(execCode, execOpts, context);
} catch (err) { } catch (err) {
// Pass empty location to include the whole file in the output. // Pass empty location to include the whole file in the output.
err.message = err.message =

View File

@ -1,27 +1,25 @@
"use strict"; "use strict";
const NOSET = `NOSET${__filename}`;
const NOWRITE = `NOWRITE${__filename}`;
Object.defineProperty(Object.prototype, NOSET, { Object.defineProperty(Object.prototype, 'NOSET', {
get(value) { get(value) {
// noop // noop
}, },
}); });
Object.defineProperty(Object.prototype, NOWRITE, { Object.defineProperty(Object.prototype, 'NOWRITE', {
writable: false, writable: false,
value: 'abc', value: 'abc',
}); });
const obj = { [NOSET]: 123 }; const obj = { 'NOSET': 123 };
// this won't work as expected if transformed as Object.assign (or equivalent) // this won't work as expected if transformed as Object.assign (or equivalent)
// because those trigger object setters (spread don't) // because those trigger object setters (spread don't)
expect(() => { expect(() => {
const objSpread = { ...obj }; const objSpread = { ...obj };
}).toThrow(); }).toThrow();
const obj2 = { [NOWRITE]: 456 }; const obj2 = { 'NOWRITE': 456 };
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'` // this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties // if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
// (spread defines them) // (spread defines them)
expect(() => { expect(() => {

View File

@ -1,27 +1,24 @@
"use strict"; "use strict";
const NOSET = `NOSET${__filename}`; Object.defineProperty(Object.prototype, 'NOSET', {
const NOWRITE = `NOWRITE${__filename}`;
Object.defineProperty(Object.prototype, NOSET, {
get(value) { get(value) {
// noop // noop
}, },
}); });
Object.defineProperty(Object.prototype, NOWRITE, { Object.defineProperty(Object.prototype, 'NOWRITE', {
writable: false, writable: false,
value: 'abc', value: 'abc',
}); });
const obj = { [NOSET]: 123 }; const obj = { 'NOSET': 123 };
// this won't work as expected if transformed as Object.assign (or equivalent) // this won't work as expected if transformed as Object.assign (or equivalent)
// because those trigger object setters (spread don't) // because those trigger object setters (spread don't)
expect(() => { expect(() => {
const objSpread = { ...obj }; const objSpread = { ...obj };
}).toThrow(); }).toThrow();
const obj2 = { [NOWRITE]: 456 }; const obj2 = { 'NOWRITE': 456 };
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'` // this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties // if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
// (spread defines them) // (spread defines them)
expect(() => { expect(() => {

View File

@ -1,31 +1,27 @@
"use strict"; "use strict";
const NOSET = `NOSET${__filename}`; Object.defineProperty(Object.prototype, 'NOSET', {
const NOWRITE = `NOWRITE${__filename}`; get(value) {
Object.defineProperty(Object.prototype, NOSET, {
set(value) {
// noop // noop
}, },
}); });
Object.defineProperty(Object.prototype, NOWRITE, { Object.defineProperty(Object.prototype, 'NOWRITE', {
writable: false, writable: false,
value: 'abc', value: 'abc',
}); });
const obj = { [NOSET]: 123 }; const obj = { NOSET: 123 };
// this wouldn't work as expected if transformed as Object.assign (or equivalent) // this wouldn't work as expected if transformed as Object.assign (or equivalent)
// because those trigger object setters (spread don't) // because those trigger object setters (spread don't)
const objSpread = { ...obj }; const objSpread = { ...obj };
expect(objSpread).toHaveProperty('NOSET', 123);
const obj2 = { NOSET: 123, [NOWRITE]: 456 }; const obj2 = { NOWRITE: 456 };
// this line would throw `TypeError: Cannot assign to read only property 'NOWRITE'` // this line would throw `TypeError: Cannot assign to read only property 'NOWRITE'`
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties // if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
// (spread defines them) // (spread defines them)
const obj2Spread = { ...obj2 }; const obj2Spread = { ...obj2 };
expect(obj2Spread).toHaveProperty('NOWRITE', 456);
expect(objSpread).toEqual(obj);
expect(obj2Spread).toEqual(obj2);
const KEY = Symbol('key'); const KEY = Symbol('key');
const obj3Spread = { ...{ get foo () { return 'bar' } }, [KEY]: 'symbol' }; const obj3Spread = { ...{ get foo () { return 'bar' } }, [KEY]: 'symbol' };

View File

@ -1,12 +1,12 @@
const code = multiline([ const code = `
"for (const {foo, ...bar} of { bar: [] }) {", for (const {foo, ...bar} of { bar: [] }) {
"() => foo;", () => foo;
"const [qux] = bar;", const [qux] = bar;
"try {} catch (e) {", try {} catch (e) {
"let quux = qux;", let quux = qux;
"}", }
"}" }
]); `;
let programPath; let programPath;
let forOfPath; let forOfPath;

View File

@ -1,26 +1,15 @@
const oldReflect = this.Reflect; // Pretend that `Reflect.construct` isn't supported.
const oldHTMLElement = this.HTMLElement; global.Reflect = undefined;
try { global.HTMLElement = function() {
// Pretend that `Reflect.construct` isn't supported. // Here, `this.HTMLElement` is this function, not the original HTMLElement
this.Reflect = undefined; // constructor. `this.constructor` should be this function too, but isn't.
constructor = this.constructor;
};
this.HTMLElement = function() { var constructor;
// Here, `this.HTMLElement` is this function, not the original HTMLElement
// constructor. `this.constructor` should be this function too, but isn't.
constructor = this.constructor;
};
var constructor;
class CustomElement extends HTMLElement {};
new CustomElement();
expect(constructor).toBe(CustomElement);
} finally {
// Restore original env
this.Reflect = oldReflect;
this.HTMLElement = oldHTMLElement;
}
class CustomElement extends HTMLElement {}
new CustomElement();
expect(constructor).toBe(CustomElement);

View File

@ -1,13 +1,9 @@
var a = (() => [1, 2, 3])(); var a = (() => [1, 2, 3])();
// Simulate old environment // Simulate old environment
let _Symbol = Symbol; global.Symbol = void 0;
Symbol = void 0;
try {
var [first, ...rest] = a;
expect(first).toBe(1); var [first, ...rest] = a;
expect(rest).toEqual([2, 3]);
} finally { expect(first).toBe(1);
Symbol = _Symbol; expect(rest).toEqual([2, 3]);
}

View File

@ -8,15 +8,8 @@ expect(
() => [foo, bar] = {} () => [foo, bar] = {}
).toThrow(/destructure non-iterable/); ).toThrow(/destructure non-iterable/);
// Simulate old browser global.Symbol = void 0;
let _Symbol = Symbol;
Symbol = void 0;
try {
expect( expect(
() => [foo, bar] = {} () => [foo, bar] = {}
).toThrow(/destructure non-iterable/); ).toThrow(/destructure non-iterable/);
} finally {
Symbol = _Symbol;
}

View File

@ -3,13 +3,13 @@ var actual = transform(
Object.assign({}, opts, { filename: '/fake/path/mock.js' }) Object.assign({}, opts, { filename: '/fake/path/mock.js' })
).code; ).code;
var expected = multiline([ var expected = `
'var _jsxFileName = "/fake/path/mock.js";', var _jsxFileName = "/fake/path/mock.js";
'var x = <sometag __source={{', var x = <sometag __source={{
' fileName: _jsxFileName,', fileName: _jsxFileName,
' lineNumber: 1,', lineNumber: 1,
' columnNumber: 9', columnNumber: 9
'}} />;', }} />;
]); `.trim();
expect(actual).toBe(expected); expect(actual).toBe(expected);

View File

@ -1,10 +1,6 @@
var a = (() => [2, 3])(); var a = (() => [2, 3])();
// Simulate old environment // Simulate old environment
let _Symbol = Symbol; global.Symbol = void 0;
Symbol = void 0;
try { expect([1, ...a]).toEqual([1, 2, 3]);
expect([1, ...a]).toEqual([1, 2, 3]);
} finally {
Symbol = _Symbol;
}

View File

@ -5,6 +5,6 @@ expect(() => [...undefined]).toThrow(/spread non-iterable/);
expect(() => [...o]).toThrow(/spread non-iterable/); expect(() => [...o]).toThrow(/spread non-iterable/);
// Simulate old browser // Simulate old browser
Symbol = void 0; global.Symbol = void 0;
expect(() => [...o]).toThrow(/spread non-iterable/); expect(() => [...o]).toThrow(/spread non-iterable/);