Skip to content

Commit bd64529

Browse files
committed
test_runner: add --experimental-test-cwd option to test runner
1 parent 3419393 commit bd64529

File tree

8 files changed

+35
-21
lines changed

8 files changed

+35
-21
lines changed

doc/api/cli.md

+12
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,18 @@ generated as part of the test runner output. If no tests are run, a coverage
10971097
report is not generated. See the documentation on
10981098
[collecting code coverage from tests][] for more details.
10991099

1100+
### `--experimental-test-cwd=directory`
1101+
1102+
<!-- YAML
1103+
added: CHANGEME
1104+
-->
1105+
1106+
> Stability: 1.0 - Early development
1107+
1108+
Set the current working directory for the test runner.
1109+
If not specified, the current working directory is used.
1110+
This flag is particularly useful when running tests from a different directory.
1111+
11001112
### `--experimental-test-isolation=mode`
11011113

11021114
<!-- YAML

lib/internal/test_runner/coverage.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ function sortCoverageFiles(a, b) {
469469

470470
function setupCoverage(options) {
471471
let originalCoverageDirectory = process.env.NODE_V8_COVERAGE;
472-
const cwd = process.cwd();
472+
const cwd = options.testCwd;
473473

474474
if (originalCoverageDirectory) {
475475
// NODE_V8_COVERAGE was already specified. Convert it to an absolute path

lib/internal/test_runner/harness.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function createTestTree(rootTestOptions, globalOptions) {
8383
return globalRoot;
8484
}
8585

86-
function createProcessEventHandler(eventName, rootTest) {
86+
function createProcessEventHandler(eventName, rootTest, cwd) {
8787
return (err) => {
8888
if (rootTest.harness.bootstrapPromise) {
8989
// Something went wrong during the asynchronous portion of bootstrapping
@@ -109,7 +109,7 @@ function createProcessEventHandler(eventName, rootTest) {
109109
const name = test.hookType ? `Test hook "${test.hookType}"` : `Test "${test.name}"`;
110110
let locInfo = '';
111111
if (test.loc) {
112-
const relPath = relative(process.cwd(), test.loc.file);
112+
const relPath = relative(cwd, test.loc.file);
113113
locInfo = ` at ${relPath}:${test.loc.line}:${test.loc.column}`;
114114
}
115115

@@ -194,9 +194,9 @@ function setupProcessState(root, globalOptions) {
194194
hook.enable();
195195

196196
const exceptionHandler =
197-
createProcessEventHandler('uncaughtException', root);
197+
createProcessEventHandler('uncaughtException', root, globalOptions.testCwd);
198198
const rejectionHandler =
199-
createProcessEventHandler('unhandledRejection', root);
199+
createProcessEventHandler('unhandledRejection', root, globalOptions.testCwd);
200200
const coverage = configureCoverage(root, globalOptions);
201201
const exitHandler = async () => {
202202
if (root.subtests.length === 0 && (root.hooks.before.length > 0 || root.hooks.after.length > 0)) {

lib/internal/test_runner/runner.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const {
3232

3333
const { spawn } = require('child_process');
3434
const { finished } = require('internal/streams/end-of-stream');
35-
const { resolve } = require('path');
35+
const { resolve, sep, isAbsolute } = require('path');
3636
const { DefaultDeserializer, DefaultSerializer } = require('v8');
3737
const { getOptionValue } = require('internal/options');
3838
const { Interface } = require('internal/readline/interface');
@@ -62,7 +62,6 @@ const { isRegExp } = require('internal/util/types');
6262
const { pathToFileURL } = require('internal/url');
6363
const {
6464
createDeferredPromise,
65-
getCWDURL,
6665
kEmptyObject,
6766
} = require('internal/util');
6867
const { kEmitMessage } = require('internal/test_runner/tests_stream');
@@ -96,7 +95,7 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
9695
});
9796

9897
const kIsolatedProcessName = Symbol('kIsolatedProcessName');
99-
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch'];
98+
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch', '--experimental-test-cwd'];
10099
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
101100
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
102101

@@ -130,7 +129,7 @@ function filterExecArgv(arg, i, arr) {
130129
!ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`));
131130
}
132131

133-
function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, testSkipPatterns, only }) {
132+
function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, testSkipPatterns, only, cwd }) {
134133
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
135134
if (forceExit === true) {
136135
ArrayPrototypePush(argv, '--test-force-exit');
@@ -629,7 +628,6 @@ function run(options = kEmptyObject) {
629628
setup, // This line can be removed when parseCommandLine() is removed here.
630629
};
631630
const root = createTestTree(rootTestOptions, globalOptions);
632-
633631
let testFiles = files ?? createTestFileList(globPatterns, cwd);
634632

635633
if (shard) {
@@ -680,7 +678,9 @@ function run(options = kEmptyObject) {
680678
};
681679
} else if (isolation === 'none') {
682680
if (watch) {
683-
filesWatcher = watchFiles(testFiles, opts);
681+
// TODO
682+
const absoluteTestFiles = ArrayPrototypeMap(testFiles, (file) => isAbsolute(file) ? file : resolve(cwd, file));
683+
filesWatcher = watchFiles(absoluteTestFiles, opts);
684684
runFiles = async () => {
685685
root.harness.bootstrapPromise = null;
686686
root.harness.buildPromise = null;
@@ -693,7 +693,7 @@ function run(options = kEmptyObject) {
693693
const { promise, resolve: finishBootstrap } = createDeferredPromise();
694694

695695
await root.runInAsyncScope(async () => {
696-
const parentURL = getCWDURL().href;
696+
const parentURL = pathToFileURL(cwd + sep).href;
697697
const cascadedLoader = esmLoader.getOrInitializeCascadedLoader();
698698
let topLevelTestCount = 0;
699699

@@ -705,8 +705,8 @@ function run(options = kEmptyObject) {
705705
}
706706

707707
for (let i = 0; i < testFiles.length; ++i) {
708-
const testFile = resolve(cwd, testFiles[i]);
709-
const fileURL = pathToFileURL(testFile);
708+
const testFile = testFiles[i];
709+
const fileURL = pathToFileURL(resolve(cwd, testFile));
710710
const parent = i === 0 ? undefined : parentURL;
711711
let threw = false;
712712
let importError;

lib/internal/test_runner/utils.js

+2
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ function parseCommandLine() {
200200
let functionCoverage;
201201
let destinations;
202202
let isolation;
203+
let testCwd = getOptionValue('--experimental-test-cwd') || process.cwd()
203204
let only = getOptionValue('--test-only');
204205
let reporters;
205206
let shard;
@@ -311,6 +312,7 @@ function parseCommandLine() {
311312
destinations,
312313
forceExit,
313314
isolation,
315+
testCwd,
314316
branchCoverage,
315317
functionCoverage,
316318
lineCoverage,

src/node_options.cc

+3
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
671671
&EnvironmentOptions::test_coverage_lines,
672672
kAllowedInEnvvar);
673673

674+
AddOption("--experimental-test-cwd",
675+
"specify the working directory for the test runner",
676+
&EnvironmentOptions::test_runner_cwd);
674677
AddOption("--experimental-test-isolation",
675678
"configures the type of test isolation used in the test runner",
676679
&EnvironmentOptions::test_isolation);

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ class EnvironmentOptions : public Options {
195195
bool test_only = false;
196196
bool test_udp_no_try_send = false;
197197
std::string test_isolation = "process";
198+
std::string test_runner_cwd;
198199
std::string test_shard;
199200
std::vector<std::string> test_skip_pattern;
200201
std::vector<std::string> coverage_include_pattern;

test/parallel/test-runner-no-isolation-different-cwd.mjs

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import { allowGlobals, mustCall } from '../common/index.mjs';
22
import * as fixtures from '../common/fixtures.mjs';
3-
import { strictEqual } from 'node:assert';
3+
import { deepStrictEqual } from 'node:assert';
44
import { run } from 'node:test';
55

66
const stream = run({
77
cwd: fixtures.path('test-runner', 'no-isolation'),
88
isolation: 'none',
99
});
1010

11-
let errors = 0;
12-
stream.on('test:fail', () => {
13-
errors++;
14-
});
11+
1512
stream.on('test:pass', mustCall(5));
1613
// eslint-disable-next-line no-unused-vars
1714
for await (const _ of stream);
18-
strictEqual(errors, 0);
1915
allowGlobals(globalThis.GLOBAL_ORDER);
20-
strictEqual(globalThis.GLOBAL_ORDER, [
16+
deepStrictEqual(globalThis.GLOBAL_ORDER, [
2117
'before one: <root>',
2218
'suite one',
2319
'before two: <root>',

0 commit comments

Comments
 (0)