Skip to content

Commit

Permalink
Support multi-root folders in test explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne committed Mar 11, 2019
1 parent ef74f44 commit d38cf5b
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 190 deletions.
1 change: 1 addition & 0 deletions news/1 Enhancements/4268.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support mult-root workspaces in test explorer.
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,21 @@
],
"view/item/context": [
{
"command": "python.runtests",
"when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests",
"group": "inline@0"
},
{
"command": "python.debugtests",
"when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests",
"group": "inline@1"
},
{
"command": "python.discoverTests",
"when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests",
"group": "inline@2"
},
{
"command": "python.openTestNodeInEditor",
"when": "view == python_tests && viewItem == testFunction",
"group": "inline@2"
Expand Down
8 changes: 5 additions & 3 deletions src/client/unittests/common/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IApplicationShell, ICommandManager } from '../../common/application/typ
import * as constants from '../../common/constants';
import { IUnitTestSettings, Product } from '../../common/types';
import { IServiceContainer } from '../../ioc/types';
import { TestDataItem } from '../types';
import { TestDataItem, TestWorkspaceFolder } from '../types';
import { CommandSource } from './constants';
import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor';
import {
Expand Down Expand Up @@ -198,8 +198,7 @@ export class TestsHelper implements ITestsHelper {
}

// Just return this as a test file.
// tslint:disable-next-line:no-object-literal-type-assertion
return <TestsToRun>{ testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] };
return { testFile: [{ resource: Uri.file(rootDirectory), name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] };
}
public displayTestErrorMessage(message: string) {
this.appShell.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => {
Expand Down Expand Up @@ -246,6 +245,9 @@ export class TestsHelper implements ITestsHelper {
}

export function getTestType(test: TestDataItem): TestType {
if (test instanceof TestWorkspaceFolder) {
return TestType.testWorkspaceFolder;
}
if (getTestFile(test)) {
return TestType.testFile;
}
Expand Down
3 changes: 2 additions & 1 deletion src/client/unittests/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export enum TestType {
testFile = 'testFile',
testFolder = 'testFolder',
testSuite = 'testSuite',
testFunction = 'testFunction'
testFunction = 'testFunction',
testWorkspaceFolder = 'testWorkspaceFolder'
}
export type TestFile = TestResult & {
resource: Uri;
Expand Down
79 changes: 16 additions & 63 deletions src/client/unittests/explorer/testTreeViewItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,15 @@

// tslint:disable:max-classes-per-file

import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, WorkspaceFolder } from 'vscode';
import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands } from '../../common/constants';
import { getIcon } from '../../common/utils/icons';
import { noop } from '../../common/utils/misc';
import { Icons } from '../common/constants';
import { getTestType } from '../common/testUtils';
import { TestFile, TestStatus, TestType } from '../common/types';
import { TestStatus, TestType, TestResult } from '../common/types';
import { TestDataItem } from '../types';

export class TestWorkspaceFolder {
constructor(public readonly workspaceFolder: WorkspaceFolder) { }
public get resource(): Uri {
return this.workspaceFolder.uri;
}
}

export class TestWorkspaceFolderTreeItem extends TreeItem {
constructor(
public readonly resource: Uri,
public readonly data: Readonly<TestWorkspaceFolder>,
label: string
) {
super(label, TreeItemCollapsibleState.Collapsed);
}
public get contextValue(): string {
return 'workspaceFolder';
}
public get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon {
return ThemeIcon.Folder;
}
}

/**
* Class that represents a visual node on the
* Test Explorer tree view. Is essentially a wrapper for the underlying
Expand All @@ -47,21 +24,23 @@ export class TestTreeItem extends TreeItem {

constructor(
public readonly resource: Uri,
public readonly data: Readonly<TestDataItem>,
private readonly parentData: TestWorkspaceFolder | TestDataItem
public readonly data: Readonly<TestDataItem>
) {
super(data.name, TreeItemCollapsibleState.Collapsed);
super(data.name, TestTreeItem.getCollapsibleState(data));
this.testType = getTestType(this.data);
this.setCommand();
if (this.testType === TestType.testFile) {
this.resourceUri = Uri.file((this.data as TestFile).fullPath);
}
}
private static getCollapsibleState(data: TestDataItem): TreeItemCollapsibleState {
return getTestType(data) === TestType.testFunction ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed;
}
public get contextValue(): string {
return this.testType;
}

public get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon {
if (this.testType === TestType.testWorkspaceFolder) {
return ThemeIcon.Folder;
}
if (!this.data) {
return '';
}
Expand All @@ -83,41 +62,26 @@ export class TestTreeItem extends TreeItem {
return getIcon(Icons.unknown);
}
default: {
switch (this.testType) {
case TestType.testFile: {
return ThemeIcon.File;
}
case TestType.testFolder: {
return ThemeIcon.Folder;
}
default: {
return getIcon(Icons.unknown);
}
}
return getIcon(Icons.unknown);
}
}
}
/**
* Parent is an extension to the TreeItem, to make it trivial to discover the node's parent.
*/
public get parent(): TestWorkspaceFolder | TestDataItem {
return this.parentData;
}

public get tooltip(): string {
if (!this.data) {
if (!this.data || this.testType === TestType.testWorkspaceFolder) {
return '';
}
const result = this.data as TestResult;
if (this.testType !== TestType.testFunction) {
return `${this.data.functionsFailed} failed, ${this.data.functionsPassed} passed in ${this.data.time} seconds`;
return `${result.functionsFailed} failed, ${result.functionsPassed} passed in ${result.time} seconds`;
}
switch (this.data.status) {
case TestStatus.Error:
case TestStatus.Fail: {
return `Failed in ${this.data.time} seconds`;
return `Failed in ${result.time} seconds`;
}
case TestStatus.Pass: {
return `Passed in ${this.data.time} seconds`;
return `Passed in ${result.time} seconds`;
}
case TestStatus.Discovering:
case TestStatus.Running: {
Expand Down Expand Up @@ -156,14 +120,3 @@ export class TestTreeItem extends TreeItem {
}
}
}

/**
* Create a TreView node from a given TestDataItem without having to specify the exact test item type.
*
* @param resource The workspace resource that this test item exists within.
* @param testData The data item being represented in this tree view node.
* @param parent The parent (or undefined, if the item is a root folder) of the test item.
*/
export function createTreeViewItemFrom(resource: Uri, testData: Readonly<TestDataItem>, parent?: TestWorkspaceFolder | TestDataItem): TreeItem {
return new TestTreeItem(resource, testData, parent!);
}
59 changes: 31 additions & 28 deletions src/client/unittests/explorer/testTreeViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { IWorkspaceService } from '../../common/application/types';
import { IDisposable, IDisposableRegistry } from '../../common/types';
import { getChildren, getParent } from '../common/testUtils';
import { ITestCollectionStorageService, TestStatus } from '../common/types';
import { ITestDataItemResource, ITestTreeViewProvider, IUnitTestManagementService, TestDataItem, WorkspaceTestStatus } from '../types';
import { createTreeViewItemFrom, TestWorkspaceFolder, TestWorkspaceFolderTreeItem } from './testTreeViewItem';
import { ITestDataItemResource, ITestTreeViewProvider, IUnitTestManagementService, TestDataItem, TestWorkspaceFolder, WorkspaceTestStatus } from '../types';
import { TestTreeItem } from './testTreeViewItem';

@injectable()
export class TestTreeViewProvider implements ITestTreeViewProvider<TestWorkspaceFolder | TestDataItem>, ITestDataItemResource, IDisposable {
public readonly onDidChangeTreeData: Event<TestWorkspaceFolder | TestDataItem | undefined>;
export class TestTreeViewProvider implements ITestTreeViewProvider, ITestDataItemResource, IDisposable {
public readonly onDidChangeTreeData: Event<TestDataItem | undefined>;
public readonly testsAreBeingDiscovered: Map<string, boolean>;

private _onDidChangeTreeData = new EventEmitter<TestWorkspaceFolder | TestDataItem | undefined>();
private _onDidChangeTreeData = new EventEmitter<TestDataItem | undefined>();
private disposables: IDisposable[] = [];

constructor(
Expand Down Expand Up @@ -46,7 +46,7 @@ export class TestTreeViewProvider implements ITestTreeViewProvider<TestWorkspace
* @param testData Test data item to map to a Uri
* @returns A Uri representing the workspace that the test data item exists within
*/
public getResource(testData: Readonly<TestWorkspaceFolder | TestDataItem>): Uri {
public getResource(testData: Readonly<TestDataItem>): Uri {
return testData.resource;
}

Expand All @@ -65,12 +65,8 @@ export class TestTreeViewProvider implements ITestTreeViewProvider<TestWorkspace
* @param element The element for which [TreeItem](#TreeItem) representation is asked for.
* @return [TreeItem](#TreeItem) representation of the element
*/
public async getTreeItem(element: TestWorkspaceFolder | TestDataItem): Promise<TreeItem> {
if (element instanceof TestWorkspaceFolder) {
return new TestWorkspaceFolderTreeItem(element.resource, element, element.workspaceFolder.name);
}
const parent = await this.getParent!(element);
return createTreeViewItemFrom(element.resource, element, parent);
public async getTreeItem(element: TestDataItem): Promise<TreeItem> {
return new TestTreeItem(element.resource, element);
}

/**
Expand All @@ -79,23 +75,30 @@ export class TestTreeViewProvider implements ITestTreeViewProvider<TestWorkspace
* @param element The element from which the provider gets children. Can be `undefined`.
* @return Children of `element` or root if no element is passed.
*/
public getChildren(element?: TestWorkspaceFolder | TestDataItem): TestWorkspaceFolder[] | TestDataItem[] {
if (!element && Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0) {
return this.workspace.workspaceFolders
.filter(workspaceFolder => this.testStore.getTests(workspaceFolder.uri))
.map(workspaceFolder => new TestWorkspaceFolder(workspaceFolder));
}
if (element instanceof TestWorkspaceFolder) {
const tests = this.testStore.getTests(element.workspaceFolder.uri);
return tests ? tests.rootTestFolders : [];
} else {
const tests = this.testStore.getTests(element.resource);
if (element === undefined) {
return tests && tests.testFolders ? tests.rootTestFolders : [];
public getChildren(element?: TestDataItem): TestDataItem[] {
if (element) {
if (element instanceof TestWorkspaceFolder) {
const tests = this.testStore.getTests(element.workspaceFolder.uri);
return tests ? tests.rootTestFolders : [];
}
return getChildren(element!);
}

return getChildren(element);
if (!Array.isArray(this.workspace.workspaceFolders) || this.workspace.workspaceFolders.length === 0) {
return [];
}

// If we are in a single workspace
if (this.workspace.workspaceFolders.length === 1) {
const tests = this.testStore.getTests(this.workspace.workspaceFolders[0].uri);
return tests ? tests.rootTestFolders : [];
}

// If we are in a mult-root workspace, then nest the test data within a
// virtual node, represending the workspace folder.
return this.workspace.workspaceFolders
.filter(workspaceFolder => this.testStore.getTests(workspaceFolder.uri))
.map(workspaceFolder => new TestWorkspaceFolder(workspaceFolder));
}

/**
Expand All @@ -107,12 +110,12 @@ export class TestTreeViewProvider implements ITestTreeViewProvider<TestWorkspace
* @param element The element for which the parent has to be returned.
* @return Parent of `element`.
*/
public async getParent?(element: TestWorkspaceFolder | TestDataItem): Promise<TestWorkspaceFolder | TestDataItem> {
public async getParent(element: TestDataItem): Promise<TestDataItem | undefined> {
if (element instanceof TestWorkspaceFolder) {
return;
}
const tests = this.testStore.getTests(element.resource);
return tests ? getParent(tests, element)! : undefined;
return tests ? getParent(tests, element) : undefined;
}

/**
Expand Down
25 changes: 20 additions & 5 deletions src/client/unittests/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ import {
import {
ITestDisplay, ITestResultDisplay, ITestTreeViewProvider,
IUnitTestConfigurationService, IUnitTestManagementService,
TestWorkspaceFolder,
WorkspaceTestStatus
} from './types';

// tslint:disable:no-any

@injectable()
export class UnitTestManagementService implements IUnitTestManagementService, Disposable {
private readonly outputChannel: OutputChannel;
Expand Down Expand Up @@ -78,8 +81,7 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
this.registerHandlers();
this.registerCommands();

// tslint:disable-next-line:no-any
const testViewProvider = this.serviceContainer.get<ITestTreeViewProvider<any>>(ITestTreeViewProvider);
const testViewProvider = this.serviceContainer.get<ITestTreeViewProvider>(ITestTreeViewProvider);
const disposable = window.registerTreeDataProvider('python_tests', testViewProvider);
disposablesRegistry.push(disposable);

Expand Down Expand Up @@ -354,7 +356,10 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);

const disposables = [
commandManager.registerCommand(constants.Commands.Tests_Discover, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => {
commandManager.registerCommand(constants.Commands.Tests_Discover, (treeNode: TestWorkspaceFolder | any, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => {
if (treeNode && treeNode instanceof TestWorkspaceFolder) {
resource = treeNode.resource;
}
// Ignore the exceptions returned.
// This command will be invoked from other places of the extension.
this.discoverTests(cmdSource, resource, true, true, false, true)
Expand All @@ -367,8 +372,18 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
.ignoreErrors();
}),
commandManager.registerCommand(constants.Commands.Tests_Run_Failed, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.runTestsImpl(cmdSource, resource, undefined, true)),
commandManager.registerCommand(constants.Commands.Tests_Run, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun)),
commandManager.registerCommand(constants.Commands.Tests_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun, false, true)),
commandManager.registerCommand(constants.Commands.Tests_Run, (treeNode: TestWorkspaceFolder | any, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri, testToRun?: TestsToRun) => {
if (treeNode && treeNode instanceof TestWorkspaceFolder) {
resource = treeNode.resource;
}
return this.runTestsImpl(cmdSource, resource, testToRun);
}),
commandManager.registerCommand(constants.Commands.Tests_Debug, (treeNode: TestWorkspaceFolder | any, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri, testToRun: TestsToRun) => {
if (treeNode && treeNode instanceof TestWorkspaceFolder) {
resource = treeNode.resource;
}
return this.runTestsImpl(cmdSource, resource, testToRun, false, true);
}),
commandManager.registerCommand(constants.Commands.Tests_View_UI, () => this.displayUI(CommandSource.commandPalette)),
commandManager.registerCommand(constants.Commands.Tests_Picker_UI, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions)),
commandManager.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions, true)),
Expand Down
3 changes: 1 addition & 2 deletions src/client/unittests/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ export function registerTypes(serviceManager: IServiceManager) {

serviceManager.addSingleton<IUnitTestDiagnosticService>(IUnitTestDiagnosticService, UnitTestDiagnosticService);
serviceManager.addSingleton<ITestMessageService>(ITestMessageService, TestMessageService, PYTEST_PROVIDER);
// tslint:disable-next-line:no-any
serviceManager.addSingleton<ITestTreeViewProvider<any>>(ITestTreeViewProvider, TestTreeViewProvider);
serviceManager.addSingleton<ITestTreeViewProvider>(ITestTreeViewProvider, TestTreeViewProvider);
serviceManager.addSingleton<ITestDataItemResource>(ITestDataItemResource, TestTreeViewProvider);
serviceManager.addSingleton<ITestExplorerCommandHandler>(ITestExplorerCommandHandler, TestExplorerCommandHandler);

Expand Down
Loading

0 comments on commit d38cf5b

Please sign in to comment.