Skip to content

Commit

Permalink
test: deflake watch mode tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Sep 12, 2022
1 parent 0076c38 commit cf98295
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 42 deletions.
4 changes: 4 additions & 0 deletions test/fixtures/watch-mode/event_loop_blocked.js
@@ -0,0 +1,4 @@
console.log('running');
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0);
console.log('don\'t show me');

2 changes: 0 additions & 2 deletions test/fixtures/watch-mode/infinite-loop.js

This file was deleted.

Expand Up @@ -14,47 +14,49 @@ import { setTimeout } from 'node:timers/promises';
if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

function restart(file) {
writeFileSync(file, readFileSync(file));
const timer = setInterval(() => writeFileSync(file, readFileSync(file)), 100);
return () => clearInterval(timer);
}

async function spawnWithRestarts({
args,
file,
restarts,
startedPredicate,
restartMethod,
watchedFile = file,
restarts = 1,
isReady,
}) {
args ??= [file];
const printedArgs = inspect(args.slice(args.indexOf(file)).join(' '));
startedPredicate ??= (data) => Boolean(data.match(new RegExp(`(Failed|Completed) running ${printedArgs.replace(/\\/g, '\\\\')}`, 'g'))?.length);
restartMethod ??= () => writeFileSync(file, readFileSync(file));
isReady ??= (data) => Boolean(data.match(new RegExp(`(Failed|Completed) running ${printedArgs.replace(/\\/g, '\\\\')}`, 'g'))?.length);

let stderr = '';
let stdout = '';
let restartCount = 0;
let completedStart = false;
let finished = false;
let cancelRestarts;

const child = spawn(execPath, ['--watch', '--no-warnings', ...args], { encoding: 'utf8' });
child.stderr.on('data', (data) => {
stderr += data;
});
child.stdout.on('data', async (data) => {
if (finished) return;
stdout += data;
const restartMessages = stdout.match(new RegExp(`Restarting ${printedArgs.replace(/\\/g, '\\\\')}`, 'g'))?.length ?? 0;
completedStart = completedStart || startedPredicate(data.toString());
if (restartMessages >= restarts && completedStart) {
finished = true;
child.kill();
const restartsCount = stdout.match(new RegExp(`Restarting ${printedArgs.replace(/\\/g, '\\\\')}`, 'g'))?.length ?? 0;
if (restarts === 0 || !isReady(data.toString())) {
return;
}
if (restartCount <= restartMessages && completedStart) {
await setTimeout(restartCount > 0 ? 1000 : 50, { ref: false }); // Prevent throttling
restartCount++;
completedStart = false;
restartMethod();
if (restartsCount >= restarts) {
cancelRestarts?.();
if (!child.kill()) {
setTimeout(() => child.kill('SIGKILL'), 1);
}
return;
}
cancelRestarts ??= restart(watchedFile);
});

await Promise.race([once(child, 'exit'), once(child, 'error')]);
await once(child, 'exit');
cancelRestarts?.();
return { stderr, stdout };
}

Expand All @@ -79,7 +81,7 @@ tmpdir.refresh();
describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
it('should watch changes to a file - event loop ended', async () => {
const file = createTmpFile();
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 1 });
const { stderr, stdout } = await spawnWithRestarts({ file });

assert.strictEqual(stderr, '');
assert.strictEqual(removeGraceMessage(stdout, file), [
Expand All @@ -90,7 +92,7 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {

it('should watch changes to a failing file', async () => {
const file = fixtures.path('watch-mode/failing.js');
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 1 });
const { stderr, stdout } = await spawnWithRestarts({ file });

assert.match(stderr, /Error: fails\r?\n/);
assert.strictEqual(stderr.match(/Error: fails\r?\n/g).length, 2);
Expand All @@ -100,7 +102,7 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {

it('should not watch when running an non-existing file', async () => {
const file = fixtures.path('watch-mode/non-existing.js');
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 0, restartMethod: () => {} });
const { stderr, stdout } = await spawnWithRestarts({ file, restarts: 0 });

assert.match(stderr, /code: 'MODULE_NOT_FOUND'/);
assert.strictEqual(stdout, [`Failed running ${inspect(file)}`, ''].join('\n'));
Expand All @@ -110,12 +112,11 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
skip: !common.isOSX && !common.isWindows
}, async () => {
const file = fixtures.path('watch-mode/subdir/non-existing.js');
const watched = fixtures.path('watch-mode/subdir/file.js');
const watchedFile = fixtures.path('watch-mode/subdir/file.js');
const { stderr, stdout } = await spawnWithRestarts({
file,
watchedFile,
args: ['--watch-path', fixtures.path('./watch-mode/subdir/'), file],
restarts: 1,
restartMethod: () => writeFileSync(watched, readFileSync(watched))
});

assert.strictEqual(stderr, '');
Expand All @@ -124,25 +125,23 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
});

it('should watch changes to a file - event loop blocked', async () => {
const file = fixtures.path('watch-mode/infinite-loop.js');
const file = fixtures.path('watch-mode/event_loop_blocked.js');
const { stderr, stdout } = await spawnWithRestarts({
file,
restarts: 2,
startedPredicate: (data) => data.startsWith('running'),
isReady: (data) => data.startsWith('running'),
});

assert.strictEqual(stderr, '');
assert.strictEqual(removeGraceMessage(stdout, file),
['running', `Restarting ${inspect(file)}`, 'running', `Restarting ${inspect(file)}`, 'running', ''].join('\n'));
['running', `Restarting ${inspect(file)}`, 'running', ''].join('\n'));
});

it('should watch changes to dependencies - cjs', async () => {
const file = fixtures.path('watch-mode/dependant.js');
const dependency = fixtures.path('watch-mode/dependency.js');
const { stderr, stdout } = await spawnWithRestarts({
file,
restarts: 1,
restartMethod: () => writeFileSync(dependency, readFileSync(dependency)),
watchedFile: dependency,
});

assert.strictEqual(stderr, '');
Expand All @@ -157,8 +156,7 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
const dependency = fixtures.path('watch-mode/dependency.mjs');
const { stderr, stdout } = await spawnWithRestarts({
file,
restarts: 1,
restartMethod: () => writeFileSync(dependency, readFileSync(dependency)),
watchedFile: dependency,
});

assert.strictEqual(stderr, '');
Expand All @@ -180,16 +178,15 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
const file = fixtures.path('watch-mode/graceful-sigterm.js');
const { stderr, stdout } = await spawnWithRestarts({
file,
restarts: 1,
startedPredicate: (data) => data.startsWith('running'),
isReady: (data) => data.startsWith('running'),
});

// This message appearing is very flaky depending on a race between the
// inner process and the outer process. it is acceptable for the message not to appear
// as long as the SIGTERM handler is respected.
if (stdout.includes('Waiting for graceful termination...')) {
assert.strictEqual(stdout, ['running', `Restarting ${inspect(file)}`, 'Waiting for graceful termination...',
'exiting gracefully', `Gracefully restarted ${inspect(file)}`, 'running', ''].join('\n'));
'exiting gracefully', `Gracefully restarted ${inspect(file)}`, 'running', 'exiting gracefully', ''].join('\n'));
} else {
assert.strictEqual(stdout, ['running', `Restarting ${inspect(file)}`, 'exiting gracefully', 'running', ''].join('\n'));
}
Expand All @@ -200,7 +197,7 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
const file = fixtures.path('watch-mode/parse_args.js');
const random = Date.now().toString();
const args = [file, '--random', random];
const { stderr, stdout } = await spawnWithRestarts({ file, args, restarts: 1 });
const { stderr, stdout } = await spawnWithRestarts({ file, args });

assert.strictEqual(stderr, '');
assert.strictEqual(removeGraceMessage(stdout, args.join(' ')), [
Expand All @@ -213,7 +210,7 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
const file = createTmpFile('');
const required = fixtures.path('watch-mode/process_exit.js');
const args = ['--require', required, file];
const { stderr, stdout } = await spawnWithRestarts({ file, args, restarts: 1 });
const { stderr, stdout } = await spawnWithRestarts({ file, args });

assert.strictEqual(stderr, '');
assert.strictEqual(removeGraceMessage(stdout, file), [
Expand All @@ -225,7 +222,7 @@ describe('watch mode', { concurrency: false, timeout: 60_0000 }, () => {
const file = createTmpFile('');
const imported = fixtures.fileURL('watch-mode/process_exit.js');
const args = ['--import', imported, file];
const { stderr, stdout } = await spawnWithRestarts({ file, args, restarts: 1 });
const { stderr, stdout } = await spawnWithRestarts({ file, args });

assert.strictEqual(stderr, '');
assert.strictEqual(removeGraceMessage(stdout, file), [
Expand Down

0 comments on commit cf98295

Please sign in to comment.