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

Detect process.exit() called from tests #3080

Merged
merged 13 commits into from Sep 4, 2022
Merged
41 changes: 30 additions & 11 deletions lib/worker/base.js
Expand Up @@ -19,6 +19,35 @@ import {flags, refs, sharedWorkerTeardowns} from './state.cjs';
import {isRunningInThread, isRunningInChildProcess} from './utils.cjs';

const currentlyUnhandled = setUpCurrentlyUnhandled();
let runner = Object.create(null);

// Override process.exit with an undetectable replacement
// to report when it is called from a test (which it should never be).
const {apply} = Reflect;
const realExit = process.exit;

async function exit(code, forceSync = false) {
dependencyTracking.flush();
if (!forceSync) {
await channel.flush();
}

apply(realExit, process, [code]);
}

process.exit = new Proxy(realExit, {
apply(fn, receiver, args) {
channel.send({type: 'internal-error', err: serializeError('Forced exit error', false, new Error('Unexpected process.exit()'), runner.file)});
gibson042 marked this conversation as resolved.
Show resolved Hide resolved
novemberborn marked this conversation as resolved.
Show resolved Hide resolved

// Make sure to extract the code only from `args` rather than e.g. `Array.prototype`.
// This level of paranoia is usually unwarranted, but we're dealing with test code
// that has already colored outside the lines.
const code = args.length > 0 ? args[0] : undefined;

// Force a synchronous exit as guaranteed by the real process.exit().
exit(code, true);
},
});

const run = async options => {
setOptions(options);
Expand All @@ -29,16 +58,6 @@ const run = async options => {
global.console = Object.assign(global.console, new console.Console({stdout, stderr, colorMode: true}));
}

async function exit(code) {
if (!process.exitCode) {
process.exitCode = code;
}

dependencyTracking.flush();
await channel.flush();
process.exit(); // eslint-disable-line unicorn/no-process-exit
}

let checkSelectedByLineNumbers;
try {
checkSelectedByLineNumbers = lineNumberSelection({
Expand All @@ -50,7 +69,7 @@ const run = async options => {
checkSelectedByLineNumbers = () => false;
}

const runner = new Runner({
runner = new Runner({
checkSelectedByLineNumbers,
experiments: options.experiments,
failFast: options.failFast,
Expand Down
11 changes: 11 additions & 0 deletions test/helpers/exec.js
Expand Up @@ -69,7 +69,9 @@ export const fixture = async (args, options = {}) => {
const stats = {
failed: [],
failedHooks: [],
internalErrors: [],
passed: [],
selectedTestCount: 0,
sharedWorkerErrors: [],
skipped: [],
todo: [],
Expand All @@ -92,7 +94,16 @@ export const fixture = async (args, options = {}) => {
break;
}

case 'internal-error': {
const {testFile} = statusEvent;
const statObject = {file: normalizePath(workingDir, testFile)};
errors.set(statObject, statusEvent.err);
stats.internalErrors.push(statObject);
break;
}

case 'selected-test': {
stats.selectedTestCount++;
if (statusEvent.skip) {
const {title, testFile} = statusEvent;
stats.skipped.push({title, file: normalizePath(workingDir, testFile)});
Expand Down
8 changes: 8 additions & 0 deletions test/test-process-exit/fixtures/package.json
@@ -0,0 +1,8 @@
{
"type": "module",
"ava": {
"files": [
"*.js"
]
}
}
17 changes: 17 additions & 0 deletions test/test-process-exit/fixtures/process-exit.js
@@ -0,0 +1,17 @@
import test from 'ava';

test('good', t => {
t.pass();
});

test('process.exit', async t => {
t.pass();
await new Promise(resolve => {
setImmediate(resolve);
});
process.exit(0); // eslint-disable-line unicorn/no-process-exit
});

test('still good', t => {
t.pass();
});
13 changes: 13 additions & 0 deletions test/test-process-exit/test.js
@@ -0,0 +1,13 @@
import test from '@ava/test';

import {fixture} from '../helpers/exec.js';

test('process.exit is intercepted', async t => {
const result = await t.throwsAsync(fixture(['process-exit.js']));
t.true(result.failed);
t.like(result, {timedOut: false, isCanceled: false, killed: false});
t.is(result.stats.selectedTestCount, 3);
t.is(result.stats.passed.length, 2);
const error = result.stats.getError(result.stats.internalErrors[0]);
t.is(error.message, 'Unexpected process.exit()');
});