Skip to content

Commit f4da1c4

Browse files
committed
Remote resolver (microsoft#21332)
This is branch will serve as a feature branch for all changes related to switching to the remote resolver. This will include - switching from using the testAdapter to parse the return data to now using this new class resultResolver - adding tests for all testAdapters, fixing for server and adding for resultResolver - moving sendCommand to a new file, out of the server, and getting pytest to adopt it - moving the server which send the test IDs to a new file and adopt it for both pytest and unittest - write tests for these two new files.
1 parent 3dc11d2 commit f4da1c4

15 files changed

+1301
-921
lines changed

src/client/testing/common/socketServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocke
123123
if ((socket as any).id) {
124124
destroyedSocketId = (socket as any).id;
125125
}
126-
this.log('socket disconnected', destroyedSocketId.toString());
126+
this.log('socket disconnected', destroyedSocketId?.toString());
127127
if (socket && socket.destroy) {
128128
socket.destroy();
129129
}
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import {
5+
CancellationToken,
6+
Position,
7+
TestController,
8+
TestItem,
9+
Uri,
10+
Range,
11+
TestMessage,
12+
Location,
13+
TestRun,
14+
} from 'vscode';
15+
import * as util from 'util';
16+
import * as path from 'path';
17+
import {
18+
DiscoveredTestItem,
19+
DiscoveredTestNode,
20+
DiscoveredTestPayload,
21+
ExecutionTestPayload,
22+
ITestResultResolver,
23+
} from './types';
24+
import { TestProvider } from '../../types';
25+
import { traceError, traceLog } from '../../../logging';
26+
import { Testing } from '../../../common/utils/localize';
27+
import {
28+
DebugTestTag,
29+
ErrorTestItemOptions,
30+
RunTestTag,
31+
clearAllChildren,
32+
createErrorTestItem,
33+
getTestCaseNodes,
34+
} from './testItemUtilities';
35+
import { sendTelemetryEvent } from '../../../telemetry';
36+
import { EventName } from '../../../telemetry/constants';
37+
import { splitLines } from '../../../common/stringUtils';
38+
import { fixLogLines } from './utils';
39+
40+
export class PythonResultResolver implements ITestResultResolver {
41+
testController: TestController;
42+
43+
testProvider: TestProvider;
44+
45+
public runIdToTestItem: Map<string, TestItem>;
46+
47+
public runIdToVSid: Map<string, string>;
48+
49+
public vsIdToRunId: Map<string, string>;
50+
51+
constructor(testController: TestController, testProvider: TestProvider, private workspaceUri: Uri) {
52+
this.testController = testController;
53+
this.testProvider = testProvider;
54+
55+
this.runIdToTestItem = new Map<string, TestItem>();
56+
this.runIdToVSid = new Map<string, string>();
57+
this.vsIdToRunId = new Map<string, string>();
58+
}
59+
60+
public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): Promise<void> {
61+
const workspacePath = this.workspaceUri.fsPath;
62+
traceLog('Using result resolver for discovery');
63+
64+
const rawTestData = payload;
65+
if (!rawTestData) {
66+
// No test data is available
67+
return Promise.resolve();
68+
}
69+
70+
// Check if there were any errors in the discovery process.
71+
if (rawTestData.status === 'error') {
72+
const testingErrorConst =
73+
this.testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery;
74+
const { errors } = rawTestData;
75+
traceError(testingErrorConst, '\r\n', errors!.join('\r\n\r\n'));
76+
77+
let errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`);
78+
const message = util.format(
79+
`${testingErrorConst} ${Testing.seePythonOutput}\r\n`,
80+
errors!.join('\r\n\r\n'),
81+
);
82+
83+
if (errorNode === undefined) {
84+
const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider);
85+
errorNode = createErrorTestItem(this.testController, options);
86+
this.testController.items.add(errorNode);
87+
}
88+
errorNode.error = message;
89+
} else {
90+
// Remove the error node if necessary,
91+
// then parse and insert test data.
92+
this.testController.items.delete(`DiscoveryError:${workspacePath}`);
93+
94+
if (rawTestData.tests) {
95+
// If the test root for this folder exists: Workspace refresh, update its children.
96+
// Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree.
97+
populateTestTree(this.testController, rawTestData.tests, undefined, this, token);
98+
} else {
99+
// Delete everything from the test controller.
100+
this.testController.items.replace([]);
101+
}
102+
}
103+
104+
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, {
105+
tool: this.testProvider,
106+
failed: false,
107+
});
108+
return Promise.resolve();
109+
}
110+
111+
public resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): Promise<void> {
112+
const rawTestExecData = payload;
113+
if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) {
114+
// Map which holds the subtest information for each test item.
115+
const subTestStats: Map<string, { passed: number; failed: number }> = new Map();
116+
117+
// iterate through payload and update the UI accordingly.
118+
for (const keyTemp of Object.keys(rawTestExecData.result)) {
119+
const testCases: TestItem[] = [];
120+
121+
// grab leaf level test items
122+
this.testController.items.forEach((i) => {
123+
const tempArr: TestItem[] = getTestCaseNodes(i);
124+
testCases.push(...tempArr);
125+
});
126+
127+
if (
128+
rawTestExecData.result[keyTemp].outcome === 'failure' ||
129+
rawTestExecData.result[keyTemp].outcome === 'passed-unexpected'
130+
) {
131+
const rawTraceback = rawTestExecData.result[keyTemp].traceback ?? '';
132+
const traceback = splitLines(rawTraceback, {
133+
trim: false,
134+
removeEmptyEntries: true,
135+
}).join('\r\n');
136+
137+
const text = `${rawTestExecData.result[keyTemp].test} failed: ${
138+
rawTestExecData.result[keyTemp].message ?? rawTestExecData.result[keyTemp].outcome
139+
}\r\n${traceback}\r\n`;
140+
const message = new TestMessage(text);
141+
142+
// note that keyTemp is a runId for unittest library...
143+
const grabVSid = this.runIdToVSid.get(keyTemp);
144+
// search through freshly built array of testItem to find the failed test and update UI.
145+
testCases.forEach((indiItem) => {
146+
if (indiItem.id === grabVSid) {
147+
if (indiItem.uri && indiItem.range) {
148+
message.location = new Location(indiItem.uri, indiItem.range);
149+
runInstance.failed(indiItem, message);
150+
runInstance.appendOutput(fixLogLines(text));
151+
}
152+
}
153+
});
154+
} else if (
155+
rawTestExecData.result[keyTemp].outcome === 'success' ||
156+
rawTestExecData.result[keyTemp].outcome === 'expected-failure'
157+
) {
158+
const grabTestItem = this.runIdToTestItem.get(keyTemp);
159+
const grabVSid = this.runIdToVSid.get(keyTemp);
160+
if (grabTestItem !== undefined) {
161+
testCases.forEach((indiItem) => {
162+
if (indiItem.id === grabVSid) {
163+
if (indiItem.uri && indiItem.range) {
164+
runInstance.passed(grabTestItem);
165+
runInstance.appendOutput('Passed here');
166+
}
167+
}
168+
});
169+
}
170+
} else if (rawTestExecData.result[keyTemp].outcome === 'skipped') {
171+
const grabTestItem = this.runIdToTestItem.get(keyTemp);
172+
const grabVSid = this.runIdToVSid.get(keyTemp);
173+
if (grabTestItem !== undefined) {
174+
testCases.forEach((indiItem) => {
175+
if (indiItem.id === grabVSid) {
176+
if (indiItem.uri && indiItem.range) {
177+
runInstance.skipped(grabTestItem);
178+
runInstance.appendOutput('Skipped here');
179+
}
180+
}
181+
});
182+
}
183+
} else if (rawTestExecData.result[keyTemp].outcome === 'subtest-failure') {
184+
// split on " " since the subtest ID has the parent test ID in the first part of the ID.
185+
const parentTestCaseId = keyTemp.split(' ')[0];
186+
const parentTestItem = this.runIdToTestItem.get(parentTestCaseId);
187+
const data = rawTestExecData.result[keyTemp];
188+
// find the subtest's parent test item
189+
if (parentTestItem) {
190+
const subtestStats = subTestStats.get(parentTestCaseId);
191+
if (subtestStats) {
192+
subtestStats.failed += 1;
193+
} else {
194+
subTestStats.set(parentTestCaseId, { failed: 1, passed: 0 });
195+
runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`));
196+
// clear since subtest items don't persist between runs
197+
clearAllChildren(parentTestItem);
198+
}
199+
const subtestId = keyTemp;
200+
const subTestItem = this.testController?.createTestItem(subtestId, subtestId);
201+
runInstance.appendOutput(fixLogLines(`${subtestId} Failed\r\n`));
202+
// create a new test item for the subtest
203+
if (subTestItem) {
204+
const traceback = data.traceback ?? '';
205+
const text = `${data.subtest} Failed: ${data.message ?? data.outcome}\r\n${traceback}\r\n`;
206+
runInstance.appendOutput(fixLogLines(text));
207+
parentTestItem.children.add(subTestItem);
208+
runInstance.started(subTestItem);
209+
const message = new TestMessage(rawTestExecData?.result[keyTemp].message ?? '');
210+
if (parentTestItem.uri && parentTestItem.range) {
211+
message.location = new Location(parentTestItem.uri, parentTestItem.range);
212+
}
213+
runInstance.failed(subTestItem, message);
214+
} else {
215+
throw new Error('Unable to create new child node for subtest');
216+
}
217+
} else {
218+
throw new Error('Parent test item not found');
219+
}
220+
} else if (rawTestExecData.result[keyTemp].outcome === 'subtest-success') {
221+
// split on " " since the subtest ID has the parent test ID in the first part of the ID.
222+
const parentTestCaseId = keyTemp.split(' ')[0];
223+
const parentTestItem = this.runIdToTestItem.get(parentTestCaseId);
224+
225+
// find the subtest's parent test item
226+
if (parentTestItem) {
227+
const subtestStats = subTestStats.get(parentTestCaseId);
228+
if (subtestStats) {
229+
subtestStats.passed += 1;
230+
} else {
231+
subTestStats.set(parentTestCaseId, { failed: 0, passed: 1 });
232+
runInstance.appendOutput(fixLogLines(`${parentTestCaseId} [subtests]:\r\n`));
233+
// clear since subtest items don't persist between runs
234+
clearAllChildren(parentTestItem);
235+
}
236+
const subtestId = keyTemp;
237+
const subTestItem = this.testController?.createTestItem(subtestId, subtestId);
238+
// create a new test item for the subtest
239+
if (subTestItem) {
240+
parentTestItem.children.add(subTestItem);
241+
runInstance.started(subTestItem);
242+
runInstance.passed(subTestItem);
243+
runInstance.appendOutput(fixLogLines(`${subtestId} Passed\r\n`));
244+
} else {
245+
throw new Error('Unable to create new child node for subtest');
246+
}
247+
} else {
248+
throw new Error('Parent test item not found');
249+
}
250+
}
251+
}
252+
}
253+
return Promise.resolve();
254+
}
255+
}
256+
// had to switch the order of the original parameter since required param cannot follow optional.
257+
function populateTestTree(
258+
testController: TestController,
259+
testTreeData: DiscoveredTestNode,
260+
testRoot: TestItem | undefined,
261+
resultResolver: ITestResultResolver,
262+
token?: CancellationToken,
263+
): void {
264+
// If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller.
265+
if (!testRoot) {
266+
testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path));
267+
268+
testRoot.canResolveChildren = true;
269+
testRoot.tags = [RunTestTag, DebugTestTag];
270+
271+
testController.items.add(testRoot);
272+
}
273+
274+
// Recursively populate the tree with test data.
275+
testTreeData.children.forEach((child) => {
276+
if (!token?.isCancellationRequested) {
277+
if (isTestItem(child)) {
278+
const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path));
279+
testItem.tags = [RunTestTag, DebugTestTag];
280+
281+
const range = new Range(
282+
new Position(Number(child.lineno) - 1, 0),
283+
new Position(Number(child.lineno), 0),
284+
);
285+
testItem.canResolveChildren = false;
286+
testItem.range = range;
287+
testItem.tags = [RunTestTag, DebugTestTag];
288+
289+
testRoot!.children.add(testItem);
290+
// add to our map
291+
resultResolver.runIdToTestItem.set(child.runID, testItem);
292+
resultResolver.runIdToVSid.set(child.runID, child.id_);
293+
resultResolver.vsIdToRunId.set(child.id_, child.runID);
294+
} else {
295+
let node = testController.items.get(child.path);
296+
297+
if (!node) {
298+
node = testController.createTestItem(child.id_, child.name, Uri.file(child.path));
299+
300+
node.canResolveChildren = true;
301+
node.tags = [RunTestTag, DebugTestTag];
302+
testRoot!.children.add(node);
303+
}
304+
populateTestTree(testController, child, node, resultResolver, token);
305+
}
306+
}
307+
});
308+
}
309+
310+
function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem {
311+
return test.type_ === 'test';
312+
}
313+
314+
export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions {
315+
const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error';
316+
return {
317+
id: `DiscoveryError:${uri.fsPath}`,
318+
label: `${labelText} [${path.basename(uri.fsPath)}]`,
319+
error: message,
320+
};
321+
}

0 commit comments

Comments
 (0)