@@ -10,20 +10,24 @@ const {
1010 ObjectAssign,
1111 PromisePrototypeThen,
1212 SafePromiseAll,
13+ SafePromiseAllReturnVoid,
14+ SafePromiseAllSettledReturnVoid,
15+ SafeMap,
1316 SafeSet,
1417} = primordials;
1518
1619const { spawn } = require('child_process');
1720const { readdirSync, statSync } = require('fs');
1821// TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern.
1922const { createInterface } = require('readline');
23+ const { FilesWatcher } = require('internal/watch_mode/files_watcher');
2024const console = require('internal/console/global');
2125const {
2226 codes: {
2327 ERR_TEST_FAILURE,
2428 },
2529} = require('internal/errors');
26- const { validateArray } = require('internal/validators');
30+ const { validateArray, validateBoolean } = require('internal/validators');
2731const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
2832const { kEmptyObject } = require('internal/util');
2933const { createTestTree } = require('internal/test_runner/harness');
@@ -34,8 +38,11 @@ const {
3438} = require('internal/test_runner/utils');
3539const { basename, join, resolve } = require('path');
3640const { once } = require('events');
41+ const {
42+ triggerUncaughtException,
43+ } = internalBinding('errors');
3744
38- const kFilterArgs = ['--test'];
45+ const kFilterArgs = ['--test', '--watch' ];
3946
4047// TODO(cjihrig): Replace this with recursive readdir once it lands.
4148function processPath(path, testFiles, options) {
@@ -112,17 +119,28 @@ function getRunArgs({ path, inspectPort }) {
112119 return argv;
113120}
114121
122+ const runningProcesses = new SafeMap();
123+ const runningSubtests = new SafeMap();
115124
116- function runTestFile(path, root, inspectPort) {
125+ function runTestFile(path, root, inspectPort, filesWatcher ) {
117126 const subtest = root.createSubtest(Test, path, async (t) => {
118127 const args = getRunArgs({ path, inspectPort });
128+ const stdio = ['pipe', 'pipe', 'pipe'];
129+ const env = { ...process.env };
130+ if (filesWatcher) {
131+ stdio.push('ipc');
132+ env.WATCH_REPORT_DEPENDENCIES = '1';
133+ }
119134
120- const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' });
135+ const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio });
136+ runningProcesses.set(path, child);
121137 // TODO(cjihrig): Implement a TAP parser to read the child's stdout
122138 // instead of just displaying it all if the child fails.
123139 let err;
124140 let stderr = '';
125141
142+ filesWatcher?.watchChildProcessModules(child, path);
143+
126144 child.on('error', (error) => {
127145 err = error;
128146 });
@@ -145,6 +163,8 @@ function runTestFile(path, root, inspectPort) {
145163 child.stdout.toArray({ signal: t.signal }),
146164 ]);
147165
166+ runningProcesses.delete(path);
167+ runningSubtests.delete(path);
148168 if (code !== 0 || signal !== null) {
149169 if (!err) {
150170 err = ObjectAssign(new ERR_TEST_FAILURE('test failed', kSubtestsFailed), {
@@ -165,21 +185,57 @@ function runTestFile(path, root, inspectPort) {
165185 return subtest.start();
166186}
167187
188+ function watchFiles(testFiles, root, inspectPort) {
189+ const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter' });
190+ filesWatcher.on('changed', ({ owners }) => {
191+ filesWatcher.unfilterFilesOwnedBy(owners);
192+ PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => {
193+ if (!owners.has(file)) {
194+ return;
195+ }
196+ const runningProcess = runningProcesses.get(file);
197+ if (runningProcess) {
198+ runningProcess.kill();
199+ await once(runningProcess, 'exit');
200+ }
201+ await runningSubtests.get(file);
202+ runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher));
203+ }, undefined, (error) => {
204+ triggerUncaughtException(error, true /* fromPromise */);
205+ }));
206+ });
207+ return filesWatcher;
208+ }
209+
168210function run(options) {
169211 if (options === null || typeof options !== 'object') {
170212 options = kEmptyObject;
171213 }
172- const { concurrency, timeout, signal, files, inspectPort } = options;
214+ const { concurrency, timeout, signal, files, inspectPort, watch } = options;
173215
174216 if (files != null) {
175217 validateArray(files, 'options.files');
176218 }
219+ if (watch != null) {
220+ validateBoolean(watch, 'options.watch');
221+ }
177222
178223 const root = createTestTree({ concurrency, timeout, signal });
179224 const testFiles = files ?? createTestFileList();
180225
181- PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root, inspectPort)),
182- () => root.postRun());
226+ let postRun = () => root.postRun();
227+ let filesWatcher;
228+ if (watch) {
229+ filesWatcher = watchFiles(testFiles, root, inspectPort);
230+ postRun = undefined;
231+ }
232+
233+ PromisePrototypeThen(SafePromiseAllSettledReturnVoid(testFiles, (path) => {
234+ const subtest = runTestFile(path, root, inspectPort, filesWatcher);
235+ runningSubtests.set(path, subtest);
236+ return subtest;
237+ }), postRun);
238+
183239
184240 return root.reporter;
185241}
0 commit comments