Skip to content

Commit cb3bce8

Browse files
committed
jest: Patch implicit else branch coverage
1 parent 86117a1 commit cb3bce8

File tree

6 files changed

+109
-7
lines changed

6 files changed

+109
-7
lines changed

packages/jest-runner/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import type { JestEnvironment } from "@jest/environment";
1818
import { TestResult } from "@jest/test-result";
1919
import { Config } from "@jest/types";
20+
import * as libCoverage from "istanbul-lib-coverage";
2021
import * as reports from "istanbul-reports";
2122
import Runtime from "jest-runtime";
2223

@@ -71,6 +72,7 @@ export default async function jazzerTestRunner(
7172
testPath,
7273
sendMessageToJest,
7374
).then((result: TestResult) => {
75+
includeImplicitElseBranches(environment.global.__coverage__);
7476
return cleanupTestResultDetails(result);
7577
});
7678
}
@@ -90,6 +92,52 @@ function cleanupTestResultDetails(result: TestResult) {
9092
return result;
9193
}
9294

95+
/**
96+
* Coverage fix from https://github.com/vitest-dev/vitest/pull/2275
97+
* In our tests this seems to only affect the coverage of TypeScript files,
98+
* hence including the fix in jest-runner should be sufficient.
99+
*
100+
* Original comment:
101+
* Work-around for #1887 and #2239 while waiting for https://github.com/istanbuljs/istanbuljs/pull/706
102+
* Goes through all files in the coverage map and checks if branchMap's have
103+
* if-statements with implicit else. When finds one, copies source location of
104+
* the if-statement into the else statement.
105+
*/
106+
export function includeImplicitElseBranches(
107+
coverageMapData: libCoverage.CoverageMapData,
108+
) {
109+
if (!coverageMapData) {
110+
return;
111+
}
112+
function isEmptyCoverageRange(range: libCoverage.Range) {
113+
return (
114+
range.start === undefined ||
115+
range.start.line === undefined ||
116+
range.start.column === undefined ||
117+
range.end === undefined ||
118+
range.end.line === undefined ||
119+
range.end.column === undefined
120+
);
121+
}
122+
const coverageMap = libCoverage.createCoverageMap(coverageMapData);
123+
for (const file of coverageMap.files()) {
124+
const fileCoverage = coverageMap.fileCoverageFor(file);
125+
for (const branchMap of Object.values(fileCoverage.branchMap)) {
126+
if (branchMap.type === "if") {
127+
const lastIndex = branchMap.locations.length - 1;
128+
if (lastIndex > 0) {
129+
const elseLocation = branchMap.locations[lastIndex];
130+
if (elseLocation && isEmptyCoverageRange(elseLocation)) {
131+
const ifLocation = branchMap.locations[0];
132+
elseLocation.start = { ...ifLocation.start };
133+
elseLocation.end = { ...ifLocation.end };
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}
140+
93141
// Global definition of the Jest fuzz test extension function.
94142
// This is required to allow the Typescript compiler to recognize it.
95143
declare global {

tests/code_coverage/sample_fuzz_test/expected_coverage/fuzz+lib+codeCoverage-fuzz.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@
104104
"start": { "line": 24, "column": 1 },
105105
"end": { "line": 26, "column": 2 }
106106
},
107-
{ "start": {}, "end": {} }
107+
{
108+
"start": { "line": 24, "column": 1 },
109+
"end": { "line": 26, "column": 2 }
110+
}
108111
],
109112
"line": 24
110113
}
@@ -164,7 +167,10 @@
164167
"start": { "line": 19, "column": 1 },
165168
"end": { "line": 21, "column": 2 }
166169
},
167-
{ "start": {}, "end": {} }
170+
{
171+
"start": { "line": 19, "column": 1 },
172+
"end": { "line": 21, "column": 2 }
173+
}
168174
],
169175
"line": 19
170176
}

tests/code_coverage/sample_fuzz_test/expected_coverage/fuzz+lib+otherCodeCoverage-fuzz.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@
113113
"start": { "line": 24, "column": 1 },
114114
"end": { "line": 26, "column": 2 }
115115
},
116-
{ "start": {}, "end": {} }
116+
{
117+
"start": { "line": 24, "column": 1 },
118+
"end": { "line": 26, "column": 2 }
119+
}
117120
],
118121
"line": 24
119122
}
@@ -173,7 +176,10 @@
173176
"start": { "line": 19, "column": 1 },
174177
"end": { "line": 21, "column": 2 }
175178
},
176-
{ "start": {}, "end": {} }
179+
{
180+
"start": { "line": 19, "column": 1 },
181+
"end": { "line": 21, "column": 2 }
182+
}
177183
],
178184
"line": 19
179185
}

tests/helpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ class FuzzTestBuilder {
426426
}
427427

428428
coverage(coverage) {
429-
this._coverage = coverage;
429+
this._coverage = coverage === undefined ? true : coverage;
430430
return this;
431431
}
432432

tests/jest_integration/integration.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,27 @@ describe("Jest TS integration", () => {
472472
});
473473
});
474474
});
475+
476+
describe("Regression mode", () => {
477+
const regressionTestBuilder = new FuzzTestBuilder()
478+
.dir(projectDir)
479+
.jestTestFile(jestTsTestFile + ".ts");
480+
481+
describe("mixed features", () => {
482+
it("cover implicit else branch", async () => {
483+
regressionTestBuilder
484+
.jestTestName("execute sync test")
485+
.coverage()
486+
.build()
487+
.execute();
488+
const coverage = readCoverageOf(projectDir, "target.ts");
489+
// Expect that every branch has two entries, one for the if and one for the else branch.
490+
Object.keys(coverage.b).forEach((branch) => {
491+
expect(coverage.b[branch]).toHaveLength(2);
492+
});
493+
});
494+
});
495+
});
475496
});
476497

477498
// Deflake the "timeout after N seconds" test to be more tolerant to small variations of N (+-1).
@@ -513,3 +534,18 @@ function cleanupProjectFiles(projectDir, jestTestFile) {
513534
});
514535
};
515536
}
537+
538+
function readCoverageOf(projectDir, fileName) {
539+
const report = JSON.parse(
540+
fs.readFileSync(
541+
path.join(projectDir, "coverage", "coverage-final.json"),
542+
"utf-8",
543+
),
544+
);
545+
for (let path of Object.keys(report)) {
546+
if (path.endsWith(fileName)) {
547+
return report[path];
548+
}
549+
}
550+
throw new Error("Could not find coverage for " + fileName);
551+
}

tests/jest_integration/jest_project_ts/target.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
*/
1616

1717
export function fuzzMe(data: Buffer) {
18-
if (data.toString() === "Awesome") {
19-
throw Error("Welcome to Awesome Fuzzing!");
18+
if (typeof data === "object") {
19+
if (data.toString() === "Awesome") {
20+
throw Error("Welcome to Awesome Fuzzing!");
21+
}
22+
return data;
2023
}
24+
// Implicit else block to test coverage error,
25+
// see: https://github.com/vitest-dev/vitest/pull/2275
26+
return data;
2127
}
2228

2329
export function callbackFuzzMe(data: Buffer, done: (e?: Error) => void) {

0 commit comments

Comments
 (0)