@@ -10,20 +10,24 @@ const {
10
10
ObjectAssign,
11
11
PromisePrototypeThen,
12
12
SafePromiseAll,
13
+ SafePromiseAllReturnVoid,
14
+ SafePromiseAllSettledReturnVoid,
15
+ SafeMap,
13
16
SafeSet,
14
17
} = primordials ;
15
18
16
19
const { spawn } = require ( 'child_process' ) ;
17
20
const { readdirSync, statSync } = require ( 'fs' ) ;
18
21
// TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern.
19
22
const { createInterface } = require ( 'readline' ) ;
23
+ const { FilesWatcher } = require ( 'internal/watch_mode/files_watcher' ) ;
20
24
const console = require ( 'internal/console/global' ) ;
21
25
const {
22
26
codes : {
23
27
ERR_TEST_FAILURE ,
24
28
} ,
25
29
} = require ( 'internal/errors' ) ;
26
- const { validateArray } = require ( 'internal/validators' ) ;
30
+ const { validateArray, validateBoolean } = require ( 'internal/validators' ) ;
27
31
const { getInspectPort, isUsingInspector, isInspectorMessage } = require ( 'internal/util/inspector' ) ;
28
32
const { kEmptyObject } = require ( 'internal/util' ) ;
29
33
const { createTestTree } = require ( 'internal/test_runner/harness' ) ;
@@ -34,9 +38,12 @@ const {
34
38
} = require ( 'internal/test_runner/utils' ) ;
35
39
const { basename, join, resolve } = require ( 'path' ) ;
36
40
const { once } = require ( 'events' ) ;
37
- const { exitCodes : { kGenericUserError } } = internalBinding ( 'errors' ) ;
41
+ const {
42
+ triggerUncaughtException,
43
+ exitCodes : { kGenericUserError } ,
44
+ } = internalBinding ( 'errors' ) ;
38
45
39
- const kFilterArgs = [ '--test' ] ;
46
+ const kFilterArgs = [ '--test' , '--watch' ] ;
40
47
41
48
// TODO(cjihrig): Replace this with recursive readdir once it lands.
42
49
function processPath ( path , testFiles , options ) {
@@ -113,17 +120,28 @@ function getRunArgs({ path, inspectPort }) {
113
120
return argv ;
114
121
}
115
122
123
+ const runningProcesses = new SafeMap ( ) ;
124
+ const runningSubtests = new SafeMap ( ) ;
116
125
117
- function runTestFile ( path , root , inspectPort ) {
126
+ function runTestFile ( path , root , inspectPort , filesWatcher ) {
118
127
const subtest = root . createSubtest ( Test , path , async ( t ) => {
119
128
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
+ }
120
135
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 ) ;
122
138
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
123
139
// instead of just displaying it all if the child fails.
124
140
let err ;
125
141
let stderr = '' ;
126
142
143
+ filesWatcher ?. watchChildProcessModules ( child , path ) ;
144
+
127
145
child . on ( 'error' , ( error ) => {
128
146
err = error ;
129
147
} ) ;
@@ -146,6 +164,8 @@ function runTestFile(path, root, inspectPort) {
146
164
child . stdout . toArray ( { signal : t . signal } ) ,
147
165
] ) ;
148
166
167
+ runningProcesses . delete ( path ) ;
168
+ runningSubtests . delete ( path ) ;
149
169
if ( code !== 0 || signal !== null ) {
150
170
if ( ! err ) {
151
171
err = ObjectAssign ( new ERR_TEST_FAILURE ( 'test failed' , kSubtestsFailed ) , {
@@ -166,21 +186,57 @@ function runTestFile(path, root, inspectPort) {
166
186
return subtest . start ( ) ;
167
187
}
168
188
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
+
169
211
function run ( options ) {
170
212
if ( options === null || typeof options !== 'object' ) {
171
213
options = kEmptyObject ;
172
214
}
173
- const { concurrency, timeout, signal, files, inspectPort } = options ;
215
+ const { concurrency, timeout, signal, files, inspectPort, watch } = options ;
174
216
175
217
if ( files != null ) {
176
218
validateArray ( files , 'options.files' ) ;
177
219
}
220
+ if ( watch != null ) {
221
+ validateBoolean ( watch , 'options.watch' ) ;
222
+ }
178
223
179
224
const root = createTestTree ( { concurrency, timeout, signal } ) ;
180
225
const testFiles = files ?? createTestFileList ( ) ;
181
226
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
+
184
240
185
241
return root . reporter ;
186
242
}
0 commit comments