peter
Version:
Peter Test Framework
175 lines (148 loc) • 5.87 kB
JavaScript
/**
* @file src/drivers/simple-context-runner.js
* @author Ryan Saweczko, ryansaweczko@kingsds.network
* @author Brandon Christie, brandon@distributive.network
* @date July 2020, July 2024
*
* This test runner will wrap the contents of a test file
* in an async function to allow top-level await, then execute
* the contents. It is run in a separate node process, spawned
* by ./simple.js
*/
;
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const getopt = require('posix-getopt');
const { enableDebugging, debugLog } = require('../utils/debug');
const debugContextRunner = debugLog.extend('simple-context-runner');
const options = {};
const parser = new getopt.BasicParser('i(invert)s(syntax)d(debug)', process.argv);
let option;
while ((option = parser.getopt()) !== undefined)
{
switch (option.option)
{
case '?':
process.exit(1);
default:
options[option.option] = option.optarg ?? true;
break;
}
}
// Add long option names from `parser.gop_aliases`
for (const alias in parser.gop_aliases)
{
if (options.hasOwnProperty(parser.gop_aliases[alias]))
options[alias] = options[parser.gop_aliases[alias]];
}
if (process.argv.slice(parser.optind()).length)
[ options.testPath ] = process.argv.slice(parser.optind());
else
{
console.error(`missing argument -- testPath`);
process.exit(1);
}
const { invert, syntax, debug, testPath } = options;
var __peter = { invert }; /* peter namespace inside test */
if (debug) {
enableDebugging();
}
const SHEBANG_REGEX = /^#!.*\r{0,1}\n/m;
async function PETER$$evalWithTopLevelAwait(filename)
{
const createRequire = require('module').createRequire || require('module').createRequireFromPath; // for compatibility with node versions before and after v12.2.0
var code; /**< code read from disk */
var bareCode; /**< the code with no comments in it */
var funPrologue; /**< the start of a symbol-injecting function wrapper factory that returns fun */
var funEpilogue; /**< the end of a symbol-injecting function wrapper factory that returns fun */
var fun; /**< the compiled function wrapper */
var isStrictMode; /**< true when code is ES5 Strict Mode code */
var symbols = {}; /**< symbols to inject into the test code */
var lineOffset = 0;
/* Parse the code to determine the line offset and if we are in Strict Mode or not */
code = fs.readFileSync(path.resolve(filename), 'utf-8');
if (code.match(SHEBANG_REGEX)) {
code = code.replace(SHEBANG_REGEX,"");
lineOffset = 1;
}
bareCode = code
.replace(/(\/\*([\s\S]*?)\*\/)|(\/\/(.*)$)/gm /* comments */, '')
.replace(/^[\s]*[\r\n]/gm, '')
;
isStrictMode = !!bareCode.match(/^[\s ]*['"]use strict['"][\s]*(;?|[\r\n])/);
/**
* createRequire uses process.mainModule as the main property of the new require.
* To keep the environment closer to outside of the test harness, where the file would
* be run directly, we need to update this.
*/
const mod = require('module');
const mainModule = new mod(path.resolve(filename));
mainModule.filename = path.resolve(filename);
mainModule.paths = mod._nodeModulePaths(path.resolve(filename));
process.mainModule = mainModule;
process.argv.splice(1,1) /* remove the simple-context-runner from argv */
/* Create a symbol object to pass to the code under test as its lexical context */
Object.assign(symbols, global);
symbols.__peter = __peter;
symbols.console = console;
symbols.__filename = path.resolve(filename);
symbols.__dirname = path.dirname(symbols.__filename);
symbols.require = createRequire(symbols.__filename);
symbols.exports = mainModule.exports;
symbols.module = mainModule;
if (!syntax)
{ /* Check for syntax errors when we can - syntax tests need to let tests with syntax errors execute */
try { /* ruler: 012345678901234567890123456789012345 */
new vm.Script(`async function syntaxTestWrapper() {${code}}`, {filename, lineOffset, columnOffset: -35});
} catch(error) {
error.code = error.code || 'ESYNTAX';
console.error(require('../utils/stack').shorten(error));
process.exit(4);
}
}
/* Create the source code for an async function which has symbols to effectively
* emulate the normal Node running environment, plus top-level await.
*/
funPrologue = `(async (${Object.keys(symbols).join(',')}) => {`
funEpilogue = '})';
if (isStrictMode)
funPrologue = '"use strict";' + funPrologue;
if (debug)
funPrologue += 'debugger;';
/* A non-default value of `null` is used to detect if process.exitCode has
* been mutated by code under test, which is needed for correct behaviour of
* inverse tests.
*/
process.exitCode = null;
/* Invoke the wrapper and code, creating a function; invoke the function, injecting the symbols */
try
{
fun = vm.runInThisContext(`${funPrologue}${code}${funEpilogue}`,
{filename, lineOffset, columnOffset: -funPrologue.length });
await fun.apply(null, Object.values(symbols));
// If unmodified, assume the test passed.
if (process.exitCode === null)
process.exitCode = 0;
}
catch(error)
{
process.exitCode = 1;
console.error(require('../utils/stack').shorten(error));
setImmediate(process.exit);
}
}
process.on('exit', () => {
debugContextRunner('Exit Code:', process.exitCode);
if (invert)
{
process.exitCode = process.exitCode === 0 ? 99 : 0;
debugContextRunner('Final Exit Code:', process.exitCode);
}
});
const __emit = process.emit;
process.emit = function (name, data) {
if (name !== 'warning' || data?.name !== 'ExperimentalWarning')
__emit.apply(process, arguments);
};
PETER$$evalWithTopLevelAwait(testPath);