Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi-workspace folders in test explorer #4709

Merged
merged 10 commits into from
Mar 12, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
DonJayamanne marked this conversation as resolved.
Show resolved Hide resolved
"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
1 change: 1 addition & 0 deletions src/client/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export enum EventName {
UNITTEST_NAVIGATE_TEST_FILE = 'UNITTEST.NAVIGATE.TEST_FILE',
UNITTEST_NAVIGATE_TEST_FUNCTION = 'UNITTEST.NAVIGATE.TEST_FUNCTION',
UNITTEST_NAVIGATE_TEST_SUITE = 'UNITTEST.NAVIGATE.TEST_SUITE',
UNITTEST_EXPLORER_WORK_SPACE_COUNT = 'UNITTEST.TEST_EXPLORER.WORK_SPACE_COUNT',
PYTHON_LANGUAGE_SERVER_ANALYSISTIME = 'PYTHON_LANGUAGE_SERVER.ANALYSIS_TIME',
PYTHON_LANGUAGE_SERVER_ENABLED = 'PYTHON_LANGUAGE_SERVER.ENABLED',
PYTHON_LANGUAGE_SERVER_EXTRACTED = 'PYTHON_LANGUAGE_SERVER.EXTRACTED',
Expand Down
1 change: 1 addition & 0 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,5 @@ interface IEventNamePropertyMapping {
[EventName.UNITTEST_NAVIGATE_TEST_FILE]: never | undefined;
[EventName.UNITTEST_NAVIGATE_TEST_FUNCTION]: { focus: boolean };
[EventName.UNITTEST_NAVIGATE_TEST_SUITE]: { focus: boolean };
[EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT]: { count: number };
}
14 changes: 0 additions & 14 deletions src/client/unittests/codeLenses/testFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,20 +203,6 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites,
if (functions.length === 0) {
return [];
}
if (functions.length === 0) {
return [
new CodeLens(range, {
title: constants.Text.CodeLensRunUnitTest,
command: constants.Commands.Tests_Run,
arguments: [undefined, CommandSource.codelens, file, <TestsToRun>{ testFunction: functions }]
}),
new CodeLens(range, {
title: constants.Text.CodeLensDebugUnitTest,
command: constants.Commands.Tests_Debug,
arguments: [undefined, CommandSource.codelens, file, <TestsToRun>{ testFunction: functions }]
})
];
}

// Find all flattened functions.
return [
Expand Down
16 changes: 9 additions & 7 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 @@ -145,7 +145,7 @@ export class TestsHelper implements ITestsHelper {
tests.testFolders = [];
const folderMap = new Map<string, TestFolder>();
folders.sort();

const resource = Uri.file(workspaceFolder);
folders.forEach(dir => {
dir.split(path.sep).reduce((parentPath, currentName, index, values) => {
let newPath = currentName;
Expand All @@ -155,7 +155,7 @@ export class TestsHelper implements ITestsHelper {
newPath = path.join(parentPath, currentName);
}
if (!folderMap.has(newPath)) {
const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 };
const testFolder: TestFolder = { resource, name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 };
folderMap.set(newPath, testFolder);
if (parentFolder) {
parentFolder!.folders.push(testFolder);
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 Expand Up @@ -285,7 +287,7 @@ export function getTestFunction(test: TestDataItem): TestFunction | undefined {
if (!test) {
return;
}
if (getTestFile(test) || getTestFolder(test) || getTestSuite(test)) {
if (test instanceof TestWorkspaceFolder || getTestFile(test) || getTestFolder(test) || getTestSuite(test)) {
return;
}
return test as TestFunction;
Expand Down Expand Up @@ -468,7 +470,7 @@ function copyResultsForFolders(source: TestFolder[], target: TestFolder[]): void
}
function copyResultsForFiles(source: TestFile[], target: TestFile[]): void {
source.forEach(sourceFile => {
const targetFile = target.find(file => file.name === sourceFile.name && file.nameToRun === sourceFile.nameToRun);
const targetFile = target.find(file => file.name === sourceFile.name);
if (!targetFile) {
return;
}
Expand Down
7 changes: 6 additions & 1 deletion src/client/unittests/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type TestRunOptions = {
export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string };

export type TestFolder = TestResult & {
resource: Uri;
name: string;
testFiles: TestFile[];
nameToRun: string;
Expand All @@ -42,9 +43,11 @@ export enum TestType {
testFile = 'testFile',
testFolder = 'testFolder',
testSuite = 'testSuite',
testFunction = 'testFunction'
testFunction = 'testFunction',
testWorkspaceFolder = 'testWorkspaceFolder'
DonJayamanne marked this conversation as resolved.
Show resolved Hide resolved
}
export type TestFile = TestResult & {
resource: Uri;
name: string;
fullPath: string;
functions: TestFunction[];
Expand All @@ -55,6 +58,7 @@ export type TestFile = TestResult & {
};

export type TestSuite = TestResult & {
resource: Uri;
name: string;
functions: TestFunction[];
suites: TestSuite[];
Expand All @@ -65,6 +69,7 @@ export type TestSuite = TestResult & {
};

export type TestFunction = TestResult & {
resource: Uri;
name: string;
nameToRun: string;
};
Expand Down
131 changes: 22 additions & 109 deletions src/client/unittests/explorer/testTreeViewItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,37 @@ import { Commands } from '../../common/constants';
import { getIcon } from '../../common/utils/icons';
import { noop } from '../../common/utils/misc';
import { Icons } from '../common/constants';
import { getTestFile, getTestFolder, getTestFunction, getTestSuite, getTestType } from '../common/testUtils';
import { TestFile, TestFolder, TestFunction, TestStatus, TestSuite, TestType } from '../common/types';
import { getTestType } from '../common/testUtils';
import { TestStatus, TestType, TestResult } from '../common/types';
import { TestDataItem } from '../types';

/**
* Base class for a TestTreeItem that represents a visual node on the
* Class that represents a visual node on the
* Test Explorer tree view. Is essentially a wrapper for the underlying
* TestDataItem.
*/
export abstract class TestTreeItem extends TreeItem {
export class TestTreeItem extends TreeItem {
public readonly testType: TestType;

constructor(
public readonly resource: Uri,
public readonly data: Readonly<TestDataItem>,
private readonly parentData: TestDataItem,
label: string,
collabsible: boolean = true
public readonly data: Readonly<TestDataItem>
) {
super(label, collabsible ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None);
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 @@ -58,41 +62,26 @@ export abstract 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(): 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 @@ -131,79 +120,3 @@ export abstract class TestTreeItem extends TreeItem {
}
}
}

class TestFunctionTreeItem extends TestTreeItem {
constructor(resource: Uri, parent: TestDataItem, fn: TestFunction) {
super(resource, fn, parent, fn.name, false);
}

public get contextValue(): string {
return TestType.testFunction;
}

/**
* Test functions have no subordinates.
*/
protected getChildrenImpl(): Readonly<TestTreeItem[]> {
return [];
}
}

class TestSuiteTreeItem extends TestTreeItem {
constructor(resource: Uri, parent: TestDataItem, suite: TestSuite) {
super(resource, suite, parent, suite.name);
}

public get contextValue(): string {
return TestType.testSuite;
}

}

class TestFileTreeItem extends TestTreeItem {
constructor(resource: Uri, parent: TestDataItem, fl: TestFile) {
super(resource, fl, parent, fl.name);
}

public get contextValue(): string {
return TestType.testFile;
}
}

class TestFolderTreeItem extends TestTreeItem {
constructor(resource: Uri, parent: TestDataItem, folder: TestFolder) {
super(resource, folder, parent, folder.name);
}

public get contextValue(): string {
return TestType.testFolder;
}
}

/**
* 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?: TestDataItem): TestTreeItem {
const testDataType = getTestType(testData);
switch (testDataType) {
case TestType.testFile: {
return new TestFileTreeItem(resource, parent!, getTestFile(testData)!);
}
case TestType.testFolder: {
return new TestFolderTreeItem(resource, parent!, getTestFolder(testData)!);
}
case TestType.testSuite: {
return new TestSuiteTreeItem(resource, parent!, getTestSuite(testData)!);
}
case TestType.testFunction: {
return new TestFunctionTreeItem(resource, parent!, getTestFunction(testData)!);
}
default: {
throw new Error(`Cannot create test view item for unknown test Data Type "${testDataType}". This item will not appear in the Test Explorer.`);
}
}
}
Loading