Skip to content

Commit c6e177d

Browse files
committed
Workspace & resolver tests (microsoft#21441)
This PR - moves populateTestTree to utils - adds tests for execution adapters (pytest and unittest) - resultResolver tests - workspaceTestAdapater tests
1 parent 20cf981 commit c6e177d

File tree

9 files changed

+1200
-374
lines changed

9 files changed

+1200
-374
lines changed

src/client/testing/testController/common/resultResolver.ts

Lines changed: 5 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import {
5-
CancellationToken,
6-
Position,
7-
TestController,
8-
TestItem,
9-
Uri,
10-
Range,
11-
TestMessage,
12-
Location,
13-
TestRun,
14-
} from 'vscode';
4+
import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode';
155
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';
6+
import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types';
247
import { TestProvider } from '../../types';
258
import { traceError, traceLog } from '../../../logging';
269
import { Testing } from '../../../common/utils/localize';
27-
import {
28-
DebugTestTag,
29-
ErrorTestItemOptions,
30-
RunTestTag,
31-
clearAllChildren,
32-
createErrorTestItem,
33-
getTestCaseNodes,
34-
} from './testItemUtilities';
10+
import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities';
3511
import { sendTelemetryEvent } from '../../../telemetry';
3612
import { EventName } from '../../../telemetry/constants';
3713
import { splitLines } from '../../../common/stringUtils';
38-
import { fixLogLines } from './utils';
14+
import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils';
3915

4016
export class PythonResultResolver implements ITestResultResolver {
4117
testController: TestController;
@@ -253,69 +229,5 @@ export class PythonResultResolver implements ITestResultResolver {
253229
return Promise.resolve();
254230
}
255231
}
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));
299232

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-
}
233+
// had to switch the order of the original parameter since required param cannot follow optional.

src/client/testing/testController/common/utils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
import * as net from 'net';
4+
import * as path from 'path';
5+
import { CancellationToken, Position, TestController, TestItem, Uri, Range } from 'vscode';
46
import { traceError, traceLog, traceVerbose } from '../../../logging';
57

68
import { EnableTestAdapterRewrite } from '../../../common/experiments/groups';
79
import { IExperimentService } from '../../../common/types';
810
import { IServiceContainer } from '../../../ioc/types';
11+
import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities';
12+
import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types';
913

1014
export function fixLogLines(content: string): string {
1115
const lines = content.split(/\r?\n/g);
@@ -111,3 +115,69 @@ export async function startTestIdServer(testIds: string[]): Promise<number> {
111115
});
112116
return 0;
113117
}
118+
119+
export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions {
120+
const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error';
121+
return {
122+
id: `DiscoveryError:${uri.fsPath}`,
123+
label: `${labelText} [${path.basename(uri.fsPath)}]`,
124+
error: message,
125+
};
126+
}
127+
128+
export function populateTestTree(
129+
testController: TestController,
130+
testTreeData: DiscoveredTestNode,
131+
testRoot: TestItem | undefined,
132+
resultResolver: ITestResultResolver,
133+
token?: CancellationToken,
134+
): void {
135+
// 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.
136+
if (!testRoot) {
137+
testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path));
138+
139+
testRoot.canResolveChildren = true;
140+
testRoot.tags = [RunTestTag, DebugTestTag];
141+
142+
testController.items.add(testRoot);
143+
}
144+
145+
// Recursively populate the tree with test data.
146+
testTreeData.children.forEach((child) => {
147+
if (!token?.isCancellationRequested) {
148+
if (isTestItem(child)) {
149+
const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path));
150+
testItem.tags = [RunTestTag, DebugTestTag];
151+
152+
const range = new Range(
153+
new Position(Number(child.lineno) - 1, 0),
154+
new Position(Number(child.lineno), 0),
155+
);
156+
testItem.canResolveChildren = false;
157+
testItem.range = range;
158+
testItem.tags = [RunTestTag, DebugTestTag];
159+
160+
testRoot!.children.add(testItem);
161+
// add to our map
162+
resultResolver.runIdToTestItem.set(child.runID, testItem);
163+
resultResolver.runIdToVSid.set(child.runID, child.id_);
164+
resultResolver.vsIdToRunId.set(child.id_, child.runID);
165+
} else {
166+
let node = testController.items.get(child.path);
167+
168+
if (!node) {
169+
node = testController.createTestItem(child.id_, child.name, Uri.file(child.path));
170+
171+
node.canResolveChildren = true;
172+
node.tags = [RunTestTag, DebugTestTag];
173+
testRoot!.children.add(node);
174+
}
175+
populateTestTree(testController, child, node, resultResolver, token);
176+
}
177+
}
178+
});
179+
}
180+
181+
function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem {
182+
return test.type_ === 'test';
183+
}

