Skip to content

Commit

Permalink
Workspace & resolver tests (#21441)
Browse files Browse the repository at this point in the history
This PR
- moves populateTestTree to utils
- adds tests for execution adapters (pytest and unittest)
- resultResolver tests
- workspaceTestAdapater tests
  • Loading branch information
eleanorjboyd authored Jun 20, 2023
1 parent 4d1c196 commit efdf65e
Show file tree
Hide file tree
Showing 9 changed files with 1,200 additions and 374 deletions.
98 changes: 5 additions & 93 deletions src/client/testing/testController/common/resultResolver.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import {
CancellationToken,
Position,
TestController,
TestItem,
Uri,
Range,
TestMessage,
Location,
TestRun,
} from 'vscode';
import { CancellationToken, TestController, TestItem, Uri, TestMessage, Location, TestRun } from 'vscode';
import * as util from 'util';
import * as path from 'path';
import {
DiscoveredTestItem,
DiscoveredTestNode,
DiscoveredTestPayload,
ExecutionTestPayload,
ITestResultResolver,
} from './types';
import { DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types';
import { TestProvider } from '../../types';
import { traceError, traceLog } from '../../../logging';
import { Testing } from '../../../common/utils/localize';
import {
DebugTestTag,
ErrorTestItemOptions,
RunTestTag,
clearAllChildren,
createErrorTestItem,
getTestCaseNodes,
} from './testItemUtilities';
import { clearAllChildren, createErrorTestItem, getTestCaseNodes } from './testItemUtilities';
import { sendTelemetryEvent } from '../../../telemetry';
import { EventName } from '../../../telemetry/constants';
import { splitLines } from '../../../common/stringUtils';
import { fixLogLines } from './utils';
import { buildErrorNodeOptions, fixLogLines, populateTestTree } from './utils';

export class PythonResultResolver implements ITestResultResolver {
testController: TestController;
Expand Down Expand Up @@ -253,69 +229,5 @@ export class PythonResultResolver implements ITestResultResolver {
return Promise.resolve();
}
}
// had to switch the order of the original parameter since required param cannot follow optional.
function populateTestTree(
testController: TestController,
testTreeData: DiscoveredTestNode,
testRoot: TestItem | undefined,
resultResolver: ITestResultResolver,
token?: CancellationToken,
): void {
// 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.
if (!testRoot) {
testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path));

testRoot.canResolveChildren = true;
testRoot.tags = [RunTestTag, DebugTestTag];

testController.items.add(testRoot);
}

// Recursively populate the tree with test data.
testTreeData.children.forEach((child) => {
if (!token?.isCancellationRequested) {
if (isTestItem(child)) {
const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path));
testItem.tags = [RunTestTag, DebugTestTag];

const range = new Range(
new Position(Number(child.lineno) - 1, 0),
new Position(Number(child.lineno), 0),
);
testItem.canResolveChildren = false;
testItem.range = range;
testItem.tags = [RunTestTag, DebugTestTag];

testRoot!.children.add(testItem);
// add to our map
resultResolver.runIdToTestItem.set(child.runID, testItem);
resultResolver.runIdToVSid.set(child.runID, child.id_);
resultResolver.vsIdToRunId.set(child.id_, child.runID);
} else {
let node = testController.items.get(child.path);

if (!node) {
node = testController.createTestItem(child.id_, child.name, Uri.file(child.path));

node.canResolveChildren = true;
node.tags = [RunTestTag, DebugTestTag];
testRoot!.children.add(node);
}
populateTestTree(testController, child, node, resultResolver, token);
}
}
});
}

function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem {
return test.type_ === 'test';
}

export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions {
const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error';
return {
id: `DiscoveryError:${uri.fsPath}`,
label: `${labelText} [${path.basename(uri.fsPath)}]`,
error: message,
};
}
// had to switch the order of the original parameter since required param cannot follow optional.
70 changes: 70 additions & 0 deletions src/client/testing/testController/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as net from 'net';
import * as path from 'path';
import { CancellationToken, Position, TestController, TestItem, Uri, Range } from 'vscode';
import { traceError, traceLog, traceVerbose } from '../../../logging';

import { EnableTestAdapterRewrite } from '../../../common/experiments/groups';
import { IExperimentService } from '../../../common/types';
import { IServiceContainer } from '../../../ioc/types';
import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities';
import { DiscoveredTestItem, DiscoveredTestNode, ITestResultResolver } from './types';

export function fixLogLines(content: string): string {
const lines = content.split(/\r?\n/g);
Expand Down Expand Up @@ -111,3 +115,69 @@ export async function startTestIdServer(testIds: string[]): Promise<number> {
});
return 0;
}

export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions {
const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error';
return {
id: `DiscoveryError:${uri.fsPath}`,
label: `${labelText} [${path.basename(uri.fsPath)}]`,
error: message,
};
}

