Skip to content

Commit 42c870a

Browse files
committed
Implement for client-side development paradigm
1 parent b1bd233 commit 42c870a

5 files changed

Lines changed: 221 additions & 252 deletions

File tree

src/commonRunTestsHandler.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as vscode from 'vscode';
2+
import { allTestRuns, extensionId, IServerSpec, osAPI } from './extension';
3+
import { relativeTestRoot } from './localTests';
4+
import logger from './logger';
5+
6+
export async function commonRunTestsHandler(controller: vscode.TestController, resolveItemChildren: (item: vscode.TestItem) => Promise<void>, request: vscode.TestRunRequest, cancellation: vscode.CancellationToken) {
7+
logger.info(`commonRunTestsHandler invoked by controller id=${controller.id}`);
8+
9+
const isResolvedMap = new WeakMap<vscode.TestItem, boolean>();
10+
11+
// For each authority (i.e. server:namespace) accumulate a map of the class-level Test nodes in the tree.
12+
// We don't yet support running only some TestXXX methods in a testclass
13+
const mapAuthorities = new Map<string, Map<string, vscode.TestItem>>();
14+
const queue: vscode.TestItem[] = [];
15+
16+
// Loop through all included tests, or all known tests, and add them to our queue
17+
if (request.include) {
18+
request.include.forEach(test => queue.push(test));
19+
} else {
20+
controller.items.forEach(test => queue.push(test));
21+
}
22+
23+
// Process every test that was queued. Recurse down to leaves (testmethods) and build a map of their parents (classes)
24+
while (queue.length > 0 && !cancellation.isCancellationRequested) {
25+
const test = queue.pop()!;
26+
27+
// Skip tests the user asked to exclude
28+
if (request.exclude?.includes(test)) {
29+
continue;
30+
}
31+
32+
// Resolve children if not already done
33+
if (test.canResolveChildren && !isResolvedMap.get(test)) {
34+
await resolveItemChildren(test);
35+
}
36+
37+
// If a leaf item (a TestXXX method in a class) note its .cls file for copying.
38+
// Every leaf must have a uri.
39+
if (test.children.size === 0 && test.uri && test.parent) {
40+
let authority = test.uri.authority;
41+
let key = test.uri.path;
42+
if (test.uri.scheme === "file") {
43+
// Client-side editing, for which we will assume objectscript.conn names a server defined in `intersystems.servers`
44+
const conn: any = vscode.workspace.getConfiguration("objectscript", test.uri).get("conn");
45+
authority = conn.server + ":" + (conn.ns as string).toLowerCase();
46+
const folder = vscode.workspace.getWorkspaceFolder(test.uri);
47+
if (folder) {
48+
key = key.slice(folder.uri.path.length + relativeTestRoot(folder).length + 1);
49+
}
50+
}
51+
52+
const mapTestClasses = mapAuthorities.get(authority) || new Map<string, vscode.TestItem>();
53+
mapTestClasses.set(key, test.parent);
54+
mapAuthorities.set(authority, mapTestClasses);
55+
}
56+
57+
// Queue any children
58+
test.children.forEach(test => queue.push(test));
59+
}
60+
61+
if (mapAuthorities.size === 0) {
62+
// Nothing included
63+
vscode.window.showWarningMessage(`Empty test run`);
64+
return;
65+
}
66+
67+
if (cancellation.isCancellationRequested) {
68+
// TODO what?
69+
}
70+
71+
for await (const mapInstance of mapAuthorities) {
72+
73+
const run = controller.createTestRun(
74+
request,
75+
'Test Results',
76+
true
77+
);
78+
const authority = mapInstance[0];
79+
const mapTestClasses = mapInstance[1];
80+
const firstClassTestItem = Array.from(mapTestClasses.values())[0];
81+
const oneUri = firstClassTestItem.uri;
82+
83+
// This will always be true since every test added to the map above required a uri
84+
if (oneUri) {
85+
86+
// First, clear out the server-side folder for the classes whose testmethods will be run
87+
const folder = vscode.workspace.getWorkspaceFolder(oneUri);
88+
const server = osAPI.serverForUri(oneUri);
89+
const username = server.username || 'UnknownUser';
90+
const testRoot = vscode.Uri.from({ scheme: 'isfs', authority, path: `/.vscode/UnitTestRoot/${username}` });
91+
try {
92+
// Limitation of the Atelier API means this can only delete the files, not the folders
93+
// but zombie folders shouldn't cause problems.
94+
await vscode.workspace.fs.delete(testRoot, { recursive: true });
95+
} catch (error) {
96+
console.log(error);
97+
}
98+
99+
// Next, copy the classes into the folder as a package hierarchy
100+
for await (const mapInstance of mapTestClasses) {
101+
const key = mapInstance[0];
102+
const classTest = mapInstance[1];
103+
const uri = classTest.uri;
104+
const keyParts = key.split('/');
105+
const clsFile = keyParts.pop() || '';
106+
const directoryUri = testRoot.with({ path: testRoot.path.concat(keyParts.join('/') + '/') });
107+
// This will always be true since every test added to the map above required a uri
108+
if (uri) {
109+
try {
110+
await vscode.workspace.fs.copy(uri, directoryUri.with({ path: directoryUri.path.concat(clsFile) }));
111+
} catch (error) {
112+
console.log(error);
113+
continue;
114+
}
115+
116+
// Unless the file copy failed, enqueue all the testitems that represent the TestXXX methods of the class
117+
classTest.children.forEach((methodTest) => {
118+
run.enqueued(methodTest);
119+
});
120+
}
121+
}
122+
123+
// Finally, run the tests using the debugger API
124+
const serverSpec: IServerSpec = {
125+
username: server.username,
126+
name: server.serverName,
127+
webServer: {
128+
host: server.host,
129+
port: server.port,
130+
pathPrefix: server.pathPrefix,
131+
scheme: server.scheme
132+
}
133+
};
134+
const namespace: string = server.namespace.toUpperCase();
135+
const runQualifiers = controller.id === `${extensionId}-Local` ? "" : "/noload/nodelete";
136+
// Run tests through the debugger but only stop at breakpoints etc if user chose "Debug Test" instead of "Run Test"
137+
const runIndex = allTestRuns.push(run) - 1;
138+
const configuration: vscode.DebugConfiguration = {
139+
"type": "objectscript",
140+
"request": "launch",
141+
"name": `${controller.id.split("-").pop()}Tests:${serverSpec.name}:${namespace}:${serverSpec.username}`,
142+
"program": `##class(%UnitTest.Manager).RunTest("${serverSpec.username}","${runQualifiers}")`,
143+
"testRunIndex": runIndex,
144+
"testIdBase": firstClassTestItem.id.split(":", 2).join(":")
145+
};
146+
const sessionOptions: vscode.DebugSessionOptions = {
147+
noDebug: request.profile?.kind !== vscode.TestRunProfileKind.Debug,
148+
suppressDebugToolbar: request.profile?.kind !== vscode.TestRunProfileKind.Debug
149+
};
150+
if (!await vscode.debug.startDebugging(folder, configuration, sessionOptions)) {
151+
await vscode.window.showErrorMessage(`Failed to launch testing`, { modal: true });
152+
run.end();
153+
allTestRuns[runIndex] = undefined;
154+
return;
155+
}
156+
}
157+
}
158+
}