src/client/testing/testController/workspaceTestAdapter.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import * as path from 'path';
54
import * as util from 'util';
65
import { CancellationToken, TestController, TestItem, TestRun, Uri } from 'vscode';
76
import { createDeferred, Deferred } from '../../common/utils/async';
@@ -10,10 +9,11 @@ import { traceError } from '../../logging';
109
import { sendTelemetryEvent } from '../../telemetry';
1110
import { EventName } from '../../telemetry/constants';
1211
import { TestProvider } from '../types';
13-
import { createErrorTestItem, ErrorTestItemOptions, getTestCaseNodes } from './common/testItemUtilities';
12+
import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities';
1413
import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types';
1514
import { IPythonExecutionFactory } from '../../common/process/types';
1615
import { ITestDebugLauncher } from '../common/types';
16+
import { buildErrorNodeOptions } from './common/utils';
1717

1818
/**
1919
* This class exposes a test-provider-agnostic way of discovering tests.
@@ -162,12 +162,3 @@ export class WorkspaceTestAdapter {
162162
return Promise.resolve();
163163
}
164164
}
165-
166-
function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions {
167-
const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error';
168-
return {
169-
id: `DiscoveryError:${uri.fsPath}`,
170-
label: `${labelText} [${path.basename(uri.fsPath)}]`,
171-
error: message,
172-
};
173-
}

src/test/mocks/vsc/index.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { EventEmitter as NodeEventEmitter } from 'events';
88
import * as vscode from 'vscode';
9+
910
// export * from './range';
1011
// export * from './position';
1112
// export * from './selection';
@@ -443,3 +444,114 @@ export enum LogLevel {
443444
*/
444445
Error = 5,
445446
}
447+
448+
export class TestMessage {
449+
/**
450+
* Human-readable message text to display.
451+
*/
452+
message: string | MarkdownString;
453+
454+
/**
455+
* Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown.
456+
*/
457+
expectedOutput?: string;
458+
459+
/**
460+
* Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown.
461+
*/
462+
actualOutput?: string;
463+
464+
/**
465+
* Associated file location.
466+
*/
467+
location?: vscode.Location;
468+
469+
/**
470+
* Creates a new TestMessage that will present as a diff in the editor.
471+
* @param message Message to display to the user.
472+
* @param expected Expected output.
473+
* @param actual Actual output.
474+
*/
475+
static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage {
476+
const testMessage = new TestMessage(message);
477+
testMessage.expectedOutput = expected;
478+
testMessage.actualOutput = actual;
479+
return testMessage;
480+
}
481+
482+
/**
483+
* Creates a new TestMessage instance.
484+
* @param message The message to show to the user.
485+
*/
486+
constructor(message: string | MarkdownString) {
487+
this.message = message;
488+
}
489+
}
490+
491+
export interface TestItemCollection extends Iterable<[string, vscode.TestItem]> {
492+
/**
493+
* Gets the number of items in the collection.
494+
*/
495+
readonly size: number;
496+
497+
/**
498+
* Replaces the items stored by the collection.
499+
* @param items Items to store.
500+
*/
501+
replace(items: readonly vscode.TestItem[]): void;
502+
503+
/**
504+
* Iterate over each entry in this collection.
505+
*
506+
* @param callback Function to execute for each entry.
507+
* @param thisArg The `this` context used when invoking the handler function.
508+
*/
509+
forEach(callback: (item: vscode.TestItem, collection: TestItemCollection) => unknown, thisArg?: unknown): void;
510+
511+
/**
512+
* Adds the test item to the children. If an item with the same ID already
513+
* exists, it'll be replaced.
514+
* @param item Item to add.
515+
*/
516+
add(item: vscode.TestItem): void;
517+
518+
/**
519+
* Removes a single test item from the collection.
520+
* @param itemId Item ID to delete.
521+
*/
522+
delete(itemId: string): void;
523+
524+
/**
525+
* Efficiently gets a test item by ID, if it exists, in the children.
526+
* @param itemId Item ID to get.
527+
* @returns The found item or undefined if it does not exist.
528+
*/
529+
get(itemId: string): vscode.TestItem | undefined;
530+
}
531+
532+
/**
533+
* Represents a location inside a resource, such as a line
534+
* inside a text file.
535+
*/
536+
export class Location {
537+
/**
538+
* The resource identifier of this location.
539+
*/
540+
uri: vscode.Uri;
541+
542+
/**
543+
* The document range of this location.
544+
*/
545+
range: vscode.Range;
546+
547+
/**
548+
* Creates a new location object.
549+
*
550+
* @param uri The resource identifier.
551+
* @param rangeOrPosition The range or position. Positions will be converted to an empty range.
552+
*/
553+
constructor(uri: vscode.Uri, rangeOrPosition: vscode.Range) {
554+
this.uri = uri;
555+
this.range = rangeOrPosition;
556+
}
557+
}

0 commit comments

Comments
 (0)