export function populateTestTree(
testController: TestController,
testTreeData: DiscoveredTestNode,
testRoot: TestItem | undefined,
resultResolver: ITestResultResolver,
token?: CancellationToken,
): void {
// 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.
if (!testRoot) {
testRoot = testController.createTestItem(testTreeData.path, testTreeData.name, Uri.file(testTreeData.path));

testRoot.canResolveChildren = true;
testRoot.tags = [RunTestTag, DebugTestTag];

testController.items.add(testRoot);
}

// Recursively populate the tree with test data.
testTreeData.children.forEach((child) => {
if (!token?.isCancellationRequested) {
if (isTestItem(child)) {
const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path));
testItem.tags = [RunTestTag, DebugTestTag];

const range = new Range(
new Position(Number(child.lineno) - 1, 0),
new Position(Number(child.lineno), 0),
);
testItem.canResolveChildren = false;
testItem.range = range;
testItem.tags = [RunTestTag, DebugTestTag];

testRoot!.children.add(testItem);
// add to our map
resultResolver.runIdToTestItem.set(child.runID, testItem);
resultResolver.runIdToVSid.set(child.runID, child.id_);
resultResolver.vsIdToRunId.set(child.id_, child.runID);
} else {
let node = testController.items.get(child.path);

if (!node) {
node = testController.createTestItem(child.id_, child.name, Uri.file(child.path));

node.canResolveChildren = true;
node.tags = [RunTestTag, DebugTestTag];
testRoot!.children.add(node);
}
populateTestTree(testController, child, node, resultResolver, token);
}
}
});
}

function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem {
return test.type_ === 'test';
}
13 changes: 2 additions & 11 deletions src/client/testing/testController/workspaceTestAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as path from 'path';
import * as util from 'util';
import { CancellationToken, TestController, TestItem, TestRun, Uri } from 'vscode';
import { createDeferred, Deferred } from '../../common/utils/async';
Expand All @@ -10,10 +9,11 @@ import { traceError } from '../../logging';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import { TestProvider } from '../types';
import { createErrorTestItem, ErrorTestItemOptions, getTestCaseNodes } from './common/testItemUtilities';
import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities';
import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types';
import { IPythonExecutionFactory } from '../../common/process/types';
import { ITestDebugLauncher } from '../common/types';
import { buildErrorNodeOptions } from './common/utils';

/**
* This class exposes a test-provider-agnostic way of discovering tests.
Expand Down Expand Up @@ -162,12 +162,3 @@ export class WorkspaceTestAdapter {
return Promise.resolve();
}
}

function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions {
const labelText = testType === 'pytest' ? 'Pytest Discovery Error' : 'Unittest Discovery Error';
return {
id: `DiscoveryError:${uri.fsPath}`,
label: `${labelText} [${path.basename(uri.fsPath)}]`,
error: message,
};
}
112 changes: 112 additions & 0 deletions src/test/mocks/vsc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { EventEmitter as NodeEventEmitter } from 'events';
import * as vscode from 'vscode';

// export * from './range';
// export * from './position';
// export * from './selection';
Expand Down Expand Up @@ -443,3 +444,114 @@ export enum LogLevel {
*/
Error = 5,
}

export class TestMessage {
/**
* Human-readable message text to display.
*/
message: string | MarkdownString;

/**
* Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown.
*/
expectedOutput?: string;

/**
* Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown.
*/
actualOutput?: string;

/**
* Associated file location.
*/
location?: vscode.Location;

/**
* Creates a new TestMessage that will present as a diff in the editor.
* @param message Message to display to the user.
* @param expected Expected output.
* @param actual Actual output.
*/
static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage {
const testMessage = new TestMessage(message);
testMessage.expectedOutput = expected;
testMessage.actualOutput = actual;
return testMessage;
}

/**
* Creates a new TestMessage instance.
* @param message The message to show to the user.
*/
constructor(message: string | MarkdownString) {
this.message = message;
}
}

export interface TestItemCollection extends Iterable<[string, vscode.TestItem]> {
/**
* Gets the number of items in the collection.
*/
readonly size: number;

/**
* Replaces the items stored by the collection.
* @param items Items to store.
*/
replace(items: readonly vscode.TestItem[]): void;

/**
* Iterate over each entry in this collection.
*
* @param callback Function to execute for each entry.
* @param thisArg The `this` context used when invoking the handler function.
*/
forEach(callback: (item: vscode.TestItem, collection: TestItemCollection) => unknown, thisArg?: unknown): void;

/**
* Adds the test item to the children. If an item with the same ID already
* exists, it'll be replaced.
* @param item Item to add.
*/
add(item: vscode.TestItem): void;

/**
* Removes a single test item from the collection.
* @param itemId Item ID to delete.
*/
delete(itemId: string): void;

/**
* Efficiently gets a test item by ID, if it exists, in the children.
* @param itemId Item ID to get.
* @returns The found item or undefined if it does not exist.
*/
get(itemId: string): vscode.TestItem | undefined;
}

/**
* Represents a location inside a resource, such as a line
* inside a text file.
*/
export class Location {
/**
* The resource identifier of this location.
*/
uri: vscode.Uri;

/**
* The document range of this location.
*/
range: vscode.Range;

/**
* Creates a new location object.
*
* @param uri The resource identifier.
* @param rangeOrPosition The range or position. Positions will be converted to an empty range.
*/
constructor(uri: vscode.Uri, rangeOrPosition: vscode.Range) {
this.uri = uri;
this.range = rangeOrPosition;
}
}
Loading

0 comments on commit efdf65e

Please sign in to comment.