From b23a410f68c7f86b746f191487e2f26966d0d2b9 Mon Sep 17 00:00:00 2001 From: Pietro Marchini Date: Wed, 7 Aug 2024 23:20:56 +0200 Subject: [PATCH] test_runner: ensure test watcher picks up new test files --- lib/internal/test_runner/runner.js | 19 ++++++++++-- test/parallel/test-runner-run.mjs | 3 ++ test/parallel/test-runner-watch-mode.mjs | 39 ++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index a14cc97ce8690c..8e4c18fd726009 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -109,7 +109,7 @@ function createTestFileList(patterns) { function filterExecArgv(arg, i, arr) { return !ArrayPrototypeIncludes(kFilterArgs, arg) && - !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); + !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); } function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, testSkipPatterns, only }) { @@ -166,7 +166,7 @@ class FileTest extends Test { if (firstSpaceIndex === -1) return false; const secondSpaceIndex = StringPrototypeIndexOf(comment, ' ', firstSpaceIndex + 1); return secondSpaceIndex === -1 && - ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex)); + ArrayPrototypeIncludes(kDiagnosticsFilterArgs, StringPrototypeSlice(comment, 0, firstSpaceIndex)); } #handleReportItem(item) { const isTopLevel = item.data.nesting === 0; @@ -416,7 +416,22 @@ function watchFiles(testFiles, opts) { const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal: opts.signal }); const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests }; opts.root.harness.watching = true; + // Watch for created/deleted files in the current directory + const allFilesWatcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'all', signal: opts.signal }); + allFilesWatcher.watchPath(process.cwd()); + allFilesWatcher.on('changed', ({ owners, eventType }) => { + if (eventType === 'rename') { + const updatedTestFiles = createTestFileList(opts.globPatterns); + + const newFiles = ArrayPrototypeFilter(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x)); + testFiles = updatedTestFiles; + newFiles.forEach((newFile) => { + watcher.emit('changed', { __proto__: null, owners: new SafeSet().add(newFile), eventType: 'rename' }); + }); + } + }); + // Watch for changes in current filtered files watcher.on('changed', ({ owners, eventType }) => { if (!opts.hasFiles && eventType === 'rename') { const updatedTestFiles = createTestFileList(opts.globPatterns); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 7a575da9c95275..b718dab30b6305 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -4,6 +4,9 @@ import { join } from 'node:path'; import { describe, it, run } from 'node:test'; import { dot, spec, tap } from 'node:test/reporters'; import assert from 'node:assert'; +import { EventEmitter } from 'events'; + +EventEmitter.setMaxListeners(35); const testFixtures = fixtures.path('test-runner'); diff --git a/test/parallel/test-runner-watch-mode.mjs b/test/parallel/test-runner-watch-mode.mjs index a55f404a4f7010..0ea8bfe19ae08a 100644 --- a/test/parallel/test-runner-watch-mode.mjs +++ b/test/parallel/test-runner-watch-mode.mjs @@ -34,12 +34,18 @@ function refresh() { .forEach(([file, content]) => writeFileSync(fixturePaths[file], content)); } -async function testWatch({ fileToUpdate, file, action = 'update' }) { +async function testWatch({ + fileToUpdate, + file, + action = 'update', + fileToCreate +}) { const ran1 = util.createDeferredPromise(); const ran2 = util.createDeferredPromise(); const child = spawn(process.execPath, ['--watch', '--test', file ? fixturePaths[file] : undefined].filter(Boolean), - { encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path }); + { encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path } + ); let stdout = ''; let currentRun = ''; const runs = []; @@ -111,9 +117,34 @@ async function testWatch({ fileToUpdate, file, action = 'update' }) { } }; + const testCreate = async () => { + await ran1.promise; + const newFilePath = tmpdir.resolve(fileToCreate); + const interval = setInterval( + () => writeFileSync( + newFilePath, + 'module.exports = {};' + ), + common.platformTimeout(1000) + ); + await ran2.promise; + runs.push(currentRun); + clearInterval(interval); + child.kill(); + await once(child, 'exit'); + + for (const run of runs) { + assert.match(run, /# tests 1/); + assert.match(run, /# pass 1/); + assert.match(run, /# fail 0/); + assert.match(run, /# cancelled 0/); + } + }; + action === 'update' && await testUpdate(); action === 'rename' && await testRename(); action === 'delete' && await testDelete(); + action === 'create' && await testCreate(); } describe('test runner watch mode', () => { @@ -141,4 +172,8 @@ describe('test runner watch mode', () => { it('should not throw when delete a watched test file', { skip: common.isAIX }, async () => { await testWatch({ fileToUpdate: 'test.js', action: 'delete' }); }); + + it('should run new tests when a new file is created in the watched directory', async () => { + await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js' }); + }); });