n8n/packages/testing/playwright/scripts/distribute-tests.mjs
Declan Carroll 66087e2dd5
chore: Apply biome formatting to playwright package (no-changelog) (#26586)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:47:20 +00:00

136 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// @ts-check
/**
* n8n CI Adapter for Test Distribution
*
* Thin wrapper that calls `janitor orchestrate` for generic shard distribution,
* then maps capabilities to n8n-specific Docker images for the CI matrix.
*
* Usage:
* node distribute-tests.mjs --matrix <shards> --orchestrate # GitHub Actions matrix with images
* node distribute-tests.mjs --matrix <shards> # Simple matrix (no distribution)
* node distribute-tests.mjs <shards> <index> # Specs for a single shard
*/
import { execFileSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PLAYWRIGHT_DIR = path.resolve(__dirname, '..');
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.js');
const PLAYWRIGHT_PREFIX = path.relative(REPO_ROOT, PLAYWRIGHT_DIR) + path.sep;
const CONTAINER_STARTUP_TIME = 22_500; // 22.5s average per fixture
const CAPABILITY_IMAGES = {
email: ['mailpit'],
kafka: ['kafka'],
observability: ['victoriaLogs', 'victoriaMetrics', 'vector', 'jaeger', 'n8nTracer'],
oidc: ['keycloak'],
proxy: ['mockserver'],
'source-control': ['gitea'],
};
const BASE_IMAGES = ['postgres', 'redis', 'caddy', 'n8n', 'taskRunner'];
function getRequiredImages(capabilities) {
const images = new Set(BASE_IMAGES);
for (const cap of capabilities) {
const capImages = CAPABILITY_IMAGES[cap];
if (capImages) {
for (const img of capImages) images.add(img);
}
}
return [...images].sort();
}
function getOrchestration(numShards, options = {}) {
const cliArgs = ['orchestrate', `--shards=${numShards}`];
if (options.impact) cliArgs.push('--impact');
if (options.files) {
// Normalize repo-root-relative paths to playwright-root-relative
// git diff gives 'packages/testing/playwright/foo.ts', janitor expects 'foo.ts'
const normalized = options.files
.split(',')
.map((f) => (f.startsWith(PLAYWRIGHT_PREFIX) ? f.slice(PLAYWRIGHT_PREFIX.length) : f))
.join(',');
cliArgs.push(`--files=${normalized}`);
}
const output = execFileSync('node', [JANITOR_CLI, ...cliArgs], {
cwd: PLAYWRIGHT_DIR,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'inherit'],
});
return JSON.parse(output);
}
const args = process.argv.slice(2);
const matrixMode = args.includes('--matrix');
const orchestrateMode = args.includes('--orchestrate');
const impactMode = args.includes('--impact');
const filesArg = args.find((a) => a.startsWith('--files='))?.slice('--files='.length) || undefined;
const shards = parseInt(args.find((a) => !a.startsWith('-')) ?? '');
if (!shards || shards < 1) {
console.error('Usage: node distribute-tests.mjs --matrix <shards> [--orchestrate] [--impact]');
console.error(' node distribute-tests.mjs <shards> <index>');
process.exit(1);
}
if (matrixMode) {
if (!orchestrateMode) {
const matrix = Array.from({ length: shards }, (_, i) => ({
shard: i + 1,
specs: '',
images: '',
}));
console.log(JSON.stringify(matrix));
} else {
const result = getOrchestration(shards, { impact: impactMode, files: filesArg });
if (result.shards.length === 0) {
console.error('\n⏭ No specs to run — all filtered out by discovery/impact. Skipping.\n');
console.log(JSON.stringify([{ shard: 1, specs: '', images: '', skip: true }]));
} else {
console.error('\n📊 Shard Distribution:');
let maxShardTime = 0;
for (const shard of result.shards) {
const overhead = shard.fixtureCount * CONTAINER_STARTUP_TIME;
const totalTime = shard.testTime + overhead;
maxShardTime = Math.max(maxShardTime, totalTime);
const testMins = (shard.testTime / 60_000).toFixed(1);
const totalMins = (totalTime / 60_000).toFixed(1);
const caps = shard.capabilities.length > 0 ? ` [${shard.capabilities.join(', ')}]` : '';
console.error(
` Shard ${shard.shard}: ${shard.specs.length} specs, ${testMins} min test + ${(overhead / 1000).toFixed(0)}s startup = ${totalMins} min${caps}`,
);
}
const totalTestMins = (result.totalTestTime / 60_000).toFixed(1);
console.error(`\n Total test time: ${totalTestMins} min`);
console.error(
` Expected wall-clock: ~${(maxShardTime / 60_000).toFixed(1)} min (longest shard)\n`,
);
const matrix = result.shards.map((shard) => ({
shard: shard.shard,
specs: shard.specs.join(' '),
images: getRequiredImages(shard.capabilities).join(' '),
}));
console.log(JSON.stringify(matrix));
}
}
} else {
const index = parseInt(args[1]);
if (isNaN(index) || index < 0 || index >= shards) {
console.error(`Index must be between 0 and ${shards - 1}`);
process.exit(1);
}
const result = getOrchestration(shards, { impact: impactMode, files: filesArg });
const shard = result.shards[index];
if (shard) {
console.log(shard.specs.join('\n'));
}
}