mirror of
https://github.com/github/codeql-action.git
synced 2025-12-27 17:50:07 +08:00
293 lines
8.9 KiB
JavaScript
293 lines
8.9 KiB
JavaScript
import {mkdir} from 'node:fs/promises';
|
|
import {createRequire} from 'node:module';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import {pathToFileURL} from 'node:url';
|
|
import {workerData} from 'node:worker_threads';
|
|
|
|
import setUpCurrentlyUnhandled from 'currently-unhandled';
|
|
import writeFileAtomic from 'write-file-atomic';
|
|
|
|
import {set as setChalk} from '../chalk.js';
|
|
import nowAndTimers from '../now-and-timers.cjs';
|
|
import providerManager from '../provider-manager.js';
|
|
import Runner from '../runner.js';
|
|
import serializeError from '../serialize-error.js';
|
|
|
|
import channel from './channel.cjs';
|
|
import {runCompletionHandlers} from './completion-handlers.js';
|
|
import lineNumberSelection from './line-numbers.js';
|
|
import {set as setOptions} from './options.cjs';
|
|
import {flags, refs, sharedWorkerTeardowns} from './state.cjs';
|
|
import {isRunningInThread, isRunningInChildProcess} from './utils.cjs';
|
|
|
|
const currentlyUnhandled = setUpCurrentlyUnhandled();
|
|
let runner;
|
|
|
|
let expectingExit = false;
|
|
|
|
const forceExit = () => {
|
|
expectingExit = true;
|
|
process.exit(1);
|
|
};
|
|
|
|
const avaIsDone = () => {
|
|
expectingExit = true;
|
|
runCompletionHandlers();
|
|
};
|
|
|
|
// Override process.exit with an undetectable replacement
|
|
// to report when it is called from a test (which it should never be).
|
|
const handleProcessExit = (target, thisArg, args) => {
|
|
if (!expectingExit) {
|
|
const error = new Error('Unexpected process.exit()');
|
|
Error.captureStackTrace(error, handleProcessExit);
|
|
channel.send({type: 'process-exit', stack: error.stack});
|
|
}
|
|
|
|
target.apply(thisArg, args);
|
|
};
|
|
|
|
process.exit = new Proxy(process.exit, {
|
|
apply: handleProcessExit,
|
|
});
|
|
|
|
const run = async options => {
|
|
setOptions(options);
|
|
setChalk(options.chalkOptions);
|
|
|
|
if (options.chalkOptions.level > 0) {
|
|
const {stdout, stderr} = process;
|
|
globalThis.console = Object.assign(globalThis.console, new console.Console({stdout, stderr, colorMode: true}));
|
|
}
|
|
|
|
let checkSelectedByLineNumbers;
|
|
try {
|
|
checkSelectedByLineNumbers = lineNumberSelection({
|
|
file: options.file,
|
|
lineNumbers: options.lineNumbers,
|
|
});
|
|
} catch (error) {
|
|
channel.send({type: 'line-number-selection-error', err: serializeError(error)});
|
|
checkSelectedByLineNumbers = () => false;
|
|
}
|
|
|
|
runner = new Runner({
|
|
checkSelectedByLineNumbers,
|
|
experiments: options.experiments,
|
|
failFast: options.failFast,
|
|
failWithoutAssertions: options.failWithoutAssertions,
|
|
file: options.file,
|
|
match: options.match,
|
|
projectDir: options.projectDir,
|
|
recordNewSnapshots: options.recordNewSnapshots,
|
|
serial: options.serial,
|
|
snapshotDir: options.snapshotDir,
|
|
updateSnapshots: options.updateSnapshots,
|
|
});
|
|
|
|
refs.runnerChain = runner.chain;
|
|
|
|
channel.peerFailed.then(() => { // eslint-disable-line promise/prefer-await-to-then
|
|
runner.interrupt();
|
|
});
|
|
|
|
runner.on('accessed-snapshots', filename => channel.send({type: 'accessed-snapshots', filename}));
|
|
runner.on('stateChange', state => channel.send(state));
|
|
|
|
runner.on('error', error => {
|
|
channel.send({type: 'internal-error', err: serializeError(error)});
|
|
forceExit();
|
|
});
|
|
|
|
runner.on('finish', async () => {
|
|
try {
|
|
const {touchedFiles} = await runner.saveSnapshotState();
|
|
if (touchedFiles) {
|
|
channel.send({type: 'touched-files', files: touchedFiles});
|
|
}
|
|
} catch (error) {
|
|
channel.send({type: 'internal-error', err: serializeError(error)});
|
|
forceExit();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await Promise.all(sharedWorkerTeardowns.map(fn => fn()));
|
|
} catch (error) {
|
|
channel.send({type: 'uncaught-exception', err: serializeError(error)});
|
|
forceExit();
|
|
return;
|
|
}
|
|
|
|
channel.send({type: 'worker-finished'});
|
|
|
|
// Reference the channel until the worker is freed. This should prevent Node.js from terminating the child process
|
|
// prematurely, which has been witnessed on Windows. See discussion at
|
|
// <https://github.com/avajs/ava/issues/3390#issuecomment-3056119361>.
|
|
channel.ref();
|
|
await channel.workerFreed;
|
|
channel.unref();
|
|
|
|
nowAndTimers.setImmediate(() => {
|
|
const unhandled = currentlyUnhandled();
|
|
if (unhandled.length === 0) {
|
|
return avaIsDone();
|
|
}
|
|
|
|
for (const rejection of unhandled) {
|
|
channel.send({type: 'unhandled-rejection', err: serializeError(rejection.reason, {testFile: options.file})});
|
|
}
|
|
|
|
forceExit();
|
|
});
|
|
});
|
|
|
|
process.on('uncaughtException', error => {
|
|
channel.send({type: 'uncaught-exception', err: serializeError(error, {testFile: options.file})});
|
|
forceExit();
|
|
});
|
|
|
|
// Store value to prevent required modules from modifying it.
|
|
const testPath = options.file;
|
|
|
|
const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
|
|
.filter(([, type]) => type === 'module')
|
|
.map(([extension]) => extension);
|
|
|
|
// Install before processing options.require, so if helpers are added to the
|
|
// require configuration the *compiled* helper will be loaded.
|
|
const {projectDir, providerStates = []} = options;
|
|
const providers = [];
|
|
await Promise.all(providerStates.map(async ({type, state, protocol}) => {
|
|
if (type === 'typescript') {
|
|
const provider = await providerManager.typescript(projectDir, {protocol});
|
|
providers.push(provider.worker({extensionsToLoadAsModules, state}));
|
|
}
|
|
}));
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const load = async ref => {
|
|
for (const provider of providers) {
|
|
if (provider.canLoad(ref)) {
|
|
return provider.load(ref, {requireFn: require});
|
|
}
|
|
}
|
|
|
|
for (const extension of extensionsToLoadAsModules) {
|
|
if (ref.endsWith(`.${extension}`)) {
|
|
return import(pathToFileURL(ref));
|
|
}
|
|
}
|
|
|
|
// We still support require() since it's more easily monkey-patched.
|
|
return require(ref);
|
|
};
|
|
|
|
const loadRequiredModule = async ref => {
|
|
// If the provider can load the module, assume it's a local file and not a
|
|
// dependency.
|
|
for (const provider of providers) {
|
|
if (provider.canLoad(ref)) {
|
|
return provider.load(ref, {requireFn: require});
|
|
}
|
|
}
|
|
|
|
// Try to load the module as a file, relative to the project directory.
|
|
// Match load() behavior.
|
|
const fullPath = path.resolve(projectDir, ref);
|
|
try {
|
|
for (const extension of extensionsToLoadAsModules) {
|
|
if (fullPath.endsWith(`.${extension}`)) {
|
|
return await import(pathToFileURL(fullPath)); // eslint-disable-line no-await-in-loop
|
|
}
|
|
}
|
|
|
|
return require(fullPath);
|
|
} catch (error) {
|
|
// If the module could not be found, assume it's not a file but a dependency.
|
|
if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') {
|
|
return importFromProject(ref);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
let importFromProject = async ref => {
|
|
// Do not use the cacheDir since it's not guaranteed to be inside node_modules.
|
|
const avaCacheDir = path.join(projectDir, 'node_modules', '.cache', 'ava');
|
|
await mkdir(avaCacheDir, {recursive: true});
|
|
const stubPath = path.join(avaCacheDir, 'import-from-project.mjs');
|
|
await writeFileAtomic(stubPath, 'export const importFromProject = ref => import(ref);\n');
|
|
({importFromProject} = await import(pathToFileURL(stubPath)));
|
|
return importFromProject(ref);
|
|
};
|
|
|
|
try {
|
|
for await (const [ref, ...args] of (options.require ?? [])) {
|
|
const loadedModule = await loadRequiredModule(ref);
|
|
|
|
if (typeof loadedModule === 'function') { // CJS module
|
|
await loadedModule(...args);
|
|
} else if (typeof loadedModule.default === 'function') { // ES module, or exports.default from CJS
|
|
const {default: fn} = loadedModule;
|
|
await fn(...args);
|
|
}
|
|
}
|
|
|
|
if (options.debug?.port !== undefined && options.debug?.host !== undefined) {
|
|
// If an inspector was active when the main process started, and is
|
|
// already active for the worker process, do not open a new one.
|
|
const {default: inspector} = await import('node:inspector');
|
|
if (!options.debug.active || inspector.url() === undefined) {
|
|
inspector.open(options.debug.port, options.debug.host, true);
|
|
}
|
|
|
|
if (options.debug.break) {
|
|
debugger; // eslint-disable-line no-debugger
|
|
}
|
|
}
|
|
|
|
await load(testPath);
|
|
|
|
if (flags.loadedMain) {
|
|
// Unreference the channel if the test file required AVA. This stops it
|
|
// from keeping the event loop busy, which means the `beforeExit` event can be
|
|
// used to detect when tests stall.
|
|
channel.unref();
|
|
} else {
|
|
channel.send({type: 'missing-ava-import'});
|
|
forceExit();
|
|
}
|
|
} catch (error) {
|
|
channel.send({type: 'uncaught-exception', err: serializeError(error, {testFile: options.file})});
|
|
forceExit();
|
|
}
|
|
};
|
|
|
|
const onError = error => {
|
|
// There shouldn't be any errors, but if there are we may not have managed
|
|
// to bootstrap enough code to serialize them. Re-throw and let the process
|
|
// crash.
|
|
setImmediate(() => {
|
|
throw error;
|
|
});
|
|
};
|
|
|
|
let options;
|
|
if (isRunningInThread) {
|
|
channel.send({type: 'starting'}); // AVA won't terminate the worker thread until it's seen this message.
|
|
({options} = workerData);
|
|
delete workerData.options; // Don't allow user code access.
|
|
} else if (isRunningInChildProcess) {
|
|
channel.send({type: 'ready-for-options'});
|
|
options = await channel.options;
|
|
}
|
|
|
|
try {
|
|
await run(options);
|
|
} catch (error) {
|
|
onError(error);
|
|
}
|