Skip to content

Commit 61a0cb5

Browse files
Copilotnev21Copilotnevware21-bot
authored
fix(merge-coverage): eliminate Windows path-separator duplication (#203)
* Initial plan * fix: normalize coverage path separators on Windows in merge-coverage - Add normalizeCoverageKeys() to convert backslash path keys and `path` properties to forward slashes in coverage JSON blobs before merging. This prevents nyc (backslash) vs karma-typescript (forward-slash) key duplication on Windows. - Normalize globby paths to forward slashes before the exclusion filter so that `coverage/coverage-final.json` is correctly excluded even when globby returns backslash-separated paths on Windows. - Add tests for normalizeCoverageKeys in merge-coverage.test.ts Agent-Logs-Url: https://github.com/nevware21/ts-build-tools/sessions/fe9562c3-39c4-4338-9207-223a423590cb Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> * chore: revert accidental package.json changes from test setup Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> * fix: use string types and const instead of any/var in merge-coverage Address code review feedback: use string type annotations instead of any in lambda parameters, and use const instead of var for jsonBlobs. Agent-Logs-Url: https://github.com/nevware21/ts-build-tools/sessions/fe9562c3-39c4-4338-9207-223a423590cb Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: use simple overwrite instead of istanbul merge for key collision in normalizeCoverageKeys The istanbul FileCoverage.merge() call requires complete statementMap location data (start/end), which may not be present. Use simple last-write-wins overwrite instead — cross-blob merging is handled by istanbul in the main mergeCoverage loop. All 23 tests pass. Agent-Logs-Url: https://github.com/nevware21/ts-build-tools/sessions/148514cb-94a7-4094-a057-e3cb05720107 Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> * chore: revert accidental package.json changes from local test setup Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> * fix: move normalizeCoverageKeys to utils.ts to fix Node 18 ESM loader issue merge-coverage.ts imports globby v13 (ESM-only). On Node 18, loading any test that imports from merge-coverage.ts triggers the ESM loader, which can't handle .ts file extensions (ERR_UNKNOWN_FILE_EXTENSION). Moving normalizeCoverageKeys to utils.ts (only CJS deps: fs, path, @nevware21/ts-utils) and updating the test to import from utils.ts avoids this ESM cascade. merge-coverage.ts re-exports the function for API compatibility. utils.ts also gets stronger types (Record<string, any> instead of any). Agent-Logs-Url: https://github.com/nevware21/ts-build-tools/sessions/4303ce38-8fd4-4261-a66a-f982cd20395a Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nev21 <82737406+nev21@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: nevware21-bot <252503968+nevware21-bot@users.noreply.github.com>
1 parent 1be4f31 commit 61a0cb5

3 files changed

Lines changed: 94 additions & 6 deletions

File tree

lib/coverage-tools/src/merge-coverage.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import * as fs from "fs";
1717
import * as path from "path";
1818
import { globbySync } from "globby";
1919
import { IMergeCoverageArgs } from "./interfaces/IMergeCoverageArgs";
20-
import { findPath, getJson } from "./utils";
20+
import { findPath, getJson, normalizeCoverageKeys } from "./utils";
2121

22-
export { IMergeCoverageArgs };
22+
export { IMergeCoverageArgs, normalizeCoverageKeys };
2323

2424
export function findCoverage(thePath: string): string {
2525
let foundPath = findPath((thePath) => {
@@ -54,14 +54,18 @@ export function mergeCoverage(cfg: IMergeCoverageArgs) {
5454

5555
console.log(`Merging coverage files in ${rootPath} - [${fs.realpathSync(rootPath)}]`);
5656
// Find any files named "coverage-final.json" (excluding any existing merged one)
57+
// Normalize to forward slashes so the filter works correctly on Windows too
5758
let jsonFiles = globbySync(`${rootPath}/**/coverage-final.json`)
58-
.filter((possibleFile: any) => (possibleFile.indexOf("report") === -1 && possibleFile.indexOf("coverage/coverage-final.json") === -1));
59+
.map((possibleFile: string) => possibleFile.replace(/\\/g, "/"))
60+
.filter((possibleFile: string) => (possibleFile.indexOf("report") === -1 && possibleFile.indexOf("coverage/coverage-final.json") === -1));
5961

6062
console.log(`Found ${jsonFiles.length} coverage files to merge:`);
61-
jsonFiles.forEach((file: any) => console.log(` merging: ${file}`));
63+
jsonFiles.forEach((file: string) => console.log(` merging: ${file}`));
6264

63-
// Load the json blobs from the discovered .json files
64-
var jsonBlobs = jsonFiles.map((file: any) => getJson(file));
65+
// Load the json blobs from the discovered .json files, normalizing path keys
66+
// to forward slashes so that nyc (backslash) and karma-typescript (forward slash)
67+
// entries are treated as the same file on Windows.
68+
const jsonBlobs = jsonFiles.map((file: string) => normalizeCoverageKeys(getJson(file)));
6569

6670
// Create an empty map and merge in all the loaded maps
6771
let mergedMap = istanbulCoverage.createCoverageMap();

lib/coverage-tools/src/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,34 @@ export function findPath(cb: (thePath: string) => string, thePath: string, cnt =
6767
return findPath(cb, thePath + "../", cnt + 1);
6868
}
6969

70+
/**
71+
* Normalize all path keys and `path` properties in a coverage JSON blob to use
72+
* forward slashes. This ensures that entries produced by tools that emit
73+
* Windows-style backslash paths (e.g. nyc) are treated as the same file as
74+
* entries produced by tools that emit POSIX-style forward-slash paths
75+
* (e.g. karma-typescript) when istanbul merges the coverage maps.
76+
*
77+
* Returns a new object; does not mutate the input.
78+
*/
79+
export function normalizeCoverageKeys(jsonBlob: Record<string, any>): Record<string, any> {
80+
const normalized: Record<string, any> = {};
81+
Object.keys(jsonBlob).forEach((key: string) => {
82+
const normalizedKey = key.replace(/\\/g, "/");
83+
const entry = jsonBlob[key];
84+
const normalizedEntry = entry && typeof entry === "object" ? { ...entry } : entry;
85+
86+
if (normalizedEntry && normalizedEntry.path) {
87+
normalizedEntry.path = normalizedEntry.path.replace(/\\/g, "/");
88+
}
89+
90+
// The later entry wins if both a backslash-keyed and a forward-slash-keyed entry
91+
// normalize to the same key. Cross-blob merging is handled by istanbul in the
92+
// main mergeCoverage loop.
93+
normalized[normalizedKey] = normalizedEntry;
94+
});
95+
return normalized;
96+
}
97+
7098
export function findRepoRoot(thePath: string): string {
7199
let foundPath = findPath((thePath) => {
72100
console.log("Checking [" + thePath + ".git]");
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* @nevware21/ts-build-tools
3+
* https://github.com/nevware21/ts-build-tools
4+
*
5+
* Copyright (c) 2022 NevWare21 Solutions LLC
6+
* Licensed under the MIT license.
7+
*/
8+
9+
import { expect } from "@nevware21/tripwire";
10+
import { normalizeCoverageKeys } from "../../src/utils";
11+
12+
describe("normalizeCoverageKeys", () => {
13+
it("should return an empty object for an empty input", () => {
14+
expect(normalizeCoverageKeys({})).deep.equals({});
15+
});
16+
17+
it("should leave forward-slash keys unchanged", () => {
18+
const input = {
19+
"D:/project/src/foo.ts": { path: "D:/project/src/foo.ts", s: {}, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} }
20+
};
21+
const result = normalizeCoverageKeys(input);
22+
expect(Object.keys(result)).deep.equals(["D:/project/src/foo.ts"]);
23+
expect(result["D:/project/src/foo.ts"].path).equals("D:/project/src/foo.ts");
24+
});
25+
26+
it("should convert backslash keys to forward slashes", () => {
27+
const input = {
28+
"D:\\project\\src\\foo.ts": { path: "D:\\project\\src\\foo.ts", s: {}, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} }
29+
};
30+
const result = normalizeCoverageKeys(input);
31+
expect(Object.keys(result)).deep.equals(["D:/project/src/foo.ts"]);
32+
expect(result["D:/project/src/foo.ts"].path).equals("D:/project/src/foo.ts");
33+
});
34+
35+
it("should normalize both backslash and forward-slash entries to the same key", () => {
36+
// Simulate nyc (backslash) + karma-typescript (forward-slash) for the same file
37+
const input = {
38+
"D:\\project\\src\\foo.ts": { path: "D:\\project\\src\\foo.ts", s: { "0": 1 }, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} },
39+
"D:/project/src/foo.ts": { path: "D:/project/src/foo.ts", s: { "0": 2 }, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} }
40+
};
41+
const result = normalizeCoverageKeys(input);
42+
const keys = Object.keys(result);
43+
expect(keys).deep.equals(["D:/project/src/foo.ts"]);
44+
expect(result["D:/project/src/foo.ts"].path).equals("D:/project/src/foo.ts");
45+
expect(result["D:/project/src/foo.ts"].s).deep.equals({ "0": 2 });
46+
});
47+
48+
it("should not modify entries without a path property", () => {
49+
const input = {
50+
"D:\\project\\src\\foo.ts": { s: {}, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} }
51+
};
52+
const result = normalizeCoverageKeys(input);
53+
expect(Object.keys(result)).deep.equals(["D:/project/src/foo.ts"]);
54+
expect(result["D:/project/src/foo.ts"].path).equals(undefined);
55+
});
56+
});

0 commit comments

Comments
 (0)