UNPKG

peter

Version:
175 lines (148 loc) 5.87 kB
/** * @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 */ 'use strict'; 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);