mirror of
https://github.com/pezkuwichain/pezkuwi-dev.git
synced 2026-04-21 23:48:03 +00:00
369 lines
9.6 KiB
JavaScript
Executable File
369 lines
9.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Copyright 2017-2025 @pezkuwi/dev authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// For Node 18, earliest usable is 18.14:
|
|
//
|
|
// - node:test added in 18.0,
|
|
// - run method exposed in 18.9,
|
|
// - mock in 18.13,
|
|
// - diagnostics changed in 18.14
|
|
//
|
|
// Node 16 is not supported:
|
|
//
|
|
// - node:test added is 16.17,
|
|
// - run method exposed in 16.19,
|
|
// - mock not available
|
|
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import { run } from 'node:test';
|
|
import { isMainThread, parentPort, Worker, workerData } from 'node:worker_threads';
|
|
|
|
// NOTE error should be defined as "Error", however the @types/node definitions doesn't include all
|
|
/** @typedef {{ file?: string; message?: string; }} DiagStat */
|
|
/** @typedef {{ details: { type: string; duration_ms: number; error: { message: string; failureType: unknown; stack: string; cause: { code: number; message: string; stack: string; generatedMessage?: any; }; code: number; } }; file?: string; name: string; testNumber: number; nesting: number; }} FailStat */
|
|
/** @typedef {{ details: { duration_ms: number }; name: string; }} PassStat */
|
|
/** @typedef {{ diag: DiagStat[]; fail: FailStat[]; pass: PassStat[]; skip: unknown[]; todo: unknown[]; total: number; [key: string]: any; }} Stats */
|
|
|
|
console.time('\t elapsed :');
|
|
|
|
const WITH_DEBUG = false;
|
|
|
|
const args = process.argv.slice(2);
|
|
/** @type {string[]} */
|
|
const files = [];
|
|
|
|
/** @type {Stats} */
|
|
const stats = {
|
|
diag: [],
|
|
fail: [],
|
|
pass: [],
|
|
skip: [],
|
|
todo: [],
|
|
total: 0
|
|
};
|
|
/** @type {string | null} */
|
|
let logFile = null;
|
|
/** @type {number} */
|
|
let startAt = 0;
|
|
/** @type {boolean} */
|
|
let bail = false;
|
|
/** @type {boolean} */
|
|
let toConsole = false;
|
|
/** @type {number} */
|
|
let progressRowCount = 0;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--bail') {
|
|
bail = true;
|
|
} else if (args[i] === '--console') {
|
|
toConsole = true;
|
|
} else if (args[i] === '--logfile') {
|
|
logFile = args[++i];
|
|
} else {
|
|
files.push(args[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* Performs an indent of the line (and containing lines) with the specific count
|
|
*
|
|
* @param {number} count
|
|
* @param {string} str
|
|
* @param {string} start
|
|
* @returns {string}
|
|
*/
|
|
function indent (count, str = '', start = '') {
|
|
let pre = '\n';
|
|
|
|
switch (count) {
|
|
case 0:
|
|
break;
|
|
|
|
case 1:
|
|
pre += '\t';
|
|
break;
|
|
|
|
case 2:
|
|
pre += '\t\t';
|
|
break;
|
|
|
|
default:
|
|
pre += '\t\t\t';
|
|
break;
|
|
}
|
|
|
|
pre += ' ';
|
|
|
|
return `${pre}${start}${
|
|
str
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.join(`${pre}${start ? ' '.padStart(start.length, ' ') : ''}`)
|
|
}\n`;
|
|
}
|
|
|
|
/**
|
|
* @param {FailStat} r
|
|
* @return {string | undefined}
|
|
*/
|
|
function getFilename (r) {
|
|
if (r.file?.includes('.spec.') || r.file?.includes('.test.')) {
|
|
return r.file;
|
|
}
|
|
|
|
if (r.details.error.cause.stack) {
|
|
const stack = r.details.error.cause.stack
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.filter((l) => l.startsWith('at ') && (l.includes('.spec.') || l.includes('.test.')))
|
|
.map((l) => l.match(/\(.*:\d\d?:\d\d?\)$/)?.[0])
|
|
.map((l) => l?.replace('(', '')?.replace(')', ''));
|
|
|
|
if (stack.length) {
|
|
return stack[0];
|
|
}
|
|
}
|
|
|
|
return r.file;
|
|
}
|
|
|
|
function complete () {
|
|
process.stdout.write('\n');
|
|
|
|
let logError = '';
|
|
|
|
stats.fail.forEach((r) => {
|
|
WITH_DEBUG && console.error(JSON.stringify(r, null, 2));
|
|
|
|
let item = '';
|
|
|
|
item += indent(1, [getFilename(r), r.name].filter((s) => !!s).join('\n'), 'x ');
|
|
item += indent(2, `${r.details.error.failureType} / ${r.details.error.code}${r.details.error.cause.code && r.details.error.cause.code !== r.details.error.code ? ` / ${r.details.error.cause.code}` : ''}`);
|
|
|
|
if (r.details.error.cause.message) {
|
|
item += indent(2, r.details.error.cause.message);
|
|
}
|
|
|
|
logError += item;
|
|
|
|
if (r.details.error.cause.stack) {
|
|
item += indent(2, r.details.error.cause.stack);
|
|
}
|
|
|
|
process.stdout.write(item);
|
|
});
|
|
|
|
if (logFile && logError) {
|
|
try {
|
|
fs.appendFileSync(path.join(process.cwd(), logFile), logError);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
console.log('\t passed ::', stats.pass.length);
|
|
console.log('\t failed ::', stats.fail.length);
|
|
console.log('\t skipped ::', stats.skip.length);
|
|
console.log('\t todo ::', stats.todo.length);
|
|
console.log('\t total ::', stats.total);
|
|
console.timeEnd('\t elapsed :');
|
|
console.log();
|
|
|
|
// The full error information can be quite useful in the case of overall failures
|
|
if ((stats.fail.length || toConsole) && stats.diag.length) {
|
|
/** @type {string | undefined} */
|
|
let lastFilename = '';
|
|
|
|
stats.diag.forEach((r) => {
|
|
WITH_DEBUG && console.error(JSON.stringify(r, null, 2));
|
|
|
|
if (typeof r === 'string') {
|
|
console.log(r); // Node.js <= 18.14
|
|
} else if (r.file && r.file.includes('@pezkuwi/dev/scripts')) {
|
|
// Ignore internal diagnostics
|
|
} else {
|
|
if (lastFilename !== r.file) {
|
|
lastFilename = r.file;
|
|
|
|
console.log(lastFilename ? `\n${lastFilename}::\n` : '\n');
|
|
}
|
|
|
|
// Edge case: We don't need additional noise that is not useful.
|
|
if (!r.message?.split(' ').includes('tests')) {
|
|
console.log(`\t${r.message?.split('\n').join('\n\t')}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (toConsole) {
|
|
stats.pass.forEach((r) => {
|
|
console.log(`pass ${r.name} ${r.details.duration_ms} ms`);
|
|
});
|
|
|
|
console.log();
|
|
|
|
stats.fail.forEach((r) => {
|
|
console.log(`fail ${r.name}`);
|
|
});
|
|
|
|
console.log();
|
|
}
|
|
|
|
if (stats.total === 0) {
|
|
console.error('FATAL: No tests executed');
|
|
console.error();
|
|
process.exit(1);
|
|
}
|
|
|
|
process.exit(stats.fail.length);
|
|
}
|
|
|
|
/**
|
|
* Prints the progress in real-time as data is passed from the worker.
|
|
*
|
|
* @param {string} symbol
|
|
*/
|
|
function printProgress (symbol) {
|
|
if (!progressRowCount) {
|
|
progressRowCount = 0;
|
|
}
|
|
|
|
if (!startAt) {
|
|
startAt = performance.now();
|
|
}
|
|
|
|
// If starting a new row, calculate and print the elapsed time
|
|
if (progressRowCount === 0) {
|
|
const now = performance.now();
|
|
const elapsed = (now - startAt) / 1000;
|
|
const minutes = Math.floor(elapsed / 60);
|
|
const seconds = elapsed - minutes * 60;
|
|
|
|
process.stdout.write(
|
|
`${`${minutes}:${seconds.toFixed(3).padStart(6, '0')}`.padStart(11)} `
|
|
);
|
|
}
|
|
|
|
// Print the symbol with formatting
|
|
process.stdout.write(symbol);
|
|
|
|
progressRowCount++;
|
|
|
|
// Add spaces for readability
|
|
if (progressRowCount % 10 === 0) {
|
|
process.stdout.write(' '); // Double space every 10 symbols
|
|
} else if (progressRowCount % 5 === 0) {
|
|
process.stdout.write(' '); // Single space every 5 symbols
|
|
}
|
|
|
|
// If the row reaches 100 symbols, start a new row
|
|
if (progressRowCount >= 100) {
|
|
process.stdout.write('\n');
|
|
progressRowCount = 0;
|
|
}
|
|
}
|
|
|
|
async function runParallel () {
|
|
const MAX_WORKERS = Math.min(os.cpus().length, files.length);
|
|
const chunks = Math.ceil(files.length / MAX_WORKERS);
|
|
|
|
try {
|
|
// Create and manage worker threads
|
|
const results = await Promise.all(
|
|
Array.from({ length: MAX_WORKERS }, (_, i) => {
|
|
const fileSubset = files.slice(i * chunks, (i + 1) * chunks);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const worker = new Worker(new URL(import.meta.url), {
|
|
workerData: { files: fileSubset }
|
|
});
|
|
|
|
worker.on('message', (message) => {
|
|
if (message.type === 'progress') {
|
|
printProgress(message.data);
|
|
} else if (message.type === 'result') {
|
|
resolve(message.data);
|
|
}
|
|
});
|
|
|
|
worker.on('error', reject);
|
|
worker.on('exit', (code) => {
|
|
if (code !== 0) {
|
|
reject(new Error(`Worker stopped with exit code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
})
|
|
);
|
|
|
|
// Aggregate results from workers
|
|
results.forEach((result) => {
|
|
Object.keys(stats).forEach((key) => {
|
|
if (Array.isArray(stats[key])) {
|
|
stats[key] = stats[key].concat(result[key]);
|
|
} else if (typeof stats[key] === 'number') {
|
|
stats[key] += result[key];
|
|
}
|
|
});
|
|
});
|
|
|
|
complete();
|
|
} catch (err) {
|
|
console.error('Error during parallel execution:', err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (isMainThread) {
|
|
console.time('\tElapsed:');
|
|
runParallel().catch((err) => console.error(err));
|
|
} else {
|
|
run({ files: workerData.files, timeout: 3_600_000 })
|
|
.on('data', () => undefined)
|
|
.on('end', () => parentPort && parentPort.postMessage(stats))
|
|
.on('test:coverage', () => undefined)
|
|
.on('test:diagnostic', (/** @type {DiagStat} */data) => {
|
|
stats.diag.push(data);
|
|
parentPort && parentPort.postMessage({ data: stats, type: 'result' });
|
|
})
|
|
.on('test:fail', (/** @type {FailStat} */ data) => {
|
|
const statFail = structuredClone(data);
|
|
|
|
if (data.details.error.cause?.stack) {
|
|
statFail.details.error.cause.stack = data.details.error.cause.stack;
|
|
}
|
|
|
|
stats.fail.push(statFail);
|
|
stats.total++;
|
|
parentPort && parentPort.postMessage({ data: 'x', type: 'progress' });
|
|
|
|
if (bail) {
|
|
complete();
|
|
}
|
|
})
|
|
.on('test:pass', (data) => {
|
|
const symbol = typeof data.skip !== 'undefined' ? '>' : typeof data.todo !== 'undefined' ? '!' : '·';
|
|
|
|
if (symbol === '>') {
|
|
stats.skip.push(data);
|
|
} else if (symbol === '!') {
|
|
stats.todo.push(data);
|
|
} else {
|
|
stats.pass.push(data);
|
|
}
|
|
|
|
stats.total++;
|
|
parentPort && parentPort.postMessage({ data: symbol, type: 'progress' });
|
|
})
|
|
.on('test:plan', () => undefined)
|
|
.on('test:start', () => undefined);
|
|
}
|