import {Buffer} from 'node:buffer';
import path from 'node:path';
import {Writable} from 'node:stream';
import {fileURLToPath, pathToFileURL} from 'node:url';
import test from '@ava/test';
import {execaNode} from 'execa';
const cliPath = fileURLToPath(new URL('../../entrypoints/cli.mjs', import.meta.url));
const ttySimulator = fileURLToPath(new URL('simulate-tty.cjs', import.meta.url));
const TEST_AVA_IMPORT_FROM = pathToFileURL(path.join(process.cwd(), 'entrypoints/main.mjs'));
const TEST_AVA_REQUIRE_FROM = path.join(process.cwd(), 'entrypoints/main.cjs');
const normalizePosixPath = string => string.replaceAll('\\', '/');
const normalizePath = (root, file) => normalizePosixPath(path.posix.normalize(path.relative(root, file)));
const compareStatObjects = (a, b) => {
if (a.file < b.file) {
return -1;
}
if (a.file > b.file) {
return 1;
}
if (a.title < b.title) {
return -1;
}
return 1;
};
export const cwd = (...paths) => path.join(path.dirname(fileURLToPath(test.meta.file)), 'fixtures', ...paths);
export const cleanOutput = string => string.replace(/^\W+/, '').replaceAll(/\W+\n+$/g, '').trim();
const NO_FORWARD_PREFIX = Buffer.from('🤗', 'utf8');
const forwardErrorOutput = chunk => {
if (chunk.length < 4 || NO_FORWARD_PREFIX.compare(chunk, 0, 4) !== 0) {
process.stderr.write(chunk);
}
};
const initState = () => {
const errors = new WeakMap();
const logs = new WeakMap();
const stats = {
failed: [],
failedHooks: [],
internalErrors: [],
processExits: [],
passed: [],
selectedTestCount: 0,
sharedWorkerErrors: [],
skipped: [],
todo: [],
uncaughtExceptions: [],
getError(statObject) {
return errors.get(statObject);
},
getLogs(statObject) {
return logs.get(statObject);
},
};
return {
errors, logs, stats, stdout: '', stderr: '',
};
};
const sortStats = stats => {
stats.failed.sort(compareStatObjects);
stats.failedHooks.sort(compareStatObjects);
stats.passed.sort(compareStatObjects);
stats.skipped.sort(compareStatObjects);
stats.todo.sort(compareStatObjects);
};
export async function * exec(args, options) {
let {errors, logs, stats, stdout, stderr} = initState();
const workingDir = options.cwd ?? cwd();
const execaProcess = execaNode(cliPath, args, {
...options,
env: {
...options.env,
TEST_AVA: 'true',
TEST_AVA_IMPORT_FROM,
TEST_AVA_REQUIRE_FROM,
},
cwd: workingDir,
nodeOptions: ['--require', ttySimulator],
stdout: ['pipe', new Writable({
write(chunk, encoding, callback) {
stdout += chunk;
callback();
},
})],
stderr: ['pipe', new Writable({
write(chunk, encoding, callback) {
stderr += chunk;
if (process.env.DEBUG_TEST_AVA) {
forwardErrorOutput(chunk);
}
callback();
},
})],
});
let runCount = 0;
const statusEvents = execaProcess.getEachMessage();
const done = execaProcess.then(result => ({execa: true, result}), error => {
sortStats(stats);
throw Object.assign(error, {stats, runCount});
});
while (true) {
const item = await Promise.race([done, statusEvents.next()]);
if (item.execa) {
sortStats(stats);
yield {
process: execaProcess, stats, stdout, stderr, runCount,
};
break;
}
if (item.done && !item.value) {
break;
}
const {value: statusEvent} = item;
switch (statusEvent.type) {
case 'end': {
sortStats(stats);
runCount++;
yield {
process: execaProcess, stats, stdout, stderr, runCount,
};
({errors, logs, stats, stdout, stderr} = initState());
break;
}
case 'hook-failed': {
const {title, testFile} = statusEvent;
const statObject = {title, file: normalizePath(workingDir, testFile)};
errors.set(statObject, statusEvent.err);
stats.failedHooks.push(statObject);
break;
}
case 'internal-error': {
const {testFile} = statusEvent;
const statObject = {file: normalizePath(workingDir, testFile ?? '')};
errors.set(statObject, statusEvent.err);
stats.internalErrors.push(statObject);
break;
}
case 'process-exit': {
const {testFile} = statusEvent;
const statObject = {file: normalizePath(workingDir, testFile)};
stats.processExits.push(statObject);
break;
}
case 'selected-test': {
stats.selectedTestCount++;
if (statusEvent.skip) {
const {title, testFile} = statusEvent;
stats.skipped.push({title, file: normalizePath(workingDir, testFile)});
}
if (statusEvent.todo) {
const {title, testFile} = statusEvent;
stats.todo.push({title, file: normalizePath(workingDir, testFile)});
}
break;
}
case 'shared-worker-error': {
const {message, name, stack} = statusEvent.err;
stats.sharedWorkerErrors.push({message, name, stack});
break;
}
case 'test-passed': {
const {title, testFile} = statusEvent;
const statObject = {title, file: normalizePath(workingDir, testFile)};
stats.passed.push(statObject);
logs.set(statObject, statusEvent.logs);
break;
}
case 'test-failed': {
const {title, testFile} = statusEvent;
const statObject = {title, file: normalizePath(workingDir, testFile)};
errors.set(statObject, statusEvent.err);
stats.failed.push(statObject);
logs.set(statObject, statusEvent.logs);
break;
}
case 'uncaught-exception': {
const {message, name, stack} = statusEvent.err;
stats.uncaughtExceptions.push({message, name, stack});
break;
}
default: {
break;
}
}
}
}
export async function fixture(args, options = {}) {
for await (const {process, ...result} of exec(args, options)) {
return {
...result,
...await process,
};
}
}