@@ -10,20 +10,23 @@ const {
1010 ObjectAssign,
1111 PromisePrototypeThen,
1212 SafePromiseAll,
13+ SafePromiseAllSettled,
14+ SafeMap,
1315 SafeSet,
1416} = primordials ;
1517
1618const { spawn } = require ( 'child_process' ) ;
1719const { readdirSync, statSync } = require ( 'fs' ) ;
1820// TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern.
1921const { createInterface } = require ( 'readline' ) ;
22+ const { FilesWatcher } = require ( 'internal/watch_mode/files_watcher' ) ;
2023const console = require ( 'internal/console/global' ) ;
2124const {
2225 codes : {
2326 ERR_TEST_FAILURE ,
2427 } ,
2528} = require ( 'internal/errors' ) ;
26- const { validateArray } = require ( 'internal/validators' ) ;
29+ const { validateArray, validateBoolean } = require ( 'internal/validators' ) ;
2730const { getInspectPort, isUsingInspector, isInspectorMessage } = require ( 'internal/util/inspector' ) ;
2831const { kEmptyObject } = require ( 'internal/util' ) ;
2932const { createTestTree } = require ( 'internal/test_runner/harness' ) ;
@@ -34,8 +37,11 @@ const {
3437} = require ( 'internal/test_runner/utils' ) ;
3538const { basename, join, resolve } = require ( 'path' ) ;
3639const { once } = require ( 'events' ) ;
40+ const {
41+ triggerUncaughtException,
42+ } = internalBinding ( 'errors' ) ;
3743
38- const kFilterArgs = [ '--test' ] ;
44+ const kFilterArgs = [ '--test' , '--watch' ] ;
3945
4046// TODO(cjihrig): Replace this with recursive readdir once it lands.
4147function processPath ( path , testFiles , options ) {
@@ -112,17 +118,28 @@ function getRunArgs({ path, inspectPort }) {
112118 return argv ;
113119}
114120
121+ const runningProcesses = new SafeMap ( ) ;
122+ const runningSubtests = new SafeMap ( ) ;
115123
116- function runTestFile ( path , root , inspectPort ) {
124+ function runTestFile ( path , root , inspectPort , filesWatcher ) {
117125 const subtest = root . createSubtest ( Test , path , async ( t ) => {
118126 const args = getRunArgs ( { path, inspectPort } ) ;
127+ const stdio = [ 'pipe' , 'pipe' , 'pipe' ] ;
128+ const env = { ...process . env } ;
129+ if ( filesWatcher ) {
130+ stdio . push ( 'ipc' ) ;
131+ env . WATCH_REPORT_DEPENDENCIES = '1' ;
132+ }
119133
120- const child = spawn ( process . execPath , args , { signal : t . signal , encoding : 'utf8' } ) ;
134+ const child = spawn ( process . execPath , args , { signal : t . signal , encoding : 'utf8' , env, stdio } ) ;
135+ runningProcesses . set ( path , child ) ;
121136 // TODO(cjihrig): Implement a TAP parser to read the child's stdout
122137 // instead of just displaying it all if the child fails.
123138 let err ;
124139 let stderr = '' ;
125140
141+ filesWatcher ?. watchChildProcessModules ( child , path ) ;
142+
126143 child . on ( 'error' , ( error ) => {
127144 err = error ;
128145 } ) ;
@@ -145,6 +162,8 @@ function runTestFile(path, root, inspectPort) {
145162 child . stdout . toArray ( { signal : t . signal } ) ,
146163 ] ) ;
147164
165+ runningProcesses . delete ( path ) ;
166+ runningSubtests . delete ( path ) ;
148167 if ( code !== 0 || signal !== null ) {
149168 if ( ! err ) {
150169 err = ObjectAssign ( new ERR_TEST_FAILURE ( 'test failed' , kSubtestsFailed ) , {
@@ -165,21 +184,57 @@ function runTestFile(path, root, inspectPort) {
165184 return subtest . start ( ) ;
166185}
167186
187+ function watchFiles ( testFiles , root , inspectPort ) {
188+ const filesWatcher = new FilesWatcher ( { throttle : 500 , mode : 'filter' } ) ;
189+ filesWatcher . on ( 'changed' , ( { owners } ) => {
190+ filesWatcher . unfilterFilesOwnedBy ( owners ) ;
191+ PromisePrototypeThen ( SafePromiseAll ( testFiles , async ( file ) => {
192+ if ( ! owners . has ( file ) ) {
193+ return ;
194+ }
195+ const runningProcess = runningProcesses . get ( file ) ;
196+ if ( runningProcess ) {
197+ runningProcess . kill ( ) ;
198+ await once ( runningProcess , 'exit' ) ;
199+ }
200+ await runningSubtests . get ( file ) ;
201+ runningSubtests . set ( file , runTestFile ( file , root , inspectPort , filesWatcher ) ) ;
202+ } , undefined , ( error ) => {
203+ triggerUncaughtException ( error , true /* fromPromise */ ) ;
204+ } ) ) ;
205+ } ) ;
206+ return filesWatcher ;
207+ }
208+
168209function run ( options ) {
169210 if ( options === null || typeof options !== 'object' ) {
170211 options = kEmptyObject ;
171212 }
172- const { concurrency, timeout, signal, files, inspectPort } = options ;
213+ const { concurrency, timeout, signal, files, inspectPort, watch } = options ;
173214
174215 if ( files != null ) {
175216 validateArray ( files , 'options.files' ) ;
176217 }
218+ if ( watch != null ) {
219+ validateBoolean ( watch , 'options.watch' ) ;
220+ }
177221
178222 const root = createTestTree ( { concurrency, timeout, signal } ) ;
179223 const testFiles = files ?? createTestFileList ( ) ;
180224
181- PromisePrototypeThen ( SafePromiseAll ( testFiles , ( path ) => runTestFile ( path , root , inspectPort ) ) ,
182- ( ) => root . postRun ( ) ) ;
225+ let postRun = ( ) => root . postRun ( ) ;
226+ let filesWatcher ;
227+ if ( watch ) {
228+ filesWatcher = watchFiles ( testFiles , root , inspectPort ) ;
229+ postRun = undefined ;
230+ }
231+
232+ PromisePrototypeThen ( SafePromiseAllSettled ( testFiles , ( path ) => {
233+ const subtest = runTestFile ( path , root , inspectPort , filesWatcher ) ;
234+ runningSubtests . set ( path , subtest ) ;
235+ return subtest ;
236+ } ) , postRun ) ;
237+
183238
184239 return root . reporter ;
185240}
0 commit comments