|
| 1 | +// Flags: --expose-internals |
| 2 | +import * as common from '../common/index.mjs'; |
| 3 | +import { describe, it, beforeEach } from 'node:test'; |
| 4 | +import assert from 'node:assert'; |
| 5 | +import { spawn } from 'node:child_process'; |
| 6 | +import { once } from 'node:events'; |
| 7 | +import { writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs'; |
| 8 | +import util from 'internal/util'; |
| 9 | +import tmpdir from '../common/tmpdir.js'; |
| 10 | +import { join } from 'node:path'; |
| 11 | + |
| 12 | +if (common.isIBMi) |
| 13 | + common.skip('IBMi does not support `fs.watch()`'); |
| 14 | + |
| 15 | +// This test updates these files repeatedly, |
| 16 | +// Reading them from disk is unreliable due to race conditions. |
| 17 | +const fixtureContent = { |
| 18 | + 'dependency.js': 'module.exports = {};', |
| 19 | + 'dependency.mjs': 'export const a = 1;', |
| 20 | + 'test.js': ` |
| 21 | +const test = require('node:test'); |
| 22 | +require('./dependency.js'); |
| 23 | +import('./dependency.mjs'); |
| 24 | +import('data:text/javascript,'); |
| 25 | +test('test has ran');`, |
| 26 | +}; |
| 27 | + |
| 28 | +let fixturePaths; |
| 29 | + |
| 30 | +function refresh() { |
| 31 | + tmpdir.refresh(); |
| 32 | + |
| 33 | + fixturePaths = Object.keys(fixtureContent) |
| 34 | + .reduce((acc, file) => ({ ...acc, [file]: tmpdir.resolve(file) }), {}); |
| 35 | + Object.entries(fixtureContent) |
| 36 | + .forEach(([file, content]) => writeFileSync(fixturePaths[file], content)); |
| 37 | +} |
| 38 | + |
| 39 | +const runner = join(import.meta.dirname, '..', 'fixtures', 'test-runner-watch.mjs'); |
| 40 | + |
| 41 | +async function testWatch({ fileToUpdate, file, action = 'update', cwd = tmpdir.path }) { |
| 42 | + const ran1 = util.createDeferredPromise(); |
| 43 | + const ran2 = util.createDeferredPromise(); |
| 44 | + const args = [runner]; |
| 45 | + if (file) args.push('--file', file); |
| 46 | + const child = spawn(process.execPath, |
| 47 | + args, |
| 48 | + { encoding: 'utf8', stdio: 'pipe', cwd }); |
| 49 | + let stdout = ''; |
| 50 | + let currentRun = ''; |
| 51 | + const runs = []; |
| 52 | + |
| 53 | + child.stdout.on('data', (data) => { |
| 54 | + stdout += data.toString(); |
| 55 | + currentRun += data.toString(); |
| 56 | + const testRuns = stdout.match(/# duration_ms\s\d+/g); |
| 57 | + if (testRuns?.length >= 1) ran1.resolve(); |
| 58 | + if (testRuns?.length >= 2) ran2.resolve(); |
| 59 | + }); |
| 60 | + |
| 61 | + const testUpdate = async () => { |
| 62 | + await ran1.promise; |
| 63 | + const content = fixtureContent[fileToUpdate]; |
| 64 | + const path = fixturePaths[fileToUpdate]; |
| 65 | + const interval = setInterval(() => writeFileSync(path, content), common.platformTimeout(1000)); |
| 66 | + await ran2.promise; |
| 67 | + runs.push(currentRun); |
| 68 | + clearInterval(interval); |
| 69 | + child.kill(); |
| 70 | + await once(child, 'exit'); |
| 71 | + for (const run of runs) { |
| 72 | + assert.doesNotMatch(run, /run\(\) is being called recursively/); |
| 73 | + assert.match(run, /# tests 1/); |
| 74 | + assert.match(run, /# pass 1/); |
| 75 | + assert.match(run, /# fail 0/); |
| 76 | + assert.match(run, /# cancelled 0/); |
| 77 | + } |
| 78 | + }; |
| 79 | + |
| 80 | + const testRename = async () => { |
| 81 | + await ran1.promise; |
| 82 | + const fileToRenamePath = tmpdir.resolve(fileToUpdate); |
| 83 | + const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`); |
| 84 | + const interval = setInterval(() => renameSync(fileToRenamePath, newFileNamePath), common.platformTimeout(1000)); |
| 85 | + await ran2.promise; |
| 86 | + runs.push(currentRun); |
| 87 | + clearInterval(interval); |
| 88 | + child.kill(); |
| 89 | + await once(child, 'exit'); |
| 90 | + |
| 91 | + for (const run of runs) { |
| 92 | + assert.doesNotMatch(run, /run\(\) is being called recursively/); |
| 93 | + if (action === 'rename2') { |
| 94 | + assert.match(run, /MODULE_NOT_FOUND/); |
| 95 | + } else { |
| 96 | + assert.doesNotMatch(run, /MODULE_NOT_FOUND/); |
| 97 | + } |
| 98 | + assert.match(run, /# tests 1/); |
| 99 | + assert.match(run, /# pass 1/); |
| 100 | + assert.match(run, /# fail 0/); |
| 101 | + assert.match(run, /# cancelled 0/); |
| 102 | + } |
| 103 | + }; |
| 104 | + |
| 105 | + const testDelete = async () => { |
| 106 | + await ran1.promise; |
| 107 | + const fileToDeletePath = tmpdir.resolve(fileToUpdate); |
| 108 | + const interval = setInterval(() => { |
| 109 | + if (existsSync(fileToDeletePath)) { |
| 110 | + unlinkSync(fileToDeletePath); |
| 111 | + } else { |
| 112 | + ran2.resolve(); |
| 113 | + } |
| 114 | + }, common.platformTimeout(1000)); |
| 115 | + await ran2.promise; |
| 116 | + runs.push(currentRun); |
| 117 | + clearInterval(interval); |
| 118 | + child.kill(); |
| 119 | + await once(child, 'exit'); |
| 120 | + |
| 121 | + for (const run of runs) { |
| 122 | + assert.doesNotMatch(run, /MODULE_NOT_FOUND/); |
| 123 | + } |
| 124 | + }; |
| 125 | + |
| 126 | + action === 'update' && await testUpdate(); |
| 127 | + action === 'rename' && await testRename(); |
| 128 | + action === 'rename2' && await testRename(); |
| 129 | + action === 'delete' && await testDelete(); |
| 130 | +} |
| 131 | + |
| 132 | +describe('test runner watch mode', () => { |
| 133 | + beforeEach(refresh); |
| 134 | + it('should run tests repeatedly', async () => { |
| 135 | + await testWatch({ file: 'test.js', fileToUpdate: 'test.js' }); |
| 136 | + }); |
| 137 | + |
| 138 | + it('should run tests with dependency repeatedly', async () => { |
| 139 | + await testWatch({ file: 'test.js', fileToUpdate: 'dependency.js' }); |
| 140 | + }); |
| 141 | + |
| 142 | + it('should run tests with ESM dependency', async () => { |
| 143 | + await testWatch({ file: 'test.js', fileToUpdate: 'dependency.mjs' }); |
| 144 | + }); |
| 145 | + |
| 146 | + it('should support running tests without a file', async () => { |
| 147 | + await testWatch({ fileToUpdate: 'test.js' }); |
| 148 | + }); |
| 149 | + |
| 150 | + it('should support a watched test file rename', async () => { |
| 151 | + await testWatch({ fileToUpdate: 'test.js', action: 'rename' }); |
| 152 | + }); |
| 153 | + |
| 154 | + it('should not throw when deleting a watched test file', { skip: common.isAIX }, async () => { |
| 155 | + await testWatch({ fileToUpdate: 'test.js', action: 'delete' }); |
| 156 | + }); |
| 157 | + |
| 158 | + it('should run tests with dependency repeatedly in a different cwd', async () => { |
| 159 | + await testWatch({ |
| 160 | + file: join(tmpdir.path, 'test.js'), |
| 161 | + fileToUpdate: 'dependency.js', |
| 162 | + cwd: import.meta.dirname, |
| 163 | + action: 'rename2' |
| 164 | + }); |
| 165 | + }); |
| 166 | + |
| 167 | + it('should handle renames in a different cwd', async () => { |
| 168 | + await testWatch({ |
| 169 | + file: join(tmpdir.path, 'test.js'), |
| 170 | + fileToUpdate: 'test.js', |
| 171 | + cwd: import.meta.dirname, |
| 172 | + action: 'rename2' |
| 173 | + }); |
| 174 | + }); |
| 175 | +}); |
0 commit comments