diff --git a/packages/istanbul-lib-report/src/reports/cobertura/index.ts b/packages/istanbul-lib-report/src/reports/cobertura/index.ts index ac569d7..1ff490b 100644 --- a/packages/istanbul-lib-report/src/reports/cobertura/index.ts +++ b/packages/istanbul-lib-report/src/reports/cobertura/index.ts @@ -115,13 +115,15 @@ class CoberturaReport extends ReportBase { this.xml!.openTag("methods"); const fnMap = fileCoverage.fnMap; + const uniqueMethodName = createUniqueNamer(); Object.entries(fnMap).forEach(([k, { name, decl, loc }]) => { const hits = fileCoverage.f[k]; // Some versions of the instrumenter in the wild populate 'loc' // but not 'decl': const start = (decl || loc).start; this.xml!.openTag("method", { - name: escape(name), + // Jenkins rejects two tags with the same name in one class. + name: escape(uniqueMethodName(name)), hits, signature: "()V", //fake out a no-args void return }); @@ -167,4 +169,39 @@ function asClassName(node: ReportNode): string { return node.getRelativeName().replace(/.*[\\/]/, ""); } +const ANONYMOUS_NAME = /^\(anonymous_(\d+)\)$/; + +/** Returns a namer that deduplicates method names within one class. */ +function createUniqueNamer() { + const used = new Set(); + return (name: string | undefined): string => { + const base = name || "(anonymous)"; + if (!used.has(base)) { + used.add(base); + return base; + } + + const anonMatch = ANONYMOUS_NAME.exec(base); + if (anonMatch) { + let i = Number(anonMatch[1]) + 1; + let next = `(anonymous_${i})`; + while (used.has(next)) { + i += 1; + next = `(anonymous_${i})`; + } + used.add(next); + return next; + } + + let i = 2; + let next = `${base}_${i}`; + while (used.has(next)) { + i += 1; + next = `${base}_${i}`; + } + used.add(next); + return next; + }; +} + export default CoberturaReport; diff --git a/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts b/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts index 7077033..5b25328 100644 --- a/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts +++ b/packages/istanbul-lib-report/test/reports/cobertura/index.test.ts @@ -3,7 +3,15 @@ import { createRequire } from "node:module"; import path from "node:path"; import * as istanbulLibCoverage from "@vitest/istanbul-lib-coverage"; -import { afterAll as after, assert, beforeAll as before, beforeEach, describe, it } from "vitest"; +import { + afterAll as after, + assert, + beforeAll as before, + beforeEach, + describe, + expect, + it, +} from "vitest"; import * as istanbulLibReport from "../../../src/index"; import { FileWriter } from "../../../src/index"; @@ -57,4 +65,85 @@ describe("CoberturaReport", () => { createTest(file); } }); + + it("emits unique method names when fnMap has collisions", () => { + const context = istanbulLibReport.createContext({ + dir: "./", + coverageMap: istanbulLibCoverage.createCoverageMap({ + "/tmp/models.js": { + path: "/tmp/models.js", + statementMap: {}, + fnMap: { + 0: { + name: "constructor", + decl: { + start: { line: 2, column: 4 }, + end: { line: 2, column: 20 }, + }, + loc: { + start: { line: 2, column: 4 }, + end: { line: 4, column: 5 }, + }, + line: 2, + }, + 1: { + name: "constructor", + decl: { + start: { line: 8, column: 4 }, + end: { line: 8, column: 20 }, + }, + loc: { + start: { line: 8, column: 4 }, + end: { line: 10, column: 5 }, + }, + line: 8, + }, + 2: { + name: "(anonymous_0)", + decl: { + start: { line: 12, column: 0 }, + end: { line: 12, column: 10 }, + }, + loc: { + start: { line: 12, column: 0 }, + end: { line: 14, column: 1 }, + }, + line: 12, + }, + 3: { + name: "(anonymous_0)", + decl: { + start: { line: 16, column: 0 }, + end: { line: 16, column: 10 }, + }, + loc: { + start: { line: 16, column: 0 }, + end: { line: 18, column: 1 }, + }, + line: 16, + }, + }, + branchMap: {}, + s: {}, + f: { 0: 1, 1: 1, 2: 1, 3: 1 }, + b: {}, + }, + }), + }); + const tree = context.getTree("pkg"); + const report = new CoberturaReport({ + file: "-", + timestamp: "123456789", + projectRoot: "/tmp", + }); + tree.visit(report, context); + const output = FileWriter.getOutput(); + const names: string[] = []; + const re = /