Skip to content

Commit 028ad5e

Browse files
joyeecheungnodejs-github-bot
authored andcommitted
test: move test-runner-watch-mode helper into common
PR-URL: #60391 Refs: #49605 Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent e0ca993 commit 028ad5e

File tree

5 files changed

+176
-161
lines changed

5 files changed

+176
-161
lines changed

test/common/watch.js

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
'use strict';
22
const common = require('./index.js');
3+
const tmpdir = require('./tmpdir.js');
4+
const fixtures = require('./fixtures.js');
5+
const { writeFileSync, readdirSync, readFileSync, renameSync, unlinkSync } = require('node:fs');
6+
const { spawn } = require('node:child_process');
7+
const { once } = require('node:events');
8+
const assert = require('node:assert');
9+
const { setTimeout } = require('node:timers/promises');
310

4-
exports.skipIfNoWatchModeSignals = function() {
11+
function skipIfNoWatch() {
12+
if (common.isIBMi) {
13+
common.skip('IBMi does not support `fs.watch()`');
14+
}
15+
16+
if (common.isAIX) {
17+
common.skip('folder watch capability is limited in AIX.');
18+
}
19+
}
20+
21+
function skipIfNoWatchModeSignals() {
522
if (common.isWindows) {
623
common.skip('no signals on Windows');
724
}
@@ -13,4 +30,144 @@ exports.skipIfNoWatchModeSignals = function() {
1330
if (common.isAIX) {
1431
common.skip('folder watch capability is limited in AIX.');
1532
}
33+
}
34+
35+
const fixturePaths = {};
36+
const fixtureContent = {};
37+
38+
function refreshForTestRunnerWatch() {
39+
tmpdir.refresh();
40+
const files = readdirSync(fixtures.path('test-runner-watch'));
41+
for (const file of files) {
42+
const src = fixtures.path('test-runner-watch', file);
43+
const dest = tmpdir.resolve(file);
44+
fixturePaths[file] = dest;
45+
fixtureContent[file] = readFileSync(src, 'utf8');
46+
writeFileSync(dest, fixtureContent[file]);
47+
}
48+
}
49+
50+
async function testRunnerWatch({
51+
fileToUpdate,
52+
file,
53+
action = 'update',
54+
fileToCreate,
55+
isolation,
56+
}) {
57+
const ran1 = Promise.withResolvers();
58+
const ran2 = Promise.withResolvers();
59+
const child = spawn(process.execPath,
60+
['--watch', '--test', '--test-reporter=spec',
61+
isolation ? `--test-isolation=${isolation}` : '',
62+
file ? fixturePaths[file] : undefined].filter(Boolean),
63+
{ encoding: 'utf8', stdio: ['inherit', 'pipe', 'inherit'], cwd: tmpdir.path });
64+
let stdout = '';
65+
let currentRun = '';
66+
const runs = [];
67+
68+
child.stdout.on('data', (data) => {
69+
stdout += data.toString();
70+
currentRun += data.toString();
71+
const testRuns = stdout.match(/duration_ms\s\d+/g);
72+
if (testRuns?.length >= 1) ran1.resolve();
73+
if (testRuns?.length >= 2) ran2.resolve();
74+
});
75+
76+
const testUpdate = async () => {
77+
await ran1.promise;
78+
runs.push(currentRun);
79+
currentRun = '';
80+
const content = fixtureContent[fileToUpdate];
81+
const path = fixturePaths[fileToUpdate];
82+
writeFileSync(path, content);
83+
await setTimeout(common.platformTimeout(1000));
84+
await ran2.promise;
85+
runs.push(currentRun);
86+
child.kill();
87+
await once(child, 'exit');
88+
89+
assert.strictEqual(runs.length, 2);
90+
91+
for (const run of runs) {
92+
assert.match(run, /tests 1/);
93+
assert.match(run, /pass 1/);
94+
assert.match(run, /fail 0/);
95+
assert.match(run, /cancelled 0/);
96+
}
97+
};
98+
99+
const testRename = async () => {
100+
await ran1.promise;
101+
runs.push(currentRun);
102+
currentRun = '';
103+
const fileToRenamePath = tmpdir.resolve(fileToUpdate);
104+
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
105+
renameSync(fileToRenamePath, newFileNamePath);
106+
await setTimeout(common.platformTimeout(1000));
107+
await ran2.promise;
108+
runs.push(currentRun);
109+
child.kill();
110+
await once(child, 'exit');
111+
112+
assert.strictEqual(runs.length, 2);
113+
114+
for (const run of runs) {
115+
assert.match(run, /tests 1/);
116+
assert.match(run, /pass 1/);
117+
assert.match(run, /fail 0/);
118+
assert.match(run, /cancelled 0/);
119+
}
120+
};
121+
122+
const testDelete = async () => {
123+
await ran1.promise;
124+
runs.push(currentRun);
125+
currentRun = '';
126+
const fileToDeletePath = tmpdir.resolve(fileToUpdate);
127+
unlinkSync(fileToDeletePath);
128+
await setTimeout(common.platformTimeout(2000));
129+
ran2.resolve();
130+
runs.push(currentRun);
131+
child.kill();
132+
await once(child, 'exit');
133+
134+
assert.strictEqual(runs.length, 2);
135+
136+
for (const run of runs) {
137+
assert.doesNotMatch(run, /MODULE_NOT_FOUND/);
138+
}
139+
};
140+
141+
const testCreate = async () => {
142+
await ran1.promise;
143+
runs.push(currentRun);
144+
currentRun = '';
145+
const newFilePath = tmpdir.resolve(fileToCreate);
146+
writeFileSync(newFilePath, 'module.exports = {};');
147+
await setTimeout(common.platformTimeout(1000));
148+
await ran2.promise;
149+
runs.push(currentRun);
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+
161+
action === 'update' && await testUpdate();
162+
action === 'rename' && await testRename();
163+
action === 'delete' && await testDelete();
164+
action === 'create' && await testCreate();
165+
}
166+
167+
168+
module.exports = {
169+
skipIfNoWatch,
170+
skipIfNoWatchModeSignals,
171+
testRunnerWatch,
172+
refreshForTestRunnerWatch,
16173
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const a = 1;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const test = require('node:test');
2+
require('./dependency.js');
3+
import('./dependency.mjs');
4+
import('data:text/javascript,');
5+
test('test has ran');

test/parallel/test-runner-watch-mode.mjs

Lines changed: 11 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,192 +1,43 @@
1-
import * as common from '../common/index.mjs';
1+
import '../common/index.mjs';
2+
import { skipIfNoWatch, refreshForTestRunnerWatch, testRunnerWatch } from '../common/watch.js';
23
import { describe, it, beforeEach } from 'node:test';
3-
import { once } from 'node:events';
4-
import assert from 'node:assert';
5-
import { spawn } from 'node:child_process';
6-
import { writeFileSync, renameSync, unlinkSync } from 'node:fs';
7-
import { setTimeout } from 'node:timers/promises';
8-
import tmpdir from '../common/tmpdir.js';
94

10-
if (common.isIBMi)
11-
common.skip('IBMi does not support `fs.watch()`');
12-
13-
if (common.isAIX)
14-
common.skip('folder watch capability is limited in AIX.');
15-
16-
let fixturePaths;
17-
18-
// This test updates these files repeatedly,
19-
// Reading them from disk is unreliable due to race conditions.
20-
const fixtureContent = {
21-
'dependency.js': 'module.exports = {};',
22-
'dependency.mjs': 'export const a = 1;',
23-
'test.js': `
24-
const test = require('node:test');
25-
require('./dependency.js');
26-
import('./dependency.mjs');
27-
import('data:text/javascript,');
28-
test('test has ran');`,
29-
};
30-
31-
function refresh() {
32-
tmpdir.refresh();
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-
async function testWatch({
40-
fileToUpdate,
41-
file,
42-
action = 'update',
43-
fileToCreate,
44-
isolation,
45-
}) {
46-
const ran1 = Promise.withResolvers();
47-
const ran2 = Promise.withResolvers();
48-
const child = spawn(process.execPath,
49-
['--watch', '--test', '--test-reporter=spec',
50-
isolation ? `--test-isolation=${isolation}` : '',
51-
file ? fixturePaths[file] : undefined].filter(Boolean),
52-
{ encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path });
53-
let stdout = '';
54-
let currentRun = '';
55-
const runs = [];
56-
57-
child.stdout.on('data', (data) => {
58-
stdout += data.toString();
59-
currentRun += data.toString();
60-
const testRuns = stdout.match(/duration_ms\s\d+/g);
61-
if (testRuns?.length >= 1) ran1.resolve();
62-
if (testRuns?.length >= 2) ran2.resolve();
63-
});
64-
65-
const testUpdate = async () => {
66-
await ran1.promise;
67-
runs.push(currentRun);
68-
currentRun = '';
69-
const content = fixtureContent[fileToUpdate];
70-
const path = fixturePaths[fileToUpdate];
71-
writeFileSync(path, content);
72-
await setTimeout(common.platformTimeout(1000));
73-
await ran2.promise;
74-
runs.push(currentRun);
75-
child.kill();
76-
await once(child, 'exit');
77-
78-
assert.strictEqual(runs.length, 2);
79-
80-
for (const run of runs) {
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-
}
86-
};
87-
88-
const testRename = async () => {
89-
await ran1.promise;
90-
runs.push(currentRun);
91-
currentRun = '';
92-
const fileToRenamePath = tmpdir.resolve(fileToUpdate);
93-
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
94-
renameSync(fileToRenamePath, newFileNamePath);
95-
await setTimeout(common.platformTimeout(1000));
96-
await ran2.promise;
97-
runs.push(currentRun);
98-
child.kill();
99-
await once(child, 'exit');
100-
101-
assert.strictEqual(runs.length, 2);
102-
103-
for (const run of runs) {
104-
assert.match(run, /tests 1/);
105-
assert.match(run, /pass 1/);
106-
assert.match(run, /fail 0/);
107-
assert.match(run, /cancelled 0/);
108-
}
109-
};
110-
111-
const testDelete = async () => {
112-
await ran1.promise;
113-
runs.push(currentRun);
114-
currentRun = '';
115-
const fileToDeletePath = tmpdir.resolve(fileToUpdate);
116-
unlinkSync(fileToDeletePath);
117-
await setTimeout(common.platformTimeout(2000));
118-
ran2.resolve();
119-
runs.push(currentRun);
120-
child.kill();
121-
await once(child, 'exit');
122-
123-
assert.strictEqual(runs.length, 2);
124-
125-
for (const run of runs) {
126-
assert.doesNotMatch(run, /MODULE_NOT_FOUND/);
127-
}
128-
};
129-
130-
const testCreate = async () => {
131-
await ran1.promise;
132-
runs.push(currentRun);
133-
currentRun = '';
134-
const newFilePath = tmpdir.resolve(fileToCreate);
135-
writeFileSync(newFilePath, 'module.exports = {};');
136-
await setTimeout(common.platformTimeout(1000));
137-
await ran2.promise;
138-
runs.push(currentRun);
139-
child.kill();
140-
await once(child, 'exit');
141-
142-
for (const run of runs) {
143-
assert.match(run, /tests 1/);
144-
assert.match(run, /pass 1/);
145-
assert.match(run, /fail 0/);
146-
assert.match(run, /cancelled 0/);
147-
}
148-
};
149-
150-
action === 'update' && await testUpdate();
151-
action === 'rename' && await testRename();
152-
action === 'delete' && await testDelete();
153-
action === 'create' && await testCreate();
154-
}
5+
skipIfNoWatch();
1556

1567
describe('test runner watch mode', () => {
157-
beforeEach(refresh);
8+
beforeEach(refreshForTestRunnerWatch);
1589
for (const isolation of ['none', 'process']) {
15910
describe(`isolation: ${isolation}`, () => {
16011
it('should run tests repeatedly', async () => {
161-
await testWatch({ file: 'test.js', fileToUpdate: 'test.js', isolation });
12+
await testRunnerWatch({ file: 'test.js', fileToUpdate: 'test.js', isolation });
16213
});
16314

16415
it('should run tests with dependency repeatedly', async () => {
165-
await testWatch({ file: 'test.js', fileToUpdate: 'dependency.js', isolation });
16+
await testRunnerWatch({ file: 'test.js', fileToUpdate: 'dependency.js', isolation });
16617
});
16718

16819
it('should run tests with ESM dependency', async () => {
169-
await testWatch({ file: 'test.js', fileToUpdate: 'dependency.mjs', isolation });
20+
await testRunnerWatch({ file: 'test.js', fileToUpdate: 'dependency.mjs', isolation });
17021
});
17122

17223
it('should support running tests without a file', async () => {
173-
await testWatch({ fileToUpdate: 'test.js', isolation });
24+
await testRunnerWatch({ fileToUpdate: 'test.js', isolation });
17425
});
17526

17627
it('should support a watched test file rename', async () => {
177-
await testWatch({ fileToUpdate: 'test.js', action: 'rename', isolation });
28+
await testRunnerWatch({ fileToUpdate: 'test.js', action: 'rename', isolation });
17829
});
17930

18031
it('should not throw when delete a watched test file', async () => {
181-
await testWatch({ fileToUpdate: 'test.js', action: 'delete', isolation });
32+
await testRunnerWatch({ fileToUpdate: 'test.js', action: 'delete', isolation });
18233
});
18334

18435
it('should run new tests when a new file is created in the watched directory', {
18536
todo: isolation === 'none' ?
18637
'This test is failing when isolation is set to none and must be fixed' :
18738
undefined,
18839
}, async () => {
189-
await testWatch({ action: 'create', fileToCreate: 'new-test-file.test.js', isolation });
40+
await testRunnerWatch({ action: 'create', fileToCreate: 'new-test-file.test.js', isolation });
19041
});
19142
});
19243
}

0 commit comments

Comments
 (0)