Skip to content

Commit 81f3c71

Browse files
authored
feat(acton): add coverage support for tests (#240)
1 parent 083b0e1 commit 81f3c71

5 files changed

Lines changed: 131 additions & 15 deletions

File tree

editors/code/src/acton/ActonCommand.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,22 @@ export class TestCommand extends ActonCommand {
4141
public target: string = "",
4242
public functionName: string = "",
4343
public clearCache: boolean = false,
44+
public coverage: boolean = false,
45+
public coverageFormat: string = "lcov",
46+
public coverageFile: string = "",
4447
) {
4548
super("test")
4649
}
4750

4851
public override getArguments(): string[] {
4952
const args: string[] = ["--reporter", "console"]
5053
if (this.clearCache) args.push("--clear-cache")
54+
if (this.coverage) {
55+
args.push("--coverage", "--coverage-format", this.coverageFormat)
56+
if (this.coverageFile.trim() !== "") {
57+
args.push("--coverage-file", this.coverageFile)
58+
}
59+
}
5160

5261
switch (this.mode) {
5362
case TestMode.FUNCTION: {

editors/code/src/acton/ActonTestController.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright © 2026 TON Core
33

4+
import * as fs from "node:fs/promises"
45
import * as path from "node:path"
56

67
import * as vscode from "vscode"
78

9+
import {FileCoverageDetail} from "vscode"
10+
811
import {Acton} from "./Acton"
912
import {TestCommand, TestMode} from "./ActonCommand"
1013

1114
export class ActonTestController implements Disposable {
1215
private readonly controller: vscode.TestController
1316
private readonly outputChannel: vscode.OutputChannel
17+
private readonly coverageDetails: Map<string, vscode.StatementCoverage[]> = new Map()
1418

1519
public constructor() {
1620
this.controller = vscode.tests.createTestController("actonTests", "Acton Tests")
@@ -25,6 +29,22 @@ export class ActonTestController implements Disposable {
2529
true,
2630
)
2731

32+
const coverageProfile = this.controller.createRunProfile(
33+
"Coverage",
34+
vscode.TestRunProfileKind.Coverage,
35+
async (request, token) => {
36+
await this.runHandler(request, token, true)
37+
},
38+
true,
39+
)
40+
41+
coverageProfile.loadDetailedCoverage = (
42+
_testRun,
43+
fileCoverage,
44+
): Thenable<FileCoverageDetail[]> => {
45+
return Promise.resolve(this.coverageDetails.get(fileCoverage.uri.toString()) ?? [])
46+
}
47+
2848
this.controller.resolveHandler = async item => {
2949
await (item ? this.discoverTestsInItem(item) : this.discoverAllTests())
3050
}
@@ -135,7 +155,11 @@ export class ActonTestController implements Disposable {
135155
private async runHandler(
136156
request: vscode.TestRunRequest,
137157
token: vscode.CancellationToken,
158+
isCoverage: boolean = false,
138159
): Promise<void> {
160+
if (isCoverage) {
161+
this.coverageDetails.clear()
162+
}
139163
const run = this.controller.createTestRun(request)
140164
const queue: vscode.TestItem[] = []
141165

@@ -154,7 +178,7 @@ export class ActonTestController implements Disposable {
154178
break
155179
}
156180

157-
await this.runTestItem(test, run, token)
181+
await this.runTestItem(test, run, token, isCoverage)
158182
}
159183

160184
run.end()
@@ -164,6 +188,7 @@ export class ActonTestController implements Disposable {
164188
item: vscode.TestItem,
165189
run: vscode.TestRun,
166190
token: vscode.CancellationToken,
191+
isCoverage: boolean = false,
167192
): Promise<void> {
168193
if (token.isCancellationRequested) {
169194
return
@@ -181,10 +206,27 @@ export class ActonTestController implements Disposable {
181206
const workingDir = tomlUri ? path.dirname(tomlUri.fsPath) : path.dirname(uri.fsPath)
182207
const relativePath = path.relative(workingDir, uri.fsPath)
183208

209+
const coverageFile = isCoverage ? path.join(workingDir, "lcov.info") : ""
184210
const command =
185211
item.children.size > 0
186-
? new TestCommand(TestMode.FILE, relativePath)
187-
: new TestCommand(TestMode.FUNCTION, relativePath, item.label)
212+
? new TestCommand(
213+
TestMode.FILE,
214+
relativePath,
215+
"",
216+
false,
217+
isCoverage,
218+
"lcov",
219+
coverageFile,
220+
)
221+
: new TestCommand(
222+
TestMode.FUNCTION,
223+
relativePath,
224+
item.label,
225+
false,
226+
isCoverage,
227+
"lcov",
228+
coverageFile,
229+
)
188230

189231
try {
190232
const {exitCode, stdout, stderr} = await Acton.getInstance().spawn(
@@ -199,6 +241,9 @@ export class ActonTestController implements Disposable {
199241

200242
if (exitCode === 0) {
201243
run.passed(item)
244+
if (isCoverage && coverageFile) {
245+
await this.processCoverage(coverageFile, run, workingDir)
246+
}
202247
} else {
203248
const rawMessage = stderr || stdout || "Test failed"
204249

@@ -220,6 +265,69 @@ export class ActonTestController implements Disposable {
220265
}
221266
}
222267

268+
private async processCoverage(
269+
coverageFile: string,
270+
run: vscode.TestRun,
271+
workingDir: string,
272+
): Promise<void> {
273+
try {
274+
const content = await fs.readFile(coverageFile, "utf8")
275+
const files = this.parseLcov(content)
276+
277+
for (const fileData of files) {
278+
const absolutePath = path.isAbsolute(fileData.file)
279+
? fileData.file
280+
: path.join(workingDir, fileData.file)
281+
const uri = vscode.Uri.file(absolutePath)
282+
283+
const statementCoverage: vscode.StatementCoverage[] = fileData.lines.map(line => {
284+
return new vscode.StatementCoverage(
285+
line.count,
286+
new vscode.Range(line.line - 1, 0, line.line - 1, 0),
287+
)
288+
})
289+
290+
this.coverageDetails.set(uri.toString(), statementCoverage)
291+
292+
const coveredLines = fileData.lines.filter(l => l.count > 0).length
293+
const totalLines = fileData.lines.length
294+
295+
const fileCoverage = new vscode.FileCoverage(
296+
uri,
297+
new vscode.TestCoverageCount(coveredLines, totalLines),
298+
)
299+
300+
run.addCoverage(fileCoverage)
301+
}
302+
} catch (error) {
303+
console.error("Failed to process coverage", error)
304+
}
305+
}
306+
307+
private parseLcov(content: string): {file: string; lines: {line: number; count: number}[]}[] {
308+
const files: {file: string; lines: {line: number; count: number}[]}[] = []
309+
let currentFile: {file: string; lines: {line: number; count: number}[]} | null = null
310+
311+
for (const line of content.split(/\r?\n/)) {
312+
if (line.startsWith("SF:")) {
313+
currentFile = {file: line.slice(3).trim(), lines: []}
314+
} else if (line.startsWith("DA:") && currentFile) {
315+
const parts = line.slice(3).split(",")
316+
if (parts.length >= 2) {
317+
const lineNum = Number.parseInt(parts[0], 10)
318+
const count = Number.parseInt(parts[1], 10)
319+
if (!Number.isNaN(lineNum) && !Number.isNaN(count)) {
320+
currentFile.lines.push({line: lineNum, count})
321+
}
322+
}
323+
} else if (line.startsWith("end_of_record") && currentFile) {
324+
files.push(currentFile)
325+
currentFile = null
326+
}
327+
}
328+
return files
329+
}
330+
223331
private extractErrorLocation(cleanMessage: string): vscode.Location | undefined {
224332
const locationMatch = /at\s+(.+):(\d+):(\d+)/.exec(cleanMessage)
225333
if (!locationMatch) return undefined

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
],
1515
"version": "0.5.1",
1616
"engines": {
17-
"vscode": "^1.63.0"
17+
"vscode": "^1.88.0"
1818
},
1919
"icon": "editors/code/src/assets/logo.png",
2020
"activationEvents": [
@@ -833,7 +833,7 @@
833833
"dependencies": {
834834
"@ton/core": "0.61.0",
835835
"@ton/crypto": "^3.3.0",
836-
"@vscode/debugadapter": "^1.51.0",
836+
"@vscode/debugadapter": "^1.68.0",
837837
"@vscode/debugprotocol": "^1.68.0",
838838
"buffer": "^6.0.3",
839839
"glob": "^11.1.0",
@@ -863,7 +863,7 @@
863863
"@types/node": "^22.2.0",
864864
"@types/react": "^18.3.3",
865865
"@types/react-dom": "^18.3.0",
866-
"@types/vscode": "^1.63.0",
866+
"@types/vscode": "^1.88.0",
867867
"@types/ws": "^8",
868868
"@vscode/test-cli": "^0.0.12",
869869
"@vscode/test-electron": "^2.4.1",

server/src/languages/tolk/inspections/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {UnusedVariableInspection} from "@server/languages/tolk/inspections/Unuse
88
import {DeprecatedSymbolUsageInspection} from "@server/languages/tolk/inspections/DeprecatedSymbolUsageInspection"
99
import {UnusedImportInspection} from "@server/languages/tolk/inspections/UnusedImportInspection"
1010
import {StructInitializationInspection} from "@server/languages/tolk/inspections/StructInitializationInspection"
11-
import {TypeCompatibilityInspection} from "@server/languages/tolk/inspections/TypeCompatibilityInspection"
1211
import {CannotReassignInspection} from "@server/languages/tolk/inspections/CannotReassignInspection"
1312
import {UnusedTopLevelDeclarationInspection} from "@server/languages/tolk/inspections/UnusedTopLevelDeclarationInspection"
1413
import {UnusedTypeParameterInspection} from "@server/languages/tolk/inspections/UnusedTypeParameterInspection"
@@ -29,7 +28,7 @@ export async function runTolkInspections(
2928
new UnusedTopLevelDeclarationInspection(),
3029
new DeprecatedSymbolUsageInspection(),
3130
new StructInitializationInspection(),
32-
new TypeCompatibilityInspection(),
31+
// new TypeCompatibilityInspection(),
3332
new CannotReassignInspection(),
3433
new NeedNotNullUnwrappingInspection(),
3534
new MissedSemicolonInspection(),

yarn.lock

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,10 +2676,10 @@ __metadata:
26762676
languageName: node
26772677
linkType: hard
26782678

2679-
"@types/vscode@npm:^1.63.0":
2680-
version: 1.104.0
2681-
resolution: "@types/vscode@npm:1.104.0"
2682-
checksum: 10c0/6d9dd470a8d430c65ce5069bc504b9b3f500fcfe13dc7e5df23994f70ea569707ea005936cf706d325b3abf28bfdc402eb8b8449c548b7428f1a6ebfe0d264b9
2679+
"@types/vscode@npm:^1.88.0":
2680+
version: 1.107.0
2681+
resolution: "@types/vscode@npm:1.107.0"
2682+
checksum: 10c0/91dcefaa646bb586189f80f64e2d63f3d2189ff521ce44872652ffbb80d4420fede685a6dbd1fa274fb5ef54501bff23e66ee5f15df9205b12c5bd6d8429988f
26832683
languageName: node
26842684
linkType: hard
26852685

@@ -2998,7 +2998,7 @@ __metadata:
29982998
languageName: node
29992999
linkType: hard
30003000

3001-
"@vscode/debugadapter@npm:^1.51.0":
3001+
"@vscode/debugadapter@npm:^1.68.0":
30023002
version: 1.68.0
30033003
resolution: "@vscode/debugadapter@npm:1.68.0"
30043004
dependencies:
@@ -12304,9 +12304,9 @@ __metadata:
1230412304
"@types/node": "npm:^22.2.0"
1230512305
"@types/react": "npm:^18.3.3"
1230612306
"@types/react-dom": "npm:^18.3.0"
12307-
"@types/vscode": "npm:^1.63.0"
12307+
"@types/vscode": "npm:^1.88.0"
1230812308
"@types/ws": "npm:^8"
12309-
"@vscode/debugadapter": "npm:^1.51.0"
12309+
"@vscode/debugadapter": "npm:^1.68.0"
1231012310
"@vscode/debugprotocol": "npm:^1.68.0"
1231112311
"@vscode/test-cli": "npm:^0.0.12"
1231212312
"@vscode/test-electron": "npm:^2.4.1"

0 commit comments

Comments
 (0)