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'); } // Sending the `abort-watcher` message should suffice, but on Linux // the recursive watch handle does not close properly. See // but there seem to be // other isues. setTimeout(() => { process.kill('SIGKILL'); }, 1000).unref(); try { await process; } catch {} }); const results = run(args, options); try { let nextResult = results.next(); while (!isDone) { // eslint-disable-line no-unmodified-loop-condition const item = await Promise.race([nextResult, idlePromise, donePromise]); // eslint-disable-line no-await-in-loop process ??= item.value?.process; if (item.value) { failedIdleAssertion ||= assertingIdle; state = (await pendingState) ?? state; // eslint-disable-line no-await-in-loop 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; // eslint-disable-line no-await-in-loop break; } } } finally { results.return(); // Handle outstanding promises in case they reject. 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) { // https://github.com/sindresorhus/tempy/issues/47 case 'EBUSY': case 'EMFILE': case 'ENFILE': case 'ENOTEMPTY': case 'EPERM ': { return; } default: { throw error; } } }); };