src/debugTracker.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,40 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
1010
private testController: vscode.TestController
1111
private run?: vscode.TestRun;
1212
private testIdBase: string;
13-
private classTestCollection?: vscode.TestItemCollection;
1413
private className?: string;
15-
private classTest?: vscode.TestItem;
1614
private testMethodName?: string;
1715
private testDuration?: number;
16+
private methodTestMap: Map<string, vscode.TestItem>;
1817
private methodTest?: vscode.TestItem;
1918
private failureMessages: vscode.TestMessage[] = [];
2019

2120
constructor(session: vscode.DebugSession) {
2221
this.session = session;
2322
let runType: string;
2423
[ runType, this.serverName, this.namespace ] = this.session.configuration.name.split(':');
25-
this.testController = runType === 'ServerTests' ? loadedTestController : localTestController;
24+
this.testController = runType === 'LoadedTests' ? loadedTestController : localTestController;
2625
this.run = allTestRuns[this.session.configuration.testRunIndex];
2726
this.testIdBase = this.session.configuration.testIdBase;
28-
this.classTestCollection = this.testController.items.get(this.testIdBase)?.children;
27+
this.methodTestMap = new Map<string, vscode.TestItem>();
28+
29+
const addToMethodTestMap = (testItem?: vscode.TestItem) => {
30+
if (!testItem) {
31+
return;
32+
}
33+
if (testItem.children.size > 0) {
34+
testItem.children.forEach(addToMethodTestMap);
35+
} else {
36+
this.methodTestMap.set(testItem.id, testItem);
37+
}
38+
}
39+
40+
if (runType === 'LoadedTests') {
41+
// This tree is flat
42+
addToMethodTestMap(this.testController.items.get(this.testIdBase));
43+
} else {
44+
// This tree is nested
45+
addToMethodTestMap(this.testController.items.get(this.testIdBase + ':'));
46+
}
2947
}
3048

3149
onDidSendMessage(message: any): void {
@@ -39,7 +57,6 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
3957
const classBegin = line.match(/^ ([%\dA-Za-z][\dA-Za-z\.]*) begins \.\.\.$/);
4058
if (classBegin) {
4159
this.className = classBegin[1];
42-
this.classTest = this.classTestCollection?.get(`${this.testIdBase}:${this.className}`);
4360
}
4461
return;
4562
}
@@ -53,7 +70,7 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
5370
const methodBegin = line.match(/^ Test([%\dA-Za-z][\dA-Za-z]*).* begins \.\.\.$/);
5471
if (methodBegin) {
5572
this.testMethodName = methodBegin[1];
56-
this.methodTest = this.classTest?.children.get(`${this.classTest.id}:Test${this.testMethodName}`);
73+
this.methodTest = this.methodTestMap.get(`${this.testIdBase}:${this.className}:Test${this.testMethodName}`);
5774
this.failureMessages = [];
5875
if (this.methodTest) {
5976
this.run.started(this.methodTest)
@@ -63,7 +80,7 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
6380
} else {
6481
if (line.startsWith(` Test${this.testMethodName} `)) {
6582
const outcome = line.split(this.testMethodName + ' ')[1];
66-
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, outcome=${outcome}`);
83+
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, outcome=${outcome}`);
6784
if (this.methodTest) {
6885
switch (outcome) {
6986
case 'passed':
@@ -89,21 +106,21 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
89106
if (this.className && this.testMethodName) {
90107
const assertPassedMatch = line.match(/^ (Assert\w+):(.*) \(passed\)$/);
91108
if (assertPassedMatch) {
92-
const macroName = assertPassedMatch[1];
93-
const message = assertPassedMatch[2];
94-
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'passed', message=${message}`);
109+
//const macroName = assertPassedMatch[1];
110+
//const message = assertPassedMatch[2];
111+
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'passed', message=${message}`);
95112
} else {
96113
const assertFailedMatch = line.match(/^(Assert\w+):(.*) \(failed\) <<====/);
97114
if (assertFailedMatch) {
98-
const macroName = assertFailedMatch[1];
115+
//const macroName = assertFailedMatch[1];
99116
const message = assertFailedMatch[2];
100-
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'failed', message=${message}`);
117+
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName ${macroName}, outcome 'failed', message=${message}`);
101118
this.failureMessages.push({ message: message });
102119
} else {
103120
const logMessageMatch = line.match(/^ LogMessage:(.*)$/);
104121
if (logMessageMatch) {
105122
const message = logMessageMatch[1];
106-
console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName LogMessage, message=${message}`);
123+
//console.log(`Class ${this.className}, Test-method ${this.testMethodName}, macroName LogMessage, message=${message}`);
107124
const duration = message.match(/^Duration of execution: (\d*\.\d+) sec.$/);
108125
if (duration) {
109126
this.testDuration = + duration[1] * 1000;
@@ -116,11 +133,11 @@ export class DebugTracker implements vscode.DebugAdapterTracker {
116133
}
117134

118135
onWillStartSession(): void {
119-
console.log(`**Starting session ${this.session.name}, run.name = ${this.run?.name}`);
136+
//console.log(`**Starting session ${this.session.name}, run.name = ${this.run?.name}`);
120137
}
121138

122139
onWillStopSession(): void {
123-
console.log(`**Stopping session ${this.session.name}`);
140+
//console.log(`**Stopping session ${this.session.name}`);
124141
if (this.run) {
125142
this.run.end();
126143
refreshHistoryRootItem(this.serverName, this.namespace);

src/historyExplorer.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,21 +235,24 @@ async function addTestAsserts(item: vscode.TestItem, controller: vscode.TestCont
235235
}
236236
}
237237

238-
/* Replace a test controller's root items with one item for each server:NAMESPACE this workspace uses
238+
/* Replace a test controller's root items with one item for each server:NAMESPACE this workspace uses.
239+
If `schemes` array is passed, a folder must use one of the named schemes in order to qualify.
239240
*/
240-
export function replaceRootItems(controller: vscode.TestController) {
241+
export function replaceRootItems(controller: vscode.TestController, schemes?: string[]) {
241242
const rootItems: vscode.TestItem[] = [];
242243
const rootMap = new Map<string, vscode.TestItem>();
243244
vscode.workspace.workspaceFolders?.forEach(folder => {
245+
if (!schemes || schemes.includes(folder.uri.scheme)) {
244246
const server = osAPI.serverForUri(folder.uri);
245247
if (server?.serverName && server.namespace) {
246-
const key = server.serverName + ':' + server.namespace.toUpperCase();
248+
const key = server.serverName + ":" + server.namespace.toUpperCase();
247249
if (!rootMap.has(key)) {
248250
const item = controller.createTestItem(key, key, folder.uri);
249251
item.canResolveChildren = true;
250252
rootMap.set(key, item);
251253
}
252254
}
255+
}
253256
});
254257
rootMap.forEach(item => rootItems.push(item));
255258
controller.items.replace(rootItems);

0 commit comments

Comments
 (0)