Skip to content

Commit b6fa23c

Browse files
fix(scripts): only swallow ENOENT in rees-coverage lcov post-process (#7771)
Narrow the bare catch to ENOENT on read so write failures surface as real CI errors. Export normalizeLcovSfPaths for unit tests; guard main() behind import.meta.url check. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ea7da0c commit b6fa23c

2 files changed

Lines changed: 90 additions & 36 deletions

File tree

scripts/rees-coverage.mjs

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,21 @@ import { readdirSync, readFileSync, writeFileSync } from "node:fs";
66
import { join, relative } from "node:path";
77
import { fileURLToPath } from "node:url";
88

9-
const root = join(fileURLToPath(new URL(".", import.meta.url)), "..");
10-
const c8Bin = join(root, "review-enrichment", "node_modules", "c8", "bin", "c8.js");
11-
const reportDir = join(root, "review-enrichment", "coverage");
12-
const testRoot = join(root, "review-enrichment", "test");
9+
/** Normalize c8's SF: paths to forward slashes for Codecov. Swallows only a missing report
10+
* (ENOENT on read) — CI's "Verify REES coverage report exists" step fails closed downstream.
11+
* Any other read/write error propagates so a real lcov post-process failure is not masked. */
12+
export function normalizeLcovSfPaths(lcovPath, { readFile = readFileSync, writeFile = writeFileSync } = {}) {
13+
try {
14+
const raw = readFile(lcovPath, "utf8");
15+
writeFile(
16+
lcovPath,
17+
raw.replace(/^SF:(.*)$/gm, (_match, path) => `SF:${String(path).replace(/\\/g, "/")}`),
18+
);
19+
} catch (error) {
20+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return;
21+
throw error;
22+
}
23+
}
1324

1425
function collectTests(dir, out = []) {
1526
for (const ent of readdirSync(dir, { withFileTypes: true })) {
@@ -20,40 +31,40 @@ function collectTests(dir, out = []) {
2031
return out;
2132
}
2233

23-
const tests = collectTests(testRoot).map((path) => relative(root, path).split("\\").join("/"));
24-
if (tests.length === 0) {
25-
console.error("rees-coverage: no review-enrichment/test/**/*.test.ts files found");
26-
process.exit(1);
27-
}
34+
function main() {
35+
const root = join(fileURLToPath(new URL(".", import.meta.url)), "..");
36+
const c8Bin = join(root, "review-enrichment", "node_modules", "c8", "bin", "c8.js");
37+
const reportDir = join(root, "review-enrichment", "coverage");
38+
const testRoot = join(root, "review-enrichment", "test");
2839

29-
const result = spawnSync(
30-
process.execPath,
31-
[
32-
c8Bin,
33-
"--reporter=lcov",
34-
"--reporter=text-summary",
35-
`--report-dir=${reportDir}`,
36-
"--include=review-enrichment/dist/**/*.js",
37-
"--exclude=**/*.d.ts",
38-
"--all",
40+
const tests = collectTests(testRoot).map((path) => relative(root, path).split("\\").join("/"));
41+
if (tests.length === 0) {
42+
console.error("rees-coverage: no review-enrichment/test/**/*.test.ts files found");
43+
process.exit(1);
44+
}
45+
46+
const result = spawnSync(
3947
process.execPath,
40-
"--test",
41-
"--experimental-strip-types",
42-
...tests,
43-
],
44-
{ cwd: root, stdio: "inherit", env: process.env },
45-
);
46-
47-
// Codecov expects forward-slash SF: paths; c8 on Windows emits backslashes.
48-
const lcovPath = join(reportDir, "lcov.info");
49-
try {
50-
const raw = readFileSync(lcovPath, "utf8");
51-
writeFileSync(
52-
lcovPath,
53-
raw.replace(/^SF:(.*)$/gm, (_match, path) => `SF:${String(path).replace(/\\/g, "/")}`),
48+
[
49+
c8Bin,
50+
"--reporter=lcov",
51+
"--reporter=text-summary",
52+
`--report-dir=${reportDir}`,
53+
"--include=review-enrichment/dist/**/*.js",
54+
"--exclude=**/*.d.ts",
55+
"--all",
56+
process.execPath,
57+
"--test",
58+
"--experimental-strip-types",
59+
...tests,
60+
],
61+
{ cwd: root, stdio: "inherit", env: process.env },
5462
);
55-
} catch {
56-
// CI's "Verify REES coverage report exists" step fails closed if the report is missing.
63+
64+
// Codecov expects forward-slash SF: paths; c8 on Windows emits backslashes.
65+
normalizeLcovSfPaths(join(reportDir, "lcov.info"));
66+
67+
process.exit(result.status === null ? 1 : result.status);
5768
}
5869

59-
process.exit(result.status === null ? 1 : result.status);
70+
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { normalizeLcovSfPaths } from "../../scripts/rees-coverage.mjs";
3+
4+
describe("rees-coverage script", () => {
5+
describe("normalizeLcovSfPaths", () => {
6+
it("swallows ENOENT when the lcov report does not exist yet", () => {
7+
const readFile = vi.fn(() => {
8+
const err = new Error("ENOENT") as NodeJS.ErrnoException;
9+
err.code = "ENOENT";
10+
throw err;
11+
});
12+
const writeFile = vi.fn();
13+
14+
expect(() => normalizeLcovSfPaths("/tmp/missing/lcov.info", { readFile, writeFile })).not.toThrow();
15+
expect(readFile).toHaveBeenCalledOnce();
16+
expect(writeFile).not.toHaveBeenCalled();
17+
});
18+
19+
it("re-throws a write failure instead of masking it as a missing report", () => {
20+
const writeErr = new Error("EACCES: permission denied") as NodeJS.ErrnoException;
21+
writeErr.code = "EACCES";
22+
const readFile = vi.fn(() => "SF:review-enrichment\\src\\foo.ts\nend_of_record\n");
23+
const writeFile = vi.fn(() => {
24+
throw writeErr;
25+
});
26+
27+
expect(() => normalizeLcovSfPaths("/tmp/lcov.info", { readFile, writeFile })).toThrow(writeErr);
28+
expect(writeFile).toHaveBeenCalledOnce();
29+
});
30+
31+
it("normalizes backslashes in SF: paths to forward slashes", () => {
32+
let written = "";
33+
const readFile = vi.fn(() => "SF:review-enrichment\\src\\foo.ts\nend_of_record\n");
34+
const writeFile = vi.fn((_path: string, content: string) => {
35+
written = content;
36+
});
37+
38+
normalizeLcovSfPaths("/tmp/lcov.info", { readFile, writeFile });
39+
40+
expect(written).toBe("SF:review-enrichment/src/foo.ts\nend_of_record\n");
41+
});
42+
});
43+
});

0 commit comments

Comments
 (0)