Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions packages/istanbul-lib-coverage/src/coverage-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ export interface CoverageMapData {
[path: string]: FileCoverage | FileCoverageData;
}

function maybeConstruct<A, T>(obj: A | T, klass: new (obj: A) => T): T {
if (obj instanceof klass) {
return obj;
export function isCoverageMap(obj: unknown): obj is CoverageMap {
if (obj instanceof CoverageMap) {
return true;
}

return new klass(obj as A);
return (
typeof obj === "object" &&
obj !== null &&
typeof (obj as CoverageMap).data === "object" &&
typeof (obj as CoverageMap).fileCoverageFor === "function" &&
typeof (obj as CoverageMap).addFileCoverage === "function"
);
}

function loadMap(source?: CoverageMapData): Record<string, FileCoverage> {
Expand All @@ -31,7 +37,7 @@ function loadMap(source?: CoverageMapData): Record<string, FileCoverage> {
}

Object.entries(source).forEach(([k, cov]) => {
data[k] = maybeConstruct(cov, FileCoverage);
data[k] = cov instanceof FileCoverage ? cov : new FileCoverage(cov);
});

return data;
Expand All @@ -49,6 +55,8 @@ class CoverageMap {
constructor(obj?: CoverageMap | CoverageMapData) {
if (obj instanceof CoverageMap) {
this.data = obj.data;
} else if (isCoverageMap(obj)) {
this.data = loadMap(obj.data);
} else {
this.data = loadMap(obj);
}
Expand All @@ -61,7 +69,7 @@ class CoverageMap {
* as needed.
*/
merge(obj: CoverageMap | CoverageMapData): void {
const other = maybeConstruct(obj, CoverageMap);
const other = obj instanceof CoverageMap ? obj : new CoverageMap(obj);
Object.values(other.data).forEach((fc) => {
this.addFileCoverage(fc);
});
Expand Down
16 changes: 15 additions & 1 deletion packages/istanbul-lib-coverage/src/coverage-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class CoverageSummary {
constructor(obj?: CoverageSummary | CoverageSummaryData) {
if (!obj) {
this.data = blankSummary();
} else if (obj instanceof CoverageSummary) {
} else if (isCoverageSummary(obj)) {
this.data = obj.data;
} else {
this.data = obj;
Expand Down Expand Up @@ -125,4 +125,18 @@ class CoverageSummary {

dataProperties(CoverageSummary, ["lines", "statements", "functions", "branches", "branchesTrue"]);

export function isCoverageSummary(obj: unknown): obj is CoverageSummary {
if (obj instanceof CoverageSummary) {
return true;
}

return (
typeof obj === "object" &&
obj !== null &&
typeof (obj as CoverageSummary).data === "object" &&
typeof (obj as CoverageSummary).isEmpty === "function" &&
typeof (obj as CoverageSummary).merge === "function"
);
}

export { CoverageSummary };
16 changes: 15 additions & 1 deletion packages/istanbul-lib-coverage/src/file-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ class FileCoverage {
}
if (typeof pathOrObj === "string") {
this.data = emptyCoverage(pathOrObj, reportLogic);
} else if (pathOrObj instanceof FileCoverage) {
} else if (isFileCoverage(pathOrObj)) {
this.data = pathOrObj.data;
} else if (typeof pathOrObj === "object") {
this.data = pathOrObj;
Expand Down Expand Up @@ -496,6 +496,20 @@ dataProperties(FileCoverage, [
"all",
]);

export function isFileCoverage(obj: unknown): obj is FileCoverage {
if (obj instanceof FileCoverage) {
return true;
}

return (
typeof obj === "object" &&
obj !== null &&
typeof (obj as FileCoverage).data === "object" &&
typeof (obj as FileCoverage).toSummary === "function" &&
typeof (obj as FileCoverage).getLineCoverage === "function"
);
}

export {
FileCoverage,
// exported for testing
Expand Down
94 changes: 94 additions & 0 deletions packages/istanbul-lib-coverage/test/foreign-instance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect } from "vitest";

import { CoverageMap } from "../src/coverage-map";
import { CoverageSummary } from "../src/coverage-summary";
import { FileCoverage } from "../src/file-coverage";
import * as index from "../src/index";

// Load second, independent copies of the modules. This simulates a duplicate
// install of `@vitest/istanbul-lib-coverage`, where objects created by one copy
// are passed to the other and `instanceof` checks fail.
async function loadForeign<T>(path: string): Promise<T> {
return (await import(/* @vite-ignore */ `${path}?foreign`)) as T;
}

const foreign = {
CoverageMap: (await loadForeign<typeof import("../src/coverage-map")>("../src/coverage-map"))
.CoverageMap,
CoverageSummary: (
await loadForeign<typeof import("../src/coverage-summary")>("../src/coverage-summary")
).CoverageSummary,
FileCoverage: (await loadForeign<typeof import("../src/file-coverage")>("../src/file-coverage"))
.FileCoverage,
};

describe("instances from a duplicate copy of the package", () => {
it("test setup produces distinct classes", () => {
expect(foreign.CoverageMap).not.toBe(CoverageMap);
expect(foreign.CoverageSummary).not.toBe(CoverageSummary);
expect(foreign.FileCoverage).not.toBe(FileCoverage);
expect(new foreign.CoverageMap()).not.toBeInstanceOf(CoverageMap);
});

it("merges a foreign coverage map", () => {
const local = new CoverageMap({ "foo.js": new FileCoverage("foo.js") });
const other = new foreign.CoverageMap({
"foo.js": new foreign.FileCoverage("foo.js"),
"bar.js": new foreign.FileCoverage("bar.js"),
});

expect(() => local.merge(other)).not.toThrow();
expect(local.files().sort()).toEqual(["bar.js", "foo.js"]);
expect(local.fileCoverageFor("bar.js")).toBeInstanceOf(FileCoverage);
});

it("constructs from a foreign coverage map", () => {
const other = new foreign.CoverageMap({ "foo.js": new foreign.FileCoverage("foo.js") });

const local = new CoverageMap(other);
expect(local.files()).toEqual(["foo.js"]);
expect(local.fileCoverageFor("foo.js")).toBeInstanceOf(FileCoverage);
expect(index.createCoverageMap(other)).toBeInstanceOf(CoverageMap);
});

it("constructs from and merges a foreign file coverage", () => {
const other = new foreign.FileCoverage("foo.js");
other.data.statementMap["0"] = {
start: { line: 1, column: 0 },
end: { line: 1, column: 10 },
};
other.data.s["0"] = 3;

const local = new FileCoverage(other);
expect(local).toBeInstanceOf(FileCoverage);
expect(local.data).toBe(other.data);
expect(index.createFileCoverage(other)).toBeInstanceOf(FileCoverage);

const map = new CoverageMap();
map.addFileCoverage(other);
expect(map.fileCoverageFor("foo.js").s).toEqual({ "0": 3 });
map.addFileCoverage(other);
expect(map.fileCoverageFor("foo.js").s).toEqual({ "0": 6 });
});

it("constructs from and merges a foreign coverage summary", () => {
const other = new foreign.CoverageSummary();
other.lines.total = 10;
other.lines.covered = 5;

const local = new CoverageSummary(other);
expect(local).toBeInstanceOf(CoverageSummary);
expect(local.data).toBe(other.data);
expect(index.createCoverageSummary(other)).toBeInstanceOf(CoverageSummary);

const merged = new CoverageSummary().merge(other);
expect(merged.lines.total).toBe(10);
expect(merged.lines.pct).toBe(50);
});

it("still treats raw data objects as data", () => {
const map = new CoverageMap({ "foo.js": new FileCoverage("foo.js").data });
expect(map.fileCoverageFor("foo.js")).toBeInstanceOf(FileCoverage);
expect(new CoverageSummary(new CoverageSummary().data).isEmpty()).toBe(true);
});
});
Loading