From 57f23c9aa016c812cb73f8f89b40b2c3b48c7ea6 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 11 May 2021 17:42:02 -0700 Subject: [PATCH] Add test view with a welcome button (#16215) * Add test view with a welcome button * Register registerTestController only when available. --- package.json | 16 +- src/client/testing/explorer/treeView.ts | 45 -- src/client/testing/main.ts | 6 + src/client/testing/serviceRegistry.ts | 2 - .../testing/testController/controller.ts | 18 + .../testing/explorer/treeView.unit.test.ts | 68 -- types/vscode.proposed.d.ts | 584 ++++++++++++++++++ 7 files changed, 615 insertions(+), 124 deletions(-) delete mode 100644 src/client/testing/explorer/treeView.ts create mode 100644 src/client/testing/testController/controller.ts delete mode 100644 src/test/testing/explorer/treeView.unit.test.ts diff --git a/package.json b/package.json index cb27ae93d6bd4..0eb308d7a45e6 100644 --- a/package.json +++ b/package.json @@ -2153,15 +2153,13 @@ } ] }, - "views": { - "test": [ - { - "id": "python_tests", - "name": "Python", - "when": "testsDiscovered" - } - ] - }, + "viewsWelcome": [ + { + "view": "testing", + "contents": "Configure a test framework to see your tests here.\n[Configure Python Tests](command:python.configureTests)", + "when": "!testsDiscovered" + } + ], "yamlValidation": [ { "fileMatch": ".condarc", diff --git a/src/client/testing/explorer/treeView.ts b/src/client/testing/explorer/treeView.ts deleted file mode 100644 index 3354de051b6fc..0000000000000 --- a/src/client/testing/explorer/treeView.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { TreeView } from 'vscode'; -import { IExtensionSingleActivationService } from '../../activation/types'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../common/types'; -import { ITestTreeViewProvider, TestDataItem } from '../common/types'; - -@injectable() -export class TreeViewService implements IExtensionSingleActivationService, IDisposable { - private _treeView!: TreeView; - private readonly disposables: IDisposable[] = []; - public get treeView(): TreeView { - return this._treeView; - } - constructor( - @inject(ITestTreeViewProvider) private readonly treeViewProvider: ITestTreeViewProvider, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - ) { - disposableRegistry.push(this); - } - public dispose() { - this.disposables.forEach((d) => d.dispose()); - } - public async activate(): Promise { - this._treeView = this.appShell.createTreeView('python_tests', { - showCollapseAll: true, - treeDataProvider: this.treeViewProvider, - }); - this.disposables.push(this._treeView); - this.disposables.push( - this.commandManager.registerCommand(Commands.Test_Reveal_Test_Item, this.onRevealTestItem, this), - ); - } - public async onRevealTestItem(testItem: TestDataItem): Promise { - await this.treeView.reveal(testItem, { select: false }); - } -} diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts index 4eb2bafc6c676..9d4723ad223e7 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -8,6 +8,7 @@ import { Event, EventEmitter, OutputChannel, + test, TextDocument, Uri, } from 'vscode'; @@ -43,6 +44,7 @@ import { } from './common/types'; import { TEST_OUTPUT_CHANNEL } from './constants'; import { ITestingService } from './types'; +import { PythonTestController } from './testController/controller'; @injectable() export class TestingService implements ITestingService { @@ -116,6 +118,10 @@ export class UnitTestManagementService implements ITestManagementService, Dispos this.autoDiscoverTests(undefined).catch((ex) => traceError('Failed to auto discover tests upon activation', ex), ); + + if (test && test.registerTestController) { + this.disposableRegistry.push(test.registerTestController(new PythonTestController())); + } } public async getTestManager( displayTestNotConfiguredMessage: boolean, diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index 4580de322f18c..28342b97b04c8 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -65,7 +65,6 @@ import { TestDisplay } from './display/picker'; import { TestExplorerCommandHandler } from './explorer/commandHandlers'; import { FailedTestHandler } from './explorer/failedTestHandler'; import { TestTreeViewProvider } from './explorer/testTreeViewProvider'; -import { TreeViewService } from './explorer/treeView'; import { TestingService, UnitTestManagementService } from './main'; import { registerTypes as registerNavigationTypes } from './navigation/serviceRegistry'; import { ITestExplorerCommandHandler } from './navigation/types'; @@ -148,7 +147,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ITestTreeViewProvider, TestTreeViewProvider); serviceManager.addSingleton(ITestDataItemResource, TestTreeViewProvider); serviceManager.addSingleton(ITestExplorerCommandHandler, TestExplorerCommandHandler); - serviceManager.addSingleton(IExtensionSingleActivationService, TreeViewService); serviceManager.addSingleton( IExtensionSingleActivationService, FailedTestHandler, diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts new file mode 100644 index 0000000000000..97f45a90a1a9e --- /dev/null +++ b/src/client/testing/testController/controller.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController } from 'vscode'; +/* eslint-disable */ + +// This is a place holder for a controller +export class PythonTestController implements TestController { + public createWorkspaceTestRoot() { + return undefined; + } + + public createDocumentTestRoot() { + return undefined; + } + + public runTests() {} +} diff --git a/src/test/testing/explorer/treeView.unit.test.ts b/src/test/testing/explorer/treeView.unit.test.ts deleted file mode 100644 index 8002eb6dc5f09..0000000000000 --- a/src/test/testing/explorer/treeView.unit.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { TreeView } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import { ITestTreeViewProvider, TestDataItem } from '../../../client/testing/common/types'; -import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; -import { TreeViewService } from '../../../client/testing/explorer/treeView'; - -suite('Unit Tests Test Explorer Tree View', () => { - let treeViewService: TreeViewService; - let treeView: typemoq.IMock>; - let commandManager: ICommandManager; - let appShell: IApplicationShell; - let treeViewProvider: ITestTreeViewProvider; - setup(() => { - commandManager = mock(CommandManager); - treeViewProvider = mock(TestTreeViewProvider); - appShell = mock(ApplicationShell); - treeView = typemoq.Mock.ofType>(); - treeViewService = new TreeViewService( - instance(treeViewProvider), - [], - instance(appShell), - instance(commandManager), - ); - }); - - test('Activation will create the treeview', async () => { - await treeViewService.activate(); - verify( - appShell.createTreeView( - 'python_tests', - deepEqual({ showCollapseAll: true, treeDataProvider: instance(treeViewProvider) }), - ), - ).once(); - }); - test('Activation will add command handlers', async () => { - await treeViewService.activate(); - verify( - commandManager.registerCommand( - Commands.Test_Reveal_Test_Item, - treeViewService.onRevealTestItem, - treeViewService, - ), - ).once(); - }); - test('Invoking the command handler will reveal the node in the tree', async () => { - const data = {} as any; - treeView - .setup((t) => t.reveal(typemoq.It.isAny(), { select: false })) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - when(appShell.createTreeView('python_tests', anything())).thenReturn(treeView.object); - - await treeViewService.activate(); - await treeViewService.onRevealTestItem(data); - - treeView.verifyAll(); - }); -}); diff --git a/types/vscode.proposed.d.ts b/types/vscode.proposed.d.ts index c81c63d8a2ca8..ef765c7203546 100644 --- a/types/vscode.proposed.d.ts +++ b/types/vscode.proposed.d.ts @@ -930,5 +930,589 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/107467 + export namespace test { + /** + * Registers a controller that can discover and + * run tests in workspaces and documents. + */ + export function registerTestController(testController: TestController): Disposable; + + /** + * Requests that tests be run by their controller. + * @param run Run options to use + * @param token Cancellation token for the test run + */ + export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; + + /** + * Returns an observer that retrieves tests in the given workspace folder. + * @stability experimental + */ + export function createWorkspaceTestObserver(workspaceFolder: WorkspaceFolder): TestObserver; + + /** + * Returns an observer that retrieves tests in the given text document. + * @stability experimental + */ + export function createDocumentTestObserver(document: TextDocument): TestObserver; + + /** + * Creates a {@link TestRun}. This should be called by the + * {@link TestRunner} when a request is made to execute tests, and may also + * be called if a test run is detected externally. Once created, tests + * that are included in the results will be moved into the + * {@link TestResultState.Pending} state. + * + * @param request Test run request. Only tests inside the `include` may be + * modified, and tests in its `exclude` are ignored. + * @param name The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + * @param persist Whether the results created by the run should be + * persisted in VS Code. This may be false if the results are coming from + * a file already saved externally, such as a coverage information file. + */ + export function createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; + + /** + * Creates a new managed {@link TestItem} instance. + * @param options Initial/required options for the item + * @param data Custom data to be stored in {@link TestItem.data} + */ + export function createTestItem(options: TestItemOptions, data: T): TestItem; + + /** + * Creates a new managed {@link TestItem} instance. + * @param options Initial/required options for the item + */ + export function createTestItem(options: TestItemOptions): TestItem; + + /** + * List of test results stored by VS Code, sorted in descnding + * order by their `completedAt` time. + * @stability experimental + */ + export const testResults: ReadonlyArray; + + /** + * Event that fires when the {@link testResults} array is updated. + * @stability experimental + */ + export const onDidChangeTestResults: Event; + } + + /** + * @stability experimental + */ + export interface TestObserver { + /** + * List of tests returned by test provider for files in the workspace. + */ + readonly tests: ReadonlyArray>; + + /** + * An event that fires when an existing test in the collection changes, or + * null if a top-level test was added or removed. When fired, the consumer + * should check the test item and all its children for changes. + */ + readonly onDidChangeTest: Event; + + /** + * An event that fires when all test providers have signalled that the tests + * the observer references have been discovered. Providers may continue to + * watch for changes and cause {@link onDidChangeTest} to fire as files + * change, until the observer is disposed. + * + * @todo as below + */ + readonly onDidDiscoverInitialTests: Event; + + /** + * Dispose of the observer, allowing VS Code to eventually tell test + * providers that they no longer need to update tests. + */ + dispose(): void; + } + + /** + * @stability experimental + */ + export interface TestsChangeEvent { + /** + * List of all tests that are newly added. + */ + readonly added: ReadonlyArray>; + + /** + * List of existing tests that have updated. + */ + readonly updated: ReadonlyArray>; + + /** + * List of existing tests that have been removed. + */ + readonly removed: ReadonlyArray>; + } + + /** + * Interface to discover and execute tests. + */ + export interface TestController { + /** + * Requests that tests be provided for the given workspace. This will + * be called when tests need to be enumerated for the workspace, such as + * when the user opens the test explorer. + * + * It's guaranteed that this method will not be called again while + * there is a previous uncancelled call for the given workspace folder. + * + * @param workspace The workspace in which to observe tests + * @param cancellationToken Token that signals the used asked to abort the test run. + * @returns the root test item for the workspace + */ + createWorkspaceTestRoot(workspace: WorkspaceFolder, token: CancellationToken): ProviderResult>; + + /** + * Requests that tests be provided for the given document. This will be + * called when tests need to be enumerated for a single open file, for + * instance by code lens UI. + * + * It's suggested that the provider listen to change events for the text + * document to provide information for tests that might not yet be + * saved. + * + * If the test system is not able to provide or estimate for tests on a + * per-file basis, this method may not be implemented. In that case, the + * editor will request and use the information from the workspace tree. + * + * @param document The document in which to observe tests + * @param cancellationToken Token that signals the used asked to abort the test run. + * @returns the root test item for the document + */ + createDocumentTestRoot?(document: TextDocument, token: CancellationToken): ProviderResult>; + + /** + * Starts a test run. When called, the controller should call + * {@link vscode.test.createTestRun}. All tasks associated with the + * run should be created before the function returns or the reutrned + * promise is resolved. + * + * @param options Options for this test run + * @param cancellationToken Token that signals the used asked to abort the test run. + */ + runTests(options: TestRunRequest, token: CancellationToken): Thenable | void; + } + + /** + * Options given to {@link test.runTests}. + */ + export interface TestRunRequest { + /** + * Array of specific tests to run. The controllers should run all of the + * given tests and all children of the given tests, excluding any tests + * that appear in {@link TestRunRequest.exclude}. + */ + tests: TestItem[]; + + /** + * An array of tests the user has marked as excluded in VS Code. May be + * omitted if no exclusions were requested. Test controllers should not run + * excluded tests or any children of excluded tests. + */ + exclude?: TestItem[]; + + /** + * Whether tests in this run should be debugged. + */ + debug: boolean; + } + + /** + * Options given to {@link TestController.runTests} + */ + export interface TestRun { + /** + * The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + */ + readonly name?: string; + + /** + * Updates the state of the test in the run. Calling with method with nodes + * outside the {@link TestRunRequest.tests} or in the + * {@link TestRunRequest.exclude} array will no-op. + * + * @param test The test to update + * @param state The state to assign to the test + * @param duration Optionally sets how long the test took to run + */ + setState(test: TestItem, state: TestResultState, duration?: number): void; + + /** + * Appends a message, such as an assertion error, to the test item. + * + * Calling with method with nodes outside the {@link TestRunRequest.tests} + * or in the {@link TestRunRequest.exclude} array will no-op. + * + * @param test The test to update + * @param state The state to assign to the test + * + */ + appendMessage(test: TestItem, message: TestMessage): void; + + /** + * Appends raw output from the test runner. On the user's request, the + * output will be displayed in a terminal. ANSI escape sequences, + * such as colors and text styles, are supported. + * + * @param output Output text to append + * @param associateTo Optionally, associate the given segment of output + */ + appendOutput(output: string): void; + + /** + * Signals that the end of the test run. Any tests whose states have not + * been updated will be moved into the {@link TestResultState.Unset} state. + */ + end(): void; + } + + /** + * Indicates the the activity state of the {@link TestItem}. + */ + export enum TestItemStatus { + /** + * All children of the test item, if any, have been discovered. + */ + Resolved = 1, + + /** + * The test item may have children who have not been discovered yet. + */ + Pending = 0, + } + + /** + * Options initially passed into `vscode.test.createTestItem` + */ + export interface TestItemOptions { + /** + * Unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This cannot change for the lifetime of the TestItem. + */ + id: string; + + /** + * URI this TestItem is associated with. May be a file or directory. + */ + uri?: Uri; + + /** + * Display name describing the test item. + */ + label: string; + } + + /** + * A test item is an item shown in the "test explorer" view. It encompasses + * both a suite and a test, since they have almost or identical capabilities. + */ + export interface TestItem { + /** + * Unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This must not change for the lifetime of the TestItem. + */ + readonly id: string; + + /** + * URI this TestItem is associated with. May be a file or directory. + */ + readonly uri?: Uri; + + /** + * A mapping of children by ID to the associated TestItem instances. + */ + readonly children: ReadonlyMap>; + + /** + * The parent of this item, if any. Assigned automatically when calling + * {@link TestItem.addChild}. + */ + readonly parent?: TestItem; + + /** + * Indicates the state of the test item's children. The editor will show + * TestItems in the `Pending` state and with a `resolveHandler` as being + * expandable, and will call the `resolveHandler` to request items. + * + * A TestItem in the `Resolved` state is assumed to have discovered and be + * watching for changes in its children if applicable. TestItems are in the + * `Resolved` state when initially created; if the editor should call + * the `resolveHandler` to discover children, set the state to `Pending` + * after creating the item. + */ + status: TestItemStatus; + + /** + * Display name describing the test case. + */ + label: string; + + /** + * Optional description that appears next to the label. + */ + description?: string; + + /** + * Location of the test item in its `uri`. This is only meaningful if the + * `uri` points to a file. + */ + range?: Range; + + /** + * May be set to an error associated with loading the test. Note that this + * is not a test result and should only be used to represent errors in + * discovery, such as syntax errors. + */ + error?: string | MarkdownString; + + /** + * Whether this test item can be run by providing it in the + * {@link TestRunRequest.tests} array. Defaults to `true`. + */ + runnable: boolean; + + /** + * Whether this test item can be debugged by providing it in the + * {@link TestRunRequest.tests} array. Defaults to `false`. + */ + debuggable: boolean; + + /** + * Custom extension data on the item. This data will never be serialized + * or shared outside the extenion who created the item. + */ + data: T; + + /** + * Marks the test as outdated. This can happen as a result of file changes, + * for example. In "auto run" mode, tests that are outdated will be + * automatically rerun after a short delay. Invoking this on a + * test with children will mark the entire subtree as outdated. + * + * Extensions should generally not override this method. + */ + invalidate(): void; + + /** + * A function provided by the extension that the editor may call to request + * children of the item, if the {@link TestItem.status} is `Pending`. + * + * When called, the item should discover tests and call {@link TestItem.addChild}. + * The items should set its {@link TestItem.status} to `Resolved` when + * discovery is finished. + * + * The item should continue watching for changes to the children and + * firing updates until the token is cancelled. The process of watching + * the tests may involve creating a file watcher, for example. After the + * token is cancelled and watching stops, the TestItem should set its + * {@link TestItem.status} back to `Pending`. + * + * The editor will only call this method when it's interested in refreshing + * the children of the item, and will not call it again while there's an + * existing, uncancelled discovery for an item. + * + * @param token Cancellation for the request. Cancellation will be + * requested if the test changes before the previous call completes. + */ + resolveHandler?: (token: CancellationToken) => void; + + /** + * Attaches a child, created from the {@link test.createTestItem} function, + * to this item. A `TestItem` may be a child of at most one other item. + */ + addChild(child: TestItem): void; + + /** + * Removes the test and its children from the tree. Any tokens passed to + * child `resolveHandler` methods will be cancelled. + */ + dispose(): void; + } + + /** + * Possible states of tests in a test run. + */ + export enum TestResultState { + // Initial state + Unset = 0, + // Test will be run, but is not currently running. + Queued = 1, + // Test is currently running + Running = 2, + // Test run has passed + Passed = 3, + // Test run has failed (on an assertion) + Failed = 4, + // Test run has been skipped + Skipped = 5, + // Test run failed for some other reason (compilation error, timeout, etc) + Errored = 6, + } + + /** + * Represents the severity of test messages. + */ + export enum TestMessageSeverity { + Error = 0, + Warning = 1, + Information = 2, + Hint = 3, + } + + /** + * Message associated with the test state. Can be linked to a specific + * source range -- useful for assertion failures, for example. + */ + export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; + + /** + * Message severity. Defaults to "Error". + */ + severity: TestMessageSeverity; + + /** + * Expected test output. If given with `actualOutput`, a diff view will be shown. + */ + expectedOutput?: string; + + /** + * Actual test output. If given with `expectedOutput`, a diff view will be shown. + */ + actualOutput?: string; + + /** + * Associated file location. + */ + location?: 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; + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString); + } + + /** + * TestResults can be provided to VS Code in {@link test.publishTestResult}, + * or read from it in {@link test.testResults}. + * + * The results contain a 'snapshot' of the tests at the point when the test + * run is complete. Therefore, information such as its {@link Range} may be + * out of date. If the test still exists in the workspace, consumers can use + * its `id` to correlate the result instance with the living test. + * + * @todo coverage and other info may eventually be provided here + */ + export interface TestRunResult { + /** + * Unix milliseconds timestamp at which the test run was completed. + */ + completedAt: number; + + /** + * Optional raw output from the test run. + */ + output?: string; + + /** + * List of test results. The items in this array are the items that + * were passed in the {@link test.runTests} method. + */ + results: ReadonlyArray>; + } + + /** + * A {@link TestItem}-like interface with an associated result, which appear + * or can be provided in {@link TestResult} interfaces. + */ + export interface TestResultSnapshot { + /** + * Unique identifier that matches that of the associated TestItem. + * This is used to correlate test results and tests in the document with + * those in the workspace (test explorer). + */ + readonly id: string; + + /** + * URI this TestItem is associated with. May be a file or file. + */ + readonly uri?: Uri; + + /** + * Display name describing the test case. + */ + readonly label: string; + + /** + * Optional description that appears next to the label. + */ + readonly description?: string; + + /** + * Location of the test item in its `uri`. This is only meaningful if the + * `uri` points to a file. + */ + readonly range?: Range; + + /** + * State of the test in each task. In the common case, a test will only + * be executed in a single task and the length of this array will be 1. + */ + readonly taskStates: ReadonlyArray; + + /** + * Optional list of nested tests for this item. + */ + readonly children: Readonly[]; + } + + export interface TestSnapshoptTaskState { + /** + * Current result of the test. + */ + readonly state: TestResultState; + + /** + * The number of milliseconds the test took to run. This is set once the + * `state` is `Passed`, `Failed`, or `Errored`. + */ + readonly duration?: number; + + /** + * Associated test run message. Can, for example, contain assertion + * failure information if the test fails. + */ + readonly messages: ReadonlyArray; + } + + //#endregion } //#endregion