@@ -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,9 +38,12 @@ const {
3438} = require('internal/test_runner/utils');
3539const { basename, join, resolve } = require('path');
3640const { once } = require('events');
37- const { exitCodes: { kGenericUserError } } = internalBinding('errors');
41+ const {
42+ triggerUncaughtException,
43+ exitCodes: { kGenericUserError },
44+ } = internalBinding('errors');
3845
39- const kFilterArgs = ['--test'];
46+ const kFilterArgs = ['--test', '--watch' ];
4047
4148// TODO(cjihrig): Replace this with recursive readdir once it lands.
4249function processPath(path, testFiles, options) {
@@ -113,17 +120,28 @@ function getRunArgs({ path, inspectPort }) {
113120 return argv;
114121}
115122
123+ const runningProcesses = new SafeMap();
124+ const runningSubtests = new SafeMap();
116125
117- function runTestFile(path, root, inspectPort) {
126+ function runTestFile(path, root, inspectPort, filesWatcher ) {
118127 const subtest = root.createSubtest(Test, path, async (t) => {
119128 const args = getRunArgs({ path, inspectPort });
129+ const stdio = ['pipe', 'pipe', 'pipe'];
130+ const env = { ...process.env };
131+ if (filesWatcher) {
132+ stdio.push('ipc');
133+ env.WATCH_REPORT_DEPENDENCIES = '1';
134+ }
120135
121- const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' });
136+ const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio });
137+ runningProcesses.set(path, child);
122138 // TODO(cjihrig): Implement a TAP parser to read the child's stdout
123139 // instead of just displaying it all if the child fails.
124140 let err;
125141 let stderr = '';
126142
143+ filesWatcher?.watchChildProcessModules(child, path);
144+
127145 child.on('error', (error) => {
128146 err = error;
129147 });
@@ -146,6 +164,8 @@ function runTestFile(path, root, inspectPort) {
146164 child.stdout.toArray({ signal: t.signal }),
147165 ]);
148166
167+ runningProcesses.delete(path);
168+ runningSubtests.delete(path);
149169 if (code !== 0 || signal !== null) {
150170 if (!err) {
151171 err = ObjectAssign(new ERR_TEST_FAILURE('test failed', kSubtestsFailed), {
@@ -166,21 +186,57 @@ function runTestFile(path, root, inspectPort) {
166186 return subtest.start();
167187}
168188
189+ function watchFiles(testFiles, root, inspectPort) {
190+ const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter' });
191+ filesWatcher.on('changed', ({ owners }) => {
192+ filesWatcher.unfilterFilesOwnedBy(owners);
193+ PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => {
194+ if (!owners.has(file)) {
195+ return;
196+ }
197+ const runningProcess = runningProcesses.get(file);
198+ if (runningProcess) {
199+ runningProcess.kill();
200+ await once(runningProcess, 'exit');
201+ }
202+ await runningSubtests.get(file);
203+ runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher));
204+ }, undefined, (error) => {
205+ triggerUncaughtException(error, true /* fromPromise */);
206+ }));
207+ });
208+ return filesWatcher;
209+ }
210+
169211function run(options) {
170212 if (options === null || typeof options !== 'object') {
171213 options = kEmptyObject;
172214 }
173- const { concurrency, timeout, signal, files, inspectPort } = options;
215+ const { concurrency, timeout, signal, files, inspectPort, watch } = options;
174216
175217 if (files != null) {
176218 validateArray(files, 'options.files');
177219 }
220+ if (watch != null) {
221+ validateBoolean(watch, 'options.watch');
222+ }
178223
179224 const root = createTestTree({ concurrency, timeout, signal });
180225 const testFiles = files ?? createTestFileList();
181226
182- PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root, inspectPort)),
183- () => root.postRun());
227+ let postRun = () => root.postRun();
228+ let filesWatcher;
229+ if (watch) {
230+ filesWatcher = watchFiles(testFiles, root, inspectPort);
231+ postRun = undefined;
232+ }
233+
234+ PromisePrototypeThen(SafePromiseAllSettledReturnVoid(testFiles, (path) => {
235+ const subtest = runTestFile(path, root, inspectPort, filesWatcher);
236+ runningSubtests.set(path, subtest);
237+ return subtest;
238+ }), postRun);
239+
184240
185241 return root.reporter;
186242}
0 commit comments