Skip to content

Commit a3105e4

Browse files
committed
Implement attributable coverage (#44)
1 parent 453dcdf commit a3105e4

File tree

9 files changed

+168
-140
lines changed

9 files changed

+168
-140
lines changed

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"objectscript"
1616
],
1717
"engines": {
18-
"vscode": "^1.93.0"
18+
"vscode": "^1.96.0"
1919
},
2020
"icon": "images/logo.png",
2121
"categories": [
@@ -53,7 +53,7 @@
5353
"@types/glob": "^7.1.1",
5454
"@types/mocha": "^9.0.0",
5555
"@types/node": "^8.10.60",
56-
"@types/vscode": "^1.93.0",
56+
"@types/vscode": "^1.96.0",
5757
"@vscode/test-electron": "^2.3.8",
5858
"glob": "^7.1.6",
5959
"mocha": "^9.2.2",

src/commonRunTestsHandler.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { relativeTestRoot } from './localTests';
55
import logger from './logger';
66
import { makeRESTRequest } from './makeRESTRequest';
77
import { OurFileCoverage } from './ourFileCoverage';
8+
import { SQL_FN_RUNTESTPROXY, UTIL_CLASSNAME } from './utils';
89

910
export async function commonRunTestsHandler(controller: vscode.TestController, resolveItemChildren: (item: vscode.TestItem) => Promise<void>, request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) {
1011
logger.debug(`commonRunTestsHandler invoked by controller id=${controller.id}`);
@@ -116,7 +117,7 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
116117

117118
// First, clear out the server-side folder for the classes whose testmethods will be run
118119
const folder = vscode.workspace.getWorkspaceFolder(oneUri);
119-
const server = osAPI.serverForUri(oneUri);
120+
const server = await osAPI.asyncServerForUri(oneUri);
120121
const serverSpec: IServerSpec = {
121122
username: server.username,
122123
password: server.password,
@@ -245,26 +246,31 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
245246
}
246247
}
247248

248-
let managerClass = "%UnitTest.Manager";
249+
let program = `##class(%UnitTest.Manager).RunTest("${testSpec}","${runQualifiers}")`;
249250
if (coverageRequest) {
250-
managerClass = "TestCoverage.Manager";
251-
request.profile.loadDetailedCoverage = async (testRun, fileCoverage, token) => {
251+
program = `##class(${UTIL_CLASSNAME}).${SQL_FN_RUNTESTPROXY}("${testSpec}","${runQualifiers}",2)`;
252+
request.profile.loadDetailedCoverage = async (_testRun, fileCoverage, _token) => {
252253
return fileCoverage instanceof OurFileCoverage ? fileCoverage.loadDetailedCoverage() : [];
253254
};
255+
request.profile.loadDetailedCoverageForTest = async (_testRun, fileCoverage, fromTestItem, _token) => {
256+
return fileCoverage instanceof OurFileCoverage ? fileCoverage.loadDetailedCoverage(fromTestItem) : [];
257+
};
254258
}
255259
const configuration = {
256-
"type": "objectscript",
257-
"request": "launch",
258-
"name": `${controller.id.split("-").pop()}Tests:${serverSpec.name}:${namespace}:${username}`,
259-
"program": `##class(${managerClass}).RunTest("${testSpec}","${runQualifiers}")`,
260+
type: "objectscript",
261+
request: "launch",
262+
name: `${controller.id.split("-").pop()}Tests:${serverSpec.name}:${namespace}:${username}`,
263+
program,
260264

261265
// Extra properties needed by our DebugAdapterTracker
262-
"testingRunIndex": runIndex,
263-
"testingIdBase": firstClassTestItem.id.split(":", 2).join(":")
266+
testingRunIndex: runIndex,
267+
testingIdBase: firstClassTestItem.id.split(":", 2).join(":")
264268
};
265269
const sessionOptions: vscode.DebugSessionOptions = {
266270
noDebug: !isDebug,
267-
suppressDebugToolbar: request.profile?.kind !== vscode.TestRunProfileKind.Debug
271+
suppressDebugToolbar: request.profile?.kind !== vscode.TestRunProfileKind.Debug,
272+
suppressDebugView: request.profile?.kind !== vscode.TestRunProfileKind.Debug,
273+
testRun: run,
268274
};
269275

270276
// ObjectScript debugger's initializeRequest handler needs to identify target server and namespace

src/coverage.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22
import { makeRESTRequest } from './makeRESTRequest';
33
import logger from './logger';
4-
import { OurTestRun } from './extension';
4+
import { OurTestRun, workspaceFolderTestClasses } from './extension';
55
import { serverSpecForUri } from './historyExplorer';
66
import { OurFileCoverage } from './ourFileCoverage';
77

@@ -23,48 +23,58 @@ export async function processCoverage(serverName: string, namespace: string, run
2323
}
2424

2525
export async function getFileCoverageResults(folderUri: vscode.Uri, namespace: string, coverageIndex: number): Promise<vscode.FileCoverage[]> {
26-
const serverSpec = serverSpecForUri(folderUri);
27-
const fileCoverageResults: vscode.FileCoverage[] = [];
26+
const serverSpec = await serverSpecForUri(folderUri);
2827
if (!serverSpec) {
2928
logger.error(`No server spec found for URI: ${folderUri.toString()}`);
30-
return fileCoverageResults;
29+
return [];
3130
}
3231
const exportSettings = vscode.workspace.getConfiguration('objectscript.export', folderUri);
3332
const response = await makeRESTRequest(
3433
"POST",
3534
serverSpec,
3635
{ apiVersion: 1, namespace, path: "/action/query" },
3736
{
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",
37+
query: "SELECT cu.Hash Hash, cu.Name Name, cu.Type, abcu.ExecutableLines, abcu.CoveredLines, ExecutableMethods, CoveredMethods, TestPath FROM TestCoverage_Data_Aggregate.ByCodeUnit abcu, TestCoverage_Data.CodeUnit cu, TestCoverage_Data.Coverage cov WHERE abcu.CodeUnit = cu.Hash AND cov.Hash = cu.Hash AND abcu.Run = ? AND cov.Run = abcu.Run ORDER BY Hash",
3938
parameters: [coverageIndex],
4039
},
4140
);
41+
const mapFileCoverages: Map<string, OurFileCoverage> = new Map();
4242
if (response) {
4343
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}`;
44+
let fileCoverage = mapFileCoverages.get(element.Hash);
45+
if (!fileCoverage) {
46+
const fileType = element.Type.toLowerCase();
47+
let pathPrefix = ''
48+
if (folderUri.scheme === 'file') {
49+
pathPrefix = exportSettings.folder;
50+
if (pathPrefix && !pathPrefix.startsWith('/')) {
51+
pathPrefix = `/${pathPrefix}`;
52+
}
53+
if (exportSettings.atelier) {
54+
pathPrefix += '/' + fileType;
55+
}
5056
}
51-
if (exportSettings.atelier) {
52-
pathPrefix += '/' + fileType;
57+
const fileUri = folderUri.with({ path: folderUri.path.concat(pathPrefix, `/${element.Name.replace(/\./g, '/')}.${fileType}`) });
58+
fileCoverage = new OurFileCoverage(
59+
coverageIndex,
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+
}
67+
const testPath: string = element.TestPath || 'all tests';
68+
if (testPath !== 'all tests') {
69+
console.log(`Find TestItem matching test path ${testPath}`);
70+
const className = testPath.split(':')[1];
71+
const testItem = workspaceFolderTestClasses[vscode.workspace.getWorkspaceFolder(folderUri)?.index || 0].get(className);
72+
if (testItem) {
73+
fileCoverage.includesTests?.push(testItem);
5374
}
5475
}
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-
coverageIndex,
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);
76+
mapFileCoverages.set(element.Hash, fileCoverage);
6777
});
6878
}
69-
return fileCoverageResults;
79+
return Array.from(mapFileCoverages.values());
7080
}

src/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export interface OurTestItem extends vscode.TestItem {
2424

2525
export const allTestRuns: (OurTestRun | undefined)[] = [];
2626

27+
// Array indexed by workspaceFolder index, each element holding a map from classname ('P1.P2.C') to TestItem
28+
export const workspaceFolderTestClasses: Map<string, OurTestItem>[] = [];
29+
2730
async function getServerManagerAPI(): Promise<serverManager.ServerManagerAPI | undefined> {
2831
const targetExtension = vscode.extensions.getExtension("intersystems-community.servermanager");
2932
if (!targetExtension) {
@@ -62,6 +65,14 @@ export async function activate(context: vscode.ExtensionContext) {
6265
smAPI = await getServerManagerAPI();
6366
// TODO notify user if either of these returned undefined (extensionDependencies setting should prevent that, but better to be safe)
6467

68+
69+
// TODO handle changes to workspace structure, e.g. workspaceFolder added or removed
70+
vscode.workspace.workspaceFolders?.forEach((folder, index) => {
71+
// Initialize the map for this workspace folder's test class items
72+
const newLength = workspaceFolderTestClasses.push(new Map<string, OurTestItem>());
73+
console.log(`Initialized workspaceFolderTestClasses[${index}] with length ${newLength}`);
74+
});
75+
6576
// Other parts of this extension will use the test controllers we create here
6677
localTestController = vscode.tests.createTestController(`${extensionId}-Local`, '$(folder-library) Local Tests');
6778
context.subscriptions.push(localTestController);

src/historyExplorer.ts

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export async function setupHistoryExplorerController() {
4646
}
4747
}
4848

49-
export function serverSpecForUri(uri: vscode.Uri): IServerSpec | undefined {
50-
const server = osAPI.serverForUri(uri);
49+
export async function serverSpecForUri(uri: vscode.Uri): Promise<IServerSpec | undefined> {
50+
const server = await osAPI.asyncServerForUri(uri);
5151
if (server) {
5252
return {
5353
username: server.username,
@@ -70,7 +70,7 @@ export async function serverSpec(item: vscode.TestItem): Promise<IServerSpec | u
7070
if (!smAPI) {
7171
return undefined;
7272
}
73-
return await smAPI.getServerSpec(serverName);
73+
return smAPI.getServerSpec(serverName);
7474
}
7575
else if (item.uri){
7676
return serverSpecForUri(item.uri);
@@ -131,20 +131,13 @@ async function addTestSuites(item: OurTestItem, controller: vscode.TestControlle
131131
},
132132
);
133133
if (response) {
134-
const run = controller.createTestRun(new vscode.TestRunRequest(), `Item '${item.label}' history`, false);
135134
response?.data?.result?.content?.forEach(element => {
136-
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Name}`);
135+
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Name}`, item.uri);
136+
child.description = element.Status.toString();
137137
child.canResolveChildren = true;
138138
child.supportsCoverage = item.supportsCoverage;
139139
item.children.add(child);
140-
if (element.Status) {
141-
run.passed(child, element.Duration * 1000);
142-
}
143-
else {
144-
run.failed(child, new vscode.TestMessage(element.ErrorDescription), element.Duration * 1000);
145-
}
146140
});
147-
run.end();
148141
}
149142
}
150143
}
@@ -165,20 +158,13 @@ async function addTestCases(item: OurTestItem, controller: vscode.TestController
165158
},
166159
);
167160
if (response) {
168-
const run = controller.createTestRun(new vscode.TestRunRequest(), `Item '${item.label}' history`, false);
169161
response?.data?.result?.content?.forEach(element => {
170-
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Name.split('.').pop()}`);
162+
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Name.split('.').pop()}`, item.uri);
163+
child.description = element.Status.toString();
171164
child.canResolveChildren = true;
172165
child.supportsCoverage = item.supportsCoverage;
173166
item.children.add(child);
174-
if (element.Status) {
175-
run.passed(child, element.Duration * 1000);
176-
}
177-
else {
178-
run.failed(child, new vscode.TestMessage(element.ErrorDescription), element.Duration * 1000);
179-
}
180167
});
181-
run.end();
182168
}
183169
}
184170
}
@@ -199,25 +185,18 @@ async function addTestMethods(item: OurTestItem, controller: vscode.TestControll
199185
},
200186
);
201187
if (response) {
202-
const run = controller.createTestRun(new vscode.TestRunRequest(), `Item '${item.label}' history`, false);
203188
response?.data?.result?.content?.forEach(element => {
204189
const methodName: string = element.Name;
205190
// We drop the first 4 characters of the method name because they should always be "Test"
206-
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${methodName.slice(4)}`);
191+
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${methodName.slice(4)}`, item.uri);
192+
child.description = element.Status.toString();
207193
child.canResolveChildren = true;
208194
child.supportsCoverage = item.supportsCoverage;
209195
item.children.add(child);
210196

211197
// Remember result fields so they can be reinstated when the descendant Asserts are 'run'
212198
resultMap.set(child, { status: element.Status, errorDescription: element.ErrorDescription, duration: element.Duration });
213-
if (element.Status) {
214-
run.passed(child, element.Duration * 1000);
215-
}
216-
else {
217-
run.failed(child, new vscode.TestMessage(element.ErrorDescription), element.Duration * 1000);
218-
}
219199
});
220-
run.end();
221200
}
222201
}
223202
}
@@ -238,35 +217,14 @@ async function addTestAsserts(item: OurTestItem, controller: vscode.TestControll
238217
},
239218
);
240219
if (response) {
241-
const run = controller.createTestRun(new vscode.TestRunRequest(), `Item '${item.label}' history`, false);
242-
243-
// Prevent this level's duration from being blanked out because of children's (absent) durations
244-
const itemResult = resultMap.get(item);
245-
if (itemResult) {
246-
if (itemResult.status) {
247-
run.passed(item, itemResult.duration * 1000);
248-
}
249-
else {
250-
run.failed(item, new vscode.TestMessage(itemResult.errorDescription || "(No error description)"), itemResult.duration * 1000);
251-
}
252-
}
253-
254220
response?.data?.result?.content?.forEach(element => {
255-
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Action}`);
221+
const child: OurTestItem = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Action}`, item.uri);
256222
child.sortText = `${element.Counter.toString().padStart(element.MaxCounter.toString().length, "0")}`;
257-
child.description = element.Description;
223+
child.description = `${element.Status} ${element.Description}`;
258224
child.canResolveChildren = false;
259225
child.supportsCoverage = item.supportsCoverage;
260226
item.children.add(child);
261-
if (element.Status) {
262-
run.passed(child);
263-
}
264-
else {
265-
run.failed(child, new vscode.TestMessage(element.Description));
266-
}
267227
});
268-
269-
run.end();
270228
}
271229
}
272230
}

0 commit comments

Comments
 (0)