@@ -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