Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const {
SafePromiseAll,
SafePromiseAllReturnVoid,
SafePromiseAllSettledReturnVoid,
SafePromiseRace,
SafeSet,
String,
StringFromCharCode,
Expand Down Expand Up @@ -293,10 +294,15 @@ class FileTest extends Test {
#rawBufferSize = 0;
#reportedChildren = 0;
#pendingPartialV8Header = false;
#reportFinished;
#resolveReportFinished;
failedSubtests = false;

constructor(options) {
super(options);
const { promise, resolve } = PromiseWithResolvers();
this.#reportFinished = promise;
this.#resolveReportFinished = resolve;
this.loc ??= {
__proto__: null,
line: 1,
Expand All @@ -306,6 +312,10 @@ class FileTest extends Test {
this.timeout = null;
}

get reportFinished() {
return this.#reportFinished;
}

willBeFilteredByTags() {
// File wrappers have no tags of their own. Tag filtering applies to the
// tests inside the file, which run in a child process (or in-process
Expand Down Expand Up @@ -518,6 +528,9 @@ class FileTest extends Test {

deserializer.readHeader();
const item = deserializer.readValue();
if (item.type === 'test:summary') {
this.#resolveReportFinished();
}
this.addToReport(item);
}
}
Expand Down Expand Up @@ -592,9 +605,17 @@ function runTestFile(path, filesWatcher, opts) {
});
});

const stdoutFinished = finished(
child.stdout,
{ __proto__: null, signal: t.signal },
);
const reportFinished = opts.forceExit ?
SafePromiseRace([stdoutFinished, subtest.reportFinished]) :
stdoutFinished;

const { 0: { 0: code, 1: signal } } = await SafePromiseAll([
once(child, 'exit', { __proto__: null, signal: t.signal }),
finished(child.stdout, { __proto__: null, signal: t.signal }),
reportFinished,
]);

// Close readline interface to prevent memory leak
Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/test-runner/force-exit-inherited-stdio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const { spawn } = require('node:child_process');
const { test } = require('node:test');

test('leaks a child with inherited stdio', () => {
spawn(
process.execPath,
[
'-e',
`setTimeout(() => {}, ${process.env.TEST_RUNNER_STALL_MS})`,
],
{ stdio: 'inherit' },
);
});
41 changes: 41 additions & 0 deletions test/parallel/test-runner-force-exit-inherited-stdio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const common = require('../common');

const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const fixtures = require('../common/fixtures');

const fixture =
fixtures.path('test-runner/force-exit-inherited-stdio.js');

const maxDuration = common.platformTimeout(2000);
const stallDuration = common.platformTimeout(4000);
const start = Date.now();

const result = spawnSync(
process.execPath,
[
'--test',
'--test-force-exit',
fixture,
],
{
encoding: 'utf8',
env: {
...process.env,
TEST_RUNNER_STALL_MS: String(stallDuration),
},
},
);

const duration = Date.now() - start;

assert.strictEqual(result.status, 0);
assert.strictEqual(result.signal, null);
assert.strictEqual(result.stderr, '');

assert.ok(
duration < maxDuration,
`test runner took ${duration}ms to exit`,
);
Loading