Skip to content

Commit 5ce9640

Browse files
committed
ci: Add unit:summary script for parallel test runs with failure overview
1 parent 0f82962 commit 5ce9640

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"lint": "eslint ./ && npm run lint --workspaces --if-present",
2828
"lint:commit": "commitlint",
2929
"unit": "npm run unit --workspaces --if-present",
30+
"unit:summary": "node scripts/unit-summary.js",
3031
"coverage": "npm run coverage --workspaces --if-present",
3132
"knip": "knip --config knip.config.js",
3233
"check-engine": "check-engine-light .",

scripts/unit-summary.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {exec} from "node:child_process";
2+
import {readFileSync, readdirSync, existsSync} from "node:fs";
3+
import {join, resolve} from "node:path";
4+
5+
const rootDir = resolve(import.meta.dirname, "..");
6+
const rootPkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"));
7+
8+
const workspaceDirs = rootPkg.workspaces.flatMap((pattern) => {
9+
const base = pattern.replace("/*", "");
10+
const dir = join(rootDir, base);
11+
if (!existsSync(dir)) return [];
12+
return readdirSync(dir, {withFileTypes: true})
13+
.filter((d) => d.isDirectory())
14+
.map((d) => join(dir, d.name));
15+
});
16+
17+
const workspaces = workspaceDirs
18+
.map((dir) => {
19+
const pkgPath = join(dir, "package.json");
20+
if (!existsSync(pkgPath)) return null;
21+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
22+
if (!pkg.scripts?.unit) return null;
23+
return {name: pkg.name, dir};
24+
})
25+
.filter(Boolean);
26+
27+
function runWorkspace(ws) {
28+
return new Promise((resolve) => {
29+
exec(`npm run unit --workspace=${ws.name}`, {
30+
cwd: rootDir,
31+
encoding: "utf8",
32+
maxBuffer: 50 * 1024 * 1024,
33+
}, (err, stdout, stderr) => {
34+
if (err) {
35+
resolve({name: ws.name, passed: false, output: stdout});
36+
} else {
37+
resolve({name: ws.name, passed: true});
38+
}
39+
});
40+
});
41+
}
42+
43+
console.log(`Running unit tests across ${workspaces.length} workspaces in parallel...\n`);
44+
45+
const results = await Promise.all(workspaces.map(runWorkspace));
46+
47+
for (const r of results) {
48+
if (r.passed) {
49+
console.log(` ${r.name} \x1b[32m✓\x1b[0m`);
50+
} else {
51+
console.log(` ${r.name} \x1b[31m✗\x1b[0m`);
52+
}
53+
}
54+
55+
const failures = results.filter((r) => !r.passed);
56+
const passes = results.filter((r) => r.passed);
57+
58+
if (failures.length > 0) {
59+
for (const f of failures) {
60+
console.log(`\n\x1b[31m${"━".repeat(60)}\x1b[0m`);
61+
console.log(`\x1b[31m Failures: ${f.name}\x1b[0m`);
62+
console.log(`\x1b[31m${"━".repeat(60)}\x1b[0m\n`);
63+
console.log(f.output);
64+
}
65+
}
66+
67+
const failedInfo = failures.length > 0 ? ` (${failures.map((f) => f.name).join(", ")})` : "";
68+
const summaryLine = ` Unit Test Summary: ${passes.length} passed, ${failures.length} failed${failedInfo}`;
69+
const frameWidth = Math.max(60, summaryLine.length + 2);
70+
console.log(`\n${"═".repeat(frameWidth)}`);
71+
console.log(` Unit Test Summary: \x1b[32m${passes.length} passed` +
72+
`\x1b[0m, \x1b[31m${failures.length} failed\x1b[0m${failedInfo}`);
73+
console.log(`${"═".repeat(frameWidth)}`);
74+
75+
process.exitCode = failures.length > 0 ? 1 : 0;

0 commit comments

Comments
 (0)