Skip to content

Commit

Permalink
[7.0] Run Babel's unittests in a custom sandbox (take 2). (#5263)
Browse files Browse the repository at this point in the history
* Run Babel's unittests in a custom sandbox (take 2).

* Add tests for sandboxing behavior.
  • Loading branch information
loganfsmyth committed Feb 4, 2017
1 parent ba0df23 commit 6fa6f59
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 21 deletions.
100 changes: 79 additions & 21 deletions packages/babel-helper-transform-fixture-test-runner/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,87 @@ import extend from "lodash/extend";
import merge from "lodash/merge";
import assert from "assert";
import chai from "chai";
import "babel-polyfill";
import fs from "fs";
import path from "path";
import vm from "vm";
import Module from "module";

const moduleCache = {};
const testContext = vm.createContext({
...helpers,
assert: chai.assert,
transform: babel.transform,
});
testContext.global = testContext;

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

// Populate the "babelHelpers" global with Babel's helper utilities.
runCodeInTestContext(buildExternalHelpers());

/**
* 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) {
// This code is a gross hack using internal APIs, but we also have the same logic in babel-core
// to resolve presets and plugins, so if this breaks, we'll have even worse issues to deal with.
const relativeMod = new Module();
relativeMod.id = relativeFilename;
relativeMod.filename = relativeFilename;
relativeMod.paths = Module._nodeModulePaths(path.dirname(relativeFilename));
const filename = Module._resolveFilename(id, relativeMod);

// Expose Node-internal modules if the tests want them. Note, this will not execute inside
// the context's global scope.
if (filename === id) return require(id);

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) {${src}\n});`;

vm.runInContext(code, testContext, {
filename,
displayErrors: true,
}).call(module.exports, module.exports, req, module, filename, dirname);

return module.exports;
}

/**
* Run the given snippet of code inside a CommonJS module.
*
* Exposed for unit tests, not for use as an API.
*/
export function runCodeInTestContext(code: string, opts: {filename?: string} = {}) {
const filename = opts.filename || null;
const dirname = filename ? path.dirname(filename) : null;
const req = filename ? ((id) => runModuleInTestContext(id, filename)) : null;

const module = {
id: filename,
exports: {},
};

const babelHelpers = eval(buildExternalHelpers(null, "var"));
// Expose the test options as "opts", but otherwise run the test in a CommonJS-like environment.
// 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) {${code}\n});`;
return vm.runInContext(src, testContext, {
filename,
displayErrors: true,
})(module.exports, req, module, filename, dirname, opts);
}

function wrapPackagesArray(type, names, optionsDir) {
return (names || []).map(function (val) {
Expand Down Expand Up @@ -68,12 +144,11 @@ function run(task) {

if (execCode) {
const execOpts = getOpts(exec);
const execDirName = path.dirname(exec.loc);
result = babel.transform(execCode, execOpts);
execCode = result.code;

try {
resultExec = runExec(execOpts, execCode, execDirName);
resultExec = runCodeInTestContext(execCode, execOpts);
} catch (err) {
err.message = exec.loc + ": " + err.message;
err.message += codeFrame(execCode);
Expand Down Expand Up @@ -114,23 +189,6 @@ function run(task) {
}
}

function runExec(opts, execCode, execDirname) {
const sandbox = {
...helpers,
babelHelpers,
assert: chai.assert,
transform: babel.transform,
opts,
exports: {},
require(id) {
return require(id[0] === "." ? path.resolve(execDirname, id) : id);
}
};

const fn = new Function(...Object.keys(sandbox), execCode);
return fn.apply(null, Object.values(sandbox));
}

export default function (
fixturesLoc: string,
name: string,
Expand Down
24 changes: 24 additions & 0 deletions packages/babel-helper-transform-fixture-test-runner/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import assert from "assert";
import { runCodeInTestContext } from "..";

describe("helper-transform-fixture-test-runner", function() {
it("should not execute code in Node's global context", function() {
try {
global.foo = "outer";
runCodeInTestContext(`
assert.equal(global.foo, undefined);
global.foo = "inner";
`);

assert.equal(global.foo, "outer");
runCodeInTestContext(`
assert.equal(global.foo, "inner");
`);
} finally {
delete global.foo;
runCodeInTestContext(`
delete global.foo;
`);
}
});
});

0 comments on commit 6fa6f59

Please sign in to comment.