Skip to content

Commit

Permalink
Add commands to run, debug and open tests (microsoft#4332)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored Feb 8, 2019
1 parent e8aa187 commit ede5b3d
Show file tree
Hide file tree
Showing 19 changed files with 409 additions and 46 deletions.
89 changes: 89 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,30 @@
"title": "%python.command.python.buildWorkspaceSymbols.title%",
"category": "Python"
},
{
"command": "python.openTestNodeInEditor",
"title": "Open",
"icon": {
"light": "resources/light/open-file.svg",
"dark": "resources/dark/open-file.svg"
}
},
{
"command": "python.runTestNode",
"title": "Run",
"icon": {
"light": "resources/light/start.svg",
"dark": "resources/dark/start.svg"
}
},
{
"command": "python.debugTestNode",
"title": "Debug",
"icon": {
"light": "resources/light/debug.svg",
"dark": "resources/dark/debug.svg"
}
},
{
"command": "python.runtests",
"title": "%python.command.python.runtests.title%",
Expand Down Expand Up @@ -439,6 +463,24 @@
}
],
"commandPalette": [
{
"command": "python.runTestNode",
"title": "Run",
"category": "Python",
"when": "config.noExists"
},
{
"command": "python.debugTestNode",
"title": "Debug",
"category": "Python",
"when": "config.noExists"
},
{
"command": "python.openTestNodeInEditor",
"title": "Open",
"category": "Python",
"when": "config.noExists"
},
{
"command": "python.datascience.runcurrentcell",
"title": "%python.command.python.datascience.runcurrentcell.title%",
Expand Down Expand Up @@ -540,6 +582,53 @@
"command": "python.runtests",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "python.openTestNodeInEditor",
"when": "view == python_tests && viewItem == testFunction",
"group": "inline"
},
{
"command": "python.debugTestNode",
"when": "view == python_tests && viewItem == testFunction",
"group": "inline"
},
{
"command": "python.runTestNode",
"when": "view == python_tests && viewItem == testFunction",
"group": "inline"
},
{
"command": "python.openTestNodeInEditor",
"when": "view == python_tests && viewItem == testFile",
"group": "inline"
},
{
"command": "python.debugTestNode",
"when": "view == python_tests && viewItem == testFile",
"group": "inline"
},
{
"command": "python.runTestNode",
"when": "view == python_tests && viewItem == testFile",
"group": "inline"
},
{
"command": "python.openTestNodeInEditor",
"when": "view == python_tests && viewItem == testSuite",
"group": "inline"
},
{
"command": "python.debugTestNode",
"when": "view == python_tests && viewItem == testSuite",
"group": "inline"
},
{
"command": "python.runTestNode",
"when": "view == python_tests && viewItem == testSuite",
"group": "inline"
}
]
},
"debuggers": [
Expand Down
4 changes: 2 additions & 2 deletions pythonFiles/symbolProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def getDataObject(self, node, namespace=""):
"name": node.name,
"range": {
"start": {
"line": node.lineno,
"line": node.lineno - 1,
"character": node.col_offset
},
"end": {
Expand All @@ -59,7 +59,7 @@ def getDataObject(self, node, namespace=""):

def getEndPosition(self, node):
if not hasattr(node, 'body') or len(node.body) == 0:
return (node.lineno, node.col_offset)
return (node.lineno - 1, node.col_offset)
return self.getEndPosition(node.body[-1])


Expand Down
7 changes: 7 additions & 0 deletions resources/dark/debug.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/dark/open-file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/dark/start.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions resources/light/debug.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/light/open-file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/light/start.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export namespace Commands {
export const navigateToTestFunction = 'navigateToTestFunction';
export const navigateToTestSuite = 'navigateToTestSuite';
export const navigateToTestFile = 'navigateToTestFile';
export const openTestNodeInEditor = 'python.openTestNodeInEditor';
export const runTestNode = 'python.runTestNode';
export const debugTestNode = 'python.debugTestNode';
}
export namespace Octicons {
export const Test_Pass = '$(check)';
Expand Down
3 changes: 2 additions & 1 deletion src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import { EditorLoadTelemetry } from './telemetry/types';
import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry';
import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types';
import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants';
import { ITestCodeNavigatorCommandHandler } from './unittests/navigation/types';
import { ITestCodeNavigatorCommandHandler, ITestExplorerCommandHandler } from './unittests/navigation/types';
import { registerTypes as unitTestsRegisterTypes } from './unittests/serviceRegistry';

durations.codeLoadingTime = stopWatch.elapsedTime;
Expand Down Expand Up @@ -293,6 +293,7 @@ function initializeServices(context: ExtensionContext, serviceManager: ServiceMa
serviceContainer.get<IInterpreterLocatorProgressService>(IInterpreterLocatorProgressService).register();
serviceContainer.get<IApplicationDiagnostics>(IApplicationDiagnostics).register();
serviceContainer.get<ITestCodeNavigatorCommandHandler>(ITestCodeNavigatorCommandHandler).register();
serviceContainer.get<ITestExplorerCommandHandler>(ITestExplorerCommandHandler).register();
}

// tslint:disable-next-line:no-any
Expand Down
2 changes: 1 addition & 1 deletion src/client/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export type TestRunTelemetry = {
tool: 'nosetest' | 'pytest' | 'unittest';
scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed';
debugging: boolean;
triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto';
triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto' | 'testExplorer';
failed: boolean;
};
export type TestDiscoverytTelemetry = {
Expand Down
3 changes: 2 additions & 1 deletion src/client/unittests/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export enum CommandSource {
auto = 'auto',
ui = 'ui',
codelens = 'codelens',
commandPalette = 'commandpalette'
commandPalette = 'commandpalette',
testExplorer = 'testExplorer'
}
export const TEST_OUTPUT_CHANNEL = 'TEST_OUTPUT_CHANNEL';
export const NOSETEST_PROVIDER: TestProvider = 'nosetest';
Expand Down
65 changes: 46 additions & 19 deletions src/client/unittests/common/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IUnitTestSettings, Product } from '../../common/types';
import { IServiceContainer } from '../../ioc/types';
import { CommandSource } from './constants';
import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor';
import { ITestsHelper, ITestVisitor, TestFile, TestFolder, TestFunction, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, TestSuite, UnitTestProduct } from './types';
import { ITestsHelper, ITestVisitor, TestFile, TestFolder, TestFunction, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, TestSuite, TestType, UnitTestProduct } from './types';

export async function selectTestWorkspace(appShell: IApplicationShell): Promise<Uri | undefined> {
if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) {
Expand Down Expand Up @@ -39,6 +39,51 @@ export class TestsHelper implements ITestsHelper {
this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell);
this.commandManager = serviceContainer.get<ICommandManager>(ICommandManager);
}
public static getTestType(test: TestFile | TestFolder | TestSuite | TestFunction): TestType {
if (TestsHelper.getTestFile(test)) {
return TestType.testFile;
}
if (TestsHelper.getTestFolder(test)) {
return TestType.testFolder;
}
if (TestsHelper.getTestSuite(test)) {
return TestType.testSuite;
}
if (TestsHelper.getTestFunction(test)) {
return TestType.testFunction;
}
throw new Error('Unknown test type');
}
public static getTestFile(test: TestFile | TestFolder | TestSuite | TestFunction): TestFile | undefined {
if (!test) {
return;
}
// Only TestFile has a `fullPath` property.
return typeof (test as TestFile).fullPath === 'string' ? test as TestFile : undefined;
}
public static getTestSuite(test: TestFile | TestFolder | TestSuite | TestFunction): TestSuite | undefined {
if (!test) {
return;
}
// Only TestSuite has a `suites` property.
return Array.isArray((test as TestSuite).suites) ? test as TestSuite : undefined;
}
public static getTestFolder(test: TestFile | TestFolder | TestSuite | TestFunction): TestFolder | undefined {
if (!test) {
return;
}
// Only TestFolder has a `folders` property.
return Array.isArray((test as TestFolder).folders) ? test as TestFolder : undefined;
}
public static getTestFunction(test: TestFile | TestFolder | TestSuite | TestFunction): TestFunction | undefined {
if (!test) {
return;
}
if (TestsHelper.getTestFile(test) || TestsHelper.getTestSuite(test) || TestsHelper.getTestSuite(test)) {
return;
}
return test as TestFunction;
}
public parseProviderName(product: UnitTestProduct): TestProvider {
switch (product) {
case Product.nosetest: return 'nosetest';
Expand Down Expand Up @@ -205,22 +250,4 @@ export class TestsHelper implements ITestsHelper {

return true;
}
public getTestFile(test: TestFile | TestFolder | TestSuite | TestFunction): TestFile | undefined {
// Only TestFile has a `fullPath` property.
return Array.isArray((test as TestFile).fullPath) ? test as TestFile : undefined;
}
public getTestSuite(test: TestFile | TestFolder | TestSuite | TestFunction): TestSuite | undefined {
// Only TestSuite has a `suites` property.
return Array.isArray((test as TestSuite).suites) ? test as TestSuite : undefined;
}
public getTestFolder(test: TestFile | TestFolder | TestSuite | TestFunction): TestFolder | undefined {
// Only TestFolder has a `folders` property.
return Array.isArray((test as TestFolder).folders) ? test as TestFolder : undefined;
}
public getTestFunction(test: TestFile | TestFolder | TestSuite | TestFunction): TestFunction | undefined {
if (this.getTestFile(test) || this.getTestSuite(test) || this.getTestSuite(test)) {
return;
}
return test as TestFunction;
}
}
5 changes: 5 additions & 0 deletions src/client/unittests/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ export interface ITestNavigatorHelper {
findSymbol(doc: TextDocument, predicate: SymbolSearch, token: CancellationToken): Promise<SymbolInformation | undefined>;
}
export type SymbolSearch = (item: SymbolInformation) => boolean;

export const ITestExplorerCommandHandler = Symbol('ITestExplorerCommandHandler');
export interface ITestExplorerCommandHandler extends IDisposable {
register(): void;
}
82 changes: 82 additions & 0 deletions src/client/unittests/providers/commandHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { ICommandManager } from '../../common/application/types';
import { Commands } from '../../common/constants';
import { traceDecorators } from '../../common/logger';
import { IDisposable } from '../../common/types';
import { swallowExceptions } from '../../common/utils/decorators';
import { CommandSource } from '../common/constants';
import { TestFile, TestFolder, TestFunction, TestsToRun, TestSuite, TestType } from '../common/types';
import { ITestExplorerCommandHandler } from '../navigation/types';
import { TestTreeItem } from './testTreeViewItem';

const testNavigationCommandMapping = {
[TestType.testFile]: Commands.navigateToTestFile,
[TestType.testFunction]: Commands.navigateToTestFunction,
[TestType.testSuite]: Commands.navigateToTestSuite
};

@injectable()
export class TestExplorerCommandHandler implements ITestExplorerCommandHandler {
private readonly disposables: IDisposable[] = [];
constructor(@inject(ICommandManager) private readonly cmdManager: ICommandManager) { }
public register(): void {
this.disposables.push(this.cmdManager.registerCommand(Commands.runTestNode, this.onRunTestNode, this));
this.disposables.push(this.cmdManager.registerCommand(Commands.debugTestNode, this.onDebugTestNode, this));
this.disposables.push(this.cmdManager.registerCommand(Commands.openTestNodeInEditor, this.onOpenTestNodeInEditor, this));
}
public dispose(): void {
this.disposables.forEach(item => item.dispose());
}
@swallowExceptions('Run test node')
@traceDecorators.error('Run test node failed')
protected async onRunTestNode(item: TestTreeItem): Promise<void> {
await this.runDebugTestNode(item, 'run');
}
@swallowExceptions('Debug test node')
@traceDecorators.error('Debug test node failed')
protected async onDebugTestNode(item: TestTreeItem): Promise<void> {
await this.runDebugTestNode(item, 'debug');
}
@swallowExceptions('Open test node in Editor')
@traceDecorators.error('Open test node in editor failed')
protected async onOpenTestNodeInEditor(item: TestTreeItem): Promise<void> {
const command = testNavigationCommandMapping[item.testType];
if (!command) {
throw new Error('Unknown Test Type');
}

this.cmdManager.executeCommand(command, item.resource, item.data);
}
protected async runDebugTestNode(item: TestTreeItem, runType: 'run' | 'debug'): Promise<void> {
let testToRun: TestsToRun;
switch (item.testType) {
case TestType.testFile: {
testToRun = { testFile: [item.data as TestFile] };
break;
}
case TestType.testFolder: {
testToRun = { testFolder: [item.data as TestFolder] };
break;
}
case TestType.testSuite: {
testToRun = { testSuite: [item.data as TestSuite] };
break;
}
case TestType.testFunction: {
testToRun = { testFunction: [item.data as TestFunction] };
break;
}
default:
throw new Error('Unknown Test Type');
}

const args = [undefined, CommandSource.testExplorer, item.resource, testToRun];
const cmd = runType === 'run' ? Commands.Tests_Run : Commands.Tests_Debug;
this.cmdManager.executeCommand(cmd, ...args);
}
}
Loading

0 comments on commit ede5b3d

Please sign in to comment.