GeezerVison is showing github.com — test_runner: ensure test watcher picks up new test files · nodejs/node@a4bebf8
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js +28-10Lines changed: 28 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -103,8 +103,7 @@ const kCanceledTests = new SafeSet() | 103 | 103 |
| 104 | 104 | let kResistStopPropagation; | 105 | 105 |
| 106 | | -function createTestFileList(patterns) { | 107 | | - const cwd = process.cwd(); | | 106 | +function createTestFileList(patterns, cwd) { | 108 | 107 | const hasUserSuppliedPattern = patterns != null; | 109 | 108 | if (!patterns || patterns.length === 0) { | 110 | 109 | patterns = [kDefaultPattern]; | @@ -361,7 +360,17 @@ function runTestFile(path, filesWatcher, opts) { | 361 | 360 | env.FORCE_COLOR = '1'; | 362 | 361 | } | 363 | 362 |
| 364 | | - const child = spawn(process.execPath, args, { __proto__: null, signal: t.signal, encoding: 'utf8', env, stdio }); | | 363 | + const child = spawn( | | 364 | + process.execPath, args, | | 365 | + { | | 366 | + __proto__: null, | | 367 | + signal: t.signal, | | 368 | + encoding: 'utf8', | | 369 | + env, | | 370 | + stdio, | | 371 | + cwd: opts.cwd, | | 372 | + }, | | 373 | + ); | 365 | 374 | if (watchMode) { | 366 | 375 | filesWatcher.runningProcesses.set(path, child); | 367 | 376 | filesWatcher.watcher.watchChildProcessModules(child, path); | @@ -437,7 +446,11 @@ function runTestFile(path, filesWatcher, opts) { | 437 | 446 | function watchFiles(testFiles, opts) { | 438 | 447 | const runningProcesses = new SafeMap(); | 439 | 448 | const runningSubtests = new SafeMap(); | 440 | | - const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal: opts.signal }); | | 449 | + const watcherMode = opts.hasFiles ? 'filter' : 'all'; | | 450 | + const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: watcherMode, signal: opts.signal }); | | 451 | + if (!opts.hasFiles) { | | 452 | + watcher.watchPath(opts.cwd); | | 453 | + } | 441 | 454 | const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests }; | 442 | 455 | opts.root.harness.watching = true; | 443 | 456 |
| @@ -455,24 +468,24 @@ function watchFiles(testFiles, opts) { | 455 | 468 | runningSubtests.set(file, runTestFile(file, filesWatcher, opts)); | 456 | 469 | } | 457 | 470 |
| | 471 | + // Watch for changes in current filtered files | 458 | 472 | watcher.on('changed', ({ owners, eventType }) => { | 459 | | - if (!opts.hasFiles && eventType === 'rename') { | 460 | | - const updatedTestFiles = createTestFileList(opts.globPatterns); | | 473 | + if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) { | | 474 | + const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd); | 461 | 475 | const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x)); | 462 | 476 | const previousFileName = ArrayPrototypeFind(testFiles, (x) => !ArrayPrototypeIncludes(updatedTestFiles, x)); | 463 | 477 |
| 464 | 478 | testFiles = updatedTestFiles; | 465 | 479 |
| 466 | | - // When file renamed | 467 | | - if (newFileName && previousFileName) { | | 480 | + // When file renamed (created / deleted) we need to update the watcher | | 481 | + if (newFileName) { | 468 | 482 | owners = new SafeSet().add(newFileName); | 469 | 483 | watcher.filterFile(resolve(newFileName), owners); | 470 | 484 | } | 471 | 485 |
| 472 | 486 | if (!newFileName && previousFileName) { | 473 | 487 | return; // Avoid rerunning files when file deleted | 474 | 488 | } | 475 | | - | 476 | 489 | } | 477 | 490 |
| 478 | 491 | if (opts.isolation === 'none') { | @@ -611,7 +624,11 @@ function run(options = kEmptyObject) { | 611 | 624 | setup, // This line can be removed when parseCommandLine() is removed here. | 612 | 625 | }; | 613 | 626 | const root = createTestTree(rootTestOptions, globalOptions); | 614 | | - let testFiles = files ?? createTestFileList(globPatterns); | | 627 | + | | 628 | + // This const should be replaced by a run option in the future. | | 629 | + const cwd = process.cwd(); | | 630 | + | | 631 | + let testFiles = files ?? createTestFileList(globPatterns, cwd); | 615 | 632 |
| 616 | 633 | if (shard) { | 617 | 634 | testFiles = ArrayPrototypeFilter(testFiles, (_, index) => index % shard.total === shard.index - 1); | @@ -632,6 +649,7 @@ function run(options = kEmptyObject) { | 632 | 649 | globPatterns, | 633 | 650 | only, | 634 | 651 | forceExit, | | 652 | + cwd, | 635 | 653 | isolation, | 636 | 654 | }; | 637 | 655 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/watch_mode/files_watcher.js +1-3Lines changed: 1 addition & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -162,9 +162,7 @@ class FilesWatcher extends EventEmitter { | 162 | 162 | if (this.#passthroughIPC) { | 163 | 163 | this.#setupIPC(child); | 164 | 164 | } | 165 | | - if (this.#mode !== 'filter') { | 166 | | - return; | 167 | | - } | | 165 | + | 168 | 166 | child.on('message', (message) => { | 169 | 167 | try { | 170 | 168 | if (ArrayIsArray(message['watch:require'])) { | |
Collapse file Copy file name to clipboard +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +const test = require('node:test'); | | 3 | + | | 4 | +test('this should pass'); |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-runner-run-watch.mjs +45-14Lines changed: 45 additions & 14 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -41,7 +41,7 @@ function refresh() { | 41 | 41 |
| 42 | 42 | const runner = join(import.meta.dirname, '..', 'fixtures', 'test-runner-watch.mjs'); | 43 | 43 |
| 44 | | -async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path }) { | | 44 | +async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path, fileToCreate }) { | 45 | 45 | const ran1 = util.createDeferredPromise(); | 46 | 46 | const ran2 = util.createDeferredPromise(); | 47 | 47 | const args = [runner]; | @@ -56,7 +56,7 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p | 56 | 56 | child.stdout.on('data', (data) => { | 57 | 57 | stdout += data.toString(); | 58 | 58 | currentRun += data.toString(); | 59 | | - const testRuns = stdout.match(/# duration_ms\s\d+/g); | | 59 | + const testRuns = stdout.match(/duration_ms\s\d+/g); | 60 | 60 | if (testRuns?.length >= 1) ran1.resolve(); | 61 | 61 | if (testRuns?.length >= 2) ran2.resolve(); | 62 | 62 | }); | @@ -78,10 +78,10 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p | 78 | 78 |
| 79 | 79 | for (const run of runs) { | 80 | 80 | assert.doesNotMatch(run, /run\(\) is being called recursively/); | 81 | | - assert.match(run, /# tests 1/); | 82 | | - assert.match(run, /# pass 1/); | 83 | | - assert.match(run, /# fail 0/); | 84 | | - assert.match(run, /# cancelled 0/); | | 81 | + assert.match(run, /tests 1/); | | 82 | + assert.match(run, /pass 1/); | | 83 | + assert.match(run, /fail 0/); | | 84 | + assert.match(run, /cancelled 0/); | 85 | 85 | } | 86 | 86 | }; | 87 | 87 |
| @@ -101,21 +101,21 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p | 101 | 101 | assert.strictEqual(runs.length, 2); | 102 | 102 |
| 103 | 103 | const [firstRun, secondRun] = runs; | 104 | | - assert.match(firstRun, /# tests 1/); | 105 | | - assert.match(firstRun, /# pass 1/); | 106 | | - assert.match(firstRun, /# fail 0/); | 107 | | - assert.match(firstRun, /# cancelled 0/); | | 104 | + assert.match(firstRun, /tests 1/); | | 105 | + assert.match(firstRun, /pass 1/); | | 106 | + assert.match(firstRun, /fail 0/); | | 107 | + assert.match(firstRun, /cancelled 0/); | 108 | 108 | assert.doesNotMatch(firstRun, /run\(\) is being called recursively/); | 109 | 109 |
| 110 | 110 | if (action === 'rename2') { | 111 | 111 | assert.match(secondRun, /MODULE_NOT_FOUND/); | 112 | 112 | return; | 113 | 113 | } | 114 | 114 |
| 115 | | - assert.match(secondRun, /# tests 1/); | 116 | | - assert.match(secondRun, /# pass 1/); | 117 | | - assert.match(secondRun, /# fail 0/); | 118 | | - assert.match(secondRun, /# cancelled 0/); | | 115 | + assert.match(secondRun, /tests 1/); | | 116 | + assert.match(secondRun, /pass 1/); | | 117 | + assert.match(secondRun, /fail 0/); | | 118 | + assert.match(secondRun, /cancelled 0/); | 119 | 119 | assert.doesNotMatch(secondRun, /run\(\) is being called recursively/); | 120 | 120 | }; | 121 | 121 |
| @@ -144,10 +144,37 @@ async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.p | 144 | 144 | } | 145 | 145 | }; | 146 | 146 |
| | 147 | + const testCreate = async () => { | | 148 | + await ran1.promise; | | 149 | + runs.push(currentRun); | | 150 | + currentRun = ''; | | 151 | + const newFilePath = tmpdir.resolve(fileToCreate); | | 152 | + const interval = setInterval( | | 153 | + () => writeFileSync( | | 154 | + newFilePath, | | 155 | + 'module.exports = {};' | | 156 | + ), | | 157 | + common.platformTimeout(1000) | | 158 | + ); | | 159 | + await ran2.promise; | | 160 | + runs.push(currentRun); | | 161 | + clearInterval(interval); | | 162 | + child.kill(); | | 163 | + await once(child, 'exit'); | | 164 | + | | 165 | + for (const run of runs) { | | 166 | + assert.match(run, /tests 1/); | | 167 | + assert.match(run, /pass 1/); | | 168 | + assert.match(run, /fail 0/); | | 169 | + assert.match(run, /cancelled 0/); | | 170 | + } | | 171 | + }; | | 172 | + | 147 | 173 | action === 'update' && await testUpdate(); | 148 | 174 | action === 'rename' && await testRename(); | 149 | 175 | action === 'rename2' && await testRename(); | 150 | 176 | action === 'delete' && await testDelete(); | | 177 | + action === 'create' && await testCreate(); | 151 | 178 | } | 152 | 179 |
| 153 | 180 | describe('test runner watch mode', () => { | @@ -193,4 +220,8 @@ describe('test runner watch mode', () => { | 193 | 220 | action: 'rename2' | 194 | 221 | }); | 195 | 222 | }); | | 223 | + | | 224 | + it('should run new tests when a new file is created in the watched directory', async () => { | | 225 | + await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' }); | | 226 | + }); | 196 | 227 | }); |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-runner-watch-mode.mjs +37-1Lines changed: 37 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -37,7 +37,12 @@ function refresh() { | 37 | 37 | .forEach(([file, content]) => writeFileSync(fixturePaths[file], content)); | 38 | 38 | } | 39 | 39 |
| 40 | | -async function testWatch({ fileToUpdate, file, action = 'update' }) { | | 40 | +async function testWatch({ | | 41 | + fileToUpdate, | | 42 | + file, | | 43 | + action = 'update', | | 44 | + fileToCreate, | | 45 | +}) { | 41 | 46 | const ran1 = util.createDeferredPromise(); | 42 | 47 | const ran2 = util.createDeferredPromise(); | 43 | 48 | const child = spawn(process.execPath, | @@ -127,9 +132,36 @@ async function testWatch({ fileToUpdate, file, action = 'update' }) { | 127 | 132 | } | 128 | 133 | }; | 129 | 134 |
| | 135 | + const testCreate = async () => { | | 136 | + await ran1.promise; | | 137 | + runs.push(currentRun); | | 138 | + currentRun = ''; | | 139 | + const newFilePath = tmpdir.resolve(fileToCreate); | | 140 | + const interval = setInterval( | | 141 | + () => writeFileSync( | | 142 | + newFilePath, | | 143 | + 'module.exports = {};' | | 144 | + ), | | 145 | + common.platformTimeout(1000) | | 146 | + ); | | 147 | + await ran2.promise; | | 148 | + runs.push(currentRun); | | 149 | + clearInterval(interval); | | 150 | + child.kill(); | | 151 | + await once(child, 'exit'); | | 152 | + | | 153 | + for (const run of runs) { | | 154 | + assert.match(run, /tests 1/); | | 155 | + assert.match(run, /pass 1/); | | 156 | + assert.match(run, /fail 0/); | | 157 | + assert.match(run, /cancelled 0/); | | 158 | + } | | 159 | + }; | | 160 | + | 130 | 161 | action === 'update' && await testUpdate(); | 131 | 162 | action === 'rename' && await testRename(); | 132 | 163 | action === 'delete' && await testDelete(); | | 164 | + action === 'create' && await testCreate(); | 133 | 165 | } | 134 | 166 |
| 135 | 167 | describe('test runner watch mode', () => { | @@ -157,4 +189,8 @@ describe('test runner watch mode', () => { | 157 | 189 | it('should not throw when delete a watched test file', { skip: common.isAIX }, async () => { | 158 | 190 | await testWatch({ fileToUpdate: 'test.js', action: 'delete' }); | 159 | 191 | }); | | 192 | + | | 193 | + it('should run new tests when a new file is created in the watched directory', async () => { | | 194 | + await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' }); | | 195 | + }); | 160 | 196 | }); |
|