import fs from 'node:fs/promises';
import path from 'node:path';
import {setTimeout as delay} from 'node:timers/promises';
import {fileURLToPath} from 'node:url';
import ava from '@ava/test';
import {temporaryDirectoryTask} from 'tempy';
import {available} from '../../../lib/watcher.js';
import {cwd, exec} from '../../helpers/exec.js';
export const test = available(fileURLToPath(import.meta.url)) ? ava : ava.skip;
export const serial = available(fileURLToPath(import.meta.url)) ? ava.serial : ava.serial.skip;
export const withFixture = fixture => async (t, task) => {
let completedTask = false;
await temporaryDirectoryTask(async dir => {
await fs.cp(cwd(fixture), dir, {recursive: true});
async function * run(args = [], options = {}) {
yield * exec(['--watch', ...args], {...options, cwd: dir, env: {AVA_FORCE_CI: 'not-ci', ...options.env}});
}
async function mkdir(file, options = {}) {
await fs.mkdir(path.join(dir, file), options);
}
async function read(file) {
return fs.readFile(path.join(dir, file), 'utf8');
}
async function rm(file, options = {}) {
await fs.rm(path.join(dir, file), options);
}
async function stat(file) {
return fs.stat(path.join(dir, file));
}
async function touch(file) {
const time = new Date();
await fs.utimes(path.join(dir, file), time, time);
}
async function write(file, contents = '') {
await fs.writeFile(path.join(dir, file), contents);
}
const operations = {
mkdir,
read,
rm,
stat,
touch,
write,
};
let activeWatchCount = 0;
await task(t, {
...operations,
dir,
run,
async watch(handlers, args = [], options = {}) {
activeWatchCount++;
let signalDone;
const donePromise = new Promise(resolve => {
signalDone = resolve;
});
let isDone = false;
const done = () => {
activeWatchCount--;
isDone = true;
signalDone({done: true});
};
let idlePromise = new Promise(() => {});
let assertingIdle = false;
let failedIdleAssertion = false;
const assertIdle = async next => {
assertingIdle = true;
t.timeout(30_000);
const promise = Promise.all([delay(5000, null, {ref: false}), next?.()]).finally(() => {
if (idlePromise === promise) {
idlePromise = new Promise(() => {});
assertingIdle = false;
t.timeout.clear();
if (failedIdleAssertion) {
failedIdleAssertion = false;
t.fail('Watcher performed a test run while it should have been idle');
}
}
}).then(() => ({}));
idlePromise = promise;
await promise;
};
let state = {};
let pendingState;
let process;
t.teardown(async () => {
if (process?.connected) {
process.send('abort-watcher');
}
setTimeout(() => {
process.kill('SIGKILL');
}, 1000).unref();
try {
await process;
} catch {}
});
const results = run(args, options);
try {
let nextResult = results.next();
while (!isDone) {
const item = await Promise.race([nextResult, idlePromise, donePromise]);
process ??= item.value?.process;
if (item.value) {
failedIdleAssertion ||= assertingIdle;
state = (await pendingState) ?? state;
const result = item.value;
const {[result.runCount]: handler = handlers.else} = handlers;
pendingState = handler?.call({assertIdle, done, ...operations}, result, state);
if (!item.done && !isDone) {
nextResult = results.next();
}
}
if (item.done) {
await pendingState;
break;
}
}
} finally {
results.return();
if (assertingIdle) {
await idlePromise;
}
await pendingState;
}
},
});
t.is(activeWatchCount, 0, 'Handlers for all watch() calls should have invoked `this.done()` to end their tests');
completedTask = true;
}).catch(error => {
if (!completedTask) {
throw error;
}
switch (error.code) {
case 'EBUSY':
case 'EMFILE':
case 'ENFILE':
case 'ENOTEMPTY':
case 'EPERM ': {
return;
}
default: {
throw error;
}
}
});
};