From 5ce964048c7aee7c8077b7277fe8df8a4fbf7c17 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 11 May 2026 17:12:11 +0200 Subject: [PATCH 1/2] ci: Add unit:summary script for parallel test runs with failure overview --- package.json | 1 + scripts/unit-summary.js | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 scripts/unit-summary.js diff --git a/package.json b/package.json index 6901e2c8a24..ff25534db14 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "lint": "eslint ./ && npm run lint --workspaces --if-present", "lint:commit": "commitlint", "unit": "npm run unit --workspaces --if-present", + "unit:summary": "node scripts/unit-summary.js", "coverage": "npm run coverage --workspaces --if-present", "knip": "knip --config knip.config.js", "check-engine": "check-engine-light .", diff --git a/scripts/unit-summary.js b/scripts/unit-summary.js new file mode 100644 index 00000000000..ba7c27a5039 --- /dev/null +++ b/scripts/unit-summary.js @@ -0,0 +1,75 @@ +import {exec} from "node:child_process"; +import {readFileSync, readdirSync, existsSync} from "node:fs"; +import {join, resolve} from "node:path"; + +const rootDir = resolve(import.meta.dirname, ".."); +const rootPkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")); + +const workspaceDirs = rootPkg.workspaces.flatMap((pattern) => { + const base = pattern.replace("/*", ""); + const dir = join(rootDir, base); + if (!existsSync(dir)) return []; + return readdirSync(dir, {withFileTypes: true}) + .filter((d) => d.isDirectory()) + .map((d) => join(dir, d.name)); +}); + +const workspaces = workspaceDirs + .map((dir) => { + const pkgPath = join(dir, "package.json"); + if (!existsSync(pkgPath)) return null; + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); + if (!pkg.scripts?.unit) return null; + return {name: pkg.name, dir}; + }) + .filter(Boolean); + +function runWorkspace(ws) { + return new Promise((resolve) => { + exec(`npm run unit --workspace=${ws.name}`, { + cwd: rootDir, + encoding: "utf8", + maxBuffer: 50 * 1024 * 1024, + }, (err, stdout, stderr) => { + if (err) { + resolve({name: ws.name, passed: false, output: stdout}); + } else { + resolve({name: ws.name, passed: true}); + } + }); + }); +} + +console.log(`Running unit tests across ${workspaces.length} workspaces in parallel...\n`); + +const results = await Promise.all(workspaces.map(runWorkspace)); + +for (const r of results) { + if (r.passed) { + console.log(` ${r.name} \x1b[32m✓\x1b[0m`); + } else { + console.log(` ${r.name} \x1b[31m✗\x1b[0m`); + } +} + +const failures = results.filter((r) => !r.passed); +const passes = results.filter((r) => r.passed); + +if (failures.length > 0) { + for (const f of failures) { + console.log(`\n\x1b[31m${"━".repeat(60)}\x1b[0m`); + console.log(`\x1b[31m Failures: ${f.name}\x1b[0m`); + console.log(`\x1b[31m${"━".repeat(60)}\x1b[0m\n`); + console.log(f.output); + } +} + +const failedInfo = failures.length > 0 ? ` (${failures.map((f) => f.name).join(", ")})` : ""; +const summaryLine = ` Unit Test Summary: ${passes.length} passed, ${failures.length} failed${failedInfo}`; +const frameWidth = Math.max(60, summaryLine.length + 2); +console.log(`\n${"═".repeat(frameWidth)}`); +console.log(` Unit Test Summary: \x1b[32m${passes.length} passed` + + `\x1b[0m, \x1b[31m${failures.length} failed\x1b[0m${failedInfo}`); +console.log(`${"═".repeat(frameWidth)}`); + +process.exitCode = failures.length > 0 ? 1 : 0; From 8ae628fafc7868d726a0c424783445c16e178829 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 11 May 2026 17:21:53 +0200 Subject: [PATCH 2/2] ci: Use matrix to run unit tests in each workspace in parallel --- .github/workflows/test.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62d65e3acd1..a97064546b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,13 +12,20 @@ permissions: contents: read jobs: - test: - name: Unit and Integration + unit: + name: "${{ matrix.workspace }} (Node ${{ matrix.version }}, ${{ matrix.os }})" strategy: - fail-fast: false # Do not stop other jobs if one fails + fail-fast: false matrix: version: [22, 24] os: [ubuntu-24.04, windows-2025, macos-15] + workspace: + - "@ui5/builder" + - "@ui5/cli" + - "@ui5/fs" + - "@ui5/logger" + - "@ui5/project" + - "@ui5/server" runs-on: ${{ matrix.os }} steps: @@ -38,4 +45,4 @@ jobs: run: npm ci - name: Run unit tests - run: npm run unit + run: npm run unit --workspace=${{ matrix.workspace }}