diff --git a/package.json b/package.json index 8d885b972b57..d10891bd112d 100644 --- a/package.json +++ b/package.json @@ -528,6 +528,12 @@ "category": "Python", "when": "python.datascience.haveinteractive && python.datascience.featureenabled" } + ], + "view/title": [ + { + "command": "python.runtests", + "group": "navigation" + } ] }, "debuggers": [ @@ -1867,7 +1873,15 @@ "fileMatch": "meta.yaml", "url": "./schemas/conda-meta.json" } - ] + ], + "views": { + "test": [ + { + "id": "python_tests", + "name": "PYTHON" + } + ] + } }, "scripts": { "package": "gulp clean && gulp prePublishBundle && vsce package", diff --git a/src/client/extension.ts b/src/client/extension.ts index a79a3d07c9b3..d06c27c7a3df 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -8,7 +8,7 @@ if ((Reflect as any).metadata === undefined) { } // Initialize source maps (this must never be moved up nor further down). -import {initialize } from './sourceMapSupport'; +import { initialize } from './sourceMapSupport'; initialize(require('vscode')); const durations: Record = {}; @@ -93,6 +93,7 @@ import { ReplProvider } from './providers/replProvider'; import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; import { TerminalProvider } from './providers/terminalProvider'; +import { PythonTestTreeViewProvider } from './providers/testTreeViewProvider'; import { ISortImportsEditingProvider } from './providers/types'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; import { sendTelemetryEvent } from './telemetry'; @@ -206,6 +207,8 @@ async function activateUnsafe(context: ExtensionContext): Promise context.subscriptions.push(languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] })); + context.subscriptions.push(window.registerTreeDataProvider('python_tests', new PythonTestTreeViewProvider())); + serviceContainer.getAll(IDebugConfigurationService).forEach(debugConfigProvider => { context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); }); diff --git a/src/client/providers/testTreeViewItem.ts b/src/client/providers/testTreeViewItem.ts new file mode 100644 index 000000000000..9afcd51f567c --- /dev/null +++ b/src/client/providers/testTreeViewItem.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TestStatus } from '../unittests/common/types'; + +export enum PythonTestTreeItemType { + Root = 'Root', + Package = 'Package', + File = 'File', + Suite = 'Suite', + Function = 'Function' +} + +export class PythonTestTreeItem extends TreeItem { + + constructor( + private kind: PythonTestTreeItemType, + private myParent: PythonTestTreeItem, + private myChildren: PythonTestTreeItem[], + private runId: string, + private name: string, + private testStatus: TestStatus = TestStatus.Unknown) { + + super(`[${kind}] ${name}`, kind === PythonTestTreeItemType.Function ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed); + } + + public get children(): PythonTestTreeItem[] { + return this.myChildren; + + } + + public get parent(): PythonTestTreeItem { + return this.myParent; + } +} diff --git a/src/client/providers/testTreeViewProvider.ts b/src/client/providers/testTreeViewProvider.ts new file mode 100644 index 000000000000..3d07ea578287 --- /dev/null +++ b/src/client/providers/testTreeViewProvider.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { + Event, EventEmitter, + ProviderResult, TreeDataProvider +} from 'vscode'; +import { + TestStatus +} from '../unittests/common/types'; +import { + PythonTestTreeItem, + PythonTestTreeItemType +} from './testTreeViewItem'; + +export class PythonTestTreeViewProvider implements TreeDataProvider { + /** + * This will trigger the view to update the changed element/root and its children recursively (if shown). + * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. + */ + public readonly onDidChangeTreeData: Event; + + private _onDidChangeTreeData: EventEmitter = new EventEmitter(); + private root: PythonTestTreeItem[]; + + constructor() { + this.onDidChangeTreeData = this._onDidChangeTreeData.event; + // set up some dummy data to just show that the test explorer loads. + this.root = this.getTestTree(); + } + + /** + * Get [TreeItem](#TreeItem) representation of the `element` + * + * @param element The element for which [TreeItem](#TreeItem) representation is asked for. + * @return [TreeItem](#TreeItem) representation of the element + */ + public async getTreeItem(element: PythonTestTreeItem): Promise { + return element; + } + + /** + * Get the children of `element` or root if no element is passed. + * + * @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?: PythonTestTreeItem): ProviderResult { + if (element === undefined) { + return this.root; + } + return element.children; + } + + /** + * Optional method to return the parent of `element`. + * Return `null` or `undefined` if `element` is a child of root. + * + * **NOTE:** This method should be implemented in order to access [reveal](#TreeView.reveal) API. + * + * @param element The element for which the parent has to be returned. + * @return Parent of `element`. + */ + public getParent?(element: PythonTestTreeItem): ProviderResult { + return element.parent; + } + + private getTestTree(): PythonTestTreeItem[] { + // create a sample tree just to get the feature up and running + const roots: PythonTestTreeItem[] = []; + const root1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Root, undefined, [], '/test', '/test'); + roots.push(root1); + + const root1_pkg1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Package, root1, [], '/test/module1', 'module1'); + root1.children.push(root1_pkg1); + + const root1_pkg1_file1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.File, root1_pkg1, [], '/test/module1/test_file1.py', 'test_file1.py'); + root1_pkg1.children.push(root1_pkg1_file1); + + const root1_pkg1_file1_fn1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Function, root1_pkg1_file1, undefined, '/test/module1/test_file1.py::test_function_1', 'test_function_1'); + root1_pkg1_file1.children.push(root1_pkg1_file1_fn1); + + const root1_pkg1_file1_fn2: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Function, root1_pkg1_file1, undefined, '/test/module1/test_file1.py::test_function_2', 'test_function_2'); + root1_pkg1_file1.children.push(root1_pkg1_file1_fn2); + + const root1_pkg1_file1_suite1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Suite, root1_pkg1_file1, [], '/test/module1/test_file1.py::TestSuite1', 'TestSuite1'); + root1_pkg1_file1.children.push(root1_pkg1_file1_suite1); + + const root1_pkg1_file1_suite1_fn1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Function, root1_pkg1_file1_suite1, undefined, '/test/module1/test_file1.py::TestSuite1::test_suite1_fn1', 'test_suite1_fn1'); + root1_pkg1_file1_suite1.children.push(root1_pkg1_file1_suite1_fn1); + + const root1_pkg1_file1_suite1_fn2: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Function, root1_pkg1_file1_suite1, undefined, '/test/module1/test_file1.py::TestSuite1::test_suite1_fn2', 'test_suite1_fn2'); + root1_pkg1_file1_suite1.children.push(root1_pkg1_file1_suite1_fn2); + + const root1_pkg1_file1_suite2: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Suite, root1_pkg1_file1, [], '/test/module1/test_file1.py::TestSuite2', 'TestSuite2'); + root1_pkg1_file1.children.push(root1_pkg1_file1_suite2); + + const root1_pkg1_file1_suite2_fn1: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Function, root1_pkg1_file1_suite2, undefined, '/test/module1/test_file1.py::TestSuite2::test_suite2_fn1', 'test_suite2_fn1'); + root1_pkg1_file1_suite2.children.push(root1_pkg1_file1_suite2_fn1); + + const root1_pkg1_file1_suite2_fn2: PythonTestTreeItem = new PythonTestTreeItem(PythonTestTreeItemType.Function, root1_pkg1_file1_suite2, undefined, '/test/module1/test_file1.py::TestSuite2::test_suite2_fn2', 'test_suite2_fn2'); + root1_pkg1_file1_suite2.children.push(root1_pkg1_file1_suite2_fn2); + + return roots; + } +}