-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test_runner: fix support watch with run(), add globPatterns option
Signed-off-by: Matteo Collina <[email protected]> PR-URL: #53866 Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Moshe Atlow <[email protected]>
- Loading branch information
Showing
8 changed files
with
324 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { run } from 'node:test'; | ||
import { tap } from 'node:test/reporters'; | ||
import { parseArgs } from 'node:util'; | ||
|
||
const options = { | ||
file: { | ||
type: 'string', | ||
}, | ||
}; | ||
const { | ||
values, | ||
positionals, | ||
} = parseArgs({ args: process.argv.slice(2), options }); | ||
|
||
let files; | ||
|
||
if (values.file) { | ||
files = [values.file]; | ||
} | ||
|
||
run({ | ||
files, | ||
watch: true | ||
}).compose(tap).pipe(process.stdout); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import * as common from '../common/index.mjs'; | ||
import tmpdir from '../common/tmpdir.js'; | ||
import { describe, it, run, beforeEach } from 'node:test'; | ||
import { dot, spec, tap } from 'node:test/reporters'; | ||
import { fork } from 'node:child_process'; | ||
import assert from 'node:assert'; | ||
|
||
if (common.hasCrypto) { | ||
console.log('1..0 # Skipped: no crypto'); | ||
process.exit(0); | ||
} | ||
|
||
if (process.env.CHILD === 'true') { | ||
describe('require(\'node:test\').run with no files', { concurrency: true }, () => { | ||
beforeEach(() => { | ||
tmpdir.refresh(); | ||
process.chdir(tmpdir.path); | ||
}); | ||
|
||
it('should neither pass or fail', async () => { | ||
const stream = run({ | ||
files: undefined | ||
}).compose(tap); | ||
stream.on('test:fail', common.mustNotCall()); | ||
stream.on('test:pass', common.mustNotCall()); | ||
|
||
// eslint-disable-next-line no-unused-vars | ||
for await (const _ of stream); | ||
}); | ||
|
||
it('can use the spec reporter', async () => { | ||
const stream = run({ | ||
files: undefined | ||
}).compose(spec); | ||
stream.on('test:fail', common.mustNotCall()); | ||
stream.on('test:pass', common.mustNotCall()); | ||
|
||
// eslint-disable-next-line no-unused-vars | ||
for await (const _ of stream); | ||
}); | ||
|
||
it('can use the dot reporter', async () => { | ||
const stream = run({ | ||
files: undefined | ||
}).compose(dot); | ||
stream.on('test:fail', common.mustNotCall()); | ||
stream.on('test:pass', common.mustNotCall()); | ||
|
||
// eslint-disable-next-line no-unused-vars | ||
for await (const _ of stream); | ||
}); | ||
}); | ||
} else if (common.isAIX) { | ||
console.log('1..0 # Skipped: test runner without specifying files fails on AIX'); | ||
} else { | ||
fork(import.meta.filename, [], { | ||
env: { CHILD: 'true' } | ||
}).on('exit', common.mustCall((code) => { | ||
assert.strictEqual(code, 0); | ||
})); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Flags: --expose-internals | ||
import * as common from '../common/index.mjs'; | ||
import { describe, it, beforeEach } from 'node:test'; | ||
import assert from 'node:assert'; | ||
import { spawn } from 'node:child_process'; | ||
import { once } from 'node:events'; | ||
import { writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs'; | ||
import util from 'internal/util'; | ||
import tmpdir from '../common/tmpdir.js'; | ||
import { join } from 'node:path'; | ||
|
||
if (common.isIBMi) | ||
common.skip('IBMi does not support `fs.watch()`'); | ||
|
||
// This test updates these files repeatedly, | ||
// Reading them from disk is unreliable due to race conditions. | ||
const fixtureContent = { | ||
'dependency.js': 'module.exports = {};', | ||
'dependency.mjs': 'export const a = 1;', | ||
'test.js': ` | ||
const test = require('node:test'); | ||
require('./dependency.js'); | ||
import('./dependency.mjs'); | ||
import('data:text/javascript,'); | ||
test('test has ran');`, | ||
}; | ||
|
||
let fixturePaths; | ||
|
||
function refresh() { | ||
tmpdir.refresh(); | ||
|
||
fixturePaths = Object.keys(fixtureContent) | ||
.reduce((acc, file) => ({ ...acc, [file]: tmpdir.resolve(file) }), {}); | ||
Object.entries(fixtureContent) | ||
.forEach(([file, content]) => writeFileSync(fixturePaths[file], content)); | ||
} | ||
|
||
const runner = join(import.meta.dirname, '..', 'fixtures', 'test-runner-watch.mjs'); | ||
|
||
async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path }) { | ||
const ran1 = util.createDeferredPromise(); | ||
const ran2 = util.createDeferredPromise(); | ||
const args = [runner]; | ||
if (file) args.push('--file', file); | ||
const child = spawn(process.execPath, | ||
args, | ||
{ encoding: 'utf8', stdio: 'pipe', cwd }); | ||
let stdout = ''; | ||
let currentRun = ''; | ||
const runs = []; | ||
|
||
child.stdout.on('data', (data) => { | ||
stdout += data.toString(); | ||
currentRun += data.toString(); | ||
const testRuns = stdout.match(/# duration_ms\s\d+/g); | ||
if (testRuns?.length >= 1) ran1.resolve(); | ||
if (testRuns?.length >= 2) ran2.resolve(); | ||
}); | ||
|
||
const testUpdate = async () => { | ||
await ran1.promise; | ||
const content = fixtureContent[fileToUpdate]; | ||
const path = fixturePaths[fileToUpdate]; | ||
const interval = setInterval(() => writeFileSync(path, content), common.platformTimeout(1000)); | ||
await ran2.promise; | ||
runs.push(currentRun); | ||
clearInterval(interval); | ||
child.kill(); | ||
await once(child, 'exit'); | ||
for (const run of runs) { | ||
assert.doesNotMatch(run, /run\(\) is being called recursively/); | ||
assert.match(run, /# tests 1/); | ||
assert.match(run, /# pass 1/); | ||
assert.match(run, /# fail 0/); | ||
assert.match(run, /# cancelled 0/); | ||
} | ||
}; | ||
|
||
const testRename = async () => { | ||
await ran1.promise; | ||
const fileToRenamePath = tmpdir.resolve(fileToUpdate); | ||
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`); | ||
const interval = setInterval(() => renameSync(fileToRenamePath, newFileNamePath), common.platformTimeout(1000)); | ||
await ran2.promise; | ||
runs.push(currentRun); | ||
clearInterval(interval); | ||
child.kill(); | ||
await once(child, 'exit'); | ||
|
||
for (const run of runs) { | ||
assert.doesNotMatch(run, /run\(\) is being called recursively/); | ||
if (action === 'rename2') { | ||
assert.match(run, /MODULE_NOT_FOUND/); | ||
} else { | ||
assert.doesNotMatch(run, /MODULE_NOT_FOUND/); | ||
} | ||
assert.match(run, /# tests 1/); | ||
assert.match(run, /# pass 1/); | ||
assert.match(run, /# fail 0/); | ||
assert.match(run, /# cancelled 0/); | ||
} | ||
}; | ||
|
||
const testDelete = async () => { | ||
await ran1.promise; | ||
const fileToDeletePath = tmpdir.resolve(fileToUpdate); | ||
const interval = setInterval(() => { | ||
if (existsSync(fileToDeletePath)) { | ||
unlinkSync(fileToDeletePath); | ||
} else { | ||
ran2.resolve(); | ||
} | ||
}, common.platformTimeout(1000)); | ||
await ran2.promise; | ||
runs.push(currentRun); | ||
clearInterval(interval); | ||
child.kill(); | ||
await once(child, 'exit'); | ||
|
||
for (const run of runs) { | ||
assert.doesNotMatch(run, /MODULE_NOT_FOUND/); | ||
} | ||
}; | ||
|
||
action === 'update' && await testUpdate(); | ||
action === 'rename' && await testRename(); | ||
action === 'rename2' && await testRename(); | ||
action === 'delete' && await testDelete(); | ||
} | ||
|
||
describe('test runner watch mode', () => { | ||
beforeEach(refresh); | ||
it('should run tests repeatedly', async () => { | ||
await testWatch({ file: 'test.js', fileToUpdate: 'test.js' }); | ||
}); | ||
|
||
it('should run tests with dependency repeatedly', async () => { | ||
await testWatch({ file: 'test.js', fileToUpdate: 'dependency.js' }); | ||
}); | ||
|
||
it('should run tests with ESM dependency', async () => { | ||
await testWatch({ file: 'test.js', fileToUpdate: 'dependency.mjs' }); | ||
}); | ||
|
||
it('should support running tests without a file', async () => { | ||
await testWatch({ fileToUpdate: 'test.js' }); | ||
}); | ||
|
||
it('should support a watched test file rename', async () => { | ||
await testWatch({ fileToUpdate: 'test.js', action: 'rename' }); | ||
}); | ||
|
||
it('should not throw when deleting a watched test file', { skip: common.isAIX }, async () => { | ||
await testWatch({ fileToUpdate: 'test.js', action: 'delete' }); | ||
}); | ||
|
||
it('should run tests with dependency repeatedly in a different cwd', async () => { | ||
await testWatch({ | ||
file: join(tmpdir.path, 'test.js'), | ||
fileToUpdate: 'dependency.js', | ||
cwd: import.meta.dirname, | ||
action: 'rename2' | ||
}); | ||
}); | ||
|
||
it('should handle renames in a different cwd', async () => { | ||
await testWatch({ | ||
file: join(tmpdir.path, 'test.js'), | ||
fileToUpdate: 'test.js', | ||
cwd: import.meta.dirname, | ||
action: 'rename2' | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.