Skip to content

Commit 4522196

Browse files
committed
add coverage support
1 parent 93ad6d6 commit 4522196

File tree

5 files changed

+247
-8
lines changed

5 files changed

+247
-8
lines changed

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
"homepage": "https://github.com/mesonbuild/vscode-meson/blob/master/README.md",
3535
"engines": {
36-
"vscode": "^1.75.0"
36+
"vscode": "^1.88.0"
3737
},
3838
"categories": [
3939
"Programming Languages"
@@ -572,7 +572,7 @@
572572
"devDependencies": {
573573
"@types/adm-zip": "^0.5.1",
574574
"@types/node": "^16.11.7",
575-
"@types/vscode": "^1.75.0",
575+
"@types/vscode": "^1.88.0",
576576
"@types/which": "^3.0.0",
577577
"husky": "^8.0.3",
578578
"lint-staged": "^14.0.1",
@@ -583,6 +583,9 @@
583583
},
584584
"dependencies": {
585585
"adm-zip": "^0.5.10",
586+
"domhandler": "^5.0.3",
587+
"domutils": "^3.2.2",
588+
"htmlparser2": "^10.0.0",
586589
"vscode-cpptools": "^6.1.0",
587590
"vscode-languageclient": "^9.0.1",
588591
"which": "^4.0.0"

src/coverage.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as htmlparser2 from "htmlparser2";
2+
import * as domutils from "domutils";
3+
import * as domhandler from "domhandler";
4+
import * as vscode from "vscode";
5+
import * as fs from "fs/promises";
6+
import * as path from "path";
7+
import { exec } from "./utils";
8+
import { workspaceState } from "./extension";
9+
10+
class MyFileCoverage extends vscode.FileCoverage {
11+
lineDetails: vscode.StatementCoverage[];
12+
13+
constructor(
14+
detailedCoverage: vscode.StatementCoverage[],
15+
uri: vscode.Uri,
16+
statementCoverage: vscode.TestCoverageCount,
17+
branchCoverage?: vscode.TestCoverageCount,
18+
declarationCoverage?: vscode.TestCoverageCount,
19+
) {
20+
super(uri, statementCoverage, branchCoverage, declarationCoverage);
21+
this.lineDetails = detailedCoverage;
22+
}
23+
}
24+
25+
export async function loadDetailedCoverage(
26+
_testRun: vscode.TestRun,
27+
fileCoverage: vscode.FileCoverage,
28+
_token: vscode.CancellationToken,
29+
): Promise<vscode.StatementCoverage[]> {
30+
return fileCoverage instanceof MyFileCoverage ? fileCoverage.lineDetails : [];
31+
}
32+
33+
/**
34+
* Read coverage data generated during test execution.
35+
* @param buildDir Meson build directory
36+
* @returns `FileCoverage[]` to be added to be used with `run.addCoverage()`
37+
*/
38+
export async function getCoverage(buildDir: string): Promise<vscode.FileCoverage[]> {
39+
await exec("ninja", ["-C", buildDir, "coverage-xml"]);
40+
return parseXml(await fs.readFile(path.join(buildDir, "meson-logs", "coverage.xml"), "utf-8"));
41+
}
42+
43+
/**
44+
* Parses a Cobertura xml file as generated by gcovr.
45+
* The schema is documented at
46+
* https://github.com/cobertura/cobertura/blob/master/cobertura/src/site/htdocs/xml/coverage-04.dtd
47+
* @param xml Contents of the Cobertura report
48+
* @returns `FileCoverage[]` to be added to be used with `run.addCoverage()`
49+
*/
50+
function parseXml(xml: string): vscode.FileCoverage[] {
51+
const ret: vscode.FileCoverage[] = [];
52+
53+
const sourceDir = workspaceState.get<string>("mesonbuild.sourceDir")!;
54+
55+
const dom = htmlparser2.parseDocument(xml, { xmlMode: true });
56+
const packages = domutils.findOne(
57+
(node) => {
58+
return node.name == "packages";
59+
},
60+
dom,
61+
true,
62+
)!;
63+
for (const pkg of packages.childNodes) {
64+
// The Cobertura format was designed for Java,
65+
// accordingly it considers classes the highest form of abstraction.
66+
// However, gcovr just creates one "class" per covered file, and fills it with line data.
67+
for (const cls of domutils.findOne((node) => node.name == "classes", pkg as domhandler.Element)!.childNodes) {
68+
const classElem = cls as domhandler.Element;
69+
const filePath = path.join(sourceDir, classElem.attribs["filename"]);
70+
const lineDetails: vscode.StatementCoverage[] = [];
71+
72+
let coveredBranches = 0;
73+
let totalBranches = 0;
74+
let coveredLines = 0;
75+
let totalLines = 0;
76+
77+
for (const line of domutils.findOne((node) => node.name == "lines", cls as domhandler.Element)!.childNodes) {
78+
totalLines++;
79+
const val = processLine(line as domhandler.Element);
80+
coveredLines += val.lineCovered ? 1 : 0;
81+
lineDetails.push(...val.lineDetails);
82+
coveredBranches += val.coveredBranches;
83+
totalBranches += val.totalBranches;
84+
}
85+
86+
const lineData = new vscode.TestCoverageCount(coveredLines, totalLines);
87+
const branchData = new vscode.TestCoverageCount(coveredBranches, totalBranches);
88+
ret.push(new MyFileCoverage(lineDetails, vscode.Uri.file(filePath), lineData, branchData));
89+
}
90+
}
91+
return ret;
92+
}
93+
94+
function processLine(line: domhandler.Element): {
95+
lineCovered: boolean;
96+
lineDetails: vscode.StatementCoverage[];
97+
coveredBranches: number;
98+
totalBranches: number;
99+
} {
100+
const hits = parseInt(line.attribs["hits"]);
101+
const lineNo = parseInt(line.attribs["number"]);
102+
// Position is 0-indexed
103+
const position = new vscode.Position(lineNo - 1, 0);
104+
105+
const lineCovered = hits > 0;
106+
const lineDetails: vscode.StatementCoverage[] = [];
107+
let coveredBranches = 0;
108+
let totalBranches = 0;
109+
110+
if (line.attribs["branch"] == "true") {
111+
const val = processBranch(line, position);
112+
coveredBranches = val.coveredBranches;
113+
totalBranches = val.totalBranches;
114+
lineDetails.push(new vscode.StatementCoverage(hits, position, val.branchCoverage));
115+
} else {
116+
lineDetails.push(new vscode.StatementCoverage(hits, position));
117+
}
118+
119+
return { lineCovered, lineDetails, coveredBranches, totalBranches };
120+
}
121+
122+
function processBranch(
123+
line: domhandler.Element,
124+
position: vscode.Position,
125+
): {
126+
branchCoverage: vscode.BranchCoverage[];
127+
coveredBranches: number;
128+
totalBranches: number;
129+
} {
130+
const branchCoverage: vscode.BranchCoverage[] = [];
131+
// format is "50% (1/2)"
132+
const conditionStr = line.attribs["condition-coverage"];
133+
const match = RegExp(/\d+% \((\d+)\/(\d+)\)/).exec(conditionStr)!;
134+
135+
const coveredBranches = parseInt(match[1]);
136+
const totalBranches = parseInt(match[2]);
137+
138+
// The gcov report does not specify which branches were missed,
139+
// so the best we can do is report whether a branch site was
140+
// fully, partially, or not at all covered
141+
if (coveredBranches > 0) {
142+
branchCoverage.push(new vscode.BranchCoverage(true, position));
143+
}
144+
if (coveredBranches < totalBranches) {
145+
branchCoverage.push(new vscode.BranchCoverage(false, position));
146+
}
147+
return { branchCoverage, coveredBranches, totalBranches };
148+
}

src/extension.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
} from "./utils";
1717
import { MesonDebugConfigurationProvider, DebuggerType } from "./debug/index";
1818
import { CpptoolsProvider, registerCppToolsProvider } from "./cpptoolsconfigprovider";
19-
import { testDebugHandler, testRunHandler, regenerateTests } from "./tests";
19+
import { testDebugHandler, testRunHandler, regenerateTests, testCoverageHandler } from "./tests";
20+
import { loadDetailedCoverage } from "./coverage";
2021
import { activateLinters } from "./linters";
2122
import { activateFormatters } from "./formatters";
2223
import { SettingsKey, TaskQuickPickItem } from "./types";
@@ -118,6 +119,12 @@ export async function activate(ctx: vscode.ExtensionContext) {
118119
(request, token) => testRunHandler(controller, request, token),
119120
true,
120121
);
122+
controller.createRunProfile(
123+
"Meson coverage",
124+
vscode.TestRunProfileKind.Coverage,
125+
(request, token) => testCoverageHandler(controller, request, token),
126+
true,
127+
).loadDetailedCoverage = loadDetailedCoverage;
121128
ctx.subscriptions.push(controller);
122129

123130
let mesonTasks: Thenable<vscode.Task[]> | null = null;

src/tests.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import * as os from "os";
22
import * as vscode from "vscode";
33
import { ExecResult, exec, extensionConfiguration, getTargetName } from "./utils";
44
import { Target, Targets, Test, Tests, DebugEnvironmentConfiguration } from "./types";
5-
import { getMesonTests, getMesonTargets } from "./introspection";
5+
import { getMesonTests, getMesonTargets, getMesonBuildOptions } from "./introspection";
66
import { workspaceState } from "./extension";
7+
import { getCoverage } from "./coverage";
78

89
// This is far from complete, but should suffice for the
910
// "test is made of a single executable is made of a single source file" usecase.
@@ -105,6 +106,7 @@ export async function testRunHandler(
105106
controller: vscode.TestController,
106107
request: vscode.TestRunRequest,
107108
token: vscode.CancellationToken,
109+
coverage: boolean = false,
108110
) {
109111
const run = controller.createTestRun(request, undefined, false);
110112
const parallelTests: vscode.TestItem[] = [];
@@ -113,6 +115,14 @@ export async function testRunHandler(
113115
const buildDir = workspaceState.get<string>("mesonbuild.buildDir")!;
114116
const mesonTests = await getMesonTests(buildDir);
115117

118+
if (coverage) {
119+
// Existing files should be cleaned so that with
120+
// tests = A, B
121+
// runTest(A); runTest(B);
122+
// the coverage results of B won't include the results of A
123+
await exec("ninja", ["-C", buildDir, "clean-gcda"]);
124+
}
125+
116126
// Look up the meson test for a given vscode test,
117127
// put it in the parallel or sequential queue,
118128
// and tell vscode about the enqueued test.
@@ -199,9 +209,30 @@ export async function testRunHandler(
199209
await dispatchTest(test);
200210
}
201211

212+
if (coverage) {
213+
for (const coverage of await getCoverage(buildDir)) {
214+
run.addCoverage(coverage);
215+
}
216+
}
217+
202218
run.end();
203219
}
204220

221+
export async function testCoverageHandler(
222+
controller: vscode.TestController,
223+
request: vscode.TestRunRequest,
224+
token: vscode.CancellationToken,
225+
) {
226+
const buildDir = workspaceState.get<string>("mesonbuild.buildDir")!;
227+
const hasCoverage = (await getMesonBuildOptions(buildDir)).find((option) => option.name == "b_coverage")
228+
?.value as boolean;
229+
if (!hasCoverage) {
230+
vscode.window.showErrorMessage("Coverage was not enabled. Reconfigure with -Db_coverage=true");
231+
return;
232+
}
233+
return testRunHandler(controller, request, token, true);
234+
}
235+
205236
export async function testDebugHandler(
206237
controller: vscode.TestController,
207238
request: vscode.TestRunRequest,

yarn.lock

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.105.tgz#7147176852774ec4d6dd626803888adf6b999feb"
4343
integrity sha512-w2d0Z9yMk07uH3+Cx0N8lqFyi3yjXZxlbYappPj+AsOlT02OyxyiuNoNHdGt6EuiSm8Wtgp2YV7vWg+GMFrvFA==
4444

45-
"@types/vscode@^1.75.0":
46-
version "1.92.0"
47-
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.92.0.tgz#b4d6bc180e7206defe643a1a5f38a1367947d418"
48-
integrity sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==
45+
"@types/vscode@^1.88.0":
46+
version "1.99.1"
47+
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.99.1.tgz#bde6e2d9ccbe0493fded98ad639bf2671b8ec9ee"
48+
integrity sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==
4949

5050
"@types/which@^3.0.0":
5151
version "3.0.4"
@@ -151,6 +151,36 @@ dir-glob@^3.0.1:
151151
dependencies:
152152
path-type "^4.0.0"
153153

154+
dom-serializer@^2.0.0:
155+
version "2.0.0"
156+
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
157+
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
158+
dependencies:
159+
domelementtype "^2.3.0"
160+
domhandler "^5.0.2"
161+
entities "^4.2.0"
162+
163+
domelementtype@^2.3.0:
164+
version "2.3.0"
165+
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
166+
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
167+
168+
domhandler@^5.0.2, domhandler@^5.0.3:
169+
version "5.0.3"
170+
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
171+
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
172+
dependencies:
173+
domelementtype "^2.3.0"
174+
175+
domutils@^3.2.1, domutils@^3.2.2:
176+
version "3.2.2"
177+
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
178+
integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==
179+
dependencies:
180+
dom-serializer "^2.0.0"
181+
domelementtype "^2.3.0"
182+
domhandler "^5.0.3"
183+
154184
eastasianwidth@^0.2.0:
155185
version "0.2.0"
156186
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -161,6 +191,16 @@ emoji-regex@^9.2.2:
161191
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
162192
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
163193

194+
entities@^4.2.0:
195+
version "4.5.0"
196+
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
197+
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
198+
199+
entities@^6.0.0:
200+
version "6.0.0"
201+
resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.0.tgz#09c9e29cb79b0a6459a9b9db9efb418ac5bb8e51"
202+
integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==
203+
164204
entities@~3.0.1:
165205
version "3.0.1"
166206
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
@@ -234,6 +274,16 @@ [email protected]:
234274
merge2 "^1.4.1"
235275
slash "^4.0.0"
236276

277+
htmlparser2@^10.0.0:
278+
version "10.0.0"
279+
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.0.0.tgz#77ad249037b66bf8cc99c6e286ef73b83aeb621d"
280+
integrity sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==
281+
dependencies:
282+
domelementtype "^2.3.0"
283+
domhandler "^5.0.3"
284+
domutils "^3.2.1"
285+
entities "^6.0.0"
286+
237287
human-signals@^4.3.0:
238288
version "4.3.1"
239289
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"

0 commit comments

Comments
 (0)