Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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