Skip to content

Commit 489a881

Browse files
committed
Fetch coverage and display it
1 parent 3e4756d commit 489a881

File tree

5 files changed

+176
-16
lines changed

5 files changed

+176
-16
lines changed

src/commonRunTestsHandler.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { allTestRuns, extensionId, osAPI } from './extension';
44
import { relativeTestRoot } from './localTests';
55
import logger from './logger';
66
import { makeRESTRequest } from './makeRESTRequest';
7+
import { OurFileCoverage } from './ourFileCoverage';
78

89
export async function commonRunTestsHandler(controller: vscode.TestController, resolveItemChildren: (item: vscode.TestItem) => Promise<void>, request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) {
910
logger.debug(`commonRunTestsHandler invoked by controller id=${controller.id}`);
@@ -229,7 +230,13 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
229230
}
230231
}
231232

232-
const managerClass = request.profile?.kind === vscode.TestRunProfileKind.Coverage ? "TestCoverage.Manager" : "%UnitTest.Manager";
233+
let managerClass = "%UnitTest.Manager";
234+
if (request.profile?.kind === vscode.TestRunProfileKind.Coverage) {
235+
managerClass = "TestCoverage.Manager";
236+
request.profile.loadDetailedCoverage = async (testRun, fileCoverage, token) => {
237+
return fileCoverage instanceof OurFileCoverage ? fileCoverage.loadDetailedCoverage() : [];
238+
};
239+
}
233240
const configuration = {
234241
"type": "objectscript",
235242
"request": "launch",

src/coverage.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as vscode from 'vscode';
2+
import { makeRESTRequest } from './makeRESTRequest';
3+
import logger from './logger';
4+
import { TestRun } from './extension';
5+
import { serverSpecForUri } from './historyExplorer';
6+
import { OurFileCoverage } from './ourFileCoverage';
7+
8+
export async function processCoverage(serverName: string, namespace: string, run: TestRun): Promise<void> {
9+
const uri = run.debugSession?.workspaceFolder?.uri;
10+
const coverageIndex = run.debugSession?.configuration.coverageIndex;
11+
logger.debug(`processCoverage: serverName=${serverName}, namespace=${namespace}, uri=${uri?.toString()}, coverageIndex=${coverageIndex}`);
12+
if (uri) {
13+
const fileCoverageResults = await getFileCoverageResults(uri, namespace, coverageIndex || 0);
14+
if (fileCoverageResults.length > 0) {
15+
logger.debug(`Coverage results for run ${coverageIndex}: ${JSON.stringify(fileCoverageResults)}`);
16+
fileCoverageResults.forEach(fileCoverage => {
17+
run.addCoverage(fileCoverage);
18+
})
19+
} else {
20+
logger.debug(`No coverage results found for run ${coverageIndex}`);
21+
}
22+
}
23+
}
24+
25+
export async function getFileCoverageResults(folderUri: vscode.Uri, namespace: string, run: number): Promise<vscode.FileCoverage[]> {
26+
const serverSpec = serverSpecForUri(folderUri);
27+
const fileCoverageResults: vscode.FileCoverage[] = [];
28+
if (!serverSpec) {
29+
logger.error(`No server spec found for URI: ${folderUri.toString()}`);
30+
return fileCoverageResults;
31+
}
32+
const exportSettings = vscode.workspace.getConfiguration('objectscript.export', folderUri);
33+
const response = await makeRESTRequest(
34+
"POST",
35+
serverSpec,
36+
{ apiVersion: 1, namespace, path: "/action/query" },
37+
{
38+
query: "SELECT cu.Hash, cu.Name Name, cu.Type, abcu.ExecutableLines, CoveredLines, ExecutableMethods, CoveredMethods, RtnLine FROM TestCoverage_Data_Aggregate.ByCodeUnit abcu, TestCoverage_Data.CodeUnit cu WHERE abcu.CodeUnit = cu.Hash AND Run = ? ORDER BY Name",
39+
parameters: [run],
40+
},
41+
);
42+
if (response) {
43+
response?.data?.result?.content?.forEach(element => {
44+
const fileType = element.Type.toLowerCase();
45+
let pathPrefix = ''
46+
if (folderUri.scheme === 'file') {
47+
pathPrefix = exportSettings.folder;
48+
if (pathPrefix && !pathPrefix.startsWith('/')) {
49+
pathPrefix = `/${pathPrefix}`;
50+
}
51+
if (exportSettings.atelier) {
52+
pathPrefix += '/' + fileType;
53+
}
54+
}
55+
const fileUri = folderUri.with({ path: folderUri.path.concat(pathPrefix, `/${element.Name.replace(/\./g, '/')}.${fileType}`) });
56+
logger.debug(`getFileCoverageResults element: ${JSON.stringify(element)}`);
57+
logger.debug(`getFileCoverageResults fileUri: ${fileUri.toString()}`);
58+
const fileCoverage = new OurFileCoverage(
59+
run,
60+
element.Hash,
61+
fileUri,
62+
new vscode.TestCoverageCount(element.CoveredLines, element.ExecutableLines),
63+
undefined,
64+
new vscode.TestCoverageCount(element.CoveredMethods, element.ExecutableMethods)
65+
);
66+
fileCoverageResults.push(fileCoverage);
67+
});
68+
}
69+
return fileCoverageResults;
70+
}

src/debugTracker.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from 'vscode';
22
import { allTestRuns, loadedTestController, localTestController, TestRun } from './extension';
33
import { refreshHistoryRootItem } from './historyExplorer';
4+
import { processCoverage } from './coverage';
45

56
export class DebugTracker implements vscode.DebugAdapterTracker {
67

@@ -56,6 +57,14 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
5657
}
5758
const line: string = (message.body.output as string).replace(/\n/, '');
5859
this.run.appendOutput(line + '\r\n');
60+
61+
const coverageMatch = line.match(/^(?:http|https):\/\/.*\/TestCoverage\.UI\.AggregateResultViewer\.cls\?Index=(\d+)/);
62+
if (coverageMatch && this.run.debugSession) {
63+
const coverageIndex = Number(coverageMatch[1]);
64+
this.run.debugSession.configuration.coverageIndex = coverageIndex;
65+
console.log(`Coverage index set to ${coverageIndex}`);
66+
}
67+
5968
if (this.className === undefined) {
6069
const classBegin = line.match(/^ ([%\dA-Za-z][\dA-Za-z0-9\.]*) begins \.\.\./);
6170
if (classBegin) {
@@ -139,10 +148,13 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
139148
//console.log(`**Starting session ${this.session.name}, run.name = ${this.run?.name}`);
140149
}
141150

142-
onWillStopSession(): void {
143-
//console.log(`**Stopping session ${this.session.name}`);
151+
async onWillStopSession(): Promise<void> {
152+
console.log(`**Stopping session ${this.session.name}`);
144153
if (this.run) {
154+
await processCoverage(this.serverName, this.namespace, this.run);
155+
//console.log(`**processCoverage done`);
145156
this.run.end();
157+
//console.log(`**run.end() done`);
146158
refreshHistoryRootItem(this.serverName, this.namespace);
147159
}
148160

src/historyExplorer.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ export async function setupHistoryExplorerController() {
4646
}
4747
}
4848

49+
export function serverSpecForUri(uri: vscode.Uri): IServerSpec | undefined {
50+
const server = osAPI.serverForUri(uri);
51+
if (server) {
52+
return {
53+
username: server.username,
54+
password: server.password,
55+
name: server.serverName,
56+
webServer: {
57+
host: server.host,
58+
port: server.port,
59+
pathPrefix: server.pathPrefix,
60+
scheme: server.scheme
61+
}
62+
};
63+
}
64+
return undefined;
65+
}
66+
4967
export async function serverSpec(item: vscode.TestItem): Promise<IServerSpec | undefined> {
5068
const serverName = item.id.split(':')[0];
5169
if (serverName) {
@@ -54,20 +72,12 @@ export async function serverSpec(item: vscode.TestItem): Promise<IServerSpec | u
5472
}
5573
return await smAPI.getServerSpec(serverName);
5674
}
75+
else if (item.uri){
76+
return serverSpecForUri(item.uri);
77+
}
5778
else {
58-
const server = osAPI.serverForUri(item.uri);
59-
const serverSpec: IServerSpec = {
60-
username: server.username,
61-
password: server.password,
62-
name: server.serverName,
63-
webServer: {
64-
host: server.host,
65-
port: server.port,
66-
pathPrefix: server.pathPrefix,
67-
scheme: server.scheme
68-
}
69-
};
70-
return serverSpec;
79+
logger.error(`serverSpec: No serverName or URI for item ${item.id}`);
80+
return undefined;
7181
}
7282
}
7383

src/ourFileCoverage.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as vscode from 'vscode';
2+
import logger from './logger';
3+
import { IServerSpec } from '@intersystems-community/intersystems-servermanager';
4+
import { makeRESTRequest } from './makeRESTRequest';
5+
import { osAPI } from './extension';
6+
7+
export class OurFileCoverage extends vscode.FileCoverage {
8+
9+
public readonly codeUnit: string;
10+
private coverageIndex: number;
11+
12+
constructor(coverageIndex: number, codeUnit: string, uri: vscode.Uri, statementCoverage: vscode.TestCoverageCount, branchCoverage?: vscode.TestCoverageCount, declarationCoverage?: vscode.TestCoverageCount) {
13+
super(uri, statementCoverage, branchCoverage, declarationCoverage);
14+
this.coverageIndex = coverageIndex;
15+
this.codeUnit = codeUnit;
16+
}
17+
18+
async loadDetailedCoverage(): Promise<vscode.FileCoverageDetail[]> {
19+
logger.debug(`loadDetailedCoverage invoked for ${this.codeUnit} (${this.uri.toString()})`);
20+
const detailedCoverage: vscode.FileCoverageDetail[] = [];
21+
const server = osAPI.serverForUri(this.uri);
22+
const serverSpec: IServerSpec = {
23+
username: server.username,
24+
password: server.password,
25+
name: server.serverName,
26+
webServer: {
27+
host: server.host,
28+
port: server.port,
29+
pathPrefix: server.pathPrefix,
30+
scheme: server.scheme
31+
}
32+
};
33+
const namespace: string = server.namespace.toUpperCase();
34+
35+
// The SqlProc Query ColoredText of TestCoverage.UI.Utils we're leveraging needs to be patched in its ...Execute method to return PlainText="" for all rows.
36+
// Unpatched, we get response.data.status.errors[0].summary = ""ERROR #5035: General exception Name &#39;Premature end of data&#39; Code &#39;12&#39; Data &#39;&#39;
37+
const response = await makeRESTRequest(
38+
"POST",
39+
serverSpec,
40+
{ apiVersion: 1, namespace, path: "/action/query" },
41+
{
42+
query: "CALL TestCoverage_UI.Utils_ColoredText(?, ?, 'all tests')",
43+
parameters: [this.coverageIndex, this.codeUnit],
44+
},
45+
);
46+
if (response) {
47+
response?.data?.result?.content?.forEach(element => {
48+
logger.debug(`getFileCoverageResults element: ${JSON.stringify(element)}`);
49+
if (element.Executable == '0') {
50+
logger.debug(`Skipping non-executable line: ${JSON.stringify(element)}`);
51+
return;
52+
}
53+
const range = new vscode.Range(new vscode.Position(Number(element.LineNumber) - 1, 0), new vscode.Position(Number(element.LineNumber) - 1, Number.MAX_VALUE));
54+
const statementCoverage = new vscode.StatementCoverage(element.Covered == '1', range);
55+
detailedCoverage.push(statementCoverage);
56+
});
57+
}
58+
return detailedCoverage;
59+
}
60+
61+
}

0 commit comments

Comments
 (0)