mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-11 22:50:06 +00:00
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
167 lines
5.3 KiB
JavaScript
167 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { globSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { parseArgs } from 'node:util';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
aggregateInventory,
|
|
collectTarget,
|
|
configFromScript,
|
|
integrationTarget,
|
|
unitTarget,
|
|
} from './test-inventory-core.mjs';
|
|
|
|
const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
const USAGE = `Usage: pnpm test:inventory -- [flags]
|
|
|
|
Lists the Vitest suites that unit and integration CI jobs select.
|
|
File mode lists configured test files. Dynamic mode imports modules and counts cases.
|
|
Change-scoped PR jobs may run fewer tests than the configured totals.
|
|
|
|
Flags:
|
|
--dynamic Import test modules and expand generated test cases
|
|
--package <name> Only inspect one workspace package
|
|
--json Print JSON instead of a table
|
|
--concurrency <n> Package collections to run at once (default: 4)
|
|
--timeout <secs> Timeout for each package (default: 60 files, 600 dynamic)
|
|
-h, --help Show this help
|
|
`;
|
|
|
|
const { values } = parseArgs({
|
|
args: process.argv[2] === '--' ? process.argv.slice(3) : process.argv.slice(2),
|
|
options: {
|
|
concurrency: { type: 'string', default: '4' },
|
|
dynamic: { type: 'boolean', default: false },
|
|
help: { type: 'boolean', short: 'h', default: false },
|
|
json: { type: 'boolean', default: false },
|
|
package: { type: 'string' },
|
|
timeout: { type: 'string' },
|
|
},
|
|
strict: true,
|
|
});
|
|
|
|
if (values.help) {
|
|
process.stdout.write(USAGE);
|
|
process.exit(0);
|
|
}
|
|
|
|
function positiveInteger(value, flag) {
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} must be positive`);
|
|
return parsed;
|
|
}
|
|
|
|
const concurrency = positiveInteger(values.concurrency, '--concurrency');
|
|
const timeoutSeconds = positiveInteger(
|
|
values.timeout ?? (values.dynamic ? '600' : '60'),
|
|
'--timeout',
|
|
);
|
|
|
|
let targets = [];
|
|
for (const packageFile of globSync('packages/**/package.json', {
|
|
cwd: REPO_ROOT,
|
|
exclude: ['**/dist/**', '**/node_modules/**'],
|
|
})) {
|
|
const packageDir = dirname(packageFile);
|
|
const packageJson = JSON.parse(readFileSync(join(REPO_ROOT, packageFile), 'utf8'));
|
|
for (const target of [unitTarget(packageJson, packageDir), integrationTarget(packageJson)]) {
|
|
if (!target) continue;
|
|
targets.push({
|
|
...target,
|
|
config: configFromScript(packageJson.scripts[target.script]),
|
|
dir: packageDir,
|
|
package: packageJson.name,
|
|
});
|
|
}
|
|
}
|
|
if (values.package) targets = targets.filter((target) => target.package === values.package);
|
|
if (!targets.length) {
|
|
const error = 'No matching Vitest package or CI target was found';
|
|
if (values.json) {
|
|
await new Promise((resolve) =>
|
|
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, error })}\n`, resolve),
|
|
);
|
|
} else {
|
|
process.stderr.write(`${error}\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
const outputDir = mkdtempSync(join(tmpdir(), 'n8n-test-inventory-'));
|
|
const results = [];
|
|
let next = 0;
|
|
try {
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
|
|
while (next < targets.length) {
|
|
const index = next++;
|
|
results[index] = await collectTarget(targets[index], index, {
|
|
outputDir,
|
|
dynamic: values.dynamic,
|
|
timeoutSeconds,
|
|
repoRoot: REPO_ROOT,
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
} finally {
|
|
rmSync(outputDir, { recursive: true, force: true });
|
|
}
|
|
|
|
const output = aggregateInventory(results, values.dynamic);
|
|
const incomplete = results.some(
|
|
(result) => result.error || (values.dynamic && result.tests === null),
|
|
);
|
|
if (values.json) {
|
|
await new Promise((resolve) =>
|
|
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`, resolve),
|
|
);
|
|
process.exit(incomplete ? 1 : 0);
|
|
}
|
|
|
|
function table(rows, columns) {
|
|
const widths = columns.map(({ header, value }) =>
|
|
Math.max(header.length, ...rows.map((row) => String(value(row)).length)),
|
|
);
|
|
const line = (cells) =>
|
|
cells.map((value, index) => String(value).padEnd(widths[index])).join(' ');
|
|
return [
|
|
line(columns.map(({ header }) => header)),
|
|
line(widths.map((width) => '-'.repeat(width))),
|
|
...rows.map((row) => line(columns.map(({ value }) => value(row)))),
|
|
].join('\n');
|
|
}
|
|
|
|
process.stdout.write(`${output.note}\n\n`);
|
|
const laneColumns = [
|
|
{ header: 'CI lane', value: (row) => row.lane },
|
|
{ header: 'Packages', value: (row) => row.packages },
|
|
{ header: 'Files', value: (row) => row.files },
|
|
...(values.dynamic
|
|
? [
|
|
{ header: 'Expanded cases', value: (row) => row.tests },
|
|
{ header: 'Missing counts', value: (row) => row.missingCaseCounts || '' },
|
|
]
|
|
: []),
|
|
{ header: 'Errors', value: (row) => row.errors || '' },
|
|
];
|
|
process.stdout.write(`${table(output.lanes, laneColumns)}\n\n`);
|
|
const packageColumns = [
|
|
{ header: 'Package', value: (row) => row.package },
|
|
{ header: 'CI lane', value: (row) => row.lane },
|
|
{ header: 'CI scope', value: (row) => row.scope },
|
|
{ header: 'Files', value: (row) => row.files ?? '-' },
|
|
...(values.dynamic ? [{ header: 'Expanded cases', value: (row) => row.tests ?? '-' }] : []),
|
|
];
|
|
process.stdout.write(`${table(output.packages, packageColumns)}\n`);
|
|
|
|
const errors = results.filter((result) => result.error);
|
|
if (errors.length) {
|
|
process.stderr.write('\nCollection errors:\n');
|
|
for (const error of errors) process.stderr.write(`- ${error.package}: ${error.error}\n`);
|
|
}
|
|
process.exitCode = incomplete ? 1 : 0;
|