Skip to content

Commit 81c839d

Browse files
Merge pull request #53 from gjsjohnmurray/fix-50
Improve positioning of assertion failure markers
2 parents f125a07 + de7c3e4 commit 81c839d

File tree

11 files changed

+201
-112
lines changed

11 files changed

+201
-112
lines changed

.vscode/settings.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"objectscript.export": {
3+
"folder": "serverSide\\src",
4+
"addCategory": false,
5+
"map": {},
6+
"atelier": true,
7+
"generated": false,
8+
"filter": "",
9+
"exactFilter": "",
10+
"category": "*",
11+
"maxConcurrentConnections": 0,
12+
"mapped": false
13+
},
14+
"objectscript.conn": {
15+
"active": true,
16+
"server": "docker-52774",
17+
"ns": "USER",
18+
"username": "_SYSTEM"
19+
},
20+
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.0.3 (dd-Mmm-yyyy)
2+
* Imporove positioning of assertion failure markers (#50)
3+
* Remove need to define helper SQL functions via DDL.
4+
* Promote use of `vscode-per-namespace-settings` package for webapp setup.
5+
* Update DC contest text in README.
6+
17
## 2.0.2 (30-Jul-2025)
28
* Fix coverage marking when `"objectscript.multilineMethodArgs": true` (#46)
39
* Improve method range highlighting accessed from coverage tree (#48)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **New in Version 2.0 - Test Coverage**
44
>
5-
> The v2.0 release has been entered into the [InterSystems Developer Tools Contest 2025](https://openexchange.intersystems.com/contest/42). Please support it with your vote between 28th July and 3rd August.
5+
> The v2.0 release was awarded first place in the [InterSystems Developer Tools Contest 2025](https://openexchange.intersystems.com/contest/42) in both voting categories, Experts and Community Members.
66
77
This extension uses VS Code's [Testing API](https://code.visualstudio.com/api/extension-guides/testing) to discover, run and debug unit test classes built with the [%UnitTest testing framework](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=TUNT_WhatIsPercentUnitTest) of the InterSystems IRIS platforms, plus Caché-based predecessors supporting the `/api/atelier` REST service.
88

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/// Helper superclass for the testing managers we use.
2+
/// Must collate ahead of its subclasses, so it is already available on the server
3+
/// when they are copied there by a directory copy.
4+
Class vscode.dc.testingmanager.BaseManager [ Abstract ]
5+
{
6+
7+
/// Keep this in sync with the version property in package.json
8+
Parameter VERSION As STRING = "2.0.3";
9+
10+
Property tmMethodMap As %String [ MultiDimensional, Private ];
11+
12+
Method tmMapOneFile(file As %String) [ Private ]
13+
{
14+
Kill ..tmMethodMap(file)
15+
Set tFlags=+..UserParam # 2 * 16 // Bit 0 f UserParam indicates we must get udl-multiline format (since Atelier API v4)
16+
Set tSC=##class(%Atelier.v1.Utils.TextServices).GetTextAsArray(file,tFlags,.tTextArray)
17+
//TODO: use the text array to create a tag-to-linenumber map
18+
For lineNumber=1:1:+$Get(tTextArray(0)) {
19+
Set line=$Get(tTextArray(lineNumber))
20+
Set keyword=$Piece(line," ",1)
21+
If keyword'="Method",keyword'="ClassMethod" Continue
22+
Set tag=$Piece($Piece(line," ",2),"(",1)
23+
// Take account of multi-line method format
24+
While ("{"'[$Get(tTextArray(lineNumber+1))) {
25+
Set lineNumber=lineNumber+1
26+
}
27+
Set ..tmMethodMap(file,tag)=lineNumber
28+
}
29+
// Note linecount as an indicator we've indexed this file, even if we found no methods
30+
Set ..tmMethodMap(file)=+$Get(tTextArray(0))
31+
}
32+
33+
/// Copied from %UnitTest.Manager and enhanced to append location information
34+
/// to some log messages.
35+
Method LogAssert(success, action, description, extra, location)
36+
{
37+
Set testsuite=i%TheStack(i%TheStack,"suite")
38+
Set testcase=i%TheStack(i%TheStack,"case")
39+
Set testmethod=i%TheStack(i%TheStack,"method")
40+
If testmethod="" Quit
41+
Do LogAssert^%SYS.UNITTEST(..OriginNS,..ResultId,testsuite,testcase,testmethod,success,action,description,$GET(location))
42+
43+
// Convert location to +offset^file if it is a type of assertion outcome we want to display inline
44+
If success'=1,$Get(location)'="" {
45+
Set file=$Piece(location,"^",2)
46+
Set tagOffset=$Piece(location,"^",1)
47+
Set offset=$Piece(tagOffset,"+",2)
48+
Set tag=$Piece(tagOffset,"+",1)
49+
If (tag'="") {
50+
// Create a tag-to-linenumber map for file if we don't have one already
51+
If '$Data(..tmMethodMap(file)) Do ..tmMapOneFile(file)
52+
// Use it to compute what to add to the offset to get an absolute line number
53+
Set tagLineNumber=$Get(..tmMethodMap(file,tag))
54+
Set location="+"_(offset+tagLineNumber)_"^"_file
55+
}
56+
}
57+
Set line=action_":"_description_" ("_..GetTestState(success)_")"
58+
if success'=1 Set line = line_"@"_location
59+
If 'success,..Display["error" {
60+
Do ..PrintErrorLine(line,.extra)
61+
} Else {
62+
Do ..PrintLine(line,4)
63+
}
64+
}
65+
66+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Class vscode.dc.testingmanager.CoverageManager Extends (BaseManager, TestCoverage.Manager)
2+
{
3+
4+
ClassMethod RunTest(ByRef testspec As %String, qspec As %String, ByRef userparam) As %Status
5+
{
6+
Set userparam("CoverageDetail")=2
7+
Return ##super(testspec, qspec, .userparam)
8+
}
9+
10+
/// SQL function to convert an IRIS bitstring to an Int8 bitstring for ease of handling in Typescript
11+
/// Example usage: SELECT vscode_dc_testingmanager_CoverageManager.tmInt8Bitstring(cu.ExecutableLines) i8bsExecutableLines
12+
ClassMethod tmInt8Bitstring(bitstring As %String) As %String [ SqlProc ]
13+
{
14+
Set output = "", iMod8=-1, char=0, weight=1
15+
For i=1:1:$BitCount(bitstring) {
16+
Set bitvalue = $Bit(bitstring, i)
17+
Set iMod8 = (i-1)#8
18+
If bitvalue {
19+
Set char = char+weight
20+
}
21+
Set weight = weight*2
22+
If iMod8 = 7 {
23+
Set output = output_$Char(char)
24+
Set char = 0, weight = 1
25+
Set iMod8 = -1
26+
}
27+
}
28+
If iMod8 > -1 {
29+
Set output = output_$Char(char)
30+
}
31+
Return output
32+
}
33+
34+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Class vscode.dc.testingmanager.StandardManager Extends (BaseManager, %UnitTest.Manager)
2+
{
3+
4+
}

src/commonRunTestsHandler.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ 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';
98

109
export async function commonRunTestsHandler(controller: vscode.TestController, resolveItemChildren: (item: vscode.TestItem) => Promise<void>, request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) {
1110
logger.debug(`commonRunTestsHandler invoked by controller id=${controller.id}`);
@@ -158,10 +157,13 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
158157
);
159158

160159
if (!responseCspapps?.data?.result?.content?.includes("/_vscode")) {
161-
const reply = await vscode.window.showErrorMessage(`A '/_vscode' web application must be configured for the %SYS namespace of server '${serverSpec.name}'. The ${namespace} namespace also requires its ^UnitTestRoot global to point to the '${namespace}/UnitTestRoot' subfolder of that web application's path.`, { modal: true }, 'Instructions');
162-
if (reply === 'Instructions') {
160+
const reply = await vscode.window.showErrorMessage(`A '/_vscode' web application must be configured for the %SYS namespace of server '${serverSpec.name}'. The ${namespace} namespace also requires its ^UnitTestRoot global to point to the '${namespace}/UnitTestRoot' subfolder of that web application's path.`, { modal: true }, 'Use IPM Package', 'Follow Manual Instructions');
161+
if (reply === 'Follow Manual Instructions') {
163162
vscode.commands.executeCommand('vscode.open', 'https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO_serverflow#GVSCO_serverflow_folderspec');
163+
} else if (reply === 'Use IPM Package') {
164+
vscode.commands.executeCommand('vscode.open', 'https://openexchange.intersystems.com/package/vscode-per-namespace-settings');
164165
}
166+
run.end();
165167
return;
166168
}
167169

@@ -170,9 +172,40 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
170172
// When client-side mode is using 'objectscript.conn.docker-compose the first piece of 'authority' is blank,
171173
if (authority.startsWith(":")) {
172174
authority = folder?.name || "";
175+
} else {
176+
authority = authority.split(":")[0];
177+
}
178+
179+
// Load our support classes if they are not already there and the correct version.
180+
const thisExtension = vscode.extensions.getExtension(extensionId);
181+
if (!thisExtension) {
182+
// Never happens, but needed to satisfy typechecking below
183+
return;
184+
}
185+
const extensionUri = thisExtension.extensionUri;
186+
const supportClassesDir = extensionUri.with({ path: extensionUri.path + '/serverSide/src' + '/vscode/dc/testingmanager'});
187+
const expectedVersion = thisExtension.packageJSON.version;
188+
const expectedCount = (await vscode.workspace.fs.readDirectory(supportClassesDir)).length;
189+
const response = await makeRESTRequest(
190+
"POST",
191+
serverSpec,
192+
{ apiVersion: 1, namespace, path: "/action/query" },
193+
{
194+
query: `SELECT parent, _Default FROM %Dictionary.CompiledParameter WHERE Name='VERSION' AND parent %STARTSWITH 'vscode.dc.testingmanager.' AND _Default=?`,
195+
parameters: [expectedVersion],
196+
},
197+
);
198+
if (response?.status !== 200 || response?.data?.result?.content?.length !== expectedCount) {
199+
const destinationDir = vscode.Uri.from({ scheme: 'isfs', authority: `${authority}:${namespace}`, path: '/vscode/dc/testingmanager'})
200+
try {
201+
await vscode.workspace.fs.copy(supportClassesDir, destinationDir, { overwrite: true });
202+
} catch (error) {
203+
await vscode.window.showErrorMessage(`Failed to copy support classes from ${supportClassesDir.path.slice(1)} to ${destinationDir.toString()}\n\n${JSON.stringify(error)}`, {modal: true});
204+
}
173205
}
206+
174207
// No longer rely on ISFS redirection of /.vscode because since ObjectScript v3.0 it no longer works for client-only workspaces.
175-
const testRoot = vscode.Uri.from({ scheme: 'isfs', authority: authority.split(":")[0], path: `/_vscode/${namespace}/UnitTestRoot/${username}`, query: "csp&ns=%SYS" });
208+
const testRoot = vscode.Uri.from({ scheme: 'isfs', authority, path: `/_vscode/${namespace}/UnitTestRoot/${username}`, query: "csp&ns=%SYS" });
176209
try {
177210
// Limitation of the Atelier API means this can only delete the files, not the folders
178211
// but zombie folders shouldn't cause problems.
@@ -254,6 +287,7 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
254287
const isClientSideMode = controller.id === `${extensionId}-Local`;
255288
const isDebug = request.profile?.kind === vscode.TestRunProfileKind.Debug;
256289
const runQualifiers = !isClientSideMode ? "/noload/nodelete" : isDebug ? "/noload" : "";
290+
const userParam = vscode.workspace.getConfiguration('objectscript', oneUri).get<boolean>('multilineMethodArgs', false) ? 1 : 0;
257291
const runIndex = allTestRuns.push(run) - 1;
258292
runIndices.push(runIndex);
259293

@@ -268,16 +302,17 @@ export async function commonRunTestsHandler(controller: vscode.TestController, r
268302
}
269303
}
270304

271-
let program = `##class(%UnitTest.Manager).RunTest("${testSpec}","${runQualifiers}")`;
305+
let program = `##class(vscode.dc.testingmanager.StandardManager).RunTest("${testSpec}","${runQualifiers}",${userParam})`;
272306
if (coverageRequest) {
273-
program = `##class(${UTIL_CLASSNAME}).${SQL_FN_RUNTESTPROXY}("${testSpec}","${runQualifiers}",2)`;
307+
program = `##class(vscode.dc.testingmanager.CoverageManager).RunTest("${testSpec}","${runQualifiers}",${userParam})`
274308
request.profile.loadDetailedCoverage = async (_testRun, fileCoverage, _token) => {
275309
return fileCoverage instanceof OurFileCoverage ? fileCoverage.loadDetailedCoverage() : [];
276310
};
277311
request.profile.loadDetailedCoverageForTest = async (_testRun, fileCoverage, fromTestItem, _token) => {
278312
return fileCoverage instanceof OurFileCoverage ? fileCoverage.loadDetailedCoverage(fromTestItem) : [];
279313
};
280314
}
315+
281316
const configuration = {
282317
type: "objectscript",
283318
request: "launch",

src/debugTracker.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
106106
break;
107107

108108
case 'failed':
109-
this.run.failed(this.methodTest, this.failureMessages.length > 0 ? this.failureMessages : { message: 'Failed with no messages' }, this.testDuration);
109+
if (this.failureMessages.length > 0) {
110+
this.run.failed(this.methodTest, this.failureMessages, this.testDuration);
111+
} else {
112+
this.run.failed(this.methodTest, { message: 'Failed with no messages' }, this.testDuration);
113+
}
110114
break;
111115

112116
default:
@@ -129,19 +133,25 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
129133
//const message = assertPassedMatch[2];
130134
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'passed', message=${message}`);
131135
} else {
132-
const assertFailedMatch = line.match(/^(Assert\w+):(.*) \(failed\) <<====/);
136+
const assertFailedMatch = line.match(/^(Assert\w+):(.*) \(failed\)@\+(\d+)\^(.*) <<====/);
133137
if (assertFailedMatch) {
134138
//const macroName = assertFailedMatch[1];
135-
const failedMessage = assertFailedMatch[2];
136-
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'failed', message=${message}`);
137-
this.failureMessages.push({ message: failedMessage });
139+
const message = assertFailedMatch[2];
140+
const offset = Number(assertFailedMatch[3]);
141+
const location = this.methodTest?.uri && this.methodTest.range
142+
? new vscode.Location(this.methodTest.uri, new vscode.Position(offset, 0))
143+
: undefined;
144+
this.failureMessages.push({ message, location });
138145
} else {
139-
const assertSkippedMatch = line.match(/^ (Test\w+):(.*) \(skipped\)$/);
146+
const assertSkippedMatch = line.match(/^ (Test\w+):(.*) \(skipped\)@\+(\d+)\^(.*)$/);
140147
if (assertSkippedMatch) {
141148
//const macroName = assertSkippedMatch[1];
142149
const message = assertSkippedMatch[2];
143-
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'skipped', message=${message}`);
144-
this.skippedMessages.push({ message: message });
150+
const offset = Number(assertSkippedMatch[3]);
151+
const location = this.methodTest?.uri && this.methodTest.range
152+
? new vscode.Location(this.methodTest.uri, new vscode.Position(offset, 0))
153+
: undefined;
154+
this.skippedMessages.push({ message, location });
145155
} else {
146156
const logMessageMatch = line.match(/^ LogMessage:(.*)$/);
147157
if (logMessageMatch) {
@@ -151,6 +161,13 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
151161
if (duration) {
152162
this.testDuration = + duration[1] * 1000;
153163
}
164+
} else {
165+
const logStateStatusMatch = line.match(/^LogStateStatus:(.*)$/);
166+
if (logStateStatusMatch) {
167+
const message = logStateStatusMatch[1];
168+
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName LogStateStatus, message=${message}`);
169+
this.failureMessages.push({ message });
170+
}
154171
}
155172
}
156173
}

src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ export let osAPI: any;
1515
export let smAPI: serverManager.ServerManagerAPI | undefined;
1616

1717
export interface OurTestRun extends vscode.TestRun {
18-
debugSession?: vscode.DebugSession
18+
debugSession?: vscode.DebugSession;
1919
}
2020

2121
export interface OurTestItem extends vscode.TestItem {
2222
ourUri?: vscode.Uri;
23-
supportsCoverage?: boolean
23+
supportsCoverage?: boolean;
2424
}
2525

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

src/ourFileCoverage.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import logger from './logger';
33
import { IServerSpec } from '@intersystems-community/intersystems-servermanager';
44
import { makeRESTRequest } from './makeRESTRequest';
55
import { osAPI } from './extension';
6-
import { SQL_FN_INT8BITSTRING } from './utils';
76

87
export class OurFileCoverage extends vscode.FileCoverage {
98

@@ -37,7 +36,7 @@ export class OurFileCoverage extends vscode.FileCoverage {
3736

3837
// When ObjectScript extension spreads method arguments over multiple lines, we need to compute offsets
3938
const mapOffsets: Map<string, number> = new Map();
40-
if (vscode.workspace.getConfiguration('objectscript', this.uri).get('multilineMethodArgs', false)) {
39+
if (vscode.workspace.getConfiguration('objectscript', this.uri).get<boolean>('multilineMethodArgs', false)) {
4140
const response = await makeRESTRequest(
4241
"POST",
4342
serverSpec,
@@ -89,7 +88,7 @@ export class OurFileCoverage extends vscode.FileCoverage {
8988
serverSpec,
9089
{ apiVersion: 1, namespace, path: "/action/query" },
9190
{
92-
query: `SELECT TestCoverage_UI.${SQL_FN_INT8BITSTRING}(cu.ExecutableLines) i8bsExecutableLines, TestCoverage_UI.${SQL_FN_INT8BITSTRING}(cov.CoveredLines) i8bsCoveredLines FROM TestCoverage_Data.CodeUnit cu, TestCoverage_Data.Coverage cov WHERE cu.Hash = cov.Hash AND Run = ? AND cu.Hash = ? AND TestPath = ?`,
91+
query: `SELECT vscode_dc_testingmanager.CoverageManager_tmInt8Bitstring(cu.ExecutableLines) i8bsExecutableLines, vscode_dc_testingmanager.CoverageManager_tmInt8Bitstring(cov.CoveredLines) i8bsCoveredLines FROM TestCoverage_Data.CodeUnit cu, TestCoverage_Data.Coverage cov WHERE cu.Hash = cov.Hash AND Run = ? AND cu.Hash = ? AND TestPath = ?`,
9392
parameters: [this.coverageIndex, this.codeUnit, testPath],
9493
},
9594
);

0 commit comments

Comments
 (0)