Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isolated exec tests #11531

Merged
merged 10 commits into from Aug 10, 2020
Merged
@@ -1,8 +1,8 @@
var code = multiline([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we also remove multiline from packages/babel-helper-transform-fixture-test-runner/src/helpers.js? It is never exported from index.js.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not without a major bump. It's installed into the exec context, so it's available to tests. It's probably unused.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a concern around people using babel-helper-transform-fixture-test-runner as a dep I think we've never supported that as an API so it's fine (https://www.npmjs.com/package/@babel/helper-transform-fixture-test-runner). It's internal only essentially (we have no docs on it either), not that it's a good thing

"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