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([
"function foo() {",
" var a = a ? a : a;",
"}",
]);
var code = `
function foo() {
var a = a ? a : a;
}
`;
transform(code, {
plugins: [

View File

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

View File

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

View File

@ -14,11 +14,16 @@ import fs from "fs";
import path from "path";
import vm from "vm";
import checkDuplicatedNodes from "babel-check-duplicated-nodes";
import QuickLRU from "quick-lru";
import diff from "jest-diff";
const moduleCache = {};
const testContext = vm.createContext({
const cachedScripts = new QuickLRU({ maxSize: 10 });
const contextModuleCache = new WeakMap();
const sharedTestContext = createContext();
function createContext() {
const context = vm.createContext({
...helpers,
process: process,
transform: babel.transform,
@ -26,22 +31,83 @@ const testContext = vm.createContext({
setImmediate: setImmediate,
expect,
});
testContext.global = testContext;
context.global = context;
const moduleCache = Object.create(null);
contextModuleCache.set(context, moduleCache);
// 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", __filename);
runModuleInTestContext(
"@babel/polyfill/dist/polyfill.min",
__filename,
context,
moduleCache,
);
// Populate the "babelHelpers" global with Babel's helper utilities.
runCodeInTestContext(buildExternalHelpers(), {
filename: path.join(__dirname, "babel-helpers-in-memory.js"),
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.
* 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, {
basedir: path.dirname(relativeFilename),
});
@ -50,23 +116,17 @@ function runModuleInTestContext(id: string, relativeFilename: string) {
// the context's global scope.
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;
const module = (moduleCache[filename] = {
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, {
const module = runCacheableScriptInTestContext(
filename,
displayErrors: true,
lineOffset: -1,
}).call(module.exports, module.exports, req, module, filename, dirname);
() => fs.readFileSync(filename, "utf8"),
context,
moduleCache,
);
moduleCache[filename] = module;
return module.exports;
}
@ -76,10 +136,15 @@ function runModuleInTestContext(id: string, relativeFilename: string) {
*
* 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 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 = {
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
// rely on 'this === global'.
const src = `(function(exports, require, module, __filename, __dirname, opts) {\n${code}\n});`;
return vm.runInContext(src, testContext, {
return vm.runInContext(src, context, {
filename,
displayErrors: true,
lineOffset: -1,
@ -183,13 +248,14 @@ function run(task) {
let resultExec;
if (execCode) {
const context = createContext();
const execOpts = getOpts(exec);
result = babel.transform(execCode, execOpts);
checkDuplicatedNodes(babel, result.ast);
execCode = result.code;
try {
resultExec = runCodeInTestContext(execCode, execOpts);
resultExec = runCodeInTestContext(execCode, execOpts, context);
} catch (err) {
// Pass empty location to include the whole file in the output.
err.message =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,6 @@
var a = (() => [2, 3])();
// Simulate old environment
let _Symbol = Symbol;
Symbol = void 0;
try {
global.Symbol = void 0;
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/);
// Simulate old browser
Symbol = void 0;
global.Symbol = void 0;
expect(() => [...o]).toThrow(/spread non-iterable/);