Skip to content

Commit 5c4bd0d

Browse files
committed
Start catching results
1 parent 181184f commit 5c4bd0d

5 files changed

Lines changed: 236 additions & 33 deletions

File tree

src/debugTracker.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as vscode from 'vscode';
2+
import { allTestRuns, loadedTestController, localTestController } from './extension';
3+
4+
export class DebugTracker implements vscode.DebugAdapterTracker {
5+
6+
private session: vscode.DebugSession;
7+
private testController: vscode.TestController
8+
private run?: vscode.TestRun;
9+
private testIdBase: string;
10+
private classTestCollection?: vscode.TestItemCollection;
11+
private className?: string;
12+
private classTest?: vscode.TestItem;
13+
private testMethodName?: string;
14+
private testDuration?: number;
15+
private methodTest?: vscode.TestItem;
16+
private failureMessages: vscode.TestMessage[] = [];
17+
18+
constructor(session: vscode.DebugSession) {
19+
this.session = session;
20+
this.testController = this.session.configuration.name.startsWith('ServerTests:') ? loadedTestController : localTestController;
21+
this.run = allTestRuns[this.session.configuration.testRunIndex];
22+
this.testIdBase = this.session.configuration.testIdBase;
23+
this.classTestCollection = this.testController.items.get(this.testIdBase)?.children;
24+
}
25+
26+
onDidSendMessage(message: any): void {
27+
if (message.type === 'event' && message.event === 'output' && message.body?.category === 'stdout') {
28+
if (!this.run) {
29+
return;
30+
}
31+
const line: string = (message.body.output as string).replace(/\n/, '');
32+
//console.log(`${line}`);
33+
if (this.className === undefined) {
34+
const classBegin = line.match(/^ ([%\dA-Za-z][\dA-Za-z\.]*) begins \.\.\.$/);
35+
if (classBegin) {
36+
this.className = classBegin[1];
37+
this.classTest = this.classTestCollection?.get(`${this.testIdBase}:${this.className}`);
38+
}
39+
return;
40+
}
41+
42+
if (line.startsWith(` ${this.className} `)) {
43+
this.className = undefined;
44+
return;
45+
}
46+
47+
if (this.testMethodName === undefined) {
48+
const methodBegin = line.match(/^ Test([%\dA-Za-z][\dA-Za-z]*).* begins \.\.\.$/);
49+
if (methodBegin) {
50+
this.testMethodName = methodBegin[1];
51+
this.methodTest = this.classTest?.children.get(`${this.classTest.id}:Test${this.testMethodName}`);
52+
this.failureMessages = [];
53+
if (this.methodTest) {
54+
this.run.started(this.methodTest)
55+
}
56+
return;
57+
}
58+
} else {
59+
if (line.startsWith(` Test${this.testMethodName} `)) {
60+
const outcome = line.split(this.testMethodName + ' ')[1];
61+
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, outcome=${outcome}`);
62+
if (this.methodTest) {
63+
switch (outcome) {
64+
case 'passed':
65+
this.run.passed(this.methodTest, this.testDuration)
66+
break;
67+
68+
case 'failed':
69+
this.run.failed(this.methodTest, this.failureMessages.length > 0 ? this.failureMessages : { message: 'Failed with no messages' }, this.testDuration);
70+
break;
71+
72+
default:
73+
break;
74+
}
75+
}
76+
this.testMethodName = undefined;
77+
this.testDuration = undefined;
78+
this.methodTest = undefined;
79+
this.failureMessages = [];
80+
return;
81+
}
82+
}
83+
84+
if (this.className && this.testMethodName) {
85+
const assertPassedMatch = line.match(/^ (Assert\w+):(.*) \(passed\)$/);
86+
if (assertPassedMatch) {
87+
const macroName = assertPassedMatch[1];
88+
const message = assertPassedMatch[2];
89+
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'passed', message=${message}`);
90+
} else {
91+
const assertFailedMatch = line.match(/^(Assert\w+):(.*) \(failed\) <<====/);
92+
if (assertFailedMatch) {
93+
const macroName = assertFailedMatch[1];
94+
const message = assertFailedMatch[2];
95+
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'failed', message=${message}`);
96+
this.failureMessages.push({ message: message });
97+
} else {
98+
const logMessageMatch = line.match(/^ LogMessage:(.*)$/);
99+
if (logMessageMatch) {
100+
const message = logMessageMatch[1];
101+
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName LogMessage, message=${message}`);
102+
const duration = message.match(/^Duration of execution: (\d*\.\d+) sec.$/);
103+
if (duration) {
104+
this.testDuration = + duration[1] * 1000;
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
}
112+
113+
onWillStartSession(): void {
114+
console.log(`**Starting session ${this.session.name}, run.name = ${this.run?.name}`);
115+
}
116+
117+
onWillStopSession(): void {
118+
console.log(`**Stopping session ${this.session.name}`);
119+
if (this.run) {
120+
this.run.end();
121+
}
122+
123+
// Clear reference to run (not known if this is necessary)
124+
allTestRuns[this.session.configuration.testRunIndex] = undefined;
125+
}
126+
}

src/debugTrackerFactory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as vscode from 'vscode';
2+
import { DebugTracker } from './debugTracker';
3+
4+
export class DebugTrackerFactory implements vscode.DebugAdapterTrackerFactory {
5+
6+
createDebugAdapterTracker(session: vscode.DebugSession): vscode.ProviderResult<vscode.DebugAdapterTracker> {
7+
return new DebugTracker(session);
8+
}
9+
}

src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import * as vscode from "vscode";
44
import { setupHistoryExplorerController } from "./historyExplorer";
55
import { setupServerTestsController } from "./serverTests";
66
import { setupLocalTestsController } from "./localTests";
7+
import { DebugTrackerFactory } from "./debugTrackerFactory";
78

89
export const extensionId = "intersystems-community.testingmanager";
910
export let localTestController: vscode.TestController;
1011
export let loadedTestController: vscode.TestController;
1112
export let historyBrowserController: vscode.TestController;
13+
export const allTestRuns: (vscode.TestRun | undefined)[] = [];
1214
export let osAPI: any;
1315
export let smAPI: any;
1416

@@ -85,6 +87,10 @@ export async function activate(context: vscode.ExtensionContext) {
8587
context.subscriptions.push(historyBrowserController);
8688
await setupHistoryExplorerController();
8789

90+
context.subscriptions.push(
91+
vscode.debug.registerDebugAdapterTrackerFactory('objectscript', new DebugTrackerFactory())
92+
);
93+
8894
// Register the commands
8995
context.subscriptions.push(
9096
//DUMMY example (remember to add entries to `contributes.commands` in package.json)

src/historyExplorer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ async function addTestAsserts(item: vscode.TestItem, controller: vscode.TestCont
190190
spec,
191191
{ apiVersion: 1, namespace, path: "/action/query" },
192192
{
193-
query: "SELECT ID, Action, Status, Description FROM %UnitTest_Result.TestAssert WHERE TestMethod = ?",
193+
query: "SELECT ID, Counter, COUNT(Counter %FOREACH(TestMethod)) AS MaxCounter, Action, Status, Description FROM %UnitTest_Result.TestAssert WHERE TestMethod = ?",
194194
parameters: [testMethod]
195195
},
196196
);
@@ -209,7 +209,8 @@ async function addTestAsserts(item: vscode.TestItem, controller: vscode.TestCont
209209
}
210210

211211
response?.data?.result?.content?.forEach(element => {
212-
const child = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Action}`);
212+
// Prefix the label with an underscore-padded integer to preserve order
213+
const child = controller.createTestItem(`${item.id}:${element.ID}`, `${element.Counter.toString().padStart(element.MaxCounter.toString().length, "_")}. ${element.Action}`);
213214
child.description = element.Description;
214215
child.canResolveChildren = false;
215216
item.children.add(child);
@@ -236,7 +237,7 @@ export function replaceRootItems(controller: vscode.TestController) {
236237
if (server?.serverName && server.namespace) {
237238
const key = server.serverName + ':' + server.namespace.toUpperCase();
238239
if (!rootMap.has(key)) {
239-
const item = controller.createTestItem(key, key);
240+
const item = controller.createTestItem(key, key, folder.uri);
240241
item.canResolveChildren = true;
241242
rootMap.set(key, item);
242243
}

src/serverTests.ts

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { loadedTestController } from './extension';
2+
import { allTestRuns, IServerSpec, loadedTestController, osAPI } from './extension';
33
import { replaceRootItems, serverSpec } from './historyExplorer';
44
import logger from './logger';
55
import { makeRESTRequest } from './makeRESTRequest';
@@ -29,7 +29,7 @@ async function resolveItemChildren(item: vscode.TestItem) {
2929
`${item.id}:${fullClassName}`,
3030
fullClassName,
3131
vscode.Uri.from({
32-
scheme: "isfs-readonly",
32+
scheme: item.uri?.scheme === "isfs" ? "isfs" : "isfs-readonly",
3333
authority: item.id.toLowerCase(),
3434
path: "/" + fullClassName.replace(/\./g, "/") + ".cls"
3535
})
@@ -84,11 +84,15 @@ export async function runTestsHandler(request: vscode.TestRunRequest, cancellati
8484

8585
const run = loadedTestController.createTestRun(
8686
request,
87-
'Fake Test Results',
87+
'Test Results',
8888
true
8989
);
90+
9091
run.appendOutput('Fake output from fake run of fake server tests.\r\nTODO');
91-
const mapAuthorities = new Map<string, Map<string, vscode.Uri>>();
92+
93+
// For each authority (i.e. server:namespace) accumulate a map of the class-level Test nodes in the tree.
94+
// We don't yet support running only some TestXXX methods in a testclass
95+
const mapAuthorities = new Map<string, Map<string, vscode.TestItem>>();
9296
const queue: vscode.TestItem[] = [];
9397

9498
// Loop through all included tests, or all known tests, and add them to our queue
@@ -111,16 +115,17 @@ export async function runTestsHandler(request: vscode.TestRunRequest, cancellati
111115

112116
// Resolve children if not already done
113117
if (test.canResolveChildren && !isResolvedMap.get(test)) {
114-
resolveItemChildren(test);
118+
await resolveItemChildren(test);
115119
}
116120

117-
// Mark each leaf item as enqueued and note its .cls file for copying
118-
if (test.children.size === 0 && test.uri) {
121+
// Mark each leaf item (a TestXXX method in a class) as enqueued and note its .cls file for copying.
122+
// Every leaf must have a uri.
123+
if (test.children.size === 0 && test.uri && test.parent) {
119124
run.enqueued(test);
120125
const authority = test.uri.authority;
121-
const mapUris = mapAuthorities.get(authority) || new Map<string, vscode.Uri>();
122-
mapUris.set(test.uri.path, test.uri);
123-
mapAuthorities.set(authority, mapUris);
126+
const mapTestClasses = mapAuthorities.get(authority) || new Map<string, vscode.TestItem>();
127+
mapTestClasses.set(test.uri.path, test.parent);
128+
mapAuthorities.set(authority, mapTestClasses);
124129
}
125130

126131
// Queue any children
@@ -131,31 +136,87 @@ export async function runTestsHandler(request: vscode.TestRunRequest, cancellati
131136
// TODO what?
132137
}
133138

134-
for await (const one of mapAuthorities) {
135-
const authority = one[0];
136-
const mapUris = one[1];
137-
const username = 'johnm'; //TODO
138-
const testRoot = vscode.Uri.from({scheme: 'isfs', authority, path: `/.vscode/UnitTestRoot/${username}`});
139-
try {
140-
await vscode.workspace.fs.delete(testRoot, { recursive: true });
141-
} catch (error) {
142-
console.log(error);
143-
}
144-
for await (const one of mapUris) {
145-
const key = one[0];
146-
const uri = one[1];
147-
const keyParts = key.split('/');
148-
const clsFile = keyParts.pop() || '';
149-
const directoryUri = testRoot.with({path: testRoot.path.concat(keyParts.join('/'))});
139+
for await (const mapInstance of mapAuthorities) {
140+
const authority = mapInstance[0];
141+
const mapTestClasses = mapInstance[1];
142+
const firstClassTestItem = Array.from(mapTestClasses.values())[0];
143+
const oneUri = firstClassTestItem.uri;
144+
145+
// This will always be true since every test added to the map above required a uri
146+
if (oneUri) {
147+
const folder = vscode.workspace.getWorkspaceFolder(oneUri);
148+
const server = osAPI.serverForUri(oneUri);
149+
const username = server.username || 'UnknownUser';
150+
const testRoot = vscode.Uri.from({scheme: 'isfs', authority, path: `/.vscode/UnitTestRoot/${username}`});
150151
try {
151-
await vscode.workspace.fs.copy(uri, directoryUri.with({path: directoryUri.path.concat(clsFile)}));
152+
// Limitation of the Atelier API means this can only delete the files, not the folders
153+
// but zombie folders shouldn't cause problems.
154+
await vscode.workspace.fs.delete(testRoot, { recursive: true });
152155
} catch (error) {
153156
console.log(error);
154157
}
158+
for await (const mapInstance of mapTestClasses) {
159+
const key = mapInstance[0];
160+
const uri = mapInstance[1].uri;
161+
const keyParts = key.split('/');
162+
const clsFile = keyParts.pop() || '';
163+
const directoryUri = testRoot.with({path: testRoot.path.concat(keyParts.join('/'))});
164+
// This will always be true since every test added to the map above required a uri
165+
if (uri) {
166+
try {
167+
await vscode.workspace.fs.copy(uri, directoryUri.with({path: directoryUri.path.concat(clsFile)}));
168+
} catch (error) {
169+
console.log(error);
170+
}
171+
}
172+
}
173+
174+
// Find this user's most recent TestInstance
175+
const serverSpec: IServerSpec = {
176+
username: server.username,
177+
name: server.serverName,
178+
webServer: {
179+
host: server.host,
180+
port: server.port,
181+
pathPrefix: server.pathPrefix,
182+
scheme: server.scheme
183+
}
184+
}
185+
const response = await makeRESTRequest(
186+
"POST",
187+
serverSpec,
188+
{ apiVersion: 1, namespace: server.namespace, path: "/action/query" },
189+
{
190+
query: "SELECT TOP 1 ID, TestInstance, Name, Duration, Status, ErrorDescription FROM %UnitTest_Result.TestSuite WHERE Name %STARTSWITH ? ORDER BY TestInstance DESC",
191+
parameters: [`${server.username}\\`]
192+
},
193+
);
194+
if (response) {
195+
const latestInstanceId = response?.data?.result?.content?.[0]?.ID;
196+
console.log(latestInstanceId);
197+
}
198+
199+
// Run tests through the debugger but only stop at breakpoints etc if user chose "Debug Test" instead of "Run Test"
200+
const runIndex = allTestRuns.push(run) - 1;
201+
const configuration: vscode.DebugConfiguration = {
202+
"type": "objectscript",
203+
"request": "launch",
204+
"name": `ServerTests:${server.username}`,
205+
"program": `##class(%UnitTest.Manager).RunTest("${server.username}","/noload/nodelete")`,
206+
"testRunIndex": runIndex,
207+
"testIdBase": firstClassTestItem.id.split(":", 2).join(":")
208+
};
209+
const sessionOptions: vscode.DebugSessionOptions = {
210+
noDebug: request.profile?.kind !== vscode.TestRunProfileKind.Debug
211+
}
212+
if (!await vscode.debug.startDebugging(folder, configuration, sessionOptions)) {
213+
await vscode.window.showErrorMessage(`Failed to launch testing`, { modal: true });
214+
run.end();
215+
allTestRuns[runIndex] = undefined;
216+
return;
217+
}
155218
}
156219
}
157220

158-
// TODO
159-
await new Promise(resolve => setTimeout(resolve, 5000));
160-
run.end();
221+
//run.end();
161222
}

0 commit comments

Comments
 (0)