From cfa6f216e555cc440db95bcc208ab7868bb1796a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 22 May 2018 22:57:29 +0200 Subject: [PATCH] Refactor Python Unit Test functionality to improve unit testing (#1713) Fixes #1068 --- news/3 Code Health/1068.md | 1 + src/client/activation/classic.ts | 11 +- .../common/managers/baseTestManager.ts | 38 +- .../managers/testConfigurationManager.ts | 22 +- .../common/services/configSettingService.ts | 56 +- src/client/unittests/common/testUtils.ts | 28 +- src/client/unittests/common/types.ts | 9 +- src/client/unittests/configuration.ts | 232 +++----- src/client/unittests/configurationFactory.ts | 35 ++ src/client/unittests/display/main.ts | 75 +-- src/client/unittests/display/picker.ts | 51 +- src/client/unittests/main.ts | 537 +++++++++--------- .../nosetest/testConfigurationManager.ts | 35 +- .../pytest/testConfigurationManager.ts | 41 +- src/client/unittests/serviceRegistry.ts | 20 +- src/client/unittests/types.ts | 69 +++ .../unittest/testConfigurationManager.ts | 15 +- src/test/common.ts | 8 +- src/test/common/installer.test.ts | 3 +- src/test/core.ts | 10 + .../testConfigurationManager.unit.test.ts | 18 +- .../configSettingService.unit.test.ts | 196 +++++++ src/test/unittests/configuration.unit.test.ts | 340 +++++++++++ .../configurationFactory.unit.test.ts | 47 ++ src/test/unittests/display/main.test.ts | 366 ++++++++++++ src/test/vscode-mock.ts | 25 +- 26 files changed, 1697 insertions(+), 591 deletions(-) create mode 100644 news/3 Code Health/1068.md create mode 100644 src/client/unittests/configurationFactory.ts create mode 100644 src/client/unittests/types.ts create mode 100644 src/test/core.ts create mode 100644 src/test/unittests/common/services/configSettingService.unit.test.ts create mode 100644 src/test/unittests/configuration.unit.test.ts create mode 100644 src/test/unittests/configurationFactory.unit.test.ts create mode 100644 src/test/unittests/display/main.test.ts diff --git a/news/3 Code Health/1068.md b/news/3 Code Health/1068.md new file mode 100644 index 000000000000..82f60782ccd9 --- /dev/null +++ b/news/3 Code Health/1068.md @@ -0,0 +1 @@ +Refactor unit testing functionality to improve testability of individual components. diff --git a/src/client/activation/classic.ts b/src/client/activation/classic.ts index 0574765f6336..76a25a426415 100644 --- a/src/client/activation/classic.ts +++ b/src/client/activation/classic.ts @@ -3,7 +3,7 @@ import { DocumentFilter, ExtensionContext, languages, OutputChannel } from 'vscode'; import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { IOutputChannel, IPythonSettings } from '../common/types'; +import { ILogger, IOutputChannel, IPythonSettings } from '../common/types'; import { IShebangCodeLensProvider } from '../interpreter/contracts'; import { IServiceManager } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; @@ -16,8 +16,7 @@ import { PythonRenameProvider } from '../providers/renameProvider'; import { PythonSignatureProvider } from '../providers/signatureProvider'; import { activateSimplePythonRefactorProvider } from '../providers/simpleRefactorProvider'; import { PythonSymbolProvider } from '../providers/symbolProvider'; -import { TEST_OUTPUT_CHANNEL } from '../unittests/common/constants'; -import * as tests from '../unittests/main'; +import { IUnitTestManagementService } from '../unittests/types'; import { IExtensionActivator } from './types'; export class ClassicExtensionActivator implements IExtensionActivator { @@ -49,8 +48,10 @@ export class ClassicExtensionActivator implements IExtensionActivator { context.subscriptions.push(languages.registerSignatureHelpProvider(this.documentSelector, new PythonSignatureProvider(jediFactory), '(', ',')); } - const unitTestOutChannel = this.serviceManager.get(IOutputChannel, TEST_OUTPUT_CHANNEL); - tests.activate(context, unitTestOutChannel, symbolProvider, this.serviceManager); + const testManagementService = this.serviceManager.get(IUnitTestManagementService); + testManagementService.activate() + .then(() => testManagementService.activateCodeLenses(symbolProvider)) + .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); return true; } diff --git a/src/client/unittests/common/managers/baseTestManager.ts b/src/client/unittests/common/managers/baseTestManager.ts index 266d269148c9..437044aa94e3 100644 --- a/src/client/unittests/common/managers/baseTestManager.ts +++ b/src/client/unittests/common/managers/baseTestManager.ts @@ -1,17 +1,13 @@ -import * as vscode from 'vscode'; -import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; +import { CancellationToken, CancellationTokenSource, Disposable, OutputChannel, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../../common/configSettings'; import { isNotInstalledError } from '../../../common/helpers'; -import { IPythonSettings } from '../../../common/types'; -import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../../../common/types'; +import { IDisposableRegistry, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry/index'; import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../../telemetry/types'; import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './../constants'; -import { displayTestErrorMessage } from './../testUtils'; -import { ITestCollectionStorageService, ITestDiscoveryService, ITestManager, ITestResultsService } from './../types'; -import { TestDiscoveryOptions, TestProvider, Tests, TestStatus, TestsToRun } from './../types'; +import { ITestCollectionStorageService, ITestDiscoveryService, ITestManager, ITestResultsService, ITestsHelper, TestDiscoveryOptions, TestProvider, Tests, TestStatus, TestsToRun } from './../types'; enum CancellationTokenType { testDiscovery, @@ -33,9 +29,9 @@ export abstract class BaseTestManager implements ITestManager { private tests?: Tests; // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; - private testDiscoveryCancellationTokenSource?: vscode.CancellationTokenSource; - private testRunnerCancellationTokenSource?: vscode.CancellationTokenSource; - private _installer: IInstaller; + private testDiscoveryCancellationTokenSource?: CancellationTokenSource; + private testRunnerCancellationTokenSource?: CancellationTokenSource; + private _installer!: IInstaller; private discoverTestsPromise?: Promise; private get installer(): IInstaller { if (!this._installer) { @@ -53,10 +49,10 @@ export abstract class BaseTestManager implements ITestManager { this.testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); this._testResultsService = this.serviceContainer.get(ITestResultsService); } - protected get testDiscoveryCancellationToken(): vscode.CancellationToken | undefined { + protected get testDiscoveryCancellationToken(): CancellationToken | undefined { return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; } - protected get testRunnerCancellationToken(): vscode.CancellationToken | undefined { + protected get testRunnerCancellationToken(): CancellationToken | undefined { return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; } public dispose() { @@ -66,7 +62,7 @@ export abstract class BaseTestManager implements ITestManager { return this._status; } public get workingDirectory(): string { - const settings = PythonSettings.getInstance(vscode.Uri.file(this.rootDirectory)); + const settings = PythonSettings.getInstance(Uri.file(this.rootDirectory)); return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.rootDirectory; } public stop() { @@ -133,9 +129,10 @@ export abstract class BaseTestManager implements ITestManager { } }); if (haveErrorsInDiscovering && !quietMode) { - displayTestErrorMessage('There were some errors in discovering unit tests'); + const testsHelper = this.serviceContainer.get(ITestsHelper); + testsHelper.displayTestErrorMessage('There were some errors in discovering unit tests'); } - const wkspace = workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory))!.uri; + const wkspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; this.testCollectionStorage.storeTests(wkspace, tests); this.disposeCancellationToken(CancellationTokenType.testDiscovery); sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); @@ -159,7 +156,7 @@ export abstract class BaseTestManager implements ITestManager { // tslint:disable-next-line:prefer-template this.outputChannel.appendLine(reason.toString()); } - const wkspace = workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory))!.uri; + const wkspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; this.testCollectionStorage.storeTests(wkspace, null); this.disposeCancellationToken(CancellationTokenType.testDiscovery); return Promise.reject(reason); @@ -217,8 +214,9 @@ export abstract class BaseTestManager implements ITestManager { if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { return Promise.reject(reason); } - displayTestErrorMessage('Errors in discovering tests, continuing with tests'); - return { + const testsHelper = this.serviceContainer.get(ITestsHelper); + testsHelper.displayTestErrorMessage('Errors in discovering tests, continuing with tests'); + return { rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuites: [], summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } }; @@ -250,9 +248,9 @@ export abstract class BaseTestManager implements ITestManager { private createCancellationToken(tokenType: CancellationTokenType) { this.disposeCancellationToken(tokenType); if (tokenType === CancellationTokenType.testDiscovery) { - this.testDiscoveryCancellationTokenSource = new vscode.CancellationTokenSource(); + this.testDiscoveryCancellationTokenSource = new CancellationTokenSource(); } else { - this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); + this.testRunnerCancellationTokenSource = new CancellationTokenSource(); } } private disposeCancellationToken(tokenType: CancellationTokenType) { diff --git a/src/client/unittests/common/managers/testConfigurationManager.ts b/src/client/unittests/common/managers/testConfigurationManager.ts index 358b8b5705b6..b6351c6bb335 100644 --- a/src/client/unittests/common/managers/testConfigurationManager.ts +++ b/src/client/unittests/common/managers/testConfigurationManager.ts @@ -1,18 +1,26 @@ import * as path from 'path'; import { OutputChannel, QuickPickItem, Uri, window } from 'vscode'; import { createDeferred } from '../../../common/helpers'; -import { IInstaller, Product } from '../../../common/types'; +import { IInstaller, IOutputChannel, Product } from '../../../common/types'; import { getSubDirectories } from '../../../common/utils'; +import { IServiceContainer } from '../../../ioc/types'; +import { ITestConfigurationManager } from '../../types'; +import { TEST_OUTPUT_CHANNEL } from '../constants'; import { ITestConfigSettingsService, UnitTestProduct } from './../types'; -export abstract class TestConfigurationManager { +export abstract class TestConfigurationManager implements ITestConfigurationManager { + protected readonly outputChannel: OutputChannel; + protected readonly installer: IInstaller; + protected readonly testConfigSettingsService: ITestConfigSettingsService; constructor(protected workspace: Uri, protected product: UnitTestProduct, - protected readonly outputChannel: OutputChannel, - protected installer: IInstaller, - protected testConfigSettingsService: ITestConfigSettingsService) { } - // tslint:disable-next-line:no-any - public abstract configure(wkspace: Uri): Promise; + protected readonly serviceContainer: IServiceContainer) { + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.installer = serviceContainer.get(IInstaller); + this.testConfigSettingsService = serviceContainer.get(ITestConfigSettingsService); + } + public abstract configure(wkspace: Uri): Promise; + public abstract requiresUserToConfigure(wkspace: Uri): Promise; public async enable() { // Disable other test frameworks. const testProducsToDisable = [Product.pytest, Product.unittest, Product.nosetest] diff --git a/src/client/unittests/common/services/configSettingService.ts b/src/client/unittests/common/services/configSettingService.ts index d7dd923e9f10..483ffa35058b 100644 --- a/src/client/unittests/common/services/configSettingService.ts +++ b/src/client/unittests/common/services/configSettingService.ts @@ -1,9 +1,31 @@ -import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { inject, injectable } from 'inversify'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; import { ITestConfigSettingsService, UnitTestProduct } from './../types'; +@injectable() export class TestConfigSettingsService implements ITestConfigSettingsService { - private static getTestArgSetting(product: UnitTestProduct) { + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { + const setting = this.getTestArgSetting(product); + return this.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, false); + } + private getTestArgSetting(product: UnitTestProduct) { switch (product) { case Product.unittest: return 'unitTest.unittestArgs'; @@ -15,7 +37,7 @@ export class TestConfigSettingsService implements ITestConfigSettingsService { throw new Error('Invalid Test Product'); } } - private static getTestEnablingSetting(product: UnitTestProduct) { + private getTestEnablingSetting(product: UnitTestProduct) { switch (product) { case Product.unittest: return 'unitTest.unittestEnabled'; @@ -28,36 +50,22 @@ export class TestConfigSettingsService implements ITestConfigSettingsService { } } // tslint:disable-next-line:no-any - private static async updateSetting(testDirectory: string | Uri, setting: string, value: any) { + private async updateSetting(testDirectory: string | Uri, setting: string, value: any) { let pythonConfig: WorkspaceConfiguration; const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - pythonConfig = workspace.getConfiguration('python'); - } else if (workspace.workspaceFolders.length === 1) { - pythonConfig = workspace.getConfiguration('python', workspace.workspaceFolders[0].uri); + if (!this.workspaceService.hasWorkspaceFolders) { + pythonConfig = this.workspaceService.getConfiguration('python'); + } else if (this.workspaceService.workspaceFolders!.length === 1) { + pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceService.workspaceFolders![0].uri); } else { - const workspaceFolder = workspace.getWorkspaceFolder(resource); + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); if (!workspaceFolder) { throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); } // tslint:disable-next-line:no-non-null-assertion - pythonConfig = workspace.getConfiguration('python', workspaceFolder!.uri); + pythonConfig = this.workspaceService.getConfiguration('python', workspaceFolder!.uri); } return pythonConfig.update(setting, value); } - public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { - const setting = TestConfigSettingsService.getTestArgSetting(product); - return TestConfigSettingsService.updateSetting(testDirectory, setting, args); - } - - public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { - const setting = TestConfigSettingsService.getTestEnablingSetting(product); - return TestConfigSettingsService.updateSetting(testDirectory, setting, true); - } - - public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { - const setting = TestConfigSettingsService.getTestEnablingSetting(product); - return TestConfigSettingsService.updateSetting(testDirectory, setting, false); - } } diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index c2eb115c924b..535f6c28c1a7 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,8 +1,10 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import { commands, Uri, window, workspace } from 'vscode'; +import { Uri, window, workspace } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; import * as constants from '../../common/constants'; 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, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, UnitTestProduct } from './types'; @@ -19,15 +21,6 @@ export async function selectTestWorkspace(): Promise { } } -export function displayTestErrorMessage(message: string) { - window.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { - if (action === constants.Button_Text_Tests_View_Output) { - commands.executeCommand(constants.Commands.Tests_ViewOutput, undefined, CommandSource.ui); - } - }); - -} - export function extractBetweenDelimiters(content: string, startDelimiter: string, endDelimiter: string): string { content = content.substring(content.indexOf(startDelimiter) + startDelimiter.length); return content.substring(0, content.lastIndexOf(endDelimiter)); @@ -40,7 +33,13 @@ export function convertFileToPackage(filePath: string): string { @injectable() export class TestsHelper implements ITestsHelper { - constructor(@inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor) { } + private readonly appShell: IApplicationShell; + private readonly commandManager: ICommandManager; + constructor(@inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor, + @inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.appShell = serviceContainer.get(IApplicationShell); + this.commandManager = serviceContainer.get(ICommandManager); + } public parseProviderName(product: UnitTestProduct): TestProvider { switch (product) { case Product.nosetest: return 'nosetest'; @@ -165,4 +164,11 @@ export class TestsHelper implements ITestsHelper { // tslint:disable-next-line:no-object-literal-type-assertion return { testFile: [{ 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 => { + if (action === constants.Button_Text_Tests_View_Output) { + this.commandManager.executeCommand(constants.Commands.Tests_ViewOutput, undefined, CommandSource.ui); + } + }); + } } diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index 2b5bc9af48b6..e5ad14fa6fd7 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -1,6 +1,5 @@ import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; -import { IUnitTestSettings } from '../../common/types'; -import { Product } from '../../common/types'; +import { IUnitTestSettings, Product } from '../../common/types'; import { CommandSource } from './constants'; export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; @@ -126,8 +125,9 @@ export type TestsToRun = { export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; +export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); export interface ITestConfigSettingsService { - updateTestArgs(testDirectory: string, product: UnitTestProduct, args: string[]): Promise; + updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise; enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; } @@ -160,6 +160,7 @@ export interface ITestsHelper { getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; flattenTestFiles(testFiles: TestFile[]): Tests; placeTestFilesIntoFolders(tests: Tests): void; + displayTestErrorMessage(message: string): void; } export const ITestVisitor = Symbol('ITestVisitor'); @@ -240,6 +241,6 @@ export type ParserOptions = TestDiscoveryOptions; export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); export interface IUnitTestSocketServer extends Disposable { on(event: string | symbol, listener: Function): this; - start(options?: { port?: number, host?: string }): Promise; + start(options?: { port?: number; host?: string }): Promise; stop(): void; } diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 850a39ae42cf..3772a90241a3 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,154 +1,110 @@ 'use strict'; -import * as path from 'path'; -import * as vscode from 'vscode'; + +import { inject, injectable } from 'inversify'; import { OutputChannel, Uri } from 'vscode'; -import { PythonSettings } from '../common/configSettings'; -import { IInstaller, Product } from '../common/types'; -import { getSubDirectories } from '../common/utils'; -import { TestConfigurationManager } from './common/managers/testConfigurationManager'; -import { TestConfigSettingsService } from './common/services/configSettingService'; +import { IApplicationShell, IWorkspaceService } from '../common/application/types'; +import { IConfigurationService, IInstaller, IOutputChannel, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { TEST_OUTPUT_CHANNEL } from './common/constants'; import { UnitTestProduct } from './common/types'; -import { ConfigurationManager } from './nosetest/testConfigurationManager'; -import * as nose from './nosetest/testConfigurationManager'; -import * as pytest from './pytest/testConfigurationManager'; -import * as unittest from './unittest/testConfigurationManager'; +import { ITestConfigurationManagerFactory, IUnitTestConfigurationService } from './types'; -// tslint:disable-next-line:no-any -async function promptToEnableAndConfigureTestFramework(wkspace: Uri, installer: IInstaller, outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { - const selectedTestRunner = await selectTestRunner(messageToDisplay); - if (typeof selectedTestRunner !== 'number') { - return Promise.reject(null); - } - const configMgr: TestConfigurationManager = createTestConfigurationManager(wkspace, selectedTestRunner, outputChannel, installer); - if (enableOnly) { - // Ensure others are disabled - [Product.unittest, Product.pytest, Product.nosetest] - .filter(prod => selectedTestRunner !== prod) - .forEach(prod => { - createTestConfigurationManager(wkspace, prod, outputChannel, installer).disable() - .catch(ex => console.error('Python Extension: createTestConfigurationManager.disable', ex)); - }); - return configMgr.enable(); +@injectable() +export class UnitTestConfigurationService implements IUnitTestConfigurationService { + private readonly configurationService: IConfigurationService; + private readonly appShell: IApplicationShell; + private readonly installer: IInstaller; + private readonly outputChannel: OutputChannel; + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.configurationService = serviceContainer.get(IConfigurationService); + this.appShell = serviceContainer.get(IApplicationShell); + this.installer = serviceContainer.get(IInstaller); + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.workspaceService = serviceContainer.get(IWorkspaceService); } - - return configMgr.configure(wkspace).then(() => { - return enableTest(wkspace, configMgr); - }).catch(reason => { - return enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); - }); -} -export function displayTestFrameworkError(wkspace: Uri, outputChannel: vscode.OutputChannel, installer: IInstaller) { - const settings = PythonSettings.getInstance(); - let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0; - enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; - enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; - if (enabledCount > 1) { - return promptToEnableAndConfigureTestFramework(wkspace, installer, outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); - } else { - const option = 'Enable and configure a Test Framework'; - return vscode.window.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option).then(item => { + public async displayTestFrameworkError(wkspace: Uri): Promise { + const settings = this.configurationService.getSettings(wkspace); + let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0; + enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; + enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; + if (enabledCount > 1) { + return this.promptToEnableAndConfigureTestFramework(wkspace, this.installer, this.outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); + } else { + const option = 'Enable and configure a Test Framework'; + const item = await this.appShell.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option); if (item === option) { - return promptToEnableAndConfigureTestFramework(wkspace, installer, outputChannel); + return this.promptToEnableAndConfigureTestFramework(wkspace, this.installer, this.outputChannel); } return Promise.reject(null); - }); - } -} -export async function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel, installer: IInstaller) { - const settings = PythonSettings.getInstance(vscode.Uri.file(rootDir)); - if (settings.unitTest.pyTestEnabled || - settings.unitTest.nosetestsEnabled || - settings.unitTest.unittestEnabled) { - return; - } - - if (!settings.unitTest.promptToConfigure) { - return; - } - - const yes = 'Yes'; - const no = 'Later'; - const noNotAgain = 'No, don\'t ask again'; - - const hasTests = checkForExistenceOfTests(rootDir); - if (!hasTests) { - return; + } } - const item = await vscode.window.showInformationMessage('You seem to have tests, would you like to enable a test framework?', yes, no, noNotAgain); - if (!item || item === no) { - return; + public async selectTestRunner(placeHolderMessage: string): Promise { + const items = [{ + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/3/library/unittest.html' + }, + { + label: 'pytest', + product: Product.pytest, + description: 'Can run unittest (including trial) and nose test suites out of the box', + // tslint:disable-next-line:no-http-string + detail: 'http://docs.pytest.org/' + }, + { + label: 'nose', + product: Product.nosetest, + description: 'nose framework', + detail: 'https://nose.readthedocs.io/' + }]; + const options = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage + }; + const selectedTestRunner = await this.appShell.showQuickPick(items, options); + // tslint:disable-next-line:prefer-type-cast + return selectedTestRunner ? selectedTestRunner.product as UnitTestProduct : undefined; } - if (item === yes) { - await promptToEnableAndConfigureTestFramework(vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDir))!.uri, installer, outputChannel); - } else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - await pythonConfig.update('unitTest.promptToConfigure', false); + public enableTest(wkspace: Uri, product: UnitTestProduct) { + const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, product); + const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); + if (pythonConfig.get('unitTest.promptToConfigure')) { + return configMgr.enable(); + } + return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { + return configMgr.enable(); + }, reason => { + return configMgr.enable().then(() => Promise.reject(reason)); + }); } -} -// Configure everything before enabling. -// Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) -// to start discovering tests when tests haven't been configured properly. -function enableTest(wkspace: Uri, configMgr: ConfigurationManager) { - const pythonConfig = vscode.workspace.getConfiguration('python', wkspace); - // tslint:disable-next-line:no-backbone-get-set-outside-model - if (pythonConfig.get('unitTest.promptToConfigure')) { - return configMgr.enable(); - } - return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { - return configMgr.enable(); - }, reason => { - return configMgr.enable().then(() => Promise.reject(reason)); - }); -} -function checkForExistenceOfTests(rootDir: string): Promise { - return getSubDirectories(rootDir).then(subDirs => { - return subDirs.map(dir => path.relative(rootDir, dir)).filter(dir => dir.match(/test/i)).length > 0; - }); -} -function createTestConfigurationManager(wkspace: Uri, product: Product, outputChannel: OutputChannel, installer: IInstaller) { - const configSettingService = new TestConfigSettingsService(); - switch (product) { - case Product.unittest: { - return new unittest.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); - } - case Product.pytest: { - return new pytest.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); - } - case Product.nosetest: { - return new nose.ConfigurationManager(wkspace, outputChannel, installer, configSettingService); + private async promptToEnableAndConfigureTestFramework(wkspace: Uri, installer: IInstaller, outputChannel: OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { + const selectedTestRunner = await this.selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + return Promise.reject(null); } - default: { - throw new Error('Invalid test configuration'); + const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, selectedTestRunner); + if (enableOnly) { + // Ensure others are disabled + [Product.unittest, Product.pytest, Product.nosetest] + .filter(prod => selectedTestRunner !== prod) + .forEach(prod => { + factory.create(wkspace, prod).disable() + .catch(ex => console.error('Python Extension: createTestConfigurationManager.disable', ex)); + }); + return configMgr.enable(); } + + // Configure everything before enabling. + // Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) + // to start discovering tests when tests haven't been configured properly. + return configMgr.configure(wkspace) + .then(() => this.enableTest(wkspace, selectedTestRunner)) + .catch(reason => { return this.enableTest(wkspace, selectedTestRunner).then(() => Promise.reject(reason)); }); } } -async function selectTestRunner(placeHolderMessage: string): Promise { - const items = [{ - label: 'unittest', - product: Product.unittest, - description: 'Standard Python test framework', - detail: 'https://docs.python.org/3/library/unittest.html' - }, - { - label: 'pytest', - product: Product.pytest, - description: 'Can run unittest (including trial) and nose test suites out of the box', - // tslint:disable-next-line:no-http-string - detail: 'http://docs.pytest.org/' - }, - { - label: 'nose', - product: Product.nosetest, - description: 'nose framework', - detail: 'https://nose.readthedocs.io/' - }]; - const options = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: placeHolderMessage - }; - const selectedTestRunner = await vscode.window.showQuickPick(items, options); - // tslint:disable-next-line:prefer-type-cast - return selectedTestRunner ? selectedTestRunner.product as UnitTestProduct : undefined; -} diff --git a/src/client/unittests/configurationFactory.ts b/src/client/unittests/configurationFactory.ts new file mode 100644 index 000000000000..ee29d6d8c1d6 --- /dev/null +++ b/src/client/unittests/configurationFactory.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import * as nose from './nosetest/testConfigurationManager'; +import * as pytest from './pytest/testConfigurationManager'; +import { ITestConfigurationManagerFactory } from './types'; +import * as unittest from './unittest/testConfigurationManager'; + +@injectable() +export class TestConfigurationManagerFactory implements ITestConfigurationManagerFactory { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public create(wkspace: Uri, product: Product) { + switch (product) { + case Product.unittest: { + return new unittest.ConfigurationManager(wkspace, this.serviceContainer); + } + case Product.pytest: { + return new pytest.ConfigurationManager(wkspace, this.serviceContainer); + } + case Product.nosetest: { + return new nose.ConfigurationManager(wkspace, this.serviceContainer); + } + default: { + throw new Error('Invalid test configuration'); + } + } + } + +} diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts index aec0139ac2cb..dc2a130e1064 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -1,25 +1,46 @@ 'use strict'; -import * as vscode from 'vscode'; +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, StatusBarAlignment, StatusBarItem } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; import * as constants from '../../common/constants'; -import { createDeferred, isNotInstalledError } from '../../common/helpers'; +import { noop } from '../../common/core.utils'; +import { isNotInstalledError } from '../../common/helpers'; +import { IConfigurationService } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { CANCELLATION_REASON } from '../common/constants'; -import { displayTestErrorMessage } from '../common/testUtils'; -import { Tests } from '../common/types'; +import { ITestsHelper, Tests } from '../common/types'; +import { ITestResultDisplay } from '../types'; -export class TestResultDisplay { - private statusBar: vscode.StatusBarItem; +@injectable() +export class TestResultDisplay implements ITestResultDisplay { + private statusBar: StatusBarItem; private discoverCounter = 0; private ticker = ['|', '/', '-', '|', '/', '-', '\\']; private progressTimeout; + private _enabled: boolean = false; private progressPrefix!: string; + private readonly didChange = new EventEmitter(); + private readonly appShell: IApplicationShell; + private readonly testsHelper: ITestsHelper; + public get onDidChange(): Event { + return this.didChange.event; + } + // tslint:disable-next-line:no-any - constructor(private onDidChange?: vscode.EventEmitter) { - this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.appShell = serviceContainer.get(IApplicationShell); + this.statusBar = this.appShell.createStatusBarItem(StatusBarAlignment.Left); + this.testsHelper = serviceContainer.get(ITestsHelper); } public dispose() { + this.clearProgressTicker(); this.statusBar.dispose(); } + public get enabled() { + return this._enabled; + } public set enabled(enable: boolean) { + this._enabled = enable; if (enable) { this.statusBar.show(); } else { @@ -32,11 +53,10 @@ export class TestResultDisplay { .then(tests => this.updateTestRunWithSuccess(tests, debug)) .catch(this.updateTestRunWithFailure.bind(this)) // We don't care about any other exceptions returned by updateTestRunWithFailure - // tslint:disable-next-line:no-empty - .catch(() => { }); + .catch(noop); } public displayDiscoverStatus(testDiscovery: Promise, quietMode: boolean = false) { - this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); + this.displayProgress('Discovering Tests', 'Discovering tests (click to stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); return testDiscovery.then(tests => { this.updateWithDiscoverSuccess(tests, quietMode); return tests; @@ -78,11 +98,9 @@ export class TestResultDisplay { this.statusBar.text = statusText.length === 0 ? 'No Tests Ran' : statusText.join(' '); this.statusBar.color = foreColor; this.statusBar.command = constants.Commands.Tests_View_UI; - if (this.onDidChange) { - this.onDidChange.fire(); - } + this.didChange.fire(); if (statusText.length === 0 && !debug) { - vscode.window.showWarningMessage('No tests ran, please check the configuration settings for the tests.'); + this.appShell.showWarningMessage('No tests ran, please check the configuration settings for the tests.'); } return tests; } @@ -97,7 +115,7 @@ export class TestResultDisplay { } else { this.statusBar.text = '$(alert) Tests Failed'; this.statusBar.tooltip = 'Running Tests Failed'; - displayTestErrorMessage('There was an error in running the tests.'); + this.testsHelper.displayTestErrorMessage('There was an error in running the tests.'); } return Promise.reject(reason); } @@ -124,23 +142,14 @@ export class TestResultDisplay { } // tslint:disable-next-line:no-any - private disableTests(): Promise { - // tslint:disable-next-line:no-any - const def = createDeferred(); - const pythonConfig = vscode.workspace.getConfiguration('python'); + private async disableTests(): Promise { + const configurationService = this.serviceContainer.get(IConfigurationService); const settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']; - function disableTest() { - if (settingsToDisable.length === 0) { - return def.resolve(); - } - pythonConfig.update(settingsToDisable.shift()!, false) - .then(disableTest.bind(this), disableTest.bind(this)); + for (const setting of settingsToDisable) { + await configurationService.updateSettingAsync(setting, false).catch(noop); } - - disableTest(); - return def.promise; } private updateWithDiscoverSuccess(tests: Tests, quietMode: boolean = false) { @@ -150,12 +159,12 @@ export class TestResultDisplay { this.statusBar.tooltip = 'Run Tests'; this.statusBar.command = constants.Commands.Tests_View_UI; this.statusBar.show(); - if (this.onDidChange) { - this.onDidChange.fire(); + if (this.didChange) { + this.didChange.fire(); } if (!haveTests && !quietMode) { - vscode.window.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { + this.appShell.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { if (item === 'Disable Tests') { this.disableTests() .catch(ex => console.error('Python Extension: disableTests', ex)); @@ -181,7 +190,7 @@ export class TestResultDisplay { // tslint:disable-next-line:no-suspicious-comment // TODO: show an option that will invoke a command 'python.test.configureTest' or similar. // This will be hanlded by main.ts that will capture input from user and configure the tests. - vscode.window.showErrorMessage('There was an error in discovering tests, please check the configuration settings for the tests.'); + this.appShell.showErrorMessage('Test discovery error, please check the configuration settings for the tests.'); } } } diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index 7c054b88a06e..4544609322af 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,13 +1,24 @@ +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { commands, QuickPickItem, Uri, window } from 'vscode'; +import { commands, QuickPickItem, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; import * as constants from '../../common/constants'; +import { noop } from '../../common/core.utils'; +import { IServiceContainer } from '../../ioc/types'; import { CommandSource } from '../common/constants'; import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; +import { ITestDisplay } from '../types'; -export class TestDisplay { - constructor(private testCollectionStorage: ITestCollectionStorageService) { } +@injectable() +export class TestDisplay implements ITestDisplay { + private readonly testCollectionStorage: ITestCollectionStorageService; + private readonly appShell: IApplicationShell; + constructor(@inject(IServiceContainer) serviceRegistry: IServiceContainer) { + this.testCollectionStorage = serviceRegistry.get(ITestCollectionStorageService); + this.appShell = serviceRegistry.get(IApplicationShell); + } public displayStopTestUI(workspace: Uri, message: string) { - window.showQuickPick([message]).then(item => { + this.appShell.showQuickPick([message]).then(item => { if (item === message) { commands.executeCommand(constants.Commands.Tests_Stop, undefined, workspace); } @@ -15,12 +26,12 @@ export class TestDisplay { } public displayTestUI(cmdSource: CommandSource, wkspace: Uri) { const tests = this.testCollectionStorage.getTests(wkspace); - window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) - .then(item => onItemSelected(cmdSource, wkspace, item, false)); + this.appShell.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then(item => item ? onItemSelected(cmdSource, wkspace, item, false) : noop()); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { - window.showQuickPick(buildItemsForFunctions(rootDirectory, tests.testFunctions), { matchOnDescription: true, matchOnDetail: true }) + this.appShell.showQuickPick(buildItemsForFunctions(rootDirectory, tests.testFunctions), { matchOnDescription: true, matchOnDetail: true }) .then(item => { if (item && item.fn) { return resolve(item.fn); @@ -31,7 +42,7 @@ export class TestDisplay { } public selectTestFile(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { - window.showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { matchOnDescription: true, matchOnDetail: true }) + this.appShell.showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { matchOnDescription: true, matchOnDetail: true }) .then(item => { if (item && item.testFile) { return resolve(item.testFile); @@ -55,10 +66,9 @@ export class TestDisplay { testFunctions.some(testFunc => testFunc.nameToRun === fn.testFunction.nameToRun); }); - window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), - { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(cmdSource, wkspace, testItem, debug); - }); + this.appShell.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), + { matchOnDescription: true, matchOnDetail: true }) + .then(testItem => testItem ? onItemSelected(cmdSource, wkspace, testItem, debug) : noop()); } } @@ -95,7 +105,7 @@ function getSummary(tests?: Tests) { if (!tests || !tests.summary) { return ''; } - const statusText = []; + const statusText: string[] = []; if (tests.summary.passed > 0) { statusText.push(`${constants.Octicons.Test_Pass} ${tests.summary.passed} Passed`); } @@ -137,7 +147,7 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct const functionItems: TestItem[] = []; tests.forEach(fn => { let icon = ''; - if (displayStatusIcons && statusIconMapping.has(fn.testFunction.status)) { + if (displayStatusIcons && fn.testFunction.status && statusIconMapping.has(fn.testFunction.status)) { icon = `${statusIconMapping.get(fn.testFunction.status)} `; } @@ -152,7 +162,7 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct functionItems.sort((a, b) => { let sortAPrefix = '5-'; let sortBPrefix = '5-'; - if (sortBasedOnResults) { + if (sortBasedOnResults && a.fn && a.fn.testFunction.status && b.fn && b.fn.testFunction.status) { sortAPrefix = statusSortPrefix[a.fn.testFunction.status] ? statusSortPrefix[a.fn.testFunction.status] : sortAPrefix; sortBPrefix = statusSortPrefix[b.fn.testFunction.status] ? statusSortPrefix[b.fn.testFunction.status] : sortBPrefix; } @@ -177,10 +187,13 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T }; }); fileItems.sort((a, b) => { - if (a.detail < b.detail) { + if (!a.detail && !b.detail) { + return 0; + } + if (!a.detail || a.detail < b.detail!) { return -1; } - if (a.detail > b.detail) { + if (!b.detail || a.detail! > b.detail) { return 1; } return 0; @@ -221,13 +234,13 @@ function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestI case Type.RunMethod: { cmd = constants.Commands.Tests_Run; // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + args.push({ testFunction: [selection.fn!.testFunction] } as TestsToRun); break; } case Type.DebugMethod: { cmd = constants.Commands.Tests_Debug; // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); + args.push({ testFunction: [selection.fn!.testFunction] } as TestsToRun); args.push(true); break; } diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 81b8ad6a9946..4dd1f63cfe3c 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,307 +1,322 @@ 'use strict'; + +// tslint:disable:no-duplicate-imports no-unnecessary-callback-wrapper + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, Disposable, OutputChannel, TextDocument, Uri } from 'vscode'; import * as vscode from 'vscode'; -// tslint:disable-next-line:no-duplicate-imports -import { Disposable, Uri, window, workspace } from 'vscode'; -import { PythonSettings } from '../common/configSettings'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; import * as constants from '../common/constants'; -import { IInstaller } from '../common/types'; +import { IConfigurationService, IDisposableRegistry, ILogger, IOutputChannel } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { PythonSymbolProvider } from '../providers/symbolProvider'; import { UNITTEST_STOP, UNITTEST_VIEW_OUTPUT } from '../telemetry/constants'; import { sendTelemetryEvent } from '../telemetry/index'; import { activateCodeLenses } from './codeLenses/main'; -import { CANCELLATION_REASON, CommandSource } from './common/constants'; +import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './common/constants'; import { selectTestWorkspace } from './common/testUtils'; import { ITestCollectionStorageService, ITestManager, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; -import { displayTestFrameworkError } from './configuration'; -import { TestResultDisplay } from './display/main'; -import { TestDisplay } from './display/picker'; - -let workspaceTestManagerService: IWorkspaceTestManagerService; -let testResultDisplay: TestResultDisplay; -let testDisplay: TestDisplay; -let outChannel: vscode.OutputChannel; -const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); -let testCollectionStorage: ITestCollectionStorageService; -let _serviceContaner: IServiceContainer; +import { ITestDisplay, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestManagementService } from './types'; -export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider, serviceContainer: IServiceContainer) { - _serviceContaner = serviceContainer; +@injectable() +export class UnitTestManagementService implements IUnitTestManagementService, Disposable { + private readonly outputChannel: vscode.OutputChannel; + private readonly disposableRegistry: Disposable[]; + private workspaceTestManagerService?: IWorkspaceTestManagerService; + private documentManager: IDocumentManager; + private workspaceService: IWorkspaceService; + private testResultDisplay?: ITestResultDisplay; + private autoDiscoverTimer?: NodeJS.Timer; + private configChangedTimer?: NodeJS.Timer; + private readonly onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); - context.subscriptions.push({ dispose: dispose }); - outChannel = outputChannel; - const disposables = registerCommands(); - context.subscriptions.push(...disposables); + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.disposableRegistry = serviceContainer.get(IDisposableRegistry); + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.documentManager = serviceContainer.get(IDocumentManager); - testCollectionStorage = serviceContainer.get(ITestCollectionStorageService); - workspaceTestManagerService = serviceContainer.get(IWorkspaceTestManagerService); - - context.subscriptions.push(autoResetTests()); - context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider, testCollectionStorage)); - context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(onDocumentSaved)); - - autoDiscoverTests(); -} -async function getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { - let wkspace: Uri | undefined; - if (resource) { - const wkspaceFolder = workspace.getWorkspaceFolder(resource); - wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; - } else { - wkspace = await selectTestWorkspace(); - } - if (!wkspace) { - return; - } - const testManager = workspaceTestManagerService.getTestManager(wkspace); - if (testManager) { - return testManager; - } - if (displayTestNotConfiguredMessage) { - await displayTestFrameworkError(wkspace, outChannel, _serviceContaner.get(IInstaller)); - } -} -let timeoutId: NodeJS.Timer; -async function onDocumentSaved(doc: vscode.TextDocument): Promise { - const testManager = await getTestManager(false, doc.uri); - if (!testManager) { - return; - } - const tests = await testManager.discoverTests(CommandSource.auto, false, true); - if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { - return; + this.disposableRegistry.push(this); } - if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { - return; + public dispose() { + if (this.workspaceTestManagerService) { + this.workspaceTestManagerService.dispose(); + } } + public async activate(): Promise { + this.workspaceTestManagerService = this.serviceContainer.get(IWorkspaceTestManagerService); - if (timeoutId) { - clearTimeout(timeoutId); + this.registerHandlers(); + this.registerCommands(); + this.autoDiscoverTests() + .catch(ex => this.serviceContainer.get(ILogger).logError('Failed to auto discover tests upon activation', ex)); + } + public async activateCodeLenses(symboldProvider: PythonSymbolProvider): Promise { + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + this.disposableRegistry.push(activateCodeLenses(this.onDidChange, symboldProvider, testCollectionStorage)); + } + public async getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + wkspace = await selectTestWorkspace(); + } + if (!wkspace) { + return; + } + const testManager = this.workspaceTestManagerService!.getTestManager(wkspace); + if (testManager) { + return testManager; + } + if (displayTestNotConfiguredMessage) { + const configurationService = this.serviceContainer.get(IUnitTestConfigurationService); + await configurationService.displayTestFrameworkError(wkspace); + } } - timeoutId = setTimeout(() => discoverTests(CommandSource.auto, doc.uri, true, false, true), 1000); -} + public async configurationChangeHandler(e: ConfigurationChangeEvent) { + // If there's one workspace, then stop the tests and restart, + // else let the user do this manually. + if (!this.workspaceService.hasWorkspaceFolders || this.workspaceService.workspaceFolders!.length > 1) { + return; + } -function dispose() { - workspaceTestManagerService.dispose(); - testCollectionStorage.dispose(); -} -function registerCommands(): vscode.Disposable[] { - const disposables: Disposable[] = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { - // Ignore the exceptions returned. - // This command will be invoked else where in the extension. - // tslint:disable-next-line:no-empty - discoverTests(cmdSource, resource, true, true).catch(() => { }); - })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => runTestsImpl(cmdSource, resource, undefined, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => runTestsImpl(cmdSource, file, testToRun))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => runTestsImpl(cmdSource, file, testToRun, false, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI(CommandSource.commandPalette))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => displayPickerUI(cmdSource, file, testFunctions, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (_, resource: Uri) => stopTests(resource))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, (_, cmdSource: CommandSource = CommandSource.commandPalette) => viewOutput(cmdSource))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => displayStopUI('Stop discovering tests'))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => displayStopUI('Stop running tests'))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => selectAndRunTestMethod(cmdSource, resource, true))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => selectAndRunTestFile(cmdSource))); - // tslint:disable-next-line:no-unnecessary-callback-wrapper - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => runCurrentTestFile(cmdSource))); + const workspaceUri = this.workspaceService.workspaceFolders![0].uri; + if (!e.affectsConfiguration('python.unitTest', workspaceUri)) { + return; + } + const settings = this.serviceContainer.get(IConfigurationService).getSettings(workspaceUri); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + if (this.testResultDisplay) { + this.testResultDisplay.enabled = false; + } + // TODO: Why are we disposing, what happens when tests are enabled. + if (this.workspaceTestManagerService) { + this.workspaceTestManagerService.dispose(); + } + return; + } + if (this.testResultDisplay) { + this.testResultDisplay.enabled = true; + } + this.autoDiscoverTests() + .catch(ex => this.serviceContainer.get(ILogger).logError('Failed to auto discover tests upon activation', ex)); + } - return disposables; -} + public async discoverTestsForDocument(doc: TextDocument): Promise { + const testManager = await this.getTestManager(false, doc.uri); + if (!testManager) { + return; + } + const tests = await testManager.discoverTests(CommandSource.auto, false, true); + if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { + return; + } + if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { + return; + } -function viewOutput(cmdSource: CommandSource) { - sendTelemetryEvent(UNITTEST_VIEW_OUTPUT); - outChannel.show(); -} -async function displayUI(cmdSource: CommandSource) { - const testManager = await getTestManager(true); - if (!testManager) { - return; + if (this.autoDiscoverTimer) { + clearTimeout(this.autoDiscoverTimer); + } + this.autoDiscoverTimer = setTimeout(() => this.discoverTests(CommandSource.auto, doc.uri, true, false, true), 1000); } + public async autoDiscoverTests() { + if (!this.workspaceService.hasWorkspaceFolders) { + return; + } + const configurationService = this.serviceContainer.get(IConfigurationService); + const settings = configurationService.getSettings(); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + return; + } - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); -} -async function displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { - const testManager = await getTestManager(true, file); - if (!testManager) { - return; + // No need to display errors. + // tslint:disable-next-line:no-empty + this.discoverTests(CommandSource.auto, this.workspaceService.workspaceFolders![0].uri, true).catch(() => { }); } + public async discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspaceFolder, testManager.workingDirectory, file, testFunctions, debug); -} -async function selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { - const testManager = await getTestManager(true, resource); - if (!testManager) { - return; - } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } + if (testManager.status === TestStatus.Discovering || testManager.status === TestStatus.Running) { + return; + } - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); - if (!selectedTestFn) { - return; - } - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - await runTestsImpl(cmdSource, testManager.workspaceFolder, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, false, debug); -} -async function selectAndRunTestFile(cmdSource: CommandSource) { - const testManager = await getTestManager(true); - if (!testManager) { - return; + if (!this.testResultDisplay) { + this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + this.testResultDisplay.onDidChange(() => this.onDidChange.fire()); + } + const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated); + this.testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode) + .catch(ex => console.error('Python Extension: displayDiscoverStatus', ex)); + await discoveryPromise; } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; + public async stopTests(resource: Uri) { + sendTelemetryEvent(UNITTEST_STOP); + const testManager = await this.getTestManager(true, resource); + if (testManager) { + testManager.stop(); + } } + public async displayStopUI(message: string): Promise { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - const selectedFile = await testDisplay.selectTestFile(testManager.workspaceFolder.fsPath, tests); - if (!selectedFile) { - return; - } - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - await runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [selectedFile] } as TestsToRun); -} -async function runCurrentTestFile(cmdSource: CommandSource) { - if (!window.activeTextEditor) { - return; - } - const testManager = await getTestManager(true, window.activeTextEditor.document.uri); - if (!testManager) { - return; + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayStopTestUI(testManager.workspaceFolder, message); } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testFiles = tests.testFiles.filter(testFile => { - return testFile.fullPath === window.activeTextEditor!.document.uri.fsPath; - }); - if (testFiles.length < 1) { - return; - } - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - await runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] } as TestsToRun); -} -async function displayStopUI(message: string) { - const testManager = await getTestManager(true); - if (!testManager) { - return; - } - - testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); - testDisplay.displayStopTestUI(testManager.workspaceFolder, message); -} + public async displayUI(cmdSource: CommandSource) { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } -let uniTestSettingsString: string; -function autoResetTests() { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { - // tslint:disable-next-line:no-empty - return { dispose: () => { } }; + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); } + public async displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await this.getTestManager(true, file); + if (!testManager) { + return; + } - const settings = PythonSettings.getInstance(); - uniTestSettingsString = JSON.stringify(settings.unitTest); - return workspace.onDidChangeConfiguration(() => setTimeout(onConfigChanged, 1000)); -} -function onConfigChanged() { - // If there's one workspace, then stop the tests and restart, - // else let the user do this manually. - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { - return; + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspaceFolder, testManager.workingDirectory, file, testFunctions, debug); } - const settings = PythonSettings.getInstance(); - - // Possible that a test framework has been enabled or some settings have changed. - // Meaning we need to re-load the discovered tests (as something could have changed). - const newSettings = JSON.stringify(settings.unitTest); - if (uniTestSettingsString === newSettings) { - return; + public viewOutput(cmdSource: CommandSource) { + sendTelemetryEvent(UNITTEST_VIEW_OUTPUT); + this.outputChannel.show(); } + public async selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } - uniTestSettingsString = newSettings; - if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { - if (testResultDisplay) { - testResultDisplay.enabled = false; + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testDisplay = this.serviceContainer.get(ITestDisplay); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); + if (!selectedTestFn) { + return; } - workspaceTestManagerService.dispose(); - return; - } - if (testResultDisplay) { - testResultDisplay.enabled = true; - } - autoDiscoverTests(); -} -function autoDiscoverTests() { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { - return; - } - const settings = PythonSettings.getInstance(); - if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { - return; + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, false, debug); } + public async selectAndRunTestFile(cmdSource: CommandSource) { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } - // No need to display errors. - // tslint:disable-next-line:no-empty - discoverTests(CommandSource.auto, workspace.workspaceFolders[0].uri, true).catch(() => { }); -} -async function stopTests(resource: Uri) { - sendTelemetryEvent(UNITTEST_STOP); - const testManager = await getTestManager(true, resource); - if (testManager) { - testManager.stop(); + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testDisplay = this.serviceContainer.get(ITestDisplay); + const selectedFile = await testDisplay.selectTestFile(testManager.workspaceFolder.fsPath, tests); + if (!selectedFile) { + return; + } + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [selectedFile] }); } -} -async function discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean) { - const testManager = await getTestManager(true, resource); - if (!testManager) { - return; + public async runCurrentTestFile(cmdSource: CommandSource) { + if (!this.documentManager.activeTextEditor) { + return; + } + const testManager = await this.getTestManager(true, this.documentManager.activeTextEditor.document.uri); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testFiles = tests.testFiles.filter(testFile => { + return testFile.fullPath === this.documentManager.activeTextEditor!.document.uri.fsPath; + }); + if (testFiles.length < 1) { + return; + } + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] }); } - if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(onDidChange); - const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated); - testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode) - .catch(ex => console.error('Python Extension: displayDiscoverStatus', ex)); - await discoveryPromise; + public async runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + + if (!this.testResultDisplay) { + this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + this.testResultDisplay.onDidChange(() => this.onDidChange.fire()); + } + + const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug) + .catch(reason => { + if (reason !== CANCELLATION_REASON) { + this.outputChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); + + this.testResultDisplay.displayProgressStatus(promise, debug); + await promise; } -} -async function runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { - const testManager = await getTestManager(true, resource); - if (!testManager) { - return; + private registerCommands(): void { + const disposablesRegistry = this.serviceContainer.get(IDisposableRegistry); + const commandManager = this.serviceContainer.get(ICommandManager); + + const disposables = [ + commandManager.registerCommand(constants.Commands.Tests_Discover, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked from other places of the extension. + this.discoverTests(cmdSource, resource, true, true).ignoreErrors(); + }), + commandManager.registerCommand(constants.Commands.Tests_Run_Failed, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.runTestsImpl(cmdSource, resource, undefined, true)), + commandManager.registerCommand(constants.Commands.Tests_Run, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun)), + commandManager.registerCommand(constants.Commands.Tests_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun, false, true)), + commandManager.registerCommand(constants.Commands.Tests_View_UI, () => this.displayUI(CommandSource.commandPalette)), + commandManager.registerCommand(constants.Commands.Tests_Picker_UI, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions)), + commandManager.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions, true)), + commandManager.registerCommand(constants.Commands.Tests_Stop, (_, resource: Uri) => this.stopTests(resource)), + commandManager.registerCommand(constants.Commands.Tests_ViewOutput, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.viewOutput(cmdSource)), + commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => this.displayStopUI('Stop discovering tests')), + commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => this.displayStopUI('Stop running tests')), + commandManager.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.selectAndRunTestMethod(cmdSource, resource)), + commandManager.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.selectAndRunTestMethod(cmdSource, resource, true)), + commandManager.registerCommand(constants.Commands.Tests_Select_And_Run_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.selectAndRunTestFile(cmdSource)), + commandManager.registerCommand(constants.Commands.Tests_Run_Current_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.runCurrentTestFile(cmdSource)) + ]; + + disposablesRegistry.push(...disposables); } + private registerHandlers() { + const documentManager = this.serviceContainer.get(IDocumentManager); - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(onDidChange); - const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug) - .catch(reason => { - if (reason !== CANCELLATION_REASON) { - outChannel.appendLine(`Error: ${reason}`); + this.disposableRegistry.push(documentManager.onDidSaveTextDocument(this.discoverTestsForDocument.bind(this))); + this.disposableRegistry.push(this.workspaceService.onDidChangeConfiguration(e => { + if (this.configChangedTimer) { + clearTimeout(this.configChangedTimer); } - return Promise.reject(reason); - }); - - testResultDisplay.displayProgressStatus(promise, debug); - await promise; + this.configChangedTimer = setTimeout(() => this.configurationChangeHandler(e), 1000); + })); + } } diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index c98e043920c9..3003e42aa1db 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -1,35 +1,30 @@ -import * as fs from 'fs'; import * as path from 'path'; -import * as vscode from 'vscode'; import { Uri } from 'vscode'; -import { IInstaller, Product } from '../../common/types'; +import { IFileSystem } from '../../common/platform/types'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, outputChannel: vscode.OutputChannel, - installer: IInstaller, testConfigSettingsService: ITestConfigSettingsService) { - super(workspace, Product.nosetest, outputChannel, installer, testConfigSettingsService); + constructor(workspace: Uri, serviceContainer: IServiceContainer) { + super(workspace, Product.nosetest, serviceContainer); } - private static async configFilesExist(rootDir: string): Promise { - const promises = ['.noserc', 'nose.cfg'].map(cfg => { - return new Promise(resolve => { - fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); - }); - }); - const values = await Promise.all(promises); - return values.filter(exists => exists.length > 0); + public async requiresUserToConfigure(wkspace: Uri): Promise { + const fs = this.serviceContainer.get(IFileSystem); + for (const cfg of ['.noserc', 'nose.cfg']) { + if (await fs.fileExists(path.join(wkspace.fsPath, cfg))) { + return true; + } + } + return false; } - // tslint:disable-next-line:no-any - public async configure(wkspace: Uri): Promise { + public async configure(wkspace: Uri): Promise { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); // If a config file exits, there's nothing to be configured. - if (configFiles.length > 0) { + if (await this.requiresUserToConfigure(wkspace)) { return; } - const subDirs = await this.getTestDirs(wkspace.fsPath); const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index f61398ae3431..54b68718661c 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -1,31 +1,27 @@ -import * as fs from 'fs'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { Uri } from 'vscode'; -import { IInstaller, Product } from '../../common/types'; +import { QuickPickItem, Uri } from 'vscode'; +import { IFileSystem } from '../../common/platform/types'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, outputChannel: vscode.OutputChannel, - installer: IInstaller, testConfigSettingsService: ITestConfigSettingsService) { - super(workspace, Product.pytest, outputChannel, installer, testConfigSettingsService); + constructor(workspace: Uri, serviceContainer: IServiceContainer) { + super(workspace, Product.pytest, serviceContainer); } - private static async configFilesExist(rootDir: string): Promise { - const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'].map(cfg => { - return new Promise(resolve => { - fs.exists(path.join(rootDir, cfg), exists => { resolve(exists ? cfg : ''); }); - }); - }); - const values = await Promise.all(promises); - return values.filter(exists => exists.length > 0); + public async requiresUserToConfigure(wkspace: Uri): Promise { + const configFiles = await this.getConfigFiles(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return false; + } + return true; } - // tslint:disable-next-line:no-any public async configure(wkspace: Uri) { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - const options: vscode.QuickPickItem[] = []; - const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); + const options: QuickPickItem[] = []; + const configFiles = await this.getConfigFiles(wkspace.fsPath); // If a config file exits, there's nothing to be configured. if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { return; @@ -48,4 +44,11 @@ export class ConfigurationManager extends TestConfigurationManager { } await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } + private async getConfigFiles(rootDir: string): Promise { + const fs = this.serviceContainer.get(IFileSystem); + const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'] + .map(async cfg => await fs.fileExists(path.join(rootDir, cfg)) ? cfg : ''); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); + } } diff --git a/src/client/unittests/serviceRegistry.ts b/src/client/unittests/serviceRegistry.ts index d500c67070dd..92a4f1b5f6e7 100644 --- a/src/client/unittests/serviceRegistry.ts +++ b/src/client/unittests/serviceRegistry.ts @@ -5,6 +5,7 @@ import { Uri } from 'vscode'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './common/constants'; import { DebugLauncher } from './common/debugLauncher'; +import { TestConfigSettingsService } from './common/services/configSettingService'; import { TestCollectionStorageService } from './common/services/storageService'; import { TestManagerService } from './common/services/testManagerService'; import { TestResultsService } from './common/services/testResultsService'; @@ -13,14 +14,22 @@ import { TestsHelper } from './common/testUtils'; import { TestFlatteningVisitor } from './common/testVisitors/flatteningVisitor'; import { TestFolderGenerationVisitor } from './common/testVisitors/folderGenerationVisitor'; import { TestResultResetVisitor } from './common/testVisitors/resultResetVisitor'; -import { ITestCollectionStorageService, ITestDebugLauncher, ITestDiscoveryService, ITestManager, ITestManagerFactory, ITestManagerService, ITestManagerServiceFactory, IUnitTestSocketServer } from './common/types'; -import { ITestResultsService, ITestsHelper, ITestsParser, ITestVisitor, IWorkspaceTestManagerService, TestProvider } from './common/types'; +import { + ITestCollectionStorageService, ITestConfigSettingsService, ITestDebugLauncher, ITestDiscoveryService, ITestManager, ITestManagerFactory, ITestManagerService, ITestManagerServiceFactory, + ITestResultsService, ITestsHelper, ITestsParser, ITestVisitor, IUnitTestSocketServer, IWorkspaceTestManagerService, TestProvider +} from './common/types'; +import { UnitTestConfigurationService } from './configuration'; +import { TestConfigurationManagerFactory } from './configurationFactory'; +import { TestResultDisplay } from './display/main'; +import { TestDisplay } from './display/picker'; +import { UnitTestManagementService } from './main'; import { TestManager as NoseTestManager } from './nosetest/main'; import { TestDiscoveryService as NoseTestDiscoveryService } from './nosetest/services/discoveryService'; import { TestsParser as NoseTestTestsParser } from './nosetest/services/parserService'; import { TestManager as PyTestTestManager } from './pytest/main'; import { TestDiscoveryService as PytestTestDiscoveryService } from './pytest/services/discoveryService'; import { TestsParser as PytestTestsParser } from './pytest/services/parserService'; +import { ITestConfigurationManagerFactory, ITestDisplay, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestManagementService } from './types'; import { TestManager as UnitTestTestManager } from './unittest/main'; import { TestDiscoveryService as UnitTestTestDiscoveryService } from './unittest/services/discoveryService'; import { TestsParser as UnitTestTestsParser } from './unittest/services/parserService'; @@ -48,6 +57,13 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.add(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); serviceManager.add(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); + serviceManager.addSingleton(IUnitTestConfigurationService, UnitTestConfigurationService); + serviceManager.addSingleton(IUnitTestManagementService, UnitTestManagementService); + serviceManager.addSingleton(ITestResultDisplay, TestResultDisplay); + serviceManager.addSingleton(ITestDisplay, TestDisplay); + serviceManager.addSingleton(ITestConfigSettingsService, TestConfigSettingsService); + serviceManager.addSingleton(ITestConfigurationManagerFactory, TestConfigurationManagerFactory); + serviceManager.addFactory(ITestManagerFactory, (context) => { return (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string) => { const serviceContainer = context.container.get(IServiceContainer); diff --git a/src/client/unittests/types.ts b/src/client/unittests/types.ts new file mode 100644 index 000000000000..ebf063581354 --- /dev/null +++ b/src/client/unittests/types.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Disposable, Event, TextDocument, Uri } from 'vscode'; +import { Product } from '../common/types'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { CommandSource } from './common/constants'; +import { FlattenedTestFunction, ITestManager, TestFile, TestFunction, Tests, TestsToRun, UnitTestProduct } from './common/types'; + +export const IUnitTestConfigurationService = Symbol('IUnitTestConfigurationService'); +export interface IUnitTestConfigurationService { + displayTestFrameworkError(wkspace: Uri): Promise; + selectTestRunner(placeHolderMessage: string): Promise; + enableTest(wkspace: Uri, product: UnitTestProduct); +} + +export const ITestResultDisplay = Symbol('ITestResultDisplay'); + +export interface ITestResultDisplay extends Disposable { + enabled: boolean; + readonly onDidChange: Event; + displayProgressStatus(testRunResult: Promise, debug?: boolean): void; + displayDiscoverStatus(testDiscovery: Promise, quietMode?: boolean): Promise; +} + +export const ITestDisplay = Symbol('ITestDisplay'); +export interface ITestDisplay { + displayStopTestUI(workspace: Uri, message: string): void; + displayTestUI(cmdSource: CommandSource, wkspace: Uri): void; + selectTestFunction(rootDirectory: string, tests: Tests): Promise; + selectTestFile(rootDirectory: string, tests: Tests): Promise; + displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean): void; +} + +export const IUnitTestManagementService = Symbol('IUnitTestManagementService'); +export interface IUnitTestManagementService { + activate(): Promise; + activateCodeLenses(symboldProvider: PythonSymbolProvider): Promise; + getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise; + discoverTestsForDocument(doc: TextDocument): Promise; + autoDiscoverTests(): Promise; + discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean): Promise; + stopTests(resource: Uri): Promise; + displayStopUI(message: string): Promise; + displayUI(cmdSource: CommandSource): Promise; + displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean): Promise; + runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; + runCurrentTestFile(cmdSource: CommandSource): Promise; + + selectAndRunTestFile(cmdSource: CommandSource): Promise; + + selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean): Promise; + + viewOutput(cmdSource: CommandSource): void; +} + +export interface ITestConfigurationManager { + requiresUserToConfigure(wkspace: Uri): Promise; + configure(wkspace: Uri): Promise; + enable(): Promise; + disable(): Promise; +} + +export const ITestConfigurationManagerFactory = Symbol('ITestConfigurationManagerFactory'); +export interface ITestConfigurationManagerFactory { + create(wkspace: Uri, product: Product): ITestConfigurationManager; +} diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 6993b0e0f980..54416559ca30 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,14 +1,15 @@ -import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, Product } from '../../common/types'; +import { Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, outputChannel: OutputChannel, - installer: IInstaller, testConfigSettingsService: ITestConfigSettingsService) { - super(workspace, Product.unittest, outputChannel, installer, testConfigSettingsService); + constructor(workspace: Uri, serviceContainer: IServiceContainer) { + super(workspace, Product.unittest, serviceContainer); + } + public async requiresUserToConfigure(_wkspace: Uri): Promise { + return true; } - // tslint:disable-next-line:no-any public async configure(wkspace: Uri) { const args = ['-v']; const subDirs = await this.getTestDirs(wkspace.fsPath); diff --git a/src/test/common.ts b/src/test/common.ts index 5f6d6640e709..15b600e7d43b 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -2,8 +2,11 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { PythonSettings } from '../client/common/configSettings'; +import { sleep } from './core'; import { IS_MULTI_ROOT_TEST } from './initialize'; +export * from './core'; + const fileInNonRootWorkspace = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); export const rootWorkspaceUri = getWorkspaceRoot(); @@ -30,6 +33,7 @@ export async function updateSetting(setting: PythonSettingKeys, value: {} | unde } // tslint:disable-next-line:await-promise await settings.update(setting, value, configTarget); + await sleep(2000); PythonSettings.dispose(); } @@ -111,10 +115,6 @@ export async function deleteFile(file: string) { } } -export async function sleep(milliseconds: number) { - return new Promise(resolve => setTimeout(resolve, milliseconds)); -} - // tslint:disable-next-line:no-non-null-assertion const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath')!.globalValue; export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index ea1fd200b983..1238dcd6f52e 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; import { ConfigurationService } from '../../client/common/configuration/service'; import { EnumEx } from '../../client/common/enumUtils'; import { createDeferred } from '../../client/common/helpers'; @@ -54,6 +54,7 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IPathUtils, PathUtils); ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); ioc.serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); + ioc.serviceManager.addSingletonInstance(ICommandManager, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingletonInstance(IApplicationShell, TypeMoq.Mock.ofType().object); ioc.serviceManager.addSingleton(IConfigurationService, ConfigurationService); diff --git a/src/test/core.ts b/src/test/core.ts new file mode 100644 index 000000000000..3e67d5543829 --- /dev/null +++ b/src/test/core.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// File without any dependencies on VS Code. + +export async function sleep(milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} diff --git a/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts b/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts index fac9e17b5235..5c7e7ac54691 100644 --- a/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts @@ -6,14 +6,18 @@ // tslint:disable:no-any import * as TypeMoq from 'typemoq'; -import { OutputChannel } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { EnumEx } from '../../../../client/common/enumUtils'; -import { IInstaller, Product } from '../../../../client/common/types'; +import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../../../client/unittests/common/constants'; import { TestConfigurationManager } from '../../../../client/unittests/common/managers/testConfigurationManager'; import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/unittests/common/types'; -import { Uri } from '../../../vscode-mock'; class MockTestConfigurationManager extends TestConfigurationManager { + public requiresUserToConfigure(wkspace: Uri): Promise { + throw new Error('Method not implemented.'); + } public configure(wkspace: any): Promise { throw new Error('Method not implemented.'); } @@ -32,9 +36,11 @@ suite('Unit Test Configuration Manager (unit)', () => { configService = TypeMoq.Mock.ofType(); const outputChannel = TypeMoq.Mock.ofType().object; const installer = TypeMoq.Mock.ofType().object; - - manager = new MockTestConfigurationManager(workspaceUri, product as UnitTestProduct, - outputChannel, installer, configService.object); + const serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))).returns(() => configService.object); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IInstaller))).returns(() => installer); + manager = new MockTestConfigurationManager(workspaceUri, product as UnitTestProduct, serviceContainer.object); }); test('Enabling a test product shoud disable other products', async () => { diff --git a/src/test/unittests/common/services/configSettingService.unit.test.ts b/src/test/unittests/common/services/configSettingService.unit.test.ts new file mode 100644 index 000000000000..722847efce2f --- /dev/null +++ b/src/test/unittests/common/services/configSettingService.unit.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { EnumEx } from '../../../../client/common/enumUtils'; +import { Product } from '../../../../client/common/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { TestConfigSettingsService } from '../../../../client/unittests/common/services/configSettingService'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/unittests/common/types'; + +use(chaiPromise); + +const updateMethods: (keyof ITestConfigSettingsService)[] = ['updateTestArgs', 'disable', 'enable']; + +suite('Unit Tests - ConfigSettingsService', () => { + [Product.pytest, Product.unittest, Product.nosetest].forEach(prodItem => { + const product = prodItem as any as UnitTestProduct; + const prods = EnumEx.getNamesAndValues(Product); + const productName = prods.filter(item => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + updateMethods.forEach(updateMethod => { + suite(`Test '${updateMethod}' method with ${productName.name}`, () => { + let testConfigSettingsService: ITestConfigSettingsService; + let workspaceService: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + workspaceService = typeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + testConfigSettingsService = new TestConfigSettingsService(serviceContainer.object); + }); + function getTestArgSetting(prod: UnitTestProduct) { + switch (prod) { + case Product.unittest: + return 'unitTest.unittestArgs'; + case Product.pytest: + return 'unitTest.pyTestArgs'; + case Product.nosetest: + return 'unitTest.nosetestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + function getTestEnablingSetting(prod: UnitTestProduct) { + switch (prod) { + case Product.unittest: + return 'unitTest.unittestEnabled'; + case Product.pytest: + return 'unitTest.pyTestEnabled'; + case Product.nosetest: + return 'unitTest.nosetestsEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + function getExpectedValueAndSettings(): { configValue: any; configName: string } { + switch (updateMethod) { + case 'disable': { + return { configValue: false, configName: getTestEnablingSetting(product) }; + } + case 'enable': { + return { configValue: true, configName: getTestEnablingSetting(product) }; + } + case 'updateTestArgs': { + return { configValue: ['one', 'two', 'three'], configName: getTestArgSetting(product) }; + } + default: { + throw new Error('Invalid Method'); + } + } + } + test('Update Test Arguments with workspace Uri without workspaces', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => false) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + + pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with one workspace', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with more than one workspace and uri belongs to a workspace', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => workspaceFolder.object) + .verifiable(typeMoq.Times.once()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Expect an exception when updating Test Arguments with workspace Uri with more than one workspace and uri does not belong to a workspace', async () => { + workspaceService.setup(w => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder.setup(w => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + + const { configValue } = getExpectedValueAndSettings(); + + const promise = testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + expect(promise).to.eventually.rejectedWith(); + workspaceService.verifyAll(); + }); + }); + }); + }); +}); diff --git a/src/test/unittests/configuration.unit.test.ts b/src/test/unittests/configuration.unit.test.ts new file mode 100644 index 000000000000..d98da27f72dd --- /dev/null +++ b/src/test/unittests/configuration.unit.test.ts @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { EnumEx } from '../../client/common/enumUtils'; +import { IConfigurationService, IInstaller, IOutputChannel, IPythonSettings, IUnitTestSettings, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; +import { UnitTestProduct } from '../../client/unittests/common/types'; +import { UnitTestConfigurationService } from '../../client/unittests/configuration'; +import { ITestConfigurationManager, ITestConfigurationManagerFactory } from '../../client/unittests/types'; + +suite('Unit Tests - ConfigurationService', () => { + [Product.pytest, Product.unittest, Product.nosetest].forEach(prodItem => { + const product = prodItem as any as UnitTestProduct; + const prods = EnumEx.getNamesAndValues(Product); + const productName = prods.filter(item => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + suite(productName.name, () => { + let testConfigService: typeMoq.IMock; + let workspaceService: typeMoq.IMock; + let factory: typeMoq.IMock; + let appShell: typeMoq.IMock; + let unitTestSettings: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const configurationService = typeMoq.Mock.ofType(); + appShell = typeMoq.Mock.ofType(); + const outputChannel = typeMoq.Mock.ofType(); + const installer = typeMoq.Mock.ofType(); + workspaceService = typeMoq.Mock.ofType(); + factory = typeMoq.Mock.ofType(); + unitTestSettings = typeMoq.Mock.ofType(); + const pythonSettings = typeMoq.Mock.ofType(); + + pythonSettings.setup(p => p.unitTest).returns(() => unitTestSettings.object); + configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigurationManagerFactory))).returns(() => factory.object); + testConfigService = typeMoq.Mock.ofType(UnitTestConfigurationService, typeMoq.MockBehavior.Loose, true, serviceContainer.object); + }); + test('Enable Test when setting unitTest.promptToConfigure is enabled', async () => { + const configMgr = typeMoq.Mock.ofType(); + configMgr.setup(c => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory.setup(f => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting unitTest.promptToConfigure is disabled', async () => { + const configMgr = typeMoq.Mock.ofType(); + configMgr.setup(c => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory.setup(f => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.update(typeMoq.It.isValue('unitTest.promptToConfigure'), typeMoq.It.isValue(undefined))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting unitTest.promptToConfigure is disabled and fail to update the settings', async () => { + const configMgr = typeMoq.Mock.ofType(); + configMgr.setup(c => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory.setup(f => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType(); + workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + const errorMessage = 'Update Failed'; + const updateFailError = new Error(errorMessage); + workspaceConfig.setup(w => w.update(typeMoq.It.isValue('unitTest.promptToConfigure'), typeMoq.It.isValue(undefined))) + .returns(() => Promise.reject(updateFailError)) + .verifiable(typeMoq.Times.once()); + + const promise = testConfigService.target.enableTest(workspaceUri, product); + + await expect(promise).to.eventually.be.rejectedWith(errorMessage); + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Select Test runner displays 3 items', async () => { + const placeHolder = 'Some message'; + appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback(items => expect(items).be.lengthOf(3)) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.selectTestRunner(placeHolder); + appShell.verifyAll(); + }); + test('Ensure selected item is returned', async () => { + const placeHolder = 'Some message'; + const indexes = [Product.unittest, Product.pytest, Product.nosetest]; + appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback(items => expect(items).be.lengthOf(3)) + .returns((items) => items[indexes.indexOf(product)]) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(product); + appShell.verifyAll(); + }); + test('Ensure undefined is returned when nothing is seleted', async () => { + const placeHolder = 'Some message'; + appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(undefined, 'invalid value'); + appShell.verifyAll(); + }); + test('Prompt to enable a test if a test framework is not enabled', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + try { + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch { + exceptionThrown = true; + } + + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('Prompt to select a test if a test framework is not enabled', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + let selectTestRunnerInvoked = false; + try { + testConfigService.callBase = false; + testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(undefined); + }); + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch { + exceptionThrown = true; + } + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Method not invoked'); + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('Configure selected test framework and disable others', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.once()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + let enableTestInvoked = false; + testConfigService.setup(t => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => { + enableTestInvoked = true; + return Promise.resolve(); + }); + + const configMgr = typeMoq.Mock.ofType(); + factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.displayTestFrameworkError(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + expect(enableTestInvoked).to.be.equal(true, 'Enable Test not invoked'); + appShell.verifyAll(); + factory.verifyAll(); + configMgr.verifyAll(); + }); + test('If more than one test framework is enabled, then prompt to select a test framework', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => true); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => true); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => true); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.never()); + + let exceptionThrown = false; + try { + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch { + exceptionThrown = true; + } + + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('If more than one test framework is enabled, then prompt to select a test framework and enable test, but do not configure', async () => { + unitTestSettings.setup(u => u.pyTestEnabled).returns(() => true); + unitTestSettings.setup(u => u.unittestEnabled).returns(() => true); + unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => true); + + appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.never()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + let enableTestInvoked = false; + testConfigService.setup(t => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => { + enableTestInvoked = true; + return Promise.resolve(); + }); + + const configMgr = typeMoq.Mock.ofType(); + factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.never()); + const configManagersToVerify: typeof configMgr[] = [configMgr]; + + [Product.unittest, Product.pytest, Product.nosetest] + .filter(prod => product !== prod) + .forEach(prod => { + const otherTestConfigMgr = typeMoq.Mock.ofType(); + factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(prod))) + .returns(() => otherTestConfigMgr.object) + .verifiable(typeMoq.Times.once()); + otherTestConfigMgr.setup(c => c.disable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + configManagersToVerify.push(otherTestConfigMgr); + }); + + await testConfigService.target.displayTestFrameworkError(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + expect(enableTestInvoked).to.be.equal(false, 'Enable Test is invoked'); + factory.verifyAll(); + appShell.verifyAll(); + for (const item of configManagersToVerify) { + item.verifyAll(); + } + }); + }); + }); +}); diff --git a/src/test/unittests/configurationFactory.unit.test.ts b/src/test/unittests/configurationFactory.unit.test.ts new file mode 100644 index 000000000000..23e316f4ef55 --- /dev/null +++ b/src/test/unittests/configurationFactory.unit.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; +import { ITestConfigSettingsService } from '../../client/unittests/common/types'; +import { TestConfigurationManagerFactory } from '../../client/unittests/configurationFactory'; +import * as nose from '../../client/unittests/nosetest/testConfigurationManager'; +import * as pytest from '../../client/unittests/pytest/testConfigurationManager'; +import { ITestConfigurationManagerFactory } from '../../client/unittests/types'; +import * as unittest from '../../client/unittests/unittest/testConfigurationManager'; + +use(chaiAsPromised); + +suite('Unit Tests - ConfigurationManagerFactory', () => { + let factory: ITestConfigurationManagerFactory; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const outputChannel = typeMoq.Mock.ofType(); + const installer = typeMoq.Mock.ofType(); + const testConfigService = typeMoq.Mock.ofType(); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigSettingsService))).returns(() => testConfigService.object); + factory = new TestConfigurationManagerFactory(serviceContainer.object); + }); + test('Create Unit Test Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.unittest); + expect(configMgr).to.be.instanceOf(unittest.ConfigurationManager); + }); + test('Create pytest Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.pytest); + expect(configMgr).to.be.instanceOf(pytest.ConfigurationManager); + }); + test('Create nose Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.nosetest); + expect(configMgr).to.be.instanceOf(nose.ConfigurationManager); + }); +}); diff --git a/src/test/unittests/display/main.test.ts b/src/test/unittests/display/main.test.ts new file mode 100644 index 000000000000..67de45bdaaf6 --- /dev/null +++ b/src/test/unittests/display/main.test.ts @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { StatusBarItem, Uri } from 'vscode'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { noop } from '../../../client/common/core.utils'; +import { createDeferred } from '../../../client/common/helpers'; +import { IConfigurationService, IPythonSettings, IUnitTestSettings } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CANCELLATION_REASON } from '../../../client/unittests/common/constants'; +import { ITestsHelper, Tests } from '../../../client/unittests/common/types'; +import { TestResultDisplay } from '../../../client/unittests/display/main'; +import { sleep } from '../../core'; + +suite('Unit Tests - TestResultDisplay', () => { + const workspaceUri = Uri.file(__filename); + let appShell: typeMoq.IMock; + let unitTestSettings: typeMoq.IMock; + let serviceContainer: typeMoq.IMock; + let display: TestResultDisplay; + let testsHelper: typeMoq.IMock; + let configurationService: typeMoq.IMock; + setup(() => { + serviceContainer = typeMoq.Mock.ofType(); + configurationService = typeMoq.Mock.ofType(); + appShell = typeMoq.Mock.ofType(); + unitTestSettings = typeMoq.Mock.ofType(); + const pythonSettings = typeMoq.Mock.ofType(); + testsHelper = typeMoq.Mock.ofType(); + + pythonSettings.setup(p => p.unitTest).returns(() => unitTestSettings.object); + configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestsHelper))).returns(() => testsHelper.object); + }); + teardown(() => { + try { + display.dispose(); + } catch { noop(); } + }); + function createTestResultDisplay() { + display = new TestResultDisplay(serviceContainer.object); + } + test('Should create a status bar item upon instantiation', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + appShell.verifyAll(); + }); + test('Should be disabled upon instantiation', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + appShell.verifyAll(); + expect(display.enabled).to.be.equal(false, 'not disabled'); + }); + test('Enable display should show the statusbar', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.enabled = true; + statusBar.verifyAll(); + }); + test('Disable display should hide the statusbar', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.hide()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.enabled = false; + statusBar.verifyAll(); + }); + test('Ensure status bar is displayed and updated with progress with ability to stop tests', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.displayProgressStatus(createDeferred().promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with success with ability to view ui without any results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + tests.setup((t: any) => t.then).returns(() => undefined); + tests.setup(t => t.summary).returns(() => { + return { errors: 0, failures: 0, passed: 0, skipped: 0 }; + }).verifiable(typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + def.resolve(tests.object); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with success with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + tests.setup((t: any) => t.then).returns(() => undefined); + tests.setup(t => t.summary).returns(() => { + return { errors: 0, failures: 0, passed: 1, skipped: 0 }; + }).verifiable(typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.never()); + + def.resolve(tests.object); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with error when cancelled by user with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + testsHelper.setup(t => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + + def.reject(CANCELLATION_REASON); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + testsHelper.verifyAll(); + }); + test('Ensure status bar is updated, and error message display with error in running tests, with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); + + testsHelper.setup(t => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.once()); + + def.reject('Some other reason'); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + testsHelper.verifyAll(); + }); + + test('Ensure status bar is displayed and updated with progress with ability to stop test discovery', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.displayDiscoverStatus(createDeferred().promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is displayed and updated with success and no tests, with ability to view ui to view results of test discovery', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + }); + test('Ensure tests are disabled when there are errors and user choses to disable tests', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType(); + appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((msg, item) => Promise.resolve(item)) + .verifiable(typeMoq.Times.once()); + + for (const setting of ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', + 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']) { + configurationService.setup(c => c.updateSettingAsync(typeMoq.It.isValue(setting), typeMoq.It.isValue(false))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + } + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); + test('Ensure status bar is displayed and updated with error info when test discovery is cancelled by the user', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showErrorMessage(typeMoq.It.isAny())) + .verifiable(typeMoq.Times.never()); + + def.reject(CANCELLATION_REASON); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Discover), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); + test('Ensure status bar is displayed and updated with error info, and message is displayed when test discovery is fails due to errors', async () => { + const statusBar = typeMoq.Mock.ofType(); + appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); + statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); + + appShell.setup(a => a.showErrorMessage(typeMoq.It.isAny())) + .verifiable(typeMoq.Times.once()); + + def.reject('some weird error'); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Discover), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); +}); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 0e150c2aef1b..2c3524f63311 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -15,15 +15,6 @@ const mockedVSCode: Partial = {}; const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; const originalLoad = Module._load; -generateMock('workspace'); -generateMock('window'); -generateMock('commands'); -generateMock('languages'); -generateMock('env'); -generateMock('debug'); -generateMock('extensions'); -generateMock('scm'); - function generateMock(name: K): void { const mockedObj = TypeMoq.Mock.ofType(); mockedVSCode[name] = mockedObj.object; @@ -31,6 +22,15 @@ function generateMock(name: K): void { } export function initialize() { + generateMock('workspace'); + generateMock('window'); + generateMock('commands'); + generateMock('languages'); + generateMock('env'); + generateMock('debug'); + generateMock('extensions'); + generateMock('scm'); + Module._load = function (request, parent) { if (request === 'vscode') { return mockedVSCode; @@ -76,9 +76,14 @@ export class Uri implements vscode.Uri { throw new Error('Not implemented'); } public toString(skipEncoding?: boolean): string { - throw new Error('Not implemented'); + return this.fsPath; } public toJSON(): any { return this.fsPath; } } + +mockedVSCode.Uri = Uri as any; +// tslint:disable-next-line:no-function-expression +mockedVSCode.EventEmitter = function () { return TypeMoq.Mock.ofType>(); } as any; +mockedVSCode.StatusBarAlignment = TypeMoq.Mock.ofType().object as any;