#!/usr/bin/env node import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const repoRoot = path.resolve(import.meta.dirname, ".."); const coverageRoot = path.join(repoRoot, "coverage"); const installTools = process.argv.includes("--install-tools"); const clientOnly = process.argv.includes("--client-only"); const engineOnly = process.argv.includes("--engine-only"); function run(label, command, args, opts = {}) { process.stdout.write(`\n==> ${label}\n`); const res = spawnSync(command, args, { cwd: opts.cwd || repoRoot, stdio: "inherit", shell: os.platform() === "win32", env: { ...process.env, ...opts.env }, }); if (res.status !== 0) { process.stderr.write(`\ncoverage: ${label} failed with exit ${res.status}\n`); process.exit(res.status || 1); } } function existsOnPath(bin) { if (os.platform() === "win32") { return spawnSync("where", [bin], { stdio: "ignore" }).status === 0; } return spawnSync("sh", ["-c", "command -v \"$1\" >/dev/null 2>&1", "sh", bin], { stdio: "ignore", }).status === 0; } function firstExisting(paths) { return paths.find((p) => fs.existsSync(p)) || null; } function llvmToolEnv() { // Caller-provided override always wins. if (process.env.LLVM_COV && process.env.LLVM_PROFDATA) return {}; // Prefer the llvm-tools that ship with the active rustup toolchain // over anything on PATH. cargo-llvm-cov otherwise resolves // llvm-profdata via PATH, which on GitHub's ubuntu-24.04 image // picks up /usr/lib/llvm-18/bin/llvm-profdata — older than rustc's // bundled LLVM and rejects current-rustc profraw with // "raw profile version mismatch: Profile uses raw profile format // version = 10; expected version = 9", failing the whole merge. // The toolchain-shipped tools are guaranteed to match rustc's // profraw output, so pin to them whenever they're present. const sysrootRes = spawnSync("rustc", ["--print", "sysroot"], { encoding: "utf8" }); const hostRes = spawnSync("rustc", ["-vV"], { encoding: "utf8" }); if (sysrootRes.status === 0 && hostRes.status === 0) { const sysroot = sysrootRes.stdout.trim(); const hostMatch = hostRes.stdout.match(/^host:\s*(.+)$/m); if (sysroot && hostMatch) { const exe = os.platform() === "win32" ? ".exe" : ""; const toolBin = path.join(sysroot, "lib", "rustlib", hostMatch[1].trim(), "bin"); const toolchainCov = path.join(toolBin, "llvm-cov" + exe); const toolchainProfdata = path.join(toolBin, "llvm-profdata" + exe); if (fs.existsSync(toolchainCov) && fs.existsSync(toolchainProfdata)) { return { LLVM_COV: toolchainCov, LLVM_PROFDATA: toolchainProfdata }; } } } // Fallback: llvm-tools-preview isn't installed. Try PATH, then // well-known LLVM prefixes. Version-mismatch risk is on the user // here — but at least the script runs instead of failing outright. if (existsOnPath("llvm-cov") && existsOnPath("llvm-profdata")) return {}; const prefixes = os.platform() === "darwin" ? ["/opt/homebrew/opt/llvm/bin", "/opt/homebrew/opt/llvm@22/bin", "/opt/homebrew/opt/llvm@18/bin", "/usr/local/opt/llvm/bin"] : ["/usr/lib/llvm-18/bin", "/usr/lib/llvm-17/bin", "/usr/lib/llvm-16/bin", "/usr/bin"]; const llvmCov = firstExisting(prefixes.map((p) => path.join(p, "llvm-cov"))); const llvmProfdata = firstExisting(prefixes.map((p) => path.join(p, "llvm-profdata"))); if (llvmCov && llvmProfdata) { return { LLVM_COV: process.env.LLVM_COV || llvmCov, LLVM_PROFDATA: process.env.LLVM_PROFDATA || llvmProfdata, }; } return {}; } fs.mkdirSync(coverageRoot, { recursive: true }); if (!engineOnly) { run("client vitest coverage", "npm", ["run", "test:coverage"], { cwd: path.join(repoRoot, "client"), }); } if (clientOnly) { process.stdout.write("\ncoverage reports:\n"); process.stdout.write(` client: ${path.relative(repoRoot, path.join(repoRoot, "client", "coverage", "index.html"))}\n`); process.exit(0); } if (!existsOnPath("cargo-llvm-cov")) { if (!installTools) { process.stderr.write( "\ncoverage: cargo-llvm-cov is not installed. Run `npm run coverage -- --install-tools` or install it with `cargo install cargo-llvm-cov --locked`.\n", ); process.exit(1); } run("install cargo-llvm-cov", "cargo", ["install", "cargo-llvm-cov", "--locked"]); } fs.mkdirSync(path.join(coverageRoot, "engine"), { recursive: true }); const llvmEnv = llvmToolEnv(); // The test suite runs once; both reports are read from that run's profiles. Running // `cargo llvm-cov --lcov` and then `--html` ran every test twice, and the second pass // pushed the CI job past its time limit. const engineOpts = { cwd: path.join(repoRoot, "engine"), env: llvmEnv }; run("engine llvm-cov test", "cargo", ["llvm-cov", "--workspace", "--no-report"], engineOpts); run("engine llvm-cov lcov", "cargo", [ "llvm-cov", "report", "--lcov", "--output-path", path.join("..", "coverage", "engine", "lcov.info"), ], engineOpts); run("engine llvm-cov html", "cargo", [ "llvm-cov", "report", "--html", "--output-dir", path.join("..", "coverage", "engine"), ], engineOpts); process.stdout.write("\ncoverage reports:\n"); if (!engineOnly) { process.stdout.write(` client: ${path.relative(repoRoot, path.join(repoRoot, "client", "coverage", "index.html"))}\n`); } process.stdout.write(` engine: ${path.relative(repoRoot, path.join(coverageRoot, "engine", "html", "index.html"))}\n`); process.stdout.write(` lcov: ${path.relative(repoRoot, path.join(coverageRoot, "engine", "lcov.info"))}\n`);