Skip to content

Commit

Permalink
Isolated exec tests (#11531)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jridgewell and JLHwung committed Aug 10, 2020
1 parent 3bff1ce commit a5bc486
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 143 deletions.
@@ -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: [
Expand Down
@@ -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: [
Expand Down
Expand Up @@ -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"
}
Expand Down
138 changes: 102 additions & 36 deletions packages/babel-helper-transform-fixture-test-runner/src/index.js
Expand Up @@ -14,34 +14,100 @@ 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({
...helpers,
process: process,
transform: babel.transform,
setTimeout: setTimeout,
setImmediate: setImmediate,
expect,
});
testContext.global = testContext;
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,
setTimeout: setTimeout,
setImmediate: setImmediate,
expect,
});
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/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;
}

// 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);
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);
}

// Populate the "babelHelpers" global with Babel's helper utilities.
runCodeInTestContext(buildExternalHelpers(), {
filename: path.join(__dirname, "babel-helpers-in-memory.js"),
});
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),
});
Expand All @@ -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;
}
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 =
Expand Down
@@ -1,27 +1,25 @@
"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 };
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
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)
expect(() => {
Expand Down
@@ -1,27 +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 };
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
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)
expect(() => {
Expand Down
@@ -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' };
Expand Down
@@ -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;
Expand Down

0 comments on commit a5bc486

Please sign in to comment.