From 482d4bebe5406c8510960647deaa29a8fe589868 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 16:53:58 -0700 Subject: [PATCH 01/23] fixes to unit tests and forgotten multiroot --- package.json | 5 +- src/client/common/contextKey.ts | 15 ++ src/client/common/installer.ts | 51 +++--- src/client/extension.ts | 39 ----- .../locators/services/virtualEnvService.ts | 4 +- .../providers/execInTerminalProvider.ts | 148 ++++++++++++------ .../providers/setInterpreterProvider.ts | 4 +- .../common/configSettings.multiroot.test.ts | 4 +- src/test/unittests/nosetest.test.ts | 44 ++---- src/test/unittests/pytest.test.ts | 44 ++---- src/test/unittests/unittest.test.ts | 40 ++--- 11 files changed, 197 insertions(+), 201 deletions(-) create mode 100644 src/client/common/contextKey.ts diff --git a/package.json b/package.json index c4be011f6f9f..887c63a47f5b 100644 --- a/package.json +++ b/package.json @@ -1626,13 +1626,13 @@ "vscode-languageserver": "^3.1.0", "winreg": "^1.2.4", "xml2js": "^0.4.17", - "vscode": "^1.0.3" + "vscode": "^1.1.5" }, "devDependencies": { "@types/fs-extra": "^4.0.2", "@types/jquery": "^1.10.31", "@types/lodash": "^4.14.74", - "@types/mocha": "^2.2.32", + "@types/mocha": "^2.2.43", "@types/node": "^6.0.40", "@types/rx": "^2.5.33", "@types/semver": "^5.4.0", @@ -1660,7 +1660,6 @@ "tslint-microsoft-contrib": "^5.0.1", "typescript": "^2.5.2", "typescript-formatter": "^6.0.0", - "vscode": "^1.1.5", "webpack": "^1.13.2" } } diff --git a/src/client/common/contextKey.ts b/src/client/common/contextKey.ts new file mode 100644 index 000000000000..87fe57f07d34 --- /dev/null +++ b/src/client/common/contextKey.ts @@ -0,0 +1,15 @@ +import { commands } from 'vscode'; + +export class ContextKey { + private lastValue: boolean; + + constructor(private name: string) { } + + public async set(value: boolean): Promise { + if (this.lastValue === value) { + return; + } + this.lastValue = value; + await commands.executeCommand('setContext', this.name, this.lastValue); + } +} diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index 6cdd8a67608f..fb8042454455 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -1,28 +1,29 @@ -import { error } from './logger'; -import * as vscode from 'vscode'; -import * as settings from './configSettings'; import * as os from 'os'; +import * as vscode from 'vscode'; import { commands, ConfigurationTarget, Disposable, OutputChannel, Terminal, Uri, window, workspace } from 'vscode'; +import * as settings from './configSettings'; import { isNotInstalledError } from './helpers'; +import { error } from './logger'; import { execPythonFile, getFullyQualifiedPythonInterpreterPath } from './utils'; export enum Product { - pytest, - nosetest, - pylint, - flake8, - pep8, - pylama, - prospector, - pydocstyle, - yapf, - autopep8, - mypy, - unittest, - ctags, - rope + pytest = 1, + nosetest = 2, + pylint = 3, + flake8 = 4, + pep8 = 5, + pylama = 6, + prospector = 7, + pydocstyle = 8, + yapf = 9, + autopep8 = 10, + mypy = 11, + unittest = 12, + ctags = 13, + rope = 14 } +// tslint:disable-next-line:variable-name const ProductInstallScripts = new Map(); ProductInstallScripts.set(Product.autopep8, ['-m', 'pip', 'install', 'autopep8']); ProductInstallScripts.set(Product.flake8, ['-m', 'pip', 'install', 'flake8']); @@ -37,6 +38,7 @@ ProductInstallScripts.set(Product.pytest, ['-m', 'pip', 'install', '-U', 'pytest ProductInstallScripts.set(Product.yapf, ['-m', 'pip', 'install', 'yapf']); ProductInstallScripts.set(Product.rope, ['-m', 'pip', 'install', 'rope']); +// tslint:disable-next-line:variable-name const ProductUninstallScripts = new Map(); ProductUninstallScripts.set(Product.autopep8, ['-m', 'pip', 'uninstall', 'autopep8', '--yes']); ProductUninstallScripts.set(Product.flake8, ['-m', 'pip', 'uninstall', 'flake8', '--yes']); @@ -51,6 +53,7 @@ ProductUninstallScripts.set(Product.pytest, ['-m', 'pip', 'uninstall', 'pytest', ProductUninstallScripts.set(Product.yapf, ['-m', 'pip', 'uninstall', 'yapf', '--yes']); ProductUninstallScripts.set(Product.rope, ['-m', 'pip', 'uninstall', 'rope', '--yes']); +// tslint:disable-next-line:variable-name export const ProductExecutableAndArgs = new Map(); ProductExecutableAndArgs.set(Product.mypy, { executable: 'python', args: ['-m', 'mypy'] }); ProductExecutableAndArgs.set(Product.nosetest, { executable: 'python', args: ['-m', 'nose'] }); @@ -77,6 +80,7 @@ switch (os.platform()) { } } +// tslint:disable-next-line:variable-name export const Linters: Product[] = [ Product.flake8, Product.pep8, @@ -87,6 +91,7 @@ export const Linters: Product[] = [ Product.pydocstyle ]; +// tslint:disable-next-line:variable-name const ProductNames = new Map(); ProductNames.set(Product.autopep8, 'autopep8'); ProductNames.set(Product.flake8, 'flake8'); @@ -101,6 +106,7 @@ ProductNames.set(Product.pytest, 'py.test'); ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.rope, 'rope'); +// tslint:disable-next-line:variable-name export const SettingToDisableProduct = new Map(); SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled'); SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled'); @@ -112,6 +118,7 @@ SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled'); SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled'); SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled'); +// tslint:disable-next-line:variable-name const ProductInstallationPrompt = new Map(); ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols'); @@ -123,6 +130,7 @@ enum ProductType { WorkspaceSymbols } +// tslint:disable-next-line:variable-name const ProductTypeNames = new Map(); ProductTypeNames.set(ProductType.Formatter, 'Formatter'); ProductTypeNames.set(ProductType.Linter, 'Linter'); @@ -130,6 +138,7 @@ ProductTypeNames.set(ProductType.RefactoringLibrary, 'Refactoring library'); ProductTypeNames.set(ProductType.TestFramework, 'Test Framework'); ProductTypeNames.set(ProductType.WorkspaceSymbols, 'Workspace Symbols'); +// tslint:disable-next-line:variable-name const ProductTypes = new Map(); ProductTypes.set(Product.flake8, ProductType.Linter); ProductTypes.set(Product.mypy, ProductType.Linter); @@ -165,23 +174,26 @@ export class Installer implements vscode.Disposable { this.disposables.forEach(d => d.dispose()); } private shouldDisplayPrompt(product: Product) { + // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; const pythonConfig = workspace.getConfiguration('python'); const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); return disablePromptForFeatures.indexOf(productName) === -1; } + // tslint:disable-next-line:member-ordering public async promptToInstall(product: Product, resource?: Uri): Promise { + // tslint:disable-next-line:no-non-null-assertion const productType = ProductTypes.get(product)!; const productTypeName = ProductTypeNames.get(productType); + // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; if (!this.shouldDisplayPrompt(product)) { const message = `${productTypeName} '${productName}' not installed.`; if (this.outputChannel) { this.outputChannel.appendLine(message); - } - else { + } else { console.warn(message); } return InstallerResponse.Ignore; @@ -229,6 +241,7 @@ export class Installer implements vscode.Disposable { } } } + // tslint:disable-next-line:member-ordering public async install(product: Product, resource?: Uri): Promise { if (!this.outputChannel && !Installer.terminal) { Installer.terminal = window.createTerminal('Python Installer'); diff --git a/src/client/extension.ts b/src/client/extension.ts index dc7c850595e6..37c2b5bc940f 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -46,8 +46,6 @@ const activationDeferred = createDeferred(); export const activated = activationDeferred.promise; export async function activate(context: vscode.ExtensionContext) { const pythonSettings = settings.PythonSettings.getInstance(); - const pythonExt = new PythonExt(); - context.subscriptions.push(pythonExt); sendStartupTelemetry(); lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow); formatOutChannel = lintingOutChannel; @@ -151,43 +149,6 @@ export async function activate(context: vscode.ExtensionContext) { activationDeferred.resolve(); } -class PythonExt implements vscode.Disposable { - - private isDjangoProject: ContextKey; - - constructor() { - this.isDjangoProject = new ContextKey('python.isDjangoProject'); - this.ensureState(); - } - public dispose() { - this.isDjangoProject = null; - } - private ensureState(): void { - // context: python.isDjangoProject - if (typeof vscode.workspace.rootPath === 'string') { - this.isDjangoProject.set(fs.existsSync(vscode.workspace.rootPath.concat("/manage.py"))); - } - else { - this.isDjangoProject.set(false); - } - } -} - -class ContextKey { - private lastValue: boolean; - - constructor(private name: string) { - } - - public set(value: boolean): void { - if (this.lastValue === value) { - return; - } - this.lastValue = value; - vscode.commands.executeCommand('setContext', this.name, this.lastValue); - } -} - function sendStartupTelemetry() { telemetryHelper.sendTelemetryEvent(telemetryContracts.EVENT_LOAD); } diff --git a/src/client/interpreter/locators/services/virtualEnvService.ts b/src/client/interpreter/locators/services/virtualEnvService.ts index a10818f1c3ec..d7430dad07ad 100644 --- a/src/client/interpreter/locators/services/virtualEnvService.ts +++ b/src/client/interpreter/locators/services/virtualEnvService.ts @@ -74,8 +74,8 @@ export function getKnownSearchPathsForVirtualEnvs(resource?: Uri): string[] { if (venvPath) { paths.push(untildify(venvPath)); } - if (workspace.rootPath) { - paths.push(workspace.rootPath); + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 0) { + paths.push(workspace.workspaceFolders[0].uri.fsPath); } return paths; } diff --git a/src/client/providers/execInTerminalProvider.ts b/src/client/providers/execInTerminalProvider.ts index 15a2277f4525..c25d784dc5d2 100644 --- a/src/client/providers/execInTerminalProvider.ts +++ b/src/client/providers/execInTerminalProvider.ts @@ -1,12 +1,15 @@ 'use strict'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; import * as vscode from 'vscode'; +import { Disposable, workspace } from 'vscode'; import * as settings from '../common/configSettings'; import { Commands, PythonLanguage } from '../common/constants'; -import { EOL } from 'os'; -let path = require('path'); -let terminal: vscode.Terminal; +import { ContextKey } from '../common/contextKey'; import { IS_WINDOWS } from '../common/utils'; +let terminal: vscode.Terminal; export function activateExecInTerminalProvider(): vscode.Disposable[] { const disposables: vscode.Disposable[] = []; disposables.push(vscode.commands.registerCommand(Commands.Exec_In_Terminal, execInTerminal)); @@ -17,13 +20,14 @@ export function activateExecInTerminalProvider(): vscode.Disposable[] { terminal = null; } })); + disposables.push(new DjangoContextInitializer()); return disposables; } function removeBlankLines(code: string): string { - let codeLines = code.split(/\r?\n/g); - let codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); - let lastLineIsEmpty = codeLines.length > 0 && codeLines[codeLines.length - 1].trim().length === 0; + const codeLines = code.split(/\r?\n/g); + const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); + const lastLineIsEmpty = codeLines.length > 0 && codeLines[codeLines.length - 1].trim().length === 0; if (lastLineIsEmpty) { codeLinesWithoutEmptyLines.unshift(''); } @@ -31,9 +35,10 @@ function removeBlankLines(code: string): string { } function execInTerminal(fileUri?: vscode.Uri) { const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - let pythonSettings = settings.PythonSettings.getInstance(fileUri); + const pythonSettings = settings.PythonSettings.getInstance(fileUri); let filePath: string; let currentPythonPath = pythonSettings.pythonPath; @@ -67,26 +72,25 @@ function execInTerminal(fileUri?: vscode.Uri) { filePath = `"${filePath}"`; } - terminal = terminal ? terminal : vscode.window.createTerminal(`Python`); + terminal = terminal ? terminal : vscode.window.createTerminal('Python'); if (pythonSettings.terminal && pythonSettings.terminal.executeInFileDir) { const fileDirPath = path.dirname(filePath); - if (fileDirPath !== vscode.workspace.rootPath && fileDirPath.substring(1) !== vscode.workspace.rootPath) { + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + if (wkspace && fileDirPath !== wkspace.uri.fsPath && fileDirPath.substring(1) !== wkspace.uri.fsPath) { terminal.sendText(`cd "${fileDirPath}"`); } } const launchArgs = settings.PythonSettings.getInstance(fileUri).terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; const command = `${currentPythonPath}${launchArgsString} ${filePath}`; if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, "/"); + const commandWin = command.replace(/\\/g, '/'); if (IS_POWERSHELL) { terminal.sendText(`& ${commandWin}`); - } - else { + } else { terminal.sendText(commandWin); } - } - else { + } else { terminal.sendText(command); } terminal.show(); @@ -99,6 +103,7 @@ function execSelectionInTerminal() { } const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; @@ -110,9 +115,8 @@ function execSelectionInTerminal() { let code: string; if (selection.isEmpty) { code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; - } - else { - let textRange = new vscode.Range(selection.start, selection.end); + } else { + const textRange = new vscode.Range(selection.start, selection.end); code = vscode.window.activeTextEditor.document.getText(textRange); } if (code.length === 0) { @@ -120,28 +124,26 @@ function execSelectionInTerminal() { } code = removeBlankLines(code); const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; const command = `${currentPythonPath}${launchArgsString}`; if (!terminal) { - terminal = vscode.window.createTerminal(`Python`); + terminal = vscode.window.createTerminal('Python'); if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, "/"); + const commandWin = command.replace(/\\/g, '/'); if (IS_POWERSHELL) { terminal.sendText(`& ${commandWin}`); - } - else { + } else { terminal.sendText(commandWin); } - } - else { + } else { terminal.sendText(command); } } - const unix_code = code.replace(/\r\n/g, "\n"); + // tslint:disable-next-line:variable-name + const unix_code = code.replace(/\r\n/g, '\n'); if (IS_WINDOWS) { - terminal.sendText(unix_code.replace(/\n/g, "\r\n")); - } - else { + terminal.sendText(unix_code.replace(/\n/g, '\r\n')); + } else { terminal.sendText(unix_code); } terminal.show(); @@ -154,6 +156,7 @@ function execSelectionInDjangoShell() { } const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); + // tslint:disable-next-line:no-backbone-get-set-outside-model const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; @@ -161,44 +164,99 @@ function execSelectionInDjangoShell() { currentPythonPath = `"${currentPythonPath}"`; } - const workspaceRoot = vscode.workspace.rootPath; - const djangoShellCmd = `"${workspaceRoot}/manage.py" shell`; + const workspaceUri = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + const defaultWorkspace = Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri.fsPath : ''; + const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; + const djangoShellCmd = `"${path.join(workspaceRoot, 'manage.py')}" shell`; const selection = vscode.window.activeTextEditor.selection; let code: string; if (selection.isEmpty) { code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; - } - else { - let textRange = new vscode.Range(selection.start, selection.end); + } else { + const textRange = new vscode.Range(selection.start, selection.end); code = vscode.window.activeTextEditor.document.getText(textRange); } if (code.length === 0) { return; } const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? " ".concat(launchArgs.join(" ")) : ""; + const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; const command = `${currentPythonPath}${launchArgsString} ${djangoShellCmd}`; if (!terminal) { - terminal = vscode.window.createTerminal(`Django Shell`); + terminal = vscode.window.createTerminal('Django Shell'); if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, "/"); + const commandWin = command.replace(/\\/g, '/'); if (IS_POWERSHELL) { terminal.sendText(`& ${commandWin}`); - } - else { + } else { terminal.sendText(commandWin); } - } - else { + } else { terminal.sendText(command); } } - const unix_code = code.replace(/\r\n/g, "\n"); + // tslint:disable-next-line:variable-name + const unix_code = code.replace(/\r\n/g, '\n'); if (IS_WINDOWS) { - terminal.sendText(unix_code.replace(/\n/g, "\r\n")); - } - else { + terminal.sendText(unix_code.replace(/\n/g, '\r\n')); + } else { terminal.sendText(unix_code); } terminal.show(); } + +class DjangoContextInitializer implements vscode.Disposable { + private isDjangoProject: ContextKey; + private monitoringActiveTextEditor: boolean; + private workspaceContextKeyValues = new Map(); + private lastCheckedWorkspace: string; + private disposables: Disposable[] = []; + constructor() { + this.isDjangoProject = new ContextKey('python.isDjangoProject'); + this.ensureState(); + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace())); + } + + public dispose() { + this.isDjangoProject = null; + this.disposables.forEach(disposable => disposable.dispose()); + } + private updateContextKeyBasedOnActiveWorkspace() { + if (this.monitoringActiveTextEditor) { + return; + } + this.monitoringActiveTextEditor = true; + this.disposables.push(vscode.window.onDidChangeActiveTextEditor(() => this.ensureState())); + } + private getActiveWorkspace(): string | undefined { + if (!Array.isArray(workspace.workspaceFolders || workspace.workspaceFolders.length === 0)) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri.fsPath; + } + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return undefined; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + private async ensureState(): Promise { + const activeWorkspace = this.getActiveWorkspace(); + if (!activeWorkspace) { + return await this.isDjangoProject.set(false); + } + if (this.lastCheckedWorkspace === activeWorkspace) { + return; + } + if (this.workspaceContextKeyValues.has(activeWorkspace)) { + await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)); + } else { + const exists = await fs.pathExists(path.join(activeWorkspace, 'manage.py')); + await this.isDjangoProject.set(exists); + this.workspaceContextKeyValues.set(activeWorkspace, exists); + this.lastCheckedWorkspace = activeWorkspace; + } + } +} diff --git a/src/client/providers/setInterpreterProvider.ts b/src/client/providers/setInterpreterProvider.ts index 47cdc95de966..29039fca8272 100644 --- a/src/client/providers/setInterpreterProvider.ts +++ b/src/client/providers/setInterpreterProvider.ts @@ -59,8 +59,8 @@ export class SetInterpreterProvider implements Disposable { const resourceUri = targetConfig ? targetConfig.folderUri : undefined; const suggestions = await this.getSuggestions(resourceUri); let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; - if (workspace.rootPath && currentPythonPath.startsWith(workspace.rootPath)) { - currentPythonPath = `.${path.sep}${path.relative(workspace.rootPath, currentPythonPath)}`; + if (targetConfig.folderUri && currentPythonPath.startsWith(targetConfig.folderUri.fsPath)) { + currentPythonPath = `.${path.sep}${path.relative(targetConfig.folderUri.fsPath, currentPythonPath)}`; } const quickPickOptions: QuickPickOptions = { matchOnDetail: true, diff --git a/src/test/common/configSettings.multiroot.test.ts b/src/test/common/configSettings.multiroot.test.ts index dc7ef9590885..e7efd550c895 100644 --- a/src/test/common/configSettings.multiroot.test.ts +++ b/src/test/common/configSettings.multiroot.test.ts @@ -8,7 +8,9 @@ import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } fr const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); // tslint:disable-next-line:max-func-body-length -suite('Multiroot Config Settings', () => { +suite('Multiroot Config Settings', function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index ddf743277aaa..4f25d95bb620 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -17,7 +17,9 @@ const filesToDelete = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Unit Tests (nosetest)', () => { +suite('Unit Tests (nosetest)', function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; const rootDirectory = UNITTEST_TEST_FILES_PATH; let testManager: nose.TestManager; @@ -54,9 +56,7 @@ suite('Unit Tests (nosetest)', () => { testManager = new nose.TestManager(rootDir, outChannel); } - test('Discover Tests (single test file)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (single test file)', async () => { createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); @@ -65,9 +65,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_one.py') && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Check that nameToRun in testSuits has class name after : (single test file)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Check that nameToRun in testSuits has class name after : (single test file)', async () => { createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); @@ -80,9 +78,7 @@ suite('Unit Tests (nosetest)', () => { const found = tests.testFiles.some(t => t.name === testFile && t.nameToRun === t.name); assert.equal(found, true, `Test File not found '${testFile}'`); } - test('Discover Tests (-m=test)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (-m=test)', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -96,9 +92,7 @@ suite('Unit Tests (nosetest)', () => { lookForTestFile(tests, 'test_root.py'); }); - test('Discover Tests (-w=specific -m=tst)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (-w=specific -m=tst)', async () => { await updateSetting('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -109,9 +103,7 @@ suite('Unit Tests (nosetest)', () => { lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); }); - test('Discover Tests (-m=test_)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (-m=test_)', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -121,9 +113,7 @@ suite('Unit Tests (nosetest)', () => { lookForTestFile(tests, 'test_root.py'); }); - test('Run Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Tests', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); @@ -133,9 +123,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.skipped, 2, 'skipped'); }); - test('Run Failed Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Failed Tests', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); @@ -151,9 +139,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.skipped, 0, 'skipped again'); }); - test('Run Specific Test File', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test File', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -168,9 +154,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Suite', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test Suite', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -185,9 +169,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Function', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test Function', async () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index 77086d86808f..123e72b03711 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -14,7 +14,9 @@ const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(__dirname, '..', '..', ' const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); // tslint:disable-next-line:max-func-body-length -suite('Unit Tests (PyTest)', () => { +suite('Unit Tests (PyTest)', function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); let rootDirectory = UNITTEST_TEST_FILES_PATH; let testManager: pytest.TestManager; let testResultDisplay: TestResultDisplay; @@ -40,9 +42,7 @@ suite('Unit Tests (PyTest)', () => { testManager = new pytest.TestManager(rootDir, outChannel); } - test('Discover Tests (single test file)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (single test file)', async () => { testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); @@ -52,9 +52,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Discover Tests (pattern = test_)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (pattern = test_)', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -69,9 +67,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Discover Tests (pattern = _test)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (pattern = _test)', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -81,9 +77,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Discover Tests (with config)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (with config)', async () => { await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); rootDirectory = UNITTEST_TEST_FILES_PATH_WITH_CONFIGS; createTestManager(); @@ -95,9 +89,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Run Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Tests', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); @@ -107,9 +99,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 3, 'skipped'); }); - test('Run Failed Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Failed Tests', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); @@ -125,9 +115,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'Failed skipped'); }); - test('Run Specific Test File', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test File', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); await testManager.discoverTests(true, true); @@ -148,9 +136,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - test('Run Specific Test Suite', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test Suite', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -162,9 +148,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Function', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test Function', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -176,9 +160,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - test('Setting cwd should return tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Setting cwd should return tests', async () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index be12df3af776..de409344da8c 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -23,7 +23,9 @@ const defaultUnitTestArgs = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Unit Tests (unittest)', () => { +suite('Unit Tests (unittest)', function () { + // tslint:disable-next-line:no-invalid-this + this.retries(3); let testManager: unittest.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: MockOutputChannel; @@ -51,9 +53,7 @@ suite('Unit Tests (unittest)', () => { testManager = new unittest.TestManager(rootDir, outChannel); } - test('Discover Tests (single test file)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (single test file)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); const tests = await testManager.discoverTests(true, true); @@ -63,9 +63,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); }); - test('Discover Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -76,9 +74,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found'); }); - test('Discover Tests (pattern = *_test_*.py)', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Discover Tests (pattern = *_test_*.py)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -88,9 +84,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found'); }); - test('Run Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', 'test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); const results = await testManager.runTest(); @@ -100,9 +94,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Failed Tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Failed Tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); let results = await testManager.runTest(); @@ -118,9 +110,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 0, 'Failed skipped'); }); - test('Run Specific Test File', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test File', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); const tests = await testManager.discoverTests(true, true); @@ -136,9 +126,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Suite', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test Suite', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestSpecificTestFilesPath); const tests = await testManager.discoverTests(true, true); @@ -154,9 +142,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 1, 'skipped'); }); - test('Run Specific Test Function', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Run Specific Test Function', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_unittest*.py'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); @@ -168,9 +154,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.skipped, 0, 'skipped'); }); - test('Setting cwd should return tests', async function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); + test('Setting cwd should return tests', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManager(unitTestTestFilesCwdPath); From 62683ff9969b9872858870e72c172a39346b5747 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 17:33:15 -0700 Subject: [PATCH 02/23] globally retry all tests 3 times --- src/test/common/configSettings.multiroot.test.ts | 4 +--- src/test/index.ts | 3 ++- src/test/unittests/nosetest.test.ts | 4 +--- src/test/unittests/pytest.test.ts | 4 +--- src/test/unittests/unittest.test.ts | 4 +--- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/test/common/configSettings.multiroot.test.ts b/src/test/common/configSettings.multiroot.test.ts index e7efd550c895..dc7ef9590885 100644 --- a/src/test/common/configSettings.multiroot.test.ts +++ b/src/test/common/configSettings.multiroot.test.ts @@ -8,9 +8,7 @@ import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } fr const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); // tslint:disable-next-line:max-func-body-length -suite('Multiroot Config Settings', function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); +suite('Multiroot Config Settings', () => { suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this diff --git a/src/test/index.ts b/src/test/index.ts index 077d65e200e8..64de9103f775 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -7,6 +7,7 @@ process.env['VSC_PYTHON_CI_TEST'] = '1'; testRunner.configure({ ui: 'tdd', useColors: true, - timeout: 25000 + timeout: 25000, + retries: 3 }); module.exports = testRunner; diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index 4f25d95bb620..d61e4e0d9944 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -17,9 +17,7 @@ const filesToDelete = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Unit Tests (nosetest)', function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); +suite('Unit Tests (nosetest)', () => { const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; const rootDirectory = UNITTEST_TEST_FILES_PATH; let testManager: nose.TestManager; diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index 123e72b03711..c0fb8dc64540 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -14,9 +14,7 @@ const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(__dirname, '..', '..', ' const unitTestTestFilesCwdPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); // tslint:disable-next-line:max-func-body-length -suite('Unit Tests (PyTest)', function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); +suite('Unit Tests (PyTest)', () => { let rootDirectory = UNITTEST_TEST_FILES_PATH; let testManager: pytest.TestManager; let testResultDisplay: TestResultDisplay; diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index de409344da8c..478111c1d620 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -23,9 +23,7 @@ const defaultUnitTestArgs = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Unit Tests (unittest)', function () { - // tslint:disable-next-line:no-invalid-this - this.retries(3); +suite('Unit Tests (unittest)', () => { let testManager: unittest.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: MockOutputChannel; From 75570ee259647c7177bb829e4d3c7bd242c5ab9d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 20:02:09 -0700 Subject: [PATCH 03/23] refactor changing interpreters --- src/client/extension.ts | 64 +++--- .../configuration/pythonPathUpdaterService.ts | 42 ++++ .../pythonPathUpdaterServiceFactory.ts | 18 ++ .../services/globalUpdaterService.ts | 16 ++ .../services/workspaceFolderUpdaterService.ts | 23 ++ .../services/workspaceUpdaterService.ts | 23 ++ .../configuration/setInterpreterProvider.ts | 114 ++++++++++ src/client/interpreter/configuration/types.ts | 12 + .../display/shebangCodeLensProvider.ts | 71 ++++++ src/client/interpreter/helpers.ts | 47 ---- src/client/interpreter/index.ts | 56 +---- .../providers/setInterpreterProvider.ts | 210 +++++++++--------- .../providers/shebangCodeLensProvider.ts | 128 +++++------ .../shebangCodeLenseProvider.test.ts | 19 +- 14 files changed, 535 insertions(+), 308 deletions(-) create mode 100644 src/client/interpreter/configuration/pythonPathUpdaterService.ts create mode 100644 src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts create mode 100644 src/client/interpreter/configuration/services/globalUpdaterService.ts create mode 100644 src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts create mode 100644 src/client/interpreter/configuration/services/workspaceUpdaterService.ts create mode 100644 src/client/interpreter/configuration/setInterpreterProvider.ts create mode 100644 src/client/interpreter/configuration/types.ts create mode 100644 src/client/interpreter/display/shebangCodeLensProvider.ts diff --git a/src/client/extension.ts b/src/client/extension.ts index 37c2b5bc940f..bb5cf5252048 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,41 +1,41 @@ 'use strict'; +import * as fs from 'fs'; +import * as os from 'os'; +import { workspace } from 'vscode'; import * as vscode from 'vscode'; -import { JediFactory } from './languageServices/jediProxyFactory'; +import * as settings from './common/configSettings'; +import { Commands } from './common/constants'; import { createDeferred } from './common/helpers'; +import * as telemetryHelper from './common/telemetry'; +import * as telemetryContracts from './common/telemetryContracts'; +import { SimpleConfigurationProvider } from './debugger'; +import { HelpProvider } from './helpProvider'; +import { InterpreterManager } from './interpreter'; +import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider'; +import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; +import * as jup from './jupyter/main'; +import { JupyterProvider } from './jupyter/provider'; +import { JediFactory } from './languageServices/jediProxyFactory'; import { PythonCompletionItemProvider } from './providers/completionProvider'; -import { PythonHoverProvider } from './providers/hoverProvider'; import { PythonDefinitionProvider } from './providers/definitionProvider'; -import { PythonReferenceProvider } from './providers/referenceProvider'; -import { PythonRenameProvider } from './providers/renameProvider'; +import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; +import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; -import { ShebangCodeLensProvider } from './providers/shebangCodeLensProvider' -import * as sortImports from './sortImports'; +import { PythonHoverProvider } from './providers/hoverProvider'; import { LintProvider } from './providers/lintProvider'; -import { PythonSymbolProvider } from './providers/symbolProvider'; +import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; +import { PythonReferenceProvider } from './providers/referenceProvider'; +import { PythonRenameProvider } from './providers/renameProvider'; +import { ReplProvider } from './providers/replProvider'; import { PythonSignatureProvider } from './providers/signatureProvider'; -import * as settings from './common/configSettings'; -import * as telemetryHelper from './common/telemetry'; -import * as telemetryContracts from './common/telemetryContracts'; import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; -import { SetInterpreterProvider } from './providers/setInterpreterProvider'; -import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; -import { Commands } from './common/constants'; -import * as tests from './unittests/main'; -import * as jup from './jupyter/main'; -import { HelpProvider } from './helpProvider'; +import { PythonSymbolProvider } from './providers/symbolProvider'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; -import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider'; -import { WorkspaceSymbols } from './workspaceSymbols/main'; +import * as sortImports from './sortImports'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; -import * as os from 'os'; -import * as fs from 'fs'; -import { JupyterProvider } from './jupyter/provider'; -import { activateGoToObjectDefinitionProvider } from './providers/objectDefinitionProvider'; -import { InterpreterManager } from './interpreter'; -import { SimpleConfigurationProvider } from './debugger'; -import { ReplProvider } from './providers/replProvider'; -import { workspace } from 'vscode'; +import * as tests from './unittests/main'; +import { WorkspaceSymbols } from './workspaceSymbols/main'; const PYTHON: vscode.DocumentFilter = { language: 'python' }; let unitTestOutChannel: vscode.OutputChannel; @@ -44,6 +44,7 @@ let lintingOutChannel: vscode.OutputChannel; let jupMain: jup.Jupyter; const activationDeferred = createDeferred(); export const activated = activationDeferred.promise; +// tslint:disable-next-line:max-func-body-length export async function activate(context: vscode.ExtensionContext) { const pythonSettings = settings.PythonSettings.getInstance(); sendStartupTelemetry(); @@ -83,11 +84,11 @@ export async function activate(context: vscode.ExtensionContext) { { beforeText: /^ *#.*$/, afterText: /.+$/, - action: { indentAction: vscode.IndentAction.None, appendText: '# ' }, + action: { indentAction: vscode.IndentAction.None, appendText: '# ' } }, { beforeText: /^\s+(continue|break|return)\b.*$/, - action: { indentAction: vscode.IndentAction.Outdent }, + action: { indentAction: vscode.IndentAction.Outdent } } ] }); @@ -99,7 +100,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(jediFactory))); context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(jediFactory), '.')); - context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider())) + context.subscriptions.push(vscode.languages.registerCodeLensProvider(PYTHON, new ShebangCodeLensProvider())); const symbolProvider = new PythonSymbolProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, symbolProvider)); @@ -113,7 +114,7 @@ export async function activate(context: vscode.ExtensionContext) { } const jupyterExtInstalled = vscode.extensions.getExtension('donjayamanne.jupyter'); - let linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false)); + const linterProvider = new LintProvider(context, lintingOutChannel, (a, b) => Promise.resolve(false)); context.subscriptions.push(); if (jupyterExtInstalled) { if (jupyterExtInstalled.isActive) { @@ -125,8 +126,7 @@ export async function activate(context: vscode.ExtensionContext) { jupyterExtInstalled.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); linterProvider.documentHasJupyterCodeCells = jupyterExtInstalled.exports.hasCodeCells; }); - } - else { + } else { jupMain = new jup.Jupyter(lintingOutChannel); const documentHasJupyterCodeCells = jupMain.hasCodeCells.bind(jupMain); jupMain.activate(); diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts new file mode 100644 index 000000000000..adeca205553b --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -0,0 +1,42 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, window } from 'vscode'; +import { WorkspacePythonPath } from '../contracts'; +import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; + +export class PythonPathUpdaterService { + constructor(private pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory) { } + public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, wkspace?: Uri): Promise { + const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); + + try { + + await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); + } catch (reason) { + // tslint:disable-next-line:no-unsafe-any prefer-type-cast + const message = reason && typeof reason.message === 'string' ? reason.message as string : ''; + window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); + console.error(reason); + } + } + private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { + switch (configTarget) { + case ConfigurationTarget.Global: { + return this.pythonPathSettingsUpdaterFactory.getGlobalPythonPathConfigurationService(); + } + case ConfigurationTarget.Workspace: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspacePythonPathConfigurationService(wkspace!); + } + default: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspaceFolderPythonPathConfigurationService(wkspace!); + } + } + } +} diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts new file mode 100644 index 000000000000..a46c18275d35 --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -0,0 +1,18 @@ +import { Uri } from 'vscode'; +import { InterpreterManager } from '../index'; +import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; +import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; +import { WorkspacePythonPathUpdaterService } from './services/workspaceUpdaterService'; +import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; + +export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { + public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { + return new GlobalPythonPathUpdaterService(); + } + public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { + return new WorkspacePythonPathUpdaterService(wkspace); + } + public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { + return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder); + } +} diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts new file mode 100644 index 000000000000..68ad3b1b173f --- /dev/null +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -0,0 +1,16 @@ +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { InterpreterManager } from '../..'; +import { WorkspacePythonPath } from '../../contracts'; +import { IPythonPathUpdaterService } from '../types'; + +export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { + public async updatePythonPath(pythonPath: string): Promise { + const pythonPathValue = workspace.getConfiguration('python').inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { + return; + } + const pythonConfig = workspace.getConfiguration('python'); + await pythonConfig.update('pythonPath', pythonPath, true); + } +} diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts new file mode 100644 index 000000000000..5cdef095267f --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { InterpreterManager } from '../..'; +import { WorkspacePythonPath } from '../../contracts'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private workspaceFolder: Uri) { + } + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python', this.workspaceFolder); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { + return; + } + if (pythonPath.startsWith(this.workspaceFolder.fsPath)) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(this.workspaceFolder.fsPath, pythonPath)); + } + await pythonConfig.update('pythonPath', pythonPath, false); + } +} diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts new file mode 100644 index 000000000000..b0da1b168345 --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { InterpreterManager } from '../..'; +import { WorkspacePythonPath } from '../../contracts'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { + constructor(private wkspace: Uri) { + } + public async updatePythonPath(pythonPath: string): Promise { + const pythonConfig = workspace.getConfiguration('python', this.wkspace); + const pythonPathValue = pythonConfig.inspect('pythonPath'); + + if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { + return; + } + if (pythonPath.startsWith(this.wkspace.fsPath)) { + // tslint:disable-next-line:no-invalid-template-strings + pythonPath = path.join('${workspaceRoot}', path.relative(this.wkspace.fsPath, pythonPath)); + } + await pythonConfig.update('pythonPath', pythonPath, false); + } +} diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts new file mode 100644 index 000000000000..b155d7caa049 --- /dev/null +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -0,0 +1,114 @@ +'use strict'; +import * as path from 'path'; +import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; +import { InterpreterManager } from '../'; +import * as settings from '../../common/configSettings'; +import { PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; +import { PythonPathUpdaterService } from './pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; +import { IPythonPathUpdaterServiceFactory } from './types'; + +// tslint:disable-next-line:interface-name +interface PythonPathQuickPickItem extends QuickPickItem { + path: string; +} + +export class SetInterpreterProvider implements Disposable { + private disposables: Disposable[] = []; + private pythonPathUpdaterService: PythonPathUpdaterService; + constructor(private interpreterManager: InterpreterManager) { + this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); + this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + } + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private async getWorkspaceToSetPythonPath(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } + if (workspace.workspaceFolders.length === 1) { + return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + + // Ok we have multiple interpreters, get the user to pick a folder. + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; + } + private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { + let detail = suggestion.path; + if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { + detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; + } + return { + // tslint:disable-next-line:no-non-null-assertion + label: suggestion.displayName!, + description: suggestion.companyDisplayName || '', + detail: detail, + path: suggestion.path + }; + } + + private async getSuggestions(resourceUri?: Uri) { + const interpreters = await this.interpreterManager.getInterpreters(resourceUri); + // tslint:disable-next-line:no-non-null-assertion + interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); + return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); + } + + private async setInterpreter() { + const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + let configTarget = ConfigurationTarget.Global; + let wkspace: Uri; + if (!setInterpreterGlobally) { + const targetConfig = await this.getWorkspaceToSetPythonPath(); + if (!targetConfig) { + return; + } + configTarget = targetConfig.configTarget; + wkspace = targetConfig.folderUri; + } + + const suggestions = await this.getSuggestions(wkspace); + let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; + if (wkspace && currentPythonPath.startsWith(wkspace.fsPath)) { + currentPythonPath = `.${path.sep}${path.relative(wkspace.fsPath, currentPythonPath)}`; + } + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${currentPythonPath}` + }; + + const selection = await window.showQuickPick(suggestions, quickPickOptions); + if (selection !== undefined) { + await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, wkspace); + } + } + + private async setShebangInterpreter(): Promise { + const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); + if (!shebang) { + return; + } + + const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; + + if (isGlobalChange) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global); + return; + } + + if (isWorkspaceChange || !workspaceFolder) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, workspace.workspaceFolders[0].uri); + return; + } + + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, workspaceFolder.uri); + } +} diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts new file mode 100644 index 000000000000..05825416f3bc --- /dev/null +++ b/src/client/interpreter/configuration/types.ts @@ -0,0 +1,12 @@ +import { Uri } from 'vscode'; +import { IPythonPathUpdaterService } from './types'; + +export interface IPythonPathUpdaterService { + updatePythonPath(pythonPath: string): Promise; +} + +export interface IPythonPathUpdaterServiceFactory { + getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService; + getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService; + getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService; +} diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts new file mode 100644 index 000000000000..a45f4a4db431 --- /dev/null +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -0,0 +1,71 @@ +'use strict'; +import * as child_process from 'child_process'; +import * as vscode from 'vscode'; +import { CancellationToken, CodeLens, TextDocument } from 'vscode'; +import * as settings from '../../common/configSettings'; +import { IS_WINDOWS } from '../../common/utils'; +import { getFirstNonEmptyLineFromMultilineString } from '../../interpreter/helpers'; + +export class ShebangCodeLensProvider implements vscode.CodeLensProvider { + public onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration; + // tslint:disable-next-line:function-name + public static async detectShebang(document: TextDocument): Promise { + const firstLine = document.lineAt(0); + if (firstLine.isEmptyOrWhitespace) { + return; + } + + if (!firstLine.text.startsWith('#!')) { + return; + } + + const shebang = firstLine.text.substr(2).trim(); + const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); + return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; + } + private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { + if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { + // In case we have pythonPath as '/usr/bin/env python' + return new Promise(resolve => { + const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); + let result = ''; + command.stdout.on('data', (data) => { + result += data.toString(); + }); + command.on('close', () => { + resolve(getFirstNonEmptyLineFromMultilineString(result)); + }); + }); + } else { + return new Promise(resolve => { + child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => { + resolve(getFirstNonEmptyLineFromMultilineString(stdout)); + }); + }); + } + } + + public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + const codeLenses = await this.createShebangCodeLens(document); + return Promise.resolve(codeLenses); + } + + private async createShebangCodeLens(document: TextDocument) { + const shebang = await ShebangCodeLensProvider.detectShebang(document); + if (!shebang || shebang === settings.PythonSettings.getInstance(document.uri).pythonPath) { + return []; + } + + const firstLine = document.lineAt(0); + const startOfShebang = new vscode.Position(0, 0); + const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); + const shebangRange = new vscode.Range(startOfShebang, endOfShebang); + + const cmd: vscode.Command = { + command: 'python.setShebangInterpreter', + title: 'Set as interpreter' + }; + + return [(new CodeLens(shebangRange, cmd))]; + } +} diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index f1371ca40922..dc8308344d2f 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -23,50 +23,3 @@ export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { } return undefined; } - -export function getInterpretersForEachFolderAndWorkspace(): WorkspacePythonPath[] { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return []; - } - const value = workspace.getConfiguration('python').inspect('pythonPath'); - const workspacePythonPath = value && typeof value.workspaceValue === 'string' ? value.workspaceValue : undefined; - - if (workspace.workspaceFolders.length === 1) { - if (workspacePythonPath) { - return [{ - folderUri: workspace.workspaceFolders[0].uri, - pytonPath: workspacePythonPath, - configTarget: ConfigurationTarget.Workspace - }]; - } - else { - return []; - } - } - - const workspaceConfig: WorkspacePythonPath[] = workspacePythonPath ? [{ - folderUri: workspace.workspaceFolders[0].uri, - pytonPath: workspacePythonPath, - configTarget: ConfigurationTarget.Workspace - }] : []; - - return workspace.workspaceFolders.reduce((accumulator, folder) => { - // tslint:disable-next-line:no-backbone-get-set-outside-model - const folderValue = workspace.getConfiguration('python', folder.uri).inspect('pythonPath'); - - if (folderValue && typeof folderValue.workspaceFolderValue === 'string' && - folderValue.workspaceFolderValue !== workspacePythonPath && - accumulator.findIndex(item => item.pytonPath === folderValue.workspaceFolderValue) === -1) { - - const info: WorkspacePythonPath = { - folderUri: folder.uri, - pytonPath: folderValue.workspaceFolderValue, - configTarget: ConfigurationTarget.WorkspaceFolder - }; - - accumulator.push(info); - } - - return accumulator; - }, workspaceConfig); -} diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index e6ebea67f7a9..a90c2592fcb0 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -3,6 +3,8 @@ import * as path from 'path'; import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { IS_WINDOWS } from '../common/utils'; +import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; import { WorkspacePythonPath } from './contracts'; import { InterpreterDisplay } from './display'; import { getActiveWorkspaceUri } from './helpers'; @@ -16,12 +18,14 @@ export class InterpreterManager implements Disposable { private disposables: Disposable[] = []; private display: InterpreterDisplay | null | undefined; private interpreterProvider: PythonInterpreterLocatorService; + private pythonPathUpdaterService: PythonPathUpdaterService; constructor() { const virtualEnvMgr = new VirtualEnvironmentManager([new VEnv(), new VirtualEnv()]); const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr); const versionService = new InterpreterVersionService(); this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); this.disposables.push(statusBar); @@ -54,33 +58,7 @@ export class InterpreterManager implements Disposable { const pythonPath = interpretersInWorkspace[0].path; const relativePath = path.dirname(pythonPath).substring(activeWorkspace.folderUri.fsPath.length); if (relativePath.split(path.sep).filter(l => l.length > 0).length === 2) { - await this.setPythonPath(pythonPath, activeWorkspace); - } - } - - /** - * Sets the python path in the settings. - * @param {string} pythonPath - * @param {WorkspacePythonPath} [workspacePythonPath] If this is not passed, then user setting will be updated - * @returns {Promise} - * @memberof InterpreterManager - */ - public async setPythonPath(pythonPath: string, workspacePythonPath?: WorkspacePythonPath): Promise { - pythonPath = IS_WINDOWS ? pythonPath.replace(/\\/g, '/') : pythonPath; - const isMultiRootWorkspace = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; - try { - if (!workspacePythonPath) { - return await this.setPythonPathInUserSettings(pythonPath); - } - if (!isMultiRootWorkspace) { - return await this.setPythonPathInSingleWorkspace(pythonPath); - } - await this.setPythonPathInWorkspace(pythonPath, workspacePythonPath.configTarget, workspacePythonPath.folderUri); - } catch (reason) { - // tslint:disable-next-line:no-unsafe-any prefer-type-cast - const message = reason && typeof reason.message === 'string' ? reason.message as string : ''; - window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); - console.error(reason); + await this.pythonPathUpdaterService.updatePythonPath(pythonPath, activeWorkspace.configTarget, activeWorkspace.folderUri); } } public dispose(): void { @@ -89,35 +67,13 @@ export class InterpreterManager implements Disposable { this.display = null; this.interpreterProvider.dispose(); } - private async setPythonPathInUserSettings(pythonPath) { - const pythonConfig = workspace.getConfiguration('python'); - return pythonConfig.update('pythonPath', pythonPath, true); - } - private async setPythonPathInSingleWorkspace(pythonPath: string) { - const pythonConfig = workspace.getConfiguration('python'); - // tslint:disable-next-line:no-non-null-assertion - const workspacePath = workspace.workspaceFolders![0].uri.fsPath; - if (pythonPath.toUpperCase().startsWith(workspacePath.toUpperCase())) { - // tslint:disable-next-line:no-invalid-template-strings - pythonPath = path.join('${workspaceRoot}', path.relative(workspacePath, pythonPath)); - } - return pythonConfig.update('pythonPath', pythonPath, false); - } - private async setPythonPathInWorkspace(pythonPath, configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder, resource?: Uri) { - const pythonConfig = workspace.getConfiguration('python', resource); - if (configTarget === ConfigurationTarget.WorkspaceFolder && resource && pythonPath.toUpperCase().startsWith(resource.fsPath.toUpperCase())) { - // tslint:disable-next-line:no-invalid-template-strings - pythonPath = path.join('${workspaceRoot}', path.relative(resource.fsPath, pythonPath)); - } - return pythonConfig.update('pythonPath', pythonPath, configTarget); - } private shouldAutoSetInterpreter() { const activeWorkspace = getActiveWorkspaceUri(); if (!activeWorkspace) { return false; } const pythonConfig = workspace.getConfiguration('python'); - const pythonPathInConfig = pythonConfig.get('pythonPath', 'python'); + const pythonPathInConfig = pythonConfig.get('pythonPath', 'python'); return path.basename(pythonPathInConfig) === pythonPathInConfig; } private onConfigChanged() { diff --git a/src/client/providers/setInterpreterProvider.ts b/src/client/providers/setInterpreterProvider.ts index 29039fca8272..a14070e34bc5 100644 --- a/src/client/providers/setInterpreterProvider.ts +++ b/src/client/providers/setInterpreterProvider.ts @@ -1,116 +1,116 @@ -'use strict'; -import * as path from 'path'; -import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; -import { InterpreterManager } from '../interpreter'; -import { PythonInterpreter, WorkspacePythonPath } from '../interpreter/contracts'; -import { getInterpretersForEachFolderAndWorkspace } from '../interpreter/helpers'; -import * as settings from './../common/configSettings'; -import { ShebangCodeLensProvider } from './shebangCodeLensProvider'; +// 'use strict'; +// import * as path from 'path'; +// import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; +// import { InterpreterManager } from '../interpreter'; +// import { PythonInterpreter, WorkspacePythonPath } from '../interpreter/contracts'; +// import * as settings from './../common/configSettings'; +// import { ShebangCodeLensProvider } from './shebangCodeLensProvider'; -// tslint:disable-next-line:interface-name -interface PythonPathQuickPickItem extends QuickPickItem { - path: string; -} +// // tslint:disable-next-line:interface-name +// interface PythonPathQuickPickItem extends QuickPickItem { +// path: string; +// } -export class SetInterpreterProvider implements Disposable { - private disposables: Disposable[] = []; - constructor(private interpreterManager: InterpreterManager) { - this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); - this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); - } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } - private async getWorkspacePythonPath(): Promise { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return undefined; - } - if (workspace.workspaceFolders.length === 1) { - return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; - } - // We could have each workspace folder with different python paths. - // Or, we could the workspace with a pythonPath and one of the workspace folders with different python paths. - // Lets just find how many different setups we have. - const configs = getInterpretersForEachFolderAndWorkspace(); - if (configs.length === 1) { - return configs[0]; - } +// export class SetInterpreterProvider implements Disposable { +// private disposables: Disposable[] = []; +// constructor(private interpreterManager: InterpreterManager) { +// this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); +// this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); +// } +// public dispose() { +// this.disposables.forEach(disposable => disposable.dispose()); +// } +// private async getWorkspaceToSetPythonPath(): Promise { +// if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { +// return undefined; +// } +// if (workspace.workspaceFolders.length === 1) { +// return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; +// } - // Ok we have multiple interpreters, get the user to pick a folder. - // tslint:disable-next-line:no-any prefer-type-cast - const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); - return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; - } - private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { - let detail = suggestion.path; - if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { - detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; - } - return { - // tslint:disable-next-line:no-non-null-assertion - label: suggestion.displayName!, - description: suggestion.companyDisplayName || '', - detail: detail, - path: suggestion.path - }; - } - private async presentQuickPick() { - const targetConfig = await this.getWorkspacePythonPath(); - const resourceUri = targetConfig ? targetConfig.folderUri : undefined; - const suggestions = await this.getSuggestions(resourceUri); - let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; - if (targetConfig.folderUri && currentPythonPath.startsWith(targetConfig.folderUri.fsPath)) { - currentPythonPath = `.${path.sep}${path.relative(targetConfig.folderUri.fsPath, currentPythonPath)}`; - } - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentPythonPath}` - }; - const selection = await window.showQuickPick(suggestions, quickPickOptions); - if (selection !== undefined) { - this.interpreterManager.setPythonPath(selection.path, targetConfig); - } - } +// // Ok we have multiple interpreters, get the user to pick a folder. +// // tslint:disable-next-line:no-any prefer-type-cast +// const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); +// return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; +// } +// private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { +// let detail = suggestion.path; +// if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { +// detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; +// } +// return { +// // tslint:disable-next-line:no-non-null-assertion +// label: suggestion.displayName!, +// description: suggestion.companyDisplayName || '', +// detail: detail, +// path: suggestion.path +// }; +// } - private async getSuggestions(resourceUri?: Uri) { - const interpreters = await this.interpreterManager.getInterpreters(resourceUri); - // tslint:disable-next-line:no-non-null-assertion - interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); - return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); - } +// private async getSuggestions(resourceUri?: Uri) { +// const interpreters = await this.interpreterManager.getInterpreters(resourceUri); +// // tslint:disable-next-line:no-non-null-assertion +// interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); +// return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); +// } - private setInterpreter() { - this.presentQuickPick(); - } +// private async setInterpreter() { +// const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; +// let targetConfig: WorkspacePythonPath; +// if (!setInterpreterGlobally) { +// targetConfig = await this.getWorkspaceToSetPythonPath(); +// if (!targetConfig) { +// return; +// } +// } - private async setShebangInterpreter(): Promise { - const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); - if (!shebang) { - return; - } +// const resourceUri = targetConfig ? targetConfig.folderUri : undefined; +// const suggestions = await this.getSuggestions(resourceUri); +// let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; +// if (targetConfig && targetConfig.folderUri && currentPythonPath.startsWith(targetConfig.folderUri.fsPath)) { +// currentPythonPath = `.${path.sep}${path.relative(targetConfig.folderUri.fsPath, currentPythonPath)}`; +// } +// const quickPickOptions: QuickPickOptions = { +// matchOnDetail: true, +// matchOnDescription: true, +// placeHolder: `current: ${currentPythonPath}` +// }; - const existingConfigs = getInterpretersForEachFolderAndWorkspace(); - const hasFoldersWithPythonPathSet = existingConfigs.filter(item => item.configTarget === ConfigurationTarget.WorkspaceFolder).length > 0; +// const selection = await window.showQuickPick(suggestions, quickPickOptions); +// if (selection !== undefined) { +// await this.interpreterManager.setPythonPath(selection.path, targetConfig); +// } +// } - const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); - // tslint:disable-next-line:no-backbone-get-set-outside-model - const value = workspace.getConfiguration('python', window.activeTextEditor.document.uri).inspect('pythonPath'); - const currentValueSetInWorkspaceFolder = value && typeof value.workspaceFolderValue === 'string'; +// private async setShebangInterpreter(): Promise { +// const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); +// if (!shebang) { +// return; +// } - const setPythonPathInSpecificFolder = hasFoldersWithPythonPathSet || currentValueSetInWorkspaceFolder; - if (setPythonPathInSpecificFolder) { - const configTarget = workspaceFolder ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - const workspaceTarget: WorkspacePythonPath = { folderUri: workspaceFolder.uri, configTarget: configTarget }; - return this.interpreterManager.setPythonPath(shebang, workspaceTarget); - } +// const pythonPathValue = workspace.getConfiguration('python', window.activeTextEditor.document.uri).inspect('pythonPath'); +// const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; +// const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); +// const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; - const setPythonPathInRootWorkspace = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0; - if (setPythonPathInRootWorkspace) { - const configTarget: WorkspacePythonPath = { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; - return this.interpreterManager.setPythonPath(shebang, configTarget); - } +// if (isGlobalChange) { +// if (!pythonPathValue || typeof pythonPathValue.globalValue !== 'string' || pythonPathValue.globalValue !== shebang) { +// await this.interpreterManager.setPythonPath(shebang); +// } +// return; +// } - return this.interpreterManager.setPythonPath(shebang); - } -} +// if (isWorkspaceChange || !workspaceFolder) { +// if (!pythonPathValue || typeof pythonPathValue.workspaceValue !== 'string' || pythonPathValue.workspaceValue !== shebang) { +// const targetInfo: WorkspacePythonPath = { configTarget: ConfigurationTarget.Workspace, folderUri: workspace.workspaceFolders[0].uri }; +// await this.interpreterManager.setPythonPath(shebang, targetInfo); +// } +// return; +// } + +// if (!pythonPathValue || typeof pythonPathValue.workspaceValue !== 'string' || pythonPathValue.workspaceValue !== shebang) { +// const targetInfo: WorkspacePythonPath = { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; +// await this.interpreterManager.setPythonPath(shebang, targetInfo); +// } +// } +// } diff --git a/src/client/providers/shebangCodeLensProvider.ts b/src/client/providers/shebangCodeLensProvider.ts index 8988d6161644..d792ef89dc1f 100644 --- a/src/client/providers/shebangCodeLensProvider.ts +++ b/src/client/providers/shebangCodeLensProvider.ts @@ -1,72 +1,72 @@ -"use strict"; -import { IS_WINDOWS } from '../common/utils'; -import * as vscode from 'vscode'; -import * as child_process from 'child_process'; -import * as settings from '../common/configSettings'; -import { TextDocument, CodeLens, CancellationToken } from 'vscode'; -import { getFirstNonEmptyLineFromMultilineString } from '../interpreter/helpers'; -export class ShebangCodeLensProvider implements vscode.CodeLensProvider { - onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration; +// "use strict"; +// import { IS_WINDOWS } from '../common/utils'; +// import * as vscode from 'vscode'; +// import * as child_process from 'child_process'; +// import * as settings from '../common/configSettings'; +// import { TextDocument, CodeLens, CancellationToken } from 'vscode'; +// import { getFirstNonEmptyLineFromMultilineString } from '../interpreter/helpers'; +// export class ShebangCodeLensProvider implements vscode.CodeLensProvider { +// onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration; - public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { - const codeLenses = await this.createShebangCodeLens(document); - return Promise.resolve(codeLenses); - } +// public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { +// const codeLenses = await this.createShebangCodeLens(document); +// return Promise.resolve(codeLenses); +// } - private async createShebangCodeLens(document: TextDocument) { - const shebang = await ShebangCodeLensProvider.detectShebang(document); - if (!shebang || shebang === settings.PythonSettings.getInstance(document.uri).pythonPath) { - return []; - } +// private async createShebangCodeLens(document: TextDocument) { +// const shebang = await ShebangCodeLensProvider.detectShebang(document); +// if (!shebang || shebang === settings.PythonSettings.getInstance(document.uri).pythonPath) { +// return []; +// } - const firstLine = document.lineAt(0); - const startOfShebang = new vscode.Position(0, 0); - const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); - const shebangRange = new vscode.Range(startOfShebang, endOfShebang); +// const firstLine = document.lineAt(0); +// const startOfShebang = new vscode.Position(0, 0); +// const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); +// const shebangRange = new vscode.Range(startOfShebang, endOfShebang); - const cmd: vscode.Command = { - command: 'python.setShebangInterpreter', - title: 'Set as interpreter' - }; +// const cmd: vscode.Command = { +// command: 'python.setShebangInterpreter', +// title: 'Set as interpreter' +// }; - const codeLenses = [(new CodeLens(shebangRange, cmd))]; - return codeLenses; - } +// const codeLenses = [(new CodeLens(shebangRange, cmd))]; +// return codeLenses; +// } - public static async detectShebang(document: TextDocument): Promise { - let firstLine = document.lineAt(0); - if (firstLine.isEmptyOrWhitespace) { - return; - } +// public static async detectShebang(document: TextDocument): Promise { +// let firstLine = document.lineAt(0); +// if (firstLine.isEmptyOrWhitespace) { +// return; +// } - if (!firstLine.text.startsWith('#!')) { - return; - } +// if (!firstLine.text.startsWith('#!')) { +// return; +// } - const shebang = firstLine.text.substr(2).trim(); - const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); - return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; - } - private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { - if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { - // In case we have pythonPath as '/usr/bin/env python' - return new Promise(resolve => { - const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); - let result = ''; - command.stdout.on('data', (data) => { - result += data.toString(); - }); - command.on('close', () => { - resolve(getFirstNonEmptyLineFromMultilineString(result)); - }); - }); - } - else { - return new Promise(resolve => { - child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - }); - }); - } - } -} +// const shebang = firstLine.text.substr(2).trim(); +// const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); +// return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; +// } +// private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { +// if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { +// // In case we have pythonPath as '/usr/bin/env python' +// return new Promise(resolve => { +// const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); +// let result = ''; +// command.stdout.on('data', (data) => { +// result += data.toString(); +// }); +// command.on('close', () => { +// resolve(getFirstNonEmptyLineFromMultilineString(result)); +// }); +// }); +// } +// else { +// return new Promise(resolve => { +// child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { +// resolve(getFirstNonEmptyLineFromMultilineString(stdout)); +// }); +// }); +// } +// } +// } diff --git a/src/test/providers/shebangCodeLenseProvider.test.ts b/src/test/providers/shebangCodeLenseProvider.test.ts index c8e914f40afd..23616ebb3e70 100644 --- a/src/test/providers/shebangCodeLenseProvider.test.ts +++ b/src/test/providers/shebangCodeLenseProvider.test.ts @@ -1,13 +1,13 @@ import * as assert from 'assert'; +import * as child_process from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; -import * as child_process from 'child_process'; +import { ConfigurationTarget } from 'vscode'; import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; +import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; +import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; import { rootWorkspaceUri, updateSetting } from '../common'; -import { ShebangCodeLensProvider } from '../../client/providers/shebangCodeLensProvider'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; -import { ConfigurationTarget } from 'vscode'; const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'shebang'); const fileShebang = path.join(autoCompPath, 'shebang.py'); @@ -16,12 +16,12 @@ const fileShebangInvalid = path.join(autoCompPath, 'shebangInvalid.py'); const filePlain = path.join(autoCompPath, 'plain.py'); suite('Shebang detection', () => { - suiteSetup(() => initialize()); + suiteSetup(initialize); suiteTeardown(async () => { await initialize(); await closeActiveWindows(); }); - setup(() => initializeTest()); + setup(initializeTest); test('A code lens will appear when sheban python and python in settings are different', async () => { const pythonPath = 'someUnknownInterpreter'; @@ -30,7 +30,7 @@ suite('Shebang detection', () => { const codeLenses = await setupCodeLens(editor); assert.equal(codeLenses.length, 1, 'No CodeLens available'); - let codeLens = codeLenses[0]; + const codeLens = codeLenses[0]; assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); }); @@ -58,7 +58,7 @@ suite('Shebang detection', () => { const codeLenses = await setupCodeLens(editor); assert.equal(codeLenses.length, 1, 'No CodeLens available'); - let codeLens = codeLenses[0]; + const codeLens = codeLenses[0]; assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range'); assert.equal(codeLens.command.command, 'python.setShebangInterpreter'); @@ -96,7 +96,6 @@ suite('Shebang detection', () => { async function setupCodeLens(editor: vscode.TextEditor) { const document = editor.document; const codeLensProvider = new ShebangCodeLensProvider(); - const codeLenses = await codeLensProvider.provideCodeLenses(document, null); - return codeLenses; + return await codeLensProvider.provideCodeLenses(document, null); } }); From e3374febe4ffdc27ebafd68c86511bac47ae7e2b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 20:53:15 -0700 Subject: [PATCH 04/23] added tests --- .../services/globalUpdaterService.ts | 4 +- .../services/workspaceFolderUpdaterService.ts | 2 +- src/test/common.ts | 10 ++ src/test/initialize.ts | 6 +- .../pythonPathUpdater.multiroot.test.ts | 75 +++++++++++++++ .../interpreters/pythonPathUpdater.test.ts | 92 +++++++++++++++++++ 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 src/test/interpreters/pythonPathUpdater.multiroot.test.ts create mode 100644 src/test/interpreters/pythonPathUpdater.test.ts diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index 68ad3b1b173f..4b775adf0b71 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -5,12 +5,12 @@ import { IPythonPathUpdaterService } from '../types'; export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { public async updatePythonPath(pythonPath: string): Promise { - const pythonPathValue = workspace.getConfiguration('python').inspect('pythonPath'); + const pythonConfig = workspace.getConfiguration('python'); + const pythonPathValue = pythonConfig.inspect('pythonPath'); if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { return; } - const pythonConfig = workspace.getConfiguration('python'); await pythonConfig.update('pythonPath', pythonPath, true); } } diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts index 5cdef095267f..d39f9a831f93 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -18,6 +18,6 @@ export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdat // tslint:disable-next-line:no-invalid-template-strings pythonPath = path.join('${workspaceRoot}', path.relative(this.workspaceFolder.fsPath, pythonPath)); } - await pythonConfig.update('pythonPath', pythonPath, false); + await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); } } diff --git a/src/test/common.ts b/src/test/common.ts index 486293b24015..00ef1caa14f2 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -82,6 +82,16 @@ async function setPythonPathInWorkspace(resource: string | Uri | undefined, conf PythonSettings.dispose(); } } +async function restoreGlobalPythonPathSetting(): Promise { + const pythonConfig = workspace.getConfiguration('python'); + const currentGlobalPythonPathSetting = pythonConfig.inspect('pythonPath').globalValue; + if (globalPythonPathSetting !== currentGlobalPythonPathSetting) { + await pythonConfig.update('pythonPath', undefined, true); + } + PythonSettings.dispose(); +} +const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath').globalValue; export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath); +export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index f67afc30b8ec..67193c2da0bd 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -4,9 +4,11 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { PythonSettings } from '../client/common/configSettings'; import { activated } from '../client/extension'; -import { clearPythonPathInWorkspaceFolder, setPythonPathInWorkspaceRoot } from './common'; +import { clearPythonPathInWorkspaceFolder, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common'; const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); //First thing to be executed. // tslint:disable-next-line:no-string-literal @@ -20,7 +22,9 @@ export const IS_MULTI_ROOT_TEST = isMultitrootTest(); // Ability to use custom python environments for testing export async function initializePython() { + await resetGlobalPythonPathSetting(); await clearPythonPathInWorkspaceFolder(dummyPythonFile); + await clearPythonPathInWorkspaceFolder(workspace3Uri); await setPythonPathInWorkspaceRoot(PYTHON_PATH); } diff --git a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts new file mode 100644 index 000000000000..e05274fb14ae --- /dev/null +++ b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts @@ -0,0 +1,75 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; +import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; +import { WorkspacePythonPath } from '../../client/interpreter/contracts'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; + +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Python Path Settings Updater', () => { + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + teardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + + test('Updating Workspace Folder Python Path should work', async () => { + const workspaceUri = workspace3Uri; + const workspaceUpdater = new WorkspaceFolderPythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Folder Python Path using the factor service should work', async () => { + const workspaceUri = workspace3Uri; + const factory = new PythonPathUpdaterServiceFactory(); + const workspaceUpdater = factory.getWorkspaceFolderPythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { + const workspaceUri = workspace3Uri; + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.WorkspaceFolder, workspace.getWorkspaceFolder(workspaceUri).uri); + const folderValue = workspace.getConfiguration('python', workspace3Uri).inspect('pythonPath').workspaceFolderValue; + assert.equal(folderValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Python Path should be relative to workspace', async () => { + const workspaceUri = workspace.getWorkspaceFolder(workspace3Uri).uri; + const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + // tslint:disable-next-line:no-invalid-template-strings + assert.equal(workspaceValue, path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); + }); +}); diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts new file mode 100644 index 000000000000..4b11c96ebd60 --- /dev/null +++ b/src/test/interpreters/pythonPathUpdater.test.ts @@ -0,0 +1,92 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; +import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; +import { WorkspacePythonPath } from '../../client/interpreter/contracts'; +import { clearPythonPathInWorkspaceFolder } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); + +// tslint:disable-next-line:max-func-body-length +suite('Python Path Settings Updater', () => { + suiteSetup(initialize); + setup(initializeTest); + suiteTeardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + teardown(async () => { + await closeActiveWindows(); + await initializeTest(); + }); + + // Create Github issue VS Code bug (global changes not reflected immediately) + + // test('Updating Global Python Path should work', async () => { + // const globalUpdater = new GlobalPythonPathUpdaterService(); + // const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + // await globalUpdater.updatePythonPath(pythonPath); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + // test('Updating Global Python Path using the factory service should work', async () => { + // const globalUpdater = new PythonPathUpdaterServiceFactory().getGlobalPythonPathConfigurationService(); + // const pythonPath = `xGlobalPythonPathFromFactory${new Date().getMilliseconds()}`; + // await globalUpdater.updatePythonPath(pythonPath); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + // test('Updating Global Python Path using the PythonPathUpdaterService should work', async () => { + // const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + // const pythonPath = `xGlobalPythonPathFromUpdater${new Date().getMilliseconds()}`; + // await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Global); + // const globalPythonValue = workspace.getConfiguration('python').inspect('pythonPath').globalValue; + // assert.equal(globalPythonValue, pythonPath, 'Global Python Path not updated'); + // }); + + test('Updating Workspace Python Path should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the factor service should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const factory = new PythonPathUpdaterServiceFactory(); + const workspaceUpdater = factory.getWorkspacePythonPathConfigurationService(workspace.getWorkspaceFolder(workspaceUri).uri); + const pythonPath = `xWorkspacePythonPathFromFactory${new Date().getMilliseconds()}`; + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Updating Workspace Python Path using the PythonPathUpdaterService should work', async () => { + const workspaceUri = Uri.file(workspaceRoot); + const updaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory()); + const pythonPath = `xWorkspacePythonPathFromUpdater${new Date().getMilliseconds()}`; + await updaterService.updatePythonPath(pythonPath, ConfigurationTarget.Workspace, workspace.getWorkspaceFolder(workspaceUri).uri); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + assert.equal(workspaceValue, pythonPath, 'Workspace Python Path not updated'); + }); + + test('Python Path should be relative to workspace', async () => { + const workspaceUri = workspace.getWorkspaceFolder(Uri.file(workspaceRoot)).uri; + const pythonInterpreter = `xWorkspacePythonPath${new Date().getMilliseconds()}`; + const pythonPath = path.join(workspaceUri.fsPath, 'x', 'y', 'z', pythonInterpreter); + const workspaceUpdater = new WorkspacePythonPathUpdaterService(workspaceUri); + await workspaceUpdater.updatePythonPath(pythonPath); + const workspaceValue = workspace.getConfiguration('python').inspect('pythonPath').workspaceValue; + // tslint:disable-next-line:no-invalid-template-strings + assert.equal(workspaceValue, path.join('${workspaceRoot}', 'x', 'y', 'z', pythonInterpreter), 'Workspace Python Path not updated'); + }); +}); From 74985368de1b873bab68c5e8104bc2dd87f06142 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 20:55:20 -0700 Subject: [PATCH 05/23] fixed linter --- src/test/interpreters/pythonPathUpdater.multiroot.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts index e05274fb14ae..7f7c1040c68b 100644 --- a/src/test/interpreters/pythonPathUpdater.multiroot.test.ts +++ b/src/test/interpreters/pythonPathUpdater.multiroot.test.ts @@ -5,11 +5,11 @@ import { PythonSettings } from '../../client/common/configSettings'; import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; import { GlobalPythonPathUpdaterService } from '../../client/interpreter/configuration/services/globalUpdaterService'; +import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; import { WorkspacePythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceUpdaterService'; import { WorkspacePythonPath } from '../../client/interpreter/contracts'; import { clearPythonPathInWorkspaceFolder } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { WorkspaceFolderPythonPathUpdaterService } from '../../client/interpreter/configuration/services/workspaceFolderUpdaterService'; const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); From 7debbcf7eec7c9cf10ea770e12d58485347f62bd Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 21:21:01 -0700 Subject: [PATCH 06/23] removed redundant files --- .../providers/setInterpreterProvider.ts | 116 ------------------ .../providers/shebangCodeLensProvider.ts | 72 ----------- src/test/index.ts | 3 +- 3 files changed, 2 insertions(+), 189 deletions(-) delete mode 100644 src/client/providers/setInterpreterProvider.ts delete mode 100644 src/client/providers/shebangCodeLensProvider.ts diff --git a/src/client/providers/setInterpreterProvider.ts b/src/client/providers/setInterpreterProvider.ts deleted file mode 100644 index a14070e34bc5..000000000000 --- a/src/client/providers/setInterpreterProvider.ts +++ /dev/null @@ -1,116 +0,0 @@ -// 'use strict'; -// import * as path from 'path'; -// import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; -// import { InterpreterManager } from '../interpreter'; -// import { PythonInterpreter, WorkspacePythonPath } from '../interpreter/contracts'; -// import * as settings from './../common/configSettings'; -// import { ShebangCodeLensProvider } from './shebangCodeLensProvider'; - -// // tslint:disable-next-line:interface-name -// interface PythonPathQuickPickItem extends QuickPickItem { -// path: string; -// } - -// export class SetInterpreterProvider implements Disposable { -// private disposables: Disposable[] = []; -// constructor(private interpreterManager: InterpreterManager) { -// this.disposables.push(commands.registerCommand('python.setInterpreter', this.setInterpreter.bind(this))); -// this.disposables.push(commands.registerCommand('python.setShebangInterpreter', this.setShebangInterpreter.bind(this))); -// } -// public dispose() { -// this.disposables.forEach(disposable => disposable.dispose()); -// } -// private async getWorkspaceToSetPythonPath(): Promise { -// if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { -// return undefined; -// } -// if (workspace.workspaceFolders.length === 1) { -// return { folderUri: workspace.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; -// } - -// // Ok we have multiple interpreters, get the user to pick a folder. -// // tslint:disable-next-line:no-any prefer-type-cast -// const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); -// return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; -// } -// private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { -// let detail = suggestion.path; -// if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { -// detail = `.${path.sep}${path.relative(workspaceUri.fsPath, suggestion.path)}`; -// } -// return { -// // tslint:disable-next-line:no-non-null-assertion -// label: suggestion.displayName!, -// description: suggestion.companyDisplayName || '', -// detail: detail, -// path: suggestion.path -// }; -// } - -// private async getSuggestions(resourceUri?: Uri) { -// const interpreters = await this.interpreterManager.getInterpreters(resourceUri); -// // tslint:disable-next-line:no-non-null-assertion -// interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); -// return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); -// } - -// private async setInterpreter() { -// const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; -// let targetConfig: WorkspacePythonPath; -// if (!setInterpreterGlobally) { -// targetConfig = await this.getWorkspaceToSetPythonPath(); -// if (!targetConfig) { -// return; -// } -// } - -// const resourceUri = targetConfig ? targetConfig.folderUri : undefined; -// const suggestions = await this.getSuggestions(resourceUri); -// let currentPythonPath = settings.PythonSettings.getInstance().pythonPath; -// if (targetConfig && targetConfig.folderUri && currentPythonPath.startsWith(targetConfig.folderUri.fsPath)) { -// currentPythonPath = `.${path.sep}${path.relative(targetConfig.folderUri.fsPath, currentPythonPath)}`; -// } -// const quickPickOptions: QuickPickOptions = { -// matchOnDetail: true, -// matchOnDescription: true, -// placeHolder: `current: ${currentPythonPath}` -// }; - -// const selection = await window.showQuickPick(suggestions, quickPickOptions); -// if (selection !== undefined) { -// await this.interpreterManager.setPythonPath(selection.path, targetConfig); -// } -// } - -// private async setShebangInterpreter(): Promise { -// const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); -// if (!shebang) { -// return; -// } - -// const pythonPathValue = workspace.getConfiguration('python', window.activeTextEditor.document.uri).inspect('pythonPath'); -// const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; -// const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); -// const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; - -// if (isGlobalChange) { -// if (!pythonPathValue || typeof pythonPathValue.globalValue !== 'string' || pythonPathValue.globalValue !== shebang) { -// await this.interpreterManager.setPythonPath(shebang); -// } -// return; -// } - -// if (isWorkspaceChange || !workspaceFolder) { -// if (!pythonPathValue || typeof pythonPathValue.workspaceValue !== 'string' || pythonPathValue.workspaceValue !== shebang) { -// const targetInfo: WorkspacePythonPath = { configTarget: ConfigurationTarget.Workspace, folderUri: workspace.workspaceFolders[0].uri }; -// await this.interpreterManager.setPythonPath(shebang, targetInfo); -// } -// return; -// } - -// if (!pythonPathValue || typeof pythonPathValue.workspaceValue !== 'string' || pythonPathValue.workspaceValue !== shebang) { -// const targetInfo: WorkspacePythonPath = { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; -// await this.interpreterManager.setPythonPath(shebang, targetInfo); -// } -// } -// } diff --git a/src/client/providers/shebangCodeLensProvider.ts b/src/client/providers/shebangCodeLensProvider.ts deleted file mode 100644 index d792ef89dc1f..000000000000 --- a/src/client/providers/shebangCodeLensProvider.ts +++ /dev/null @@ -1,72 +0,0 @@ -// "use strict"; -// import { IS_WINDOWS } from '../common/utils'; -// import * as vscode from 'vscode'; -// import * as child_process from 'child_process'; -// import * as settings from '../common/configSettings'; -// import { TextDocument, CodeLens, CancellationToken } from 'vscode'; -// import { getFirstNonEmptyLineFromMultilineString } from '../interpreter/helpers'; -// export class ShebangCodeLensProvider implements vscode.CodeLensProvider { -// onDidChangeCodeLenses: vscode.Event = vscode.workspace.onDidChangeConfiguration; - -// public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { -// const codeLenses = await this.createShebangCodeLens(document); -// return Promise.resolve(codeLenses); -// } - -// private async createShebangCodeLens(document: TextDocument) { -// const shebang = await ShebangCodeLensProvider.detectShebang(document); -// if (!shebang || shebang === settings.PythonSettings.getInstance(document.uri).pythonPath) { -// return []; -// } - -// const firstLine = document.lineAt(0); -// const startOfShebang = new vscode.Position(0, 0); -// const endOfShebang = new vscode.Position(0, firstLine.text.length - 1); -// const shebangRange = new vscode.Range(startOfShebang, endOfShebang); - -// const cmd: vscode.Command = { -// command: 'python.setShebangInterpreter', -// title: 'Set as interpreter' -// }; - -// const codeLenses = [(new CodeLens(shebangRange, cmd))]; -// return codeLenses; -// } - -// public static async detectShebang(document: TextDocument): Promise { -// let firstLine = document.lineAt(0); -// if (firstLine.isEmptyOrWhitespace) { -// return; -// } - -// if (!firstLine.text.startsWith('#!')) { -// return; -// } - -// const shebang = firstLine.text.substr(2).trim(); -// const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang); -// return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; -// } -// private static async getFullyQualifiedPathToInterpreter(pythonPath: string) { -// if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) { -// // In case we have pythonPath as '/usr/bin/env python' -// return new Promise(resolve => { -// const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`); -// let result = ''; -// command.stdout.on('data', (data) => { -// result += data.toString(); -// }); -// command.on('close', () => { -// resolve(getFirstNonEmptyLineFromMultilineString(result)); -// }); -// }); -// } -// else { -// return new Promise(resolve => { -// child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => { -// resolve(getFirstNonEmptyLineFromMultilineString(stdout)); -// }); -// }); -// } -// } -// } diff --git a/src/test/index.ts b/src/test/index.ts index 64de9103f775..c704c5684564 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -8,6 +8,7 @@ testRunner.configure({ ui: 'tdd', useColors: true, timeout: 25000, - retries: 3 + retries: 3, + grep: 'Python Path Settings Updater' }); module.exports = testRunner; From 0c23972fd5d8d712e7eaafc33c03247049953018 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 26 Oct 2017 23:03:35 -0700 Subject: [PATCH 07/23] temp changes --- src/client/unittests/codeLenses/testFiles.ts | 80 +-- .../unittests/common/baseTestManager.ts | 73 +-- .../unittests/common/configSettingService.ts | 67 +++ src/client/unittests/common/contracts.ts | 76 ++- src/client/unittests/common/debugLauncher.ts | 26 +- .../common/testConfigurationManager.ts | 46 +- .../unittests/common/testManagerService.ts | 53 ++ .../common/testManagerServiceFactory.ts | 10 + src/client/unittests/common/testUtils.ts | 527 +++++++++++------- .../common/workspaceTestManagerService.ts | 74 +++ src/client/unittests/common/xUnitParser.ts | 31 +- src/client/unittests/configuration.ts | 222 ++++---- src/client/unittests/display/main.ts | 75 +-- src/client/unittests/display/picker.ts | 31 +- src/client/unittests/main.ts | 4 +- .../nosetest/testConfigurationManager.ts | 66 +-- .../pytest/testConfigurationManager.ts | 76 ++- .../unittest/testConfigurationManager.ts | 59 +- 18 files changed, 994 insertions(+), 602 deletions(-) create mode 100644 src/client/unittests/common/configSettingService.ts create mode 100644 src/client/unittests/common/testManagerService.ts create mode 100644 src/client/unittests/common/testManagerServiceFactory.ts create mode 100644 src/client/unittests/common/workspaceTestManagerService.ts diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index a9976899b74e..98489aedc22d 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,37 +1,37 @@ 'use strict'; -import * as vscode from 'vscode'; -import { CodeLensProvider, TextDocument, CancellationToken, CodeLens, SymbolInformation } from 'vscode'; -import { TestFile, TestsToRun, TestSuite, TestFunction, TestStatus } from '../common/contracts'; +import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument } from 'vscode'; import * as constants from '../../common/constants'; -import { getDiscoveredTests } from '../common/testUtils'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/contracts'; +import { getDiscoveredTests } from '../common/testUtils'; -interface CodeLensData { - symbolKind: vscode.SymbolKind; +type CodeLensData = { + symbolKind: SymbolKind; symbolName: string; fileName: string; -} -interface FunctionsAndSuites { +}; +type FunctionsAndSuites = { functions: TestFunction[]; suites: TestSuite[]; -} +}; export class TestFileCodeLensProvider implements CodeLensProvider { - constructor(private _onDidChange: vscode.EventEmitter, private symbolProvider: PythonSymbolProvider) { + // tslint:disable-next-line:variable-name + constructor(private _onDidChange: EventEmitter, private symbolProvider: PythonSymbolProvider) { } - get onDidChangeCodeLenses(): vscode.Event { + get onDidChangeCodeLenses(): Event { return this._onDidChange.event; } public provideCodeLenses(document: TextDocument, token: CancellationToken): Thenable { - let testItems = getDiscoveredTests(); + const testItems = getDiscoveredTests(document.uri); if (!testItems || testItems.testFiles.length === 0 || testItems.testFunctions.length === 0) { return Promise.resolve([]); } - let cancelTokenSrc = new vscode.CancellationTokenSource(); + const cancelTokenSrc = new CancellationTokenSource(); token.onCancellationRequested(() => { cancelTokenSrc.cancel(); }); // Strop trying to build the code lenses if unable to get a list of @@ -45,35 +45,35 @@ export class TestFileCodeLensProvider implements CodeLensProvider { return getCodeLenses(document, token, this.symbolProvider); } - resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { + public resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { codeLens.command = { command: 'python.runtests', title: 'Test' }; return Promise.resolve(codeLens); } } -function getCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken, symbolProvider: PythonSymbolProvider): Thenable { +function getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider): Thenable { const documentUri = document.uri; - const tests = getDiscoveredTests(); + const tests = getDiscoveredTests(document.uri); if (!tests) { return null; } - const file = tests.testFiles.find(file => file.fullPath === documentUri.fsPath); + const file = tests.testFiles.find(item => item.fullPath === documentUri.fsPath); if (!file) { return Promise.resolve([]); } const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); return symbolProvider.provideDocumentSymbolsForInternalUse(document, token) - .then((symbols: vscode.SymbolInformation[]) => { + .then((symbols: SymbolInformation[]) => { return symbols.filter(symbol => { - return symbol.kind === vscode.SymbolKind.Function || - symbol.kind === vscode.SymbolKind.Method || - symbol.kind === vscode.SymbolKind.Class; + return symbol.kind === SymbolKind.Function || + symbol.kind === SymbolKind.Method || + symbol.kind === SymbolKind.Class; }).map(symbol => { // This is bloody crucial, if the start and end columns are the same // then vscode goes bonkers when ever you edit a line (start scrolling magically) - const range = new vscode.Range(symbol.location.range.start, - new vscode.Position(symbol.location.range.end.line, + const range = new Range(symbol.location.range.start, + new Position(symbol.location.range.end.line, symbol.location.range.end.character + 1)); return getCodeLens(documentUri.fsPath, allFuncsAndSuites, @@ -88,15 +88,15 @@ function getCodeLenses(document: vscode.TextDocument, token: vscode.Cancellation } function getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, - range: vscode.Range, symbolName: string, symbolKind: vscode.SymbolKind, symbolContainer: string): vscode.CodeLens[] { + range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { switch (symbolKind) { - case vscode.SymbolKind.Function: - case vscode.SymbolKind.Method: { + case SymbolKind.Function: + case SymbolKind.Method: { return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range, symbolContainer); } - case vscode.SymbolKind.Class: { - const cls = allFuncsAndSuites.suites.find(cls => cls.name === symbolName); + case SymbolKind.Class: { + const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); if (!cls) { return null; } @@ -113,9 +113,10 @@ function getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, }) ]; } + default: { + return []; + } } - - return null; } function getTestStatusIcon(status: TestStatus): string { @@ -137,7 +138,7 @@ function getTestStatusIcon(status: TestStatus): string { } function getTestStatusIcons(fns: TestFunction[]): string { - let statuses: string[] = []; + const statuses: string[] = []; let count = fns.filter(fn => fn.status === TestStatus.Pass).length; if (count > 0) { statuses.push(`✔ ${count}`); @@ -154,13 +155,12 @@ function getTestStatusIcons(fns: TestFunction[]): string { return statuses.join(' '); } function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndSuites, - symbolName: string, range: vscode.Range, symbolContainer: string): vscode.CodeLens[] { + symbolName: string, range: Range, symbolContainer: string): CodeLens[] { - let fn: TestFunction; + let fn: TestFunction | undefined; if (symbolContainer.length === 0) { - fn = functionsAndSuites.functions.find(fn => fn.name === symbolName); - } - else { + fn = functionsAndSuites.functions.find(func => func.name === symbolName); + } else { // Assume single levels for now functionsAndSuites.suites .filter(s => s.name === symbolContainer) @@ -189,7 +189,7 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS // Ok, possible we're dealing with parameterized unit tests // If we have [ in the name, then this is a parameterized function - let functions = functionsAndSuites.functions.filter(fn => fn.name.startsWith(symbolName + '[') && fn.name.endsWith(']')); + const functions = functionsAndSuites.functions.filter(func => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']')); if (functions.length === 0) { return null; } @@ -211,12 +211,12 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS // Find all flattened functions return [ new CodeLens(range, { - title: getTestStatusIcons(functions) + constants.Text.CodeLensRunUnitTest + ' (Multiple)', + title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, arguments: [filePath, functions] }), new CodeLens(range, { - title: getTestStatusIcons(functions) + constants.Text.CodeLensDebugUnitTest + ' (Multiple)', + title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, arguments: [filePath, functions] }) @@ -247,4 +247,4 @@ function getAllTestSuitesAndFunctions(testSuite: TestSuite): FunctionsAndSuites all.suites.push(...allChildItems.suites); }); return all; -} \ No newline at end of file +} diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index e1f3dde1e5ab..0ab133b4c6cb 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -1,37 +1,44 @@ // import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestFunction, TestStatus, FlattenedTestFunction, FlattenedTestSuite, CANCELLATION_REASON} from './contracts'; -import { Tests, TestStatus, TestsToRun, CANCELLATION_REASON } from './contracts'; import * as vscode from 'vscode'; -import { resetTestResults, displayTestErrorMessage, storeDiscoveredTests } from './testUtils'; -import { Installer, Product } from '../../common/installer'; -import { isNotInstalledError } from '../../common/helpers'; import { IPythonSettings, PythonSettings } from '../../common/configSettings'; +import { isNotInstalledError } from '../../common/helpers'; +import { Installer, Product } from '../../common/installer'; +import { CANCELLATION_REASON, Tests, TestStatus, TestsToRun } from './contracts'; +import { displayTestErrorMessage, resetTestResults, storeDiscoveredTests } from './testUtils'; export abstract class BaseTestManager { + protected readonly settings: IPythonSettings; private tests: Tests; + // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; private cancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; + private discoverTestsPromise: Promise; + constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel) { + this._status = TestStatus.Unknown; + this.installer = new Installer(); + this.settings = PythonSettings.getInstance(this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); + } protected get cancellationToken(): vscode.CancellationToken { if (this.cancellationTokenSource) { return this.cancellationTokenSource.token; } } + // tslint:disable-next-line:no-empty public dispose() { } public get status(): TestStatus { return this._status; } + public get workingDirectory(): string { + const settings = PythonSettings.getInstance(vscode.Uri.file(this.rootDirectory)); + return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.rootDirectory; + } public stop() { if (this.cancellationTokenSource) { this.cancellationTokenSource.cancel(); } } - protected readonly settings: IPythonSettings; - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel) { - this._status = TestStatus.Unknown; - this.installer = new Installer(); - this.settings = PythonSettings.getInstance(vscode.Uri.file(this.rootDirectory)); - } public reset() { this._status = TestStatus.Unknown; this.tests = null; @@ -43,18 +50,7 @@ export abstract class BaseTestManager { resetTestResults(this.tests); } - private createCancellationToken() { - this.disposeCancellationToken(); - this.cancellationTokenSource = new vscode.CancellationTokenSource(); - } - private disposeCancellationToken() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.dispose(); - } - this.cancellationTokenSource = null; - } - private discoverTestsPromise: Promise; - discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { + public discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { if (this.discoverTestsPromise) { return this.discoverTestsPromise; } @@ -87,13 +83,13 @@ export abstract class BaseTestManager { if (haveErrorsInDiscovering && !quietMode) { displayTestErrorMessage('There were some errors in disovering unit tests'); } - storeDiscoveredTests(tests); + storeDiscoveredTests(tests, this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); this.disposeCancellationToken(); return tests; }).catch(reason => { if (isNotInstalledError(reason) && !quietMode) { - this.installer.promptToInstall(this.product, vscode.Uri.file(this.rootDirectory)); + this.installer.promptToInstall(this.product, this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); } this.tests = null; @@ -101,24 +97,25 @@ export abstract class BaseTestManager { if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; - } - else { + } else { this._status = TestStatus.Error; this.outputChannel.appendLine('Test Disovery failed: '); + // tslint:disable-next-line:prefer-template this.outputChannel.appendLine('' + reason); } - storeDiscoveredTests(null); + storeDiscoveredTests(null, vscode.Uri.file(this.rootDirectory)); this.disposeCancellationToken(); return Promise.reject(reason); }); } - abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; public runTest(testsToRun?: TestsToRun, debug?: boolean): Promise; + // tslint:disable-next-line:unified-signatures public runTest(runFailedTests?: boolean, debug?: boolean): Promise; + // tslint:disable-next-line:no-any public runTest(args: any, debug?: boolean): Promise { let runFailedTests = false; let testsToRun: TestsToRun = null; - let moreInfo = { + const moreInfo = { Test_Provider: this.testProvider, Run_Failed_Tests: 'false', Run_Specific_File: 'false', @@ -128,6 +125,7 @@ export abstract class BaseTestManager { if (typeof args === 'boolean') { runFailedTests = args === true; + // tslint:disable-next-line:prefer-template moreInfo.Run_Failed_Tests = runFailedTests + ''; } if (typeof args === 'object' && args !== null) { @@ -173,13 +171,24 @@ export abstract class BaseTestManager { if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; - } - else { + } else { this._status = TestStatus.Error; } this.disposeCancellationToken(); return Promise.reject(reason); }); } - abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; + // tslint:disable-next-line:no-any + protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; + protected abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; + private createCancellationToken() { + this.disposeCancellationToken(); + this.cancellationTokenSource = new vscode.CancellationTokenSource(); + } + private disposeCancellationToken() { + if (this.cancellationTokenSource) { + this.cancellationTokenSource.dispose(); + } + this.cancellationTokenSource = null; + } } diff --git a/src/client/unittests/common/configSettingService.ts b/src/client/unittests/common/configSettingService.ts new file mode 100644 index 000000000000..1e005c8973bc --- /dev/null +++ b/src/client/unittests/common/configSettingService.ts @@ -0,0 +1,67 @@ +import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { Product } from '../../common/installer'; +import { ITestConfigSettingsService, UnitTestProduct } from './contracts'; + +export class TestConfigSettingsService implements ITestConfigSettingsService { + private static getTestArgSetting(product: UnitTestProduct) { + switch (product) { + 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'); + } + } + private static getTestEnablingSetting(product: UnitTestProduct) { + switch (product) { + 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'); + } + } + // tslint:disable-next-line:no-any + private static async updateSetting(testDirectory: string | Uri, setting: string, value: any) { + let pythonConfig: WorkspaceConfiguration; + let configTarget: ConfigurationTarget; + const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + configTarget = ConfigurationTarget.Workspace; + pythonConfig = workspace.getConfiguration('python'); + } else if (workspace.workspaceFolders.length === 1) { + configTarget = ConfigurationTarget.Workspace; + pythonConfig = workspace.getConfiguration('python', workspace.workspaceFolders[0].uri); + } else { + configTarget = ConfigurationTarget.Workspace; + const workspaceFolder = workspace.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); + } + + 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 enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = TestConfigSettingsService.getTestEnablingSetting(product); + return TestConfigSettingsService.updateSetting(testDirectory, setting, true); + } + + public 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/contracts.ts b/src/client/unittests/common/contracts.ts index 02dc03c18980..0e69ab169873 100644 --- a/src/client/unittests/common/contracts.ts +++ b/src/client/unittests/common/contracts.ts @@ -1,13 +1,18 @@ +import { Uri, Disposable } from 'vscode'; +import { Product } from '../../common/installer'; +import { BaseTestManager } from './baseTestManager'; + export const CANCELLATION_REASON = 'cancelled_user_request'; -export interface TestFolder extends TestResult { +export type TestFolder = TestResult & { name: string; testFiles: TestFile[]; nameToRun: string; status?: TestStatus; folders: TestFolder[]; -} -export interface TestFile extends TestResult { +}; + +export type TestFile = TestResult & { name: string; fullPath: string; functions: TestFunction[]; @@ -16,8 +21,9 @@ export interface TestFile extends TestResult { xmlName: string; status?: TestStatus; errorsWhenDiscovering?: string; -} -export interface TestSuite extends TestResult { +}; + +export type TestSuite = TestResult & { name: string; functions: TestFunction[]; suites: TestSuite[]; @@ -26,13 +32,15 @@ export interface TestSuite extends TestResult { nameToRun: string; xmlName: string; status?: TestStatus; -} -export interface TestFunction extends TestResult { +}; + +export type TestFunction = TestResult & { name: string; nameToRun: string; status?: TestStatus; -} -export interface TestResult extends Node { +}; + +export type TestResult = Node & { passed?: boolean; time: number; line?: number; @@ -42,35 +50,40 @@ export interface TestResult extends Node { functionsFailed?: number; functionsDidNotRun?: number; } -export interface Node { +export type Node = { expanded?: Boolean; -} -export interface FlattenedTestFunction { +}; + +export type FlattenedTestFunction = { testFunction: TestFunction; parentTestSuite?: TestSuite; parentTestFile: TestFile; xmlClassName: string; -} -export interface FlattenedTestSuite { +}; + +export type FlattenedTestSuite = { testSuite: TestSuite; parentTestSuite?: TestSuite; parentTestFile: TestFile; xmlClassName: string; -} -export interface TestSummary { +}; + +export type TestSummary = { passed: number; failures: number; errors: number; skipped: number; -} -export interface Tests { +}; + +export type Tests = { summary: TestSummary; testFiles: TestFile[]; testFunctions: FlattenedTestFunction[]; testSuits: FlattenedTestSuite[]; testFolders: TestFolder[]; rootTestFolders: TestFolder[]; -} +}; + export enum TestStatus { Unknown, Discovering, @@ -81,9 +94,32 @@ export enum TestStatus { Skipped, Pass } -export interface TestsToRun { + +export type TestsToRun = { testFolder?: TestFolder[]; testFile?: TestFile[]; testSuite?: TestSuite[]; testFunction?: TestFunction[]; +}; + +export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; + +export interface ITestConfigSettingsService { + updateTestArgs(testDirectory: string, product: UnitTestProduct, args: string[]): Promise; + enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; +} + +export interface ITestManagerService extends Disposable { + getTestManager(): BaseTestManager | undefined; + getTestWorkingDirectory(): string; + getPreferredTestManager(): UnitTestProduct; +} +export interface ITestManagerServiceFactory { + createTestManagerService(wkspace: Uri): ITestManagerService; +} +export interface IWorkspaceTestManagerService extends Disposable { + getTestManager(wkspace: Uri): BaseTestManager | undefined; + getTestWorkingDirectory(wkspace: Uri): string; + getPreferredTestManager(wkspace: Uri): UnitTestProduct; } diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts index a1d754f20dbd..67d741018674 100644 --- a/src/client/unittests/common/debugLauncher.ts +++ b/src/client/unittests/common/debugLauncher.ts @@ -1,19 +1,21 @@ import * as os from 'os'; -import { CancellationToken, debug, OutputChannel, workspace, Uri } from 'vscode'; +import { CancellationToken, debug, OutputChannel, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { execPythonFile } from './../../common/utils'; import { createDeferred } from './../../common/helpers'; +import { execPythonFile } from './../../common/utils'; export function launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel) { const pythonSettings = PythonSettings.getInstance(rootDirectory ? Uri.file(rootDirectory) : undefined); + // tslint:disable-next-line:no-any const def = createDeferred(); + // tslint:disable-next-line:no-any const launchDef = createDeferred(); let outputChannelShown = false; execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { - if (data.startsWith('READY' + os.EOL)) { + if (data.startsWith(`READY${os.EOL}`)) { // debug socket server has started. launchDef.resolve(); - data = data.substring(('READY' + os.EOL).length); + data = data.substring((`READY${os.EOL}`).length); } if (!outputChannelShown) { @@ -44,14 +46,14 @@ export function launchDebugger(rootDirectory: string, testArgs: string[], token? workspaceFolder = workspace.workspaceFolders[0]; } return debug.startDebugging(workspaceFolder, { - "name": "Debug Unit Test", - "type": "python", - "request": "attach", - "localRoot": rootDirectory, - "remoteRoot": rootDirectory, - "port": pythonSettings.unitTest.debugPort, - "secret": "my_secret", - "host": "localhost" + name: 'Debug Unit Test', + type: 'python', + request: 'attach', + localRoot: rootDirectory, + remoteRoot: rootDirectory, + port: pythonSettings.unitTest.debugPort, + secret: 'my_secret', + host: 'localhost' }); }).catch(reason => { if (!def.rejected && !def.resolved) { diff --git a/src/client/unittests/common/testConfigurationManager.ts b/src/client/unittests/common/testConfigurationManager.ts index 8b031b7a63b2..84843f7c69d5 100644 --- a/src/client/unittests/common/testConfigurationManager.ts +++ b/src/client/unittests/common/testConfigurationManager.ts @@ -1,14 +1,26 @@ +import * as path from 'path'; import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { createDeferred } from '../../common/helpers'; +import { Installer } from '../../common/installer'; import { getSubDirectories } from '../../common/utils'; -import * as path from 'path'; +import { ITestConfigSettingsService, UnitTestProduct } from './contracts'; export abstract class TestConfigurationManager { - public abstract enable(): Thenable; - public abstract disable(): Thenable; - constructor(protected readonly outputChannel: vscode.OutputChannel) { } + constructor(protected workspace: Uri, + protected product: UnitTestProduct, + protected readonly outputChannel: vscode.OutputChannel, + protected installer: Installer, + protected testConfigSettingsService: ITestConfigSettingsService) { } + // tslint:disable-next-line:no-any public abstract configure(rootDir: string): Promise; - + public enable() { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } + // tslint:disable-next-line:no-any + public disable(): Thenable { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } protected selectTestDir(rootDir: string, subDirs: string[], customOptions: vscode.QuickPickItem[] = []): Promise { const options = { matchOnDescription: true, @@ -22,7 +34,7 @@ export abstract class TestConfigurationManager { } return { label: dirName, - description: '', + description: '' }; }).filter(item => item !== null); @@ -46,12 +58,12 @@ export abstract class TestConfigurationManager { matchOnDetail: true, placeHolder: 'Select the pattern to identify test files' }; - let items: vscode.QuickPickItem[] = [ - { label: '*test.py', description: `Python Files ending with 'test'` }, - { label: '*_test.py', description: `Python Files ending with '_test'` }, - { label: 'test*.py', description: `Python Files begining with 'test'` }, - { label: 'test_*.py', description: `Python Files begining with 'test_'` }, - { label: '*test*.py', description: `Python Files containing the word 'test'` } + const items: vscode.QuickPickItem[] = [ + { label: '*test.py', description: 'Python Files ending with \'test\'' }, + { label: '*_test.py', description: 'Python Files ending with \'_test\'' }, + { label: 'test*.py', description: 'Python Files begining with \'test\'' }, + { label: 'test_*.py', description: 'Python Files begining with \'test_\'' }, + { label: '*test*.py', description: 'Python Files containing the word \'test\'' } ]; const def = createDeferred(); @@ -69,13 +81,13 @@ export abstract class TestConfigurationManager { return getSubDirectories(rootDir).then(subDirs => { subDirs.sort(); - // Find out if there are any dirs with the name test and place them on the top - let possibleTestDirs = subDirs.filter(dir => dir.match(/test/i)); - let nonTestDirs = subDirs.filter(dir => possibleTestDirs.indexOf(dir) === -1); + // Find out if there are any dirs with the name test and place them on the top. + const possibleTestDirs = subDirs.filter(dir => dir.match(/test/i)); + const nonTestDirs = subDirs.filter(dir => possibleTestDirs.indexOf(dir) === -1); possibleTestDirs.push(...nonTestDirs); - // The test dirs are now on top + // The test dirs are now on top. return possibleTestDirs; }); } -} \ No newline at end of file +} diff --git a/src/client/unittests/common/testManagerService.ts b/src/client/unittests/common/testManagerService.ts new file mode 100644 index 000000000000..e07ee373fa33 --- /dev/null +++ b/src/client/unittests/common/testManagerService.ts @@ -0,0 +1,53 @@ +import { Disposable, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; +import { TestManager as NoseTestManager } from '../nosetest/main'; +import { TestManager as PyTestTestManager } from '../pytest/main'; +import { TestManager as UnitTestTestManager } from '../unittest/main'; +import { BaseTestManager } from './baseTestManager'; +import { ITestManagerService, UnitTestProduct } from './contracts'; + +type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; + +export class TestManagerService implements ITestManagerService { + private testManagers = new Map(); + constructor(private wkspace: Uri, private outChannel: OutputChannel) { + this.testManagers.set(Product.nosetest, { create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel) }); + this.testManagers.set(Product.pytest, { create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel) }); + this.testManagers.set(Product.unittest, { create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel) }); + } + public dispose() { + this.testManagers.forEach(info => { + if (info.instance) { + info.instance.dispose(); + } + }); + } + public getTestManager(): BaseTestManager | undefined { + const preferredTestManager = this.getPreferredTestManager(); + if (!preferredTestManager) { + return; + } + + const info = this.testManagers.get(preferredTestManager); + if (!info.instance) { + const testDirectory = this.getTestWorkingDirectory(); + info.instance = info.create(testDirectory); + } + return info.instance; + } + public getTestWorkingDirectory() { + const settings = PythonSettings.getInstance(this.wkspace); + return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.wkspace.fsPath; + } + public getPreferredTestManager(): UnitTestProduct { + const settings = PythonSettings.getInstance(this.wkspace); + if (settings.unitTest.nosetestsEnabled) { + return Product.nosetest; + } else if (settings.unitTest.pyTestEnabled) { + return Product.pytest; + } else if (settings.unitTest.unittestEnabled) { + return Product.unittest; + } + } +} diff --git a/src/client/unittests/common/testManagerServiceFactory.ts b/src/client/unittests/common/testManagerServiceFactory.ts new file mode 100644 index 000000000000..d48b60bed15f --- /dev/null +++ b/src/client/unittests/common/testManagerServiceFactory.ts @@ -0,0 +1,10 @@ +import { OutputChannel, Uri } from 'vscode'; +import { ITestManagerService, ITestManagerServiceFactory } from './contracts'; +import { TestManagerService } from './testManagerService'; + +export class TestManagerServiceFactory implements ITestManagerServiceFactory { + constructor(private outChannel: OutputChannel) { } + public createTestManagerService(wkspace: Uri): ITestManagerService { + return new TestManagerService(wkspace, this.outChannel); + } +} diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 9df1e2ca73ef..85122b9a33b3 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,9 +1,48 @@ -import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestStatus, FlattenedTestFunction, FlattenedTestSuite} from './contracts'; -import * as vscode from 'vscode'; +import { ITestVisitor } from './testUtils'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri, workspace } from 'vscode'; import * as constants from '../../common/constants'; +import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, Tests, TestStatus, TestsToRun, TestSuite, TestFunction } from './contracts'; + +export interface ITestCollectionStorageService { + getTests(wkspace: Uri): Tests | undefined; + storeTests(wkspace: Uri, tests: Tests): void; +} -let discoveredTests: Tests; +export class TestCollectionStorageService implements ITestCollectionStorageService { + private testsIndexedByWorkspaceUri: Map; + constructor() { + this.testsIndexedByWorkspaceUri = new Map(); + } + public getTests(wkspace: Uri): Tests | undefined { + const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; + return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; + } + public storeTests(wkspace: Uri, tests: Tests): void { + const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; + this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); + } +} + +function getWorkspaceFolderPath(resource?: Uri): string | undefined { + if (!resource) { + return undefined; + } + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.path : undefined; +} +export async function selectTestWorkspace(): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } else if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } else { + // tslint:disable-next-line:no-any prefer-type-cast + const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspace ? workspaceFolder.uri : undefined; + } +} export function displayTestErrorMessage(message: string) { vscode.window.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { @@ -13,28 +52,22 @@ export function displayTestErrorMessage(message: string) { }); } -export function getDiscoveredTests(): Tests { - return discoveredTests; -} -export function storeDiscoveredTests(tests: Tests) { - discoveredTests = tests; -} -export function resolveValueAsTestToRun(name: string, rootDirectory: string): TestsToRun { - // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we +export function parseTestName(name: string, rootDirectory: string): TestsToRun { + // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. // use to identify a file given the full file name, similary for a folder and function // Perhaps something like a parser or methods like TestFunction.fromString()... something) - let tests = getDiscoveredTests(); + const tests = getDiscoveredTests(); if (!tests) { return null; } const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - let testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); - if (testFolders.length > 0) { return { testFolder: testFolders }; }; + const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); + if (testFolders.length > 0) { return { testFolder: testFolders }; } - let testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); - if (testFiles.length > 0) { return { testFile: testFiles }; }; + const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); + if (testFiles.length > 0) { return { testFile: testFiles }; } - let testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); - if (testFns.length > 0) { return { testFunction: testFns }; }; + const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); + if (testFns.length > 0) { return { testFunction: testFns }; } // Just return this as a test file return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; @@ -45,225 +78,335 @@ export function extractBetweenDelimiters(content: string, startDelimiter: string } export function convertFileToPackage(filePath: string): string { - let lastIndex = filePath.lastIndexOf('.'); + const lastIndex = filePath.lastIndexOf('.'); return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); } -export function updateResults(tests: Tests) { - tests.testFiles.forEach(updateResultsUpstream); - tests.testFolders.forEach(updateFolderResultsUpstream); +export interface ITestResultService { + resetResults(tests: Tests): void; + updateResults(tests: Tests): void; + updateTestSuiteResults(test: TestSuite): void; + updateTestFileResults(test: TestFile): void; + updateTestFolderResults(testFolder: TestFolder): void; } -export function updateFolderResultsUpstream(testFolder: TestFolder) { - let allFilesPassed = true; - let allFilesRan = true; - - testFolder.testFiles.forEach(fl => { - if (allFilesPassed && typeof fl.passed === 'boolean') { - if (!fl.passed) { - allFilesPassed = false; +export class TestResultService implements ITestResultService { + public resetResults(tests: Tests): void { + tests.testFolders.forEach(f => { + f.functionsDidNotRun = 0; + f.functionsFailed = 0; + f.functionsPassed = 0; + f.passed = null; + f.status = TestStatus.Unknown; + }); + tests.testFunctions.forEach(fn => { + fn.testFunction.passed = null; + fn.testFunction.time = 0; + fn.testFunction.message = ''; + fn.testFunction.traceback = ''; + fn.testFunction.status = TestStatus.Unknown; + fn.testFunction.functionsFailed = 0; + fn.testFunction.functionsPassed = 0; + fn.testFunction.functionsDidNotRun = 0; + }); + tests.testSuits.forEach(suite => { + suite.testSuite.passed = null; + suite.testSuite.time = 0; + suite.testSuite.status = TestStatus.Unknown; + suite.testSuite.functionsFailed = 0; + suite.testSuite.functionsPassed = 0; + suite.testSuite.functionsDidNotRun = 0; + }); + tests.testFiles.forEach(testFile => { + testFile.passed = null; + testFile.time = 0; + testFile.status = TestStatus.Unknown; + testFile.functionsFailed = 0; + testFile.functionsPassed = 0; + testFile.functionsDidNotRun = 0; + }); + } + public updateResults(tests: Tests): void { + tests.testFiles.forEach(test => this.updateTestFileResults(test)); + tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); + } + public updateTestSuiteResults(test: TestSuite): void { + this.updateTestSuiteAndFileResults(test); + } + public updateTestFileResults(test: TestFile): void { + this.updateTestSuiteAndFileResults(test); + } + public updateTestFolderResults(testFolder: TestFolder): void { + let allFilesPassed = true; + let allFilesRan = true; + + testFolder.testFiles.forEach(fl => { + if (allFilesPassed && typeof fl.passed === 'boolean') { + if (!fl.passed) { + allFilesPassed = false; + } + } else { + allFilesRan = false; } - } - else { - allFilesRan = false; - } - testFolder.functionsFailed += fl.functionsFailed; - testFolder.functionsPassed += fl.functionsPassed; - }); + testFolder.functionsFailed += fl.functionsFailed; + testFolder.functionsPassed += fl.functionsPassed; + }); - let allFoldersPassed = true; - let allFoldersRan = true; + let allFoldersPassed = true; + let allFoldersRan = true; - testFolder.folders.forEach(folder => { - updateFolderResultsUpstream(folder); - if (allFoldersPassed && typeof folder.passed === 'boolean') { - if (!folder.passed) { - allFoldersPassed = false; + testFolder.folders.forEach(folder => { + this.updateTestFolderResults(folder); + if (allFoldersPassed && typeof folder.passed === 'boolean') { + if (!folder.passed) { + allFoldersPassed = false; + } + } else { + allFoldersRan = false; } - } - else { - allFoldersRan = false; - } - testFolder.functionsFailed += folder.functionsFailed; - testFolder.functionsPassed += folder.functionsPassed; - }); + testFolder.functionsFailed += folder.functionsFailed; + testFolder.functionsPassed += folder.functionsPassed; + }); - if (allFilesRan && allFoldersRan) { - testFolder.passed = allFilesPassed && allFoldersPassed; - testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; - } - else { - testFolder.passed = null; - testFolder.status = TestStatus.Unknown; + if (allFilesRan && allFoldersRan) { + testFolder.passed = allFilesPassed && allFoldersPassed; + testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; + } else { + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } } -} + private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { + let totalTime = 0; + let allFunctionsPassed = true; + let allFunctionsRan = true; + + test.functions.forEach(fn => { + totalTime += fn.time; + if (typeof fn.passed === 'boolean') { + if (fn.passed) { + test.functionsPassed += 1; + } else { + test.functionsFailed += 1; + allFunctionsPassed = false; + } + } else { + allFunctionsRan = false; + } + }); -export function updateResultsUpstream(test: TestSuite | TestFile) { - let totalTime = 0; - let allFunctionsPassed = true; - let allFunctionsRan = true; + let allSuitesPassed = true; + let allSuitesRan = true; - test.functions.forEach(fn => { - totalTime += fn.time; - if (typeof fn.passed === 'boolean') { - if (fn.passed) { - test.functionsPassed += 1; - } - else { - test.functionsFailed += 1; - allFunctionsPassed = false; + test.suites.forEach(suite => { + this.updateTestSuiteResults(suite); + totalTime += suite.time; + if (allSuitesRan && typeof suite.passed === 'boolean') { + if (!suite.passed) { + allSuitesPassed = false; + } + } else { + allSuitesRan = false; } - } - else { - allFunctionsRan = false; - } - }); - let allSuitesPassed = true; - let allSuitesRan = true; + test.functionsFailed += suite.functionsFailed; + test.functionsPassed += suite.functionsPassed; + }); - test.suites.forEach(suite => { - updateResultsUpstream(suite); - totalTime += suite.time; - if (allSuitesRan && typeof suite.passed === 'boolean') { - if (!suite.passed) { - allSuitesPassed = false; - } - } - else { - allSuitesRan = false; + test.time = totalTime; + if (allSuitesRan && allFunctionsRan) { + test.passed = allFunctionsPassed && allSuitesPassed; + test.status = test.passed ? TestStatus.Idle : TestStatus.Error; + } else { + test.passed = null; + test.status = TestStatus.Unknown; } + } +} +export function updateResults(tests: Tests) { + new TestResultService().updateResults(tests); +} - test.functionsFailed += suite.functionsFailed; - test.functionsPassed += suite.functionsPassed; - }); +export interface ITestsHelper { + flattenTestFiles(testFiles: TestFile[]): Tests; + flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite): void; + placeTestFilesIntoFolders(tests: Tests): void; +} - test.time = totalTime; - if (allSuitesRan && allFunctionsRan) { - test.passed = allFunctionsPassed && allSuitesPassed; - test.status = test.passed ? TestStatus.Idle : TestStatus.Error; +export interface ITestVisitor { + visitTestFunction(testFunction: TestFunction): void; + visitTestSuite(testSuite: TestSuite): void; + visitTestFile(testFile: TestFile): void; +} + +export class TestFlatteningVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _testFunctions: TestFunction[] = []; + // tslint:disable-next-line:variable-name + private _testSuites: TestSuite[] = []; + // tslint:disable-next-line:variable-name + private _testFiles: TestFile[] = []; + public get testFunctions(): TestFunction[] { + return [...this._testFunctions]; + } + public get testSuites(): TestFunction[] { + return [...this._testSuites]; } - else { - test.passed = null; - test.status = TestStatus.Unknown; + public get testFiles(): TestFunction[] { + return [...this._testFiles]; + } + public visitTestFunction(testFunction: TestFunction): void { + this._testFunctions.push(testFunction); + } + public visitTestSuite(testSuite: TestSuite): void { + this._testSuites.push(testSuite); + } + public visitTestFile(testFile: TestFile): void { + this._testFiles.push(testFile); } } -export function placeTestFilesInFolders(tests: Tests) { - // First get all the unique folders - const folders: string[] = []; - tests.testFiles.forEach(file => { - let dir = path.dirname(file.name); - if (folders.indexOf(dir) === -1) { - folders.push(dir); +// tslint:disable-next-line:max-classes-per-file +export class TestFolderGenerationVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _testFolders: TestFolder[] = []; + // tslint:disable-next-line:variable-name + private _rootTestFolders: TestFolder[] = []; + private folderMap = new Map(); + public get testFolders(): Readonly { + return [...this._testFolders]; + } + public get rootTestFolders(): Readonly { + return [...this._rootTestFolders]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // First get all the unique folders + const folders: string[] = []; + const dir = path.dirname(testFile.name); + if (this.folderMap.has(dir)) { + const folder = this.folderMap.get(dir); + folder.testFiles.push(testFile); + return; } - }); - tests.testFolders = []; - const folderMap = new Map(); - folders.sort(); - - folders.forEach(dir => { - dir.split(path.sep).reduce((parentPath, currentName, index, values) => { + dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { let newPath = currentName; let parentFolder: TestFolder; - if (parentPath.length > 0) { - parentFolder = folderMap.get(parentPath); - newPath = path.join(parentPath, currentName); + if (accumulatedPath.length > 0) { + parentFolder = this.folderMap.get(accumulatedPath); + newPath = path.join(accumulatedPath, currentName); } - if (!folderMap.has(newPath)) { + if (!this.folderMap.has(newPath)) { const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - folderMap.set(newPath, testFolder); + this.folderMap.set(newPath, testFolder); if (parentFolder) { parentFolder.folders.push(testFolder); + } else { + this._rootTestFolders.push(testFolder); } - else { - tests.rootTestFolders.push(testFolder); - } - tests.testFiles.filter(fl => path.dirname(fl.name) === newPath).forEach(testFile => { - testFolder.testFiles.push(testFile); - }); - tests.testFolders.push(testFolder); + this._testFolders.push(testFolder); } return newPath; }, ''); - }); + + // tslint:disable-next-line:no-non-null-assertion + this.folderMap.get(dir)!.testFiles.push(testFile); + } } -export function flattenTestFiles(testFiles: TestFile[]): Tests { - let fns: FlattenedTestFunction[] = []; - let suites: FlattenedTestSuite[] = []; - testFiles.forEach(testFile => { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => { - fns.push({ testFunction: fn, xmlClassName: packageName, parentTestFile: testFile }); - }); - testFile.suites.forEach(suite => { - suites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - flattenTestSuites(fns, suites, testFile, suite); +// tslint:disable-next-line:max-classes-per-file +export class TestsHelper implements ITestsHelper { + public flattenTestFiles(testFiles: TestFile[]): Tests { + const fns: FlattenedTestFunction[] = []; + const suites: FlattenedTestSuite[] = []; + testFiles.forEach(testFile => { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach(fn => { + fns.push({ testFunction: fn, xmlClassName: packageName, parentTestFile: testFile }); + }); + + testFile.suites.forEach(suite => { + suites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); + this.flattenTestSuites(fns, suites, testFile, suite); + }); }); - }); - let tests = { - testFiles: testFiles, - testFunctions: fns, testSuits: suites, - testFolders: [], - rootTestFolders: [], - summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } - }; + const tests = { + testFiles: testFiles, + testFunctions: fns, testSuits: suites, + testFolders: [], + rootTestFolders: [], + summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } + }; + + this.placeTestFilesIntoFolders(tests); + + return tests; + } + public flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite) { + testSuite.functions.forEach(fn => { + flattenedFns.push({ testFunction: fn, xmlClassName: testSuite.xmlName, parentTestFile: testFile, parentTestSuite: testSuite }); + }); - placeTestFilesInFolders(tests); + // We may have child classes + testSuite.suites.forEach(suite => { + flattenedSuites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); + this.flattenTestSuites(flattenedFns, flattenedSuites, testFile, suite); + }); + } + public placeTestFilesIntoFolders(tests: Tests): void { + // First get all the unique folders + const folders: string[] = []; + tests.testFiles.forEach(file => { + const dir = path.dirname(file.name); + if (folders.indexOf(dir) === -1) { + folders.push(dir); + } + }); - return tests; + tests.testFolders = []; + const folderMap = new Map(); + folders.sort(); + + folders.forEach(dir => { + dir.split(path.sep).reduce((parentPath, currentName, index, values) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (parentPath.length > 0) { + parentFolder = folderMap.get(parentPath); + newPath = path.join(parentPath, currentName); + } + if (!folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + tests.rootTestFolders.push(testFolder); + } + tests.testFiles.filter(fl => path.dirname(fl.name) === newPath).forEach(testFile => { + testFolder.testFiles.push(testFile); + }); + tests.testFolders.push(testFolder); + } + return newPath; + }, ''); + }); + } } -export function flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite) { - testSuite.functions.forEach(fn => { - flattenedFns.push({ testFunction: fn, xmlClassName: testSuite.xmlName, parentTestFile: testFile, parentTestSuite: testSuite }); - }); - // We may have child classes - testSuite.suites.forEach(suite => { - flattenedSuites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - flattenTestSuites(flattenedFns, flattenedSuites, testFile, suite); - }); +export function flattenTestFiles(testFiles: TestFile[]): Tests { + return new TestsHelper().flattenTestFiles(testFiles); } export function resetTestResults(tests: Tests) { - tests.testFolders.forEach(f => { - f.functionsDidNotRun = 0; - f.functionsFailed = 0; - f.functionsPassed = 0; - f.passed = null; - f.status = TestStatus.Unknown; - }); - tests.testFunctions.forEach(fn => { - fn.testFunction.passed = null; - fn.testFunction.time = 0; - fn.testFunction.message = ''; - fn.testFunction.traceback = ''; - fn.testFunction.status = TestStatus.Unknown; - fn.testFunction.functionsFailed = 0; - fn.testFunction.functionsPassed = 0; - fn.testFunction.functionsDidNotRun = 0; - }); - tests.testSuits.forEach(suite => { - suite.testSuite.passed = null; - suite.testSuite.time = 0; - suite.testSuite.status = TestStatus.Unknown; - suite.testSuite.functionsFailed = 0; - suite.testSuite.functionsPassed = 0; - suite.testSuite.functionsDidNotRun = 0; - }); - tests.testFiles.forEach(testFile => { - testFile.passed = null; - testFile.time = 0; - testFile.status = TestStatus.Unknown; - testFile.functionsFailed = 0; - testFile.functionsPassed = 0; - testFile.functionsDidNotRun = 0; - }); -} \ No newline at end of file + new TestResultService().resetResults(tests); +} diff --git a/src/client/unittests/common/workspaceTestManagerService.ts b/src/client/unittests/common/workspaceTestManagerService.ts new file mode 100644 index 000000000000..17178d7d3684 --- /dev/null +++ b/src/client/unittests/common/workspaceTestManagerService.ts @@ -0,0 +1,74 @@ +import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; +import { TestManager as NoseTestManager } from '../nosetest/main'; +import { TestManager as PyTestTestManager } from '../pytest/main'; +import { TestManager as UnitTestTestManager } from '../unittest/main'; +import { BaseTestManager } from './baseTestManager'; +import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './contracts'; +import { TestManagerService } from './testManagerService'; + +type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; + +export class WorkspaceTestManagerService implements IWorkspaceTestManagerService, Disposable { + private workspaceTestManagers = new Map(); + private workspaceTestSettings = new Map(); + private disposables: Disposable[] = []; + constructor(private outChannel: OutputChannel, + private testManagerServiceFactory: ITestManagerServiceFactory) { + // workspace.onDidChangeConfiguration(this.monitorChangesToTestSettings, this, this.disposables); + } + public dispose() { + this.workspaceTestManagers.forEach(info => info.dispose()); + } + public getTestManager(wkspace: Uri): BaseTestManager | undefined { + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getTestManager(); + } + public getTestWorkingDirectory(wkspace: Uri) { + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getTestWorkingDirectory(); + } + public getPreferredTestManager(wkspace: Uri): UnitTestProduct { + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath).getPreferredTestManager(); + } + private ensureTestManagerService(wkspace: Uri) { + if (!this.workspaceTestManagers.has(wkspace.fsPath)) { + this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory.createTestManagerService(wkspace)); + // this.trackTestSettings(wkspace); + } + } + // private trackTestSettings(wkspace: Uri) { + // const pythonConfig = workspace.getConfiguration('python', wkspace); + // // tslint:disable-next-line:no-backbone-get-set-outside-model + // const unitTestSettings = pythonConfig.get<{}>('unitTest'); + // this.workspaceTestSettings.set(wkspace.fsPath, JSON.stringify(unitTestSettings)); + // } + // private monitorChangesToTestSettings() { + // this.workspaceTestSettings.forEach((_, workspacePath) => { + // const testSettingsChanged = this.checkForChangesInSettings(Uri.file(workspacePath)); + // if (testSettingsChanged) { + // this.rebuildTestManagers(workspace); + // } + // }); + // } + // private rebuildTestManagers(wkspace: Uri) { + // if (!this.workspaceTestManagers.get(wkspace.fsPath)) { + // return; + // } + // const service = this.workspaceTestManagers.get(wkspace.fsPath); + // const mgr = service.getTestManager(); + // mgr.stop(); + // mgr.dispose(); + // service.dispose(); + // this.ensureTestManagerService(wkspace); + // service.getTestManager(); + // } + // private checkForChangesInSettings(wkspace: Uri) { + // const currentSettings = this.workspaceTestSettings.get(wkspace.fsPath); + // this.trackTestSettings(wkspace); + // const newSettings = this.workspaceTestSettings.get(wkspace.fsPath); + // return currentSettings !== newSettings; + // } +} diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts index 2f02601266a0..df26a46d0201 100644 --- a/src/client/unittests/common/xUnitParser.ts +++ b/src/client/unittests/common/xUnitParser.ts @@ -6,7 +6,7 @@ export enum PassCalculationFormulae { pytest, nosetests } -interface TestSuiteResult { +type TestSuiteResult = { $: { errors: string; failures: string; @@ -17,8 +17,8 @@ interface TestSuiteResult { time: string; }; testcase: TestCaseResult[]; -} -interface TestCaseResult { +}; +type TestCaseResult = { $: { classname: string; file: string; @@ -38,30 +38,32 @@ interface TestCaseResult { _: string; $: { message: string, type: string } }[]; -} +}; +// tslint:disable-next-line:no-any function getSafeInt(value: string, defaultValue: any = 0): number { - const num = parseInt(value); + const num = parseInt(value, 10); if (isNaN(num)) { return defaultValue; } return num; } -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { +export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<{}> { + // tslint:disable-next-line:no-any return new Promise((resolve, reject) => { fs.readFile(outputXmlFile, 'utf8', (err, data) => { if (err) { return reject(err); } - xml2js.parseString(data, (err, result) => { - if (err) { - return reject(err); + xml2js.parseString(data, (error, parserResult) => { + if (error) { + return reject(error); } - let testSuiteResult: TestSuiteResult = result.testsuite; + const testSuiteResult: TestSuiteResult = parserResult.testsuite; tests.summary.errors = getSafeInt(testSuiteResult.$.errors); tests.summary.failures = getSafeInt(testSuiteResult.$.failures); tests.summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); - let testCount = getSafeInt(testSuiteResult.$.tests); + const testCount = getSafeInt(testSuiteResult.$.tests); switch (passCalculationFormulae) { case PassCalculationFormulae.pytest: { @@ -73,7 +75,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, break; } default: { - throw new Error("Unknown Test Pass Calculation"); + throw new Error('Unknown Test Pass Calculation'); } } @@ -83,7 +85,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { const xmlClassName = testcase.$.classname.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, ''); - let result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); + const result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); if (!result) { // Possible we're dealing with nosetests, where the file name isn't returned to us // When dealing with nose tests @@ -94,7 +96,7 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, // fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname); // Look for failed file test - let fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); + const fileTest = testcase.$.file && tests.testFiles.find(file => file.nameToRun === testcase.$.file); if (fileTest && testcase.error) { fileTest.status = TestStatus.Error; fileTest.passed = false; @@ -109,7 +111,6 @@ export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, result.testFunction.passed = true; result.testFunction.status = TestStatus.Pass; - if (testcase.failure) { result.testFunction.status = TestStatus.Fail; result.testFunction.passed = false; diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index cd2b7614179e..5f24149a888d 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,107 +1,57 @@ 'use strict'; +import * as path from 'path'; +import { OutputChannel, Uri } from 'vscode'; import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { Product } from '../common/installer'; +import { Installer, Product } from '../common/installer'; +import { getSubDirectories } from '../common/utils'; +import { TestConfigSettingsService } from './common/configSettingService'; +import { UnitTestProduct } from './common/contracts'; import { TestConfigurationManager } from './common/testConfigurationManager'; +import { selectTestWorkspace } from './common/testUtils'; +import { ConfigurationManager } from './nosetest/testConfigurationManager'; import * as nose from './nosetest/testConfigurationManager'; import * as pytest from './pytest/testConfigurationManager'; import * as unittest from './unittest/testConfigurationManager'; -import { getSubDirectories } from '../common/utils'; -import * as path from 'path'; -function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { - const items = [{ - label: 'unittest', - product: Product.unittest, - description: 'Standard Python test framework', - detail: 'https://docs.python.org/2/library/unittest.html' - }, - { - label: 'pytest', - product: Product.pytest, - description: 'Can run unittest (including trial) and nose test suites out of the box', - detail: 'http://docs.pytest.org/en/latest/' - }, - { - label: 'nose', - product: Product.nosetest, - description: 'nose framework', - detail: 'https://docs.python.org/2/library/unittest.html' - }]; - const options = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: messageToDisplay - }; - return vscode.window.showQuickPick(items, options).then(item => { - if (!item) { - return Promise.reject(null); +async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { + const wkspace = await selectTestWorkspace(); + if (!wkspace) { + return; + } + const selectedTestRunner = await selectTestRunner(messageToDisplay); + if (!selectedTestRunner) { + return Promise.reject(null); + } + const configMgr: TestConfigurationManager = createTestConfigurationManager(wkspace, selectedTestRunner, outputChannel); + if (enableOnly) { + // Ensure others are disabled + if (selectedTestRunner !== Product.unittest) { + createTestConfigurationManager(wkspace, Product.unittest, outputChannel).disable(); } - let configMgr: TestConfigurationManager; - switch (item.product) { - case Product.unittest: { - configMgr = new unittest.ConfigurationManager(outputChannel); - break; - } - case Product.pytest: { - configMgr = new pytest.ConfigurationManager(outputChannel); - break; - } - case Product.nosetest: { - configMgr = new nose.ConfigurationManager(outputChannel); - break; - } - default: { - throw new Error('Invalid test configuration'); - } + if (selectedTestRunner !== Product.pytest) { + createTestConfigurationManager(wkspace, Product.pytest, outputChannel).disable(); } - - if (enableOnly) { - // Ensure others are disabled - if (item.product !== Product.unittest) { - (new unittest.ConfigurationManager(outputChannel)).disable(); - } - if (item.product !== Product.pytest) { - (new pytest.ConfigurationManager(outputChannel)).disable(); - } - if (item.product !== Product.nosetest) { - (new nose.ConfigurationManager(outputChannel)).disable(); - } - return configMgr.enable(); + if (selectedTestRunner !== Product.nosetest) { + createTestConfigurationManager(wkspace, Product.nosetest, outputChannel).disable(); } + 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 - function enableTest(): Thenable { - // TODO: Enable multi workspace root support - const pythonConfig = vscode.workspace.getConfiguration('python'); - 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)); - }); - } - return configMgr.configure(vscode.workspace.rootPath).then(() => { - return enableTest(); - }).catch(reason => { - return enableTest().then(() => Promise.reject(reason)); - }); + return configMgr.configure(vscode.workspace.rootPath).then(() => { + return enableTest(wkspace, configMgr); + }).catch(reason => { + return enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); }); } -export function displayTestFrameworkError(outputChannel: vscode.OutputChannel): Thenable { - // TODO: Enable multi workspace root support +export function displayTestFrameworkError(wkspace: Uri, outputChannel: vscode.OutputChannel) { 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(outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); - } - else { + } else { const option = 'Enable and configure a Test Framework'; return vscode.window.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option).then(item => { if (item === option) { @@ -111,42 +61,102 @@ export function displayTestFrameworkError(outputChannel: vscode.OutputChannel): }); } } -export function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel): Thenable { +export async function displayPromptToEnableTests(rootDir: string, outputChannel: vscode.OutputChannel) { const settings = PythonSettings.getInstance(vscode.Uri.file(rootDir)); if (settings.unitTest.pyTestEnabled || settings.unitTest.nosetestsEnabled || settings.unitTest.unittestEnabled) { - return Promise.reject(null); + return; } if (!settings.unitTest.promptToConfigure) { - return Promise.reject(null); + return; } const yes = 'Yes'; - const no = `Later`; - const noNotAgain = `No, don't ask again`; + const no = 'Later'; + const noNotAgain = 'No, don\'t ask again'; - return checkIfHasTestDirs(rootDir).then(hasTests => { - if (!hasTests) { - return Promise.reject(null); - } - return vscode.window.showInformationMessage('You seem to have tests, would you like to enable a test framework?', yes, no, noNotAgain).then(item => { - if (!item || item === no) { - return Promise.reject(null); - } - if (item === yes) { - return promptToEnableAndConfigureTestFramework(outputChannel); - } - else { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.promptToConfigure', false); - } - }); + 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; + } + if (item === yes) { + await promptToEnableAndConfigureTestFramework(outputChannel); + } else { + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('unitTest.promptToConfigure', false); + } +} + +// 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 checkIfHasTestDirs(rootDir: string): Promise { +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) { + const installer = new Installer(outputChannel); + 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); + } + default: { + throw new Error('Invalid test configuration'); + } + } +} +async function selectTestRunner(placeHolderMessage: string): Promise { + const items = [{ + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/2/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/en/latest/' + }, + { + label: 'nose', + product: Product.nosetest, + description: 'nose framework', + detail: 'https://docs.python.org/2/library/unittest.html' + }]; + 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/display/main.ts b/src/client/unittests/display/main.ts index 7b9ee8a5a534..3d12da43b2bd 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -1,12 +1,17 @@ 'use strict'; import * as vscode from 'vscode'; -import { Tests, CANCELLATION_REASON } from '../common/contracts'; import * as constants from '../../common/constants'; +import { createDeferred, isNotInstalledError } from '../../common/helpers'; +import { CANCELLATION_REASON, Tests } from '../common/contracts'; import { displayTestErrorMessage } from '../common/testUtils'; -import { isNotInstalledError, createDeferred } from '../../common/helpers'; export class TestResultDisplay { private statusBar: vscode.StatusBarItem; + private discoverCounter = 0; + private ticker = ['|', '/', '-', '|', '/', '-', '\\']; + private progressTimeout; + private progressPrefix: string; + // tslint:disable-next-line:no-any constructor(private outputChannel: vscode.OutputChannel, private onDidChange: vscode.EventEmitter = null) { this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); } @@ -16,19 +21,29 @@ export class TestResultDisplay { public set enabled(enable: boolean) { if (enable) { this.statusBar.show(); - } - else { + } else { this.statusBar.hide(); } } - public DisplayProgressStatus(tests: Promise, debug: boolean = false) { - this.displayProgress('Running Tests', `Running Tests (Click to Stop)`, constants.Commands.Tests_Ask_To_Stop_Test); - tests + public displayProgressStatus(testRunResult: Promise, debug: boolean = false) { + this.displayProgress('Running Tests', 'Running Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Test); + testRunResult .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(() => { }); } + public displayDiscoverStatus(testDiscovery: Promise) { + this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); + return testDiscovery.then(tests => { + this.updateWithDiscoverSuccess(tests); + return tests; + }).catch(reason => { + this.updateWithDiscoverFailure(reason); + return Promise.reject(reason); + }); + } private updateTestRunWithSuccess(tests: Tests, debug: boolean = false): Tests { this.clearProgressTicker(); @@ -58,7 +73,7 @@ export class TestResultDisplay { toolTip.push(`${tests.summary.errors} Error${tests.summary.errors > 1 ? 's' : ''}`); foreColor = 'yellow'; } - this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : toolTip.join(', ') + ' (Tests)'; + this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : `${toolTip.join(', ')} (Tests)`; this.statusBar.text = statusText.length === 0 ? 'No Tests Ran' : statusText.join(' '); this.statusBar.color = foreColor; this.statusBar.command = constants.Commands.Tests_View_UI; @@ -71,27 +86,23 @@ export class TestResultDisplay { return tests; } + // tslint:disable-next-line:no-any private updateTestRunWithFailure(reason: any): Promise { this.clearProgressTicker(); this.statusBar.command = constants.Commands.Tests_View_UI; if (reason === CANCELLATION_REASON) { this.statusBar.text = '$(zap) Run Tests'; this.statusBar.tooltip = 'Run Tests'; - } - else { - this.statusBar.text = `$(alert) Tests Failed`; + } else { + this.statusBar.text = '$(alert) Tests Failed'; this.statusBar.tooltip = 'Running Tests Failed'; displayTestErrorMessage('There was an error in running the tests.'); } return Promise.reject(reason); } - private discoverCounter = 0; - private ticker = ['|', '/', '-', '|', '/', '-', '\\']; - private progressTimeout; - private progressPrefix: string; private displayProgress(message: string, tooltip: string, command: string) { - this.progressPrefix = this.statusBar.text = '$(stop) ' + message; + this.progressPrefix = this.statusBar.text = `$(stop) ${message}`; this.statusBar.command = command; this.statusBar.tooltip = tooltip; this.statusBar.show(); @@ -99,7 +110,7 @@ export class TestResultDisplay { this.progressTimeout = setInterval(() => this.updateProgressTicker(), 150); } private updateProgressTicker() { - let text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; + const text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; this.discoverCounter += 1; this.statusBar.text = text; } @@ -111,21 +122,12 @@ export class TestResultDisplay { this.discoverCounter = 0; } - public DisplayDiscoverStatus(tests: Promise) { - this.displayProgress('Discovering Tests', 'Discovering Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); - return tests.then(tests => { - this.updateWithDiscoverSuccess(tests); - return tests; - }).catch(reason => { - this.updateWithDiscoverFailure(reason); - return Promise.reject(reason); - }); - } - + // tslint:disable-next-line:no-any private disableTests(): Promise { + // tslint:disable-next-line:no-any const def = createDeferred(); const pythonConfig = vscode.workspace.getConfiguration('python'); - let settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', + const settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']; function disableTest() { @@ -160,22 +162,23 @@ export class TestResultDisplay { } } + // tslint:disable-next-line:no-any private updateWithDiscoverFailure(reason: any) { this.clearProgressTicker(); - this.statusBar.text = `$(zap) Discover Tests`; + this.statusBar.text = '$(zap) Discover Tests'; this.statusBar.tooltip = 'Discover Tests'; this.statusBar.command = constants.Commands.Tests_Discover; this.statusBar.show(); this.statusBar.color = 'yellow'; if (reason !== CANCELLATION_REASON) { - this.statusBar.text = `$(alert) Test discovery failed`; - this.statusBar.tooltip = `Discovering Tests failed (view 'Python Test Log' output panel for details)`; - // TODO: ignore this quitemode, always display the error message (inform the user) + this.statusBar.text = '$(alert) Test discovery failed'; + this.statusBar.tooltip = 'Discovering Tests failed (view \'Python Test Log\' output panel for details)'; + // TODO: ignore this quitemode, always display the error message (inform the user). if (!isNotInstalledError(reason)) { - // 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 + // 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.'); } } } -} \ No newline at end of file +} diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index 67338d2f71c4..4a0c91cd5b20 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,13 +1,11 @@ +import * as path from 'path'; import { QuickPickItem, window } from 'vscode'; import * as vscode from 'vscode'; -import { Tests, TestFile, TestFunction, FlattenedTestFunction, TestStatus } from '../common/contracts'; -import { getDiscoveredTests } from '../common/testUtils'; import * as constants from '../../common/constants'; -import * as path from 'path'; +import { FlattenedTestFunction, TestFile, TestFunction, Tests, TestStatus } from '../common/contracts'; +import { getDiscoveredTests } from '../common/testUtils'; export class TestDisplay { - constructor() { - } public displayStopTestUI(message: string) { window.showQuickPick([message]).then(item => { if (item === message) { @@ -16,7 +14,7 @@ export class TestDisplay { }); } public displayTestUI(rootDirectory: string) { - const tests = getDiscoveredTests(); + const tests = getDiscoveredTests(rootDirectory ? vscode.Uri.file(rootDirectory) : undefined); window.showQuickPick(buildItems(rootDirectory, tests), { matchOnDescription: true, matchOnDetail: true }).then(onItemSelected); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { @@ -42,7 +40,7 @@ export class TestDisplay { }); } public displayFunctionTestPickerUI(rootDirectory: string, fileName: string, testFunctions: TestFunction[], debug?: boolean) { - const tests = getDiscoveredTests(); + const tests = getDiscoveredTests(rootDirectory ? vscode.Uri.file(rootDirectory) : undefined); if (!tests) { return; } @@ -102,7 +100,7 @@ function getSummary(tests?: Tests) { } if (tests.summary.errors > 0) { const plural = tests.summary.errors === 1 ? '' : 's'; - statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error` + plural); + statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error${plural}`); } if (tests.summary.skipped > 0) { statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped} Skipped`); @@ -115,7 +113,7 @@ function buildItems(rootDirectory: string, tests?: Tests): TestItem[] { items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); items.push({ description: '', label: 'Run Unit Test Method ...', type: Type.SelectAndRunMethod }); - let summary = getSummary(tests); + const summary = getSummary(tests); items.push({ description: '', label: 'View Unit Test Output', type: Type.ViewTestOutput, detail: summary }); if (tests && tests.summary.failures > 0) { @@ -132,7 +130,7 @@ statusSortPrefix[TestStatus.Skipped] = '3'; statusSortPrefix[TestStatus.Pass] = '4'; function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunction[], sortBasedOnResults: boolean = false, displayStatusIcons: boolean = false, debug: boolean = false): TestItem[] { - let functionItems: TestItem[] = []; + const functionItems: TestItem[] = []; tests.forEach(fn => { let icon = ''; if (displayStatusIcons && statusIconMapping.has(fn.testFunction.status)) { @@ -165,15 +163,15 @@ function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunct return functionItems; } function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): TestFileItem[] { - let fileItems: TestFileItem[] = testFiles.map(testFile => { + const fileItems: TestFileItem[] = testFiles.map(testFile => { return { description: '', detail: path.relative(rootDirectory, testFile.fullPath), type: Type.RunFile, label: path.basename(testFile.fullPath), testFile: testFile - } - }) + }; + }); fileItems.sort((a, b) => { if (a.detail < b.detail) { return -1; @@ -182,7 +180,7 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T return 1; } return 0; - }) + }); return fileItems; } function onItemSelected(selection: TestItem, debug?: boolean) { @@ -190,7 +188,7 @@ function onItemSelected(selection: TestItem, debug?: boolean) { return; } let cmd = ''; - let args = []; + const args = []; switch (selection.type) { case Type.Null: { return; @@ -226,6 +224,9 @@ function onItemSelected(selection: TestItem, debug?: boolean) { args.push(true); break; } + default: { + return; + } } vscode.commands.executeCommand(cmd, ...args); diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 2fab50340bb5..b4c201f30f5b 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -12,7 +12,7 @@ import { TestStatus, TestsToRun, } from './common/contracts'; -import { resolveValueAsTestToRun, getDiscoveredTests } from './common/testUtils'; +import { parseTestName, getDiscoveredTests } from './common/testUtils'; import { BaseTestManager } from './common/baseTestManager'; import { PythonSettings, IUnitTestSettings } from '../common/configSettings'; import { TestResultDisplay } from './display/main'; @@ -300,7 +300,7 @@ function identifyTestType(rootDirectory: string, arg?: vscode.Uri | TestsToRun | return { testFunction: [arg.testFunction] }; } if (isUri(arg)) { - return resolveValueAsTestToRun(arg.fsPath, rootDirectory); + return parseTestName(arg.fsPath, rootDirectory); } return null; } diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index 48826176ab89..ff6738f124ff 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -1,54 +1,44 @@ -import * as vscode from 'vscode'; -import { TestConfigurationManager } from '../common/testConfigurationManager'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; +import { ITestConfigSettingsService } from '../common/contracts'; +import { TestConfigurationManager } from '../common/testConfigurationManager'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestsEnabled', true); - } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestsEnabled', false); + constructor(workspace: Uri, outputChannel: vscode.OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.nosetest, outputChannel, installer, testConfigSettingsService); } - - private static configFilesExist(rootDir: string): Promise { + 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 : ''); }); }); }); - return Promise.all(promises).then(values => { - return values.filter(exists => exists.length > 0); - }); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); } - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(rootDir: string): Promise { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - let installer = new Installer(this.outputChannel); - return ConfigurationManager.configFilesExist(rootDir).then(configFiles => { - if (configFiles.length > 0) { - return Promise.resolve(); - } + const configFiles = await ConfigurationManager.configFilesExist(rootDir); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0) { + return; + } - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs); - }).then(testDir => { - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } - }); - }).then(() => { - return installer.isInstalled(Product.nosetest); - }).then(installed => { - if (!installed) { - return installer.install(Product.nosetest); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.nosetestArgs', args); - }); + const subDirs = await this.getTestDirs(rootDir); + const testDir = await this.selectTestDir(rootDir, subDirs); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.nosetest); + if (!installed) { + await this.installer.install(Product.nosetest); + } + await this.testConfigSettingsService.updateTestArgs(rootDir, Product.nosetest, args); } -} \ No newline at end of file +} diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index 09ae098ede9a..a1a0d3b898d5 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -1,61 +1,51 @@ -import * as vscode from 'vscode'; -import { TestConfigurationManager } from '../common/testConfigurationManager'; import * as fs from 'fs'; import * as path from 'path'; +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; +import { ITestConfigSettingsService } from '../common/contracts'; +import { TestConfigurationManager } from '../common/testConfigurationManager'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestEnabled', true); + constructor(workspace: Uri, outputChannel: vscode.OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.pytest, outputChannel, installer, testConfigSettingsService); } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestEnabled', false); - } - - private static configFilesExist(rootDir: string): Promise { + 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 : ''); }); }); }); - return Promise.all(promises).then(values => { - return values.filter(exists => exists.length > 0); - }); + const values = await Promise.all(promises); + return values.filter(exists => exists.length > 0); } - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(rootDir: string) { const args = []; const configFileOptionLabel = 'Use existing config file'; const options: vscode.QuickPickItem[] = []; - let installer = new Installer(this.outputChannel); - return ConfigurationManager.configFilesExist(rootDir).then(configFiles => { - if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { - return Promise.resolve(); - } + const configFiles = await ConfigurationManager.configFilesExist(rootDir); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return; + } - if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { - options.push({ - label: configFileOptionLabel, - description: 'setup.cfg' - }); - } - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs, options); - }).then(testDir => { - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } + if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { + options.push({ + label: configFileOptionLabel, + description: 'setup.cfg' }); - }).then(() => { - return installer.isInstalled(Product.pytest); - }).then(installed => { - if (!installed) { - return installer.install(Product.pytest); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.pyTestArgs', args); - }); + } + const subDirs = await this.getTestDirs(rootDir); + const testDir = await this.selectTestDir(rootDir, subDirs, options); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.pytest); + if (!installed) { + await this.installer.install(Product.pytest); + } + await this.testConfigSettingsService.updateTestArgs(rootDir, Product.pytest, args); } -} \ No newline at end of file +} diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 85041676e06b..2b3c12bba765 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,42 +1,33 @@ -import * as vscode from 'vscode'; import * as path from 'path'; +import { OutputChannel, Uri } from 'vscode'; +import { Installer, Product } from '../../common/installer'; +import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; export class ConfigurationManager extends TestConfigurationManager { - public enable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestEnabled', true); + constructor(workspace: Uri, outputChannel: OutputChannel, + installer: Installer, testConfigSettingsService: ITestConfigSettingsService) { + super(workspace, Product.unittest, outputChannel, installer, testConfigSettingsService); } - public disable(): Thenable { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestEnabled', false); - } - - public configure(rootDir: string): Promise { + // tslint:disable-next-line:no-any + public async configure(rootDir: string) { const args = ['-v']; - return this.getTestDirs(rootDir).then(subDirs => { - return this.selectTestDir(rootDir, subDirs); - }).then(testDir => { - args.push('-s'); - if (typeof testDir === 'string' && testDir !== '.') { - args.push(`.${path.sep}${testDir}`); - } - else { - args.push('.'); - } + const subDirs = await this.getTestDirs(rootDir); + const testDir = await this.selectTestDir(rootDir, subDirs); + args.push('-s'); + if (typeof testDir === 'string' && testDir !== '.') { + args.push(`.${path.sep}${testDir}`); + } else { + args.push('.'); + } - return this.selectTestFilePattern(); - }).then(testfilePattern => { - args.push('-p'); - if (typeof testfilePattern === 'string') { - args.push(testfilePattern); - } - else { - args.push('test*.py'); - } - }).then(() => { - const pythonConfig = vscode.workspace.getConfiguration('python'); - return pythonConfig.update('unitTest.unittestArgs', args); - }); + const testfilePattern = await this.selectTestFilePattern(); + args.push('-p'); + if (typeof testfilePattern === 'string') { + args.push(testfilePattern); + } else { + args.push('test*.py'); + } + await this.testConfigSettingsService.updateTestArgs(rootDir, Product.unittest, args); } -} \ No newline at end of file +} From a6d89f7b9968edd1eb48e37f57347ad75ae06cc1 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Oct 2017 00:03:45 -0700 Subject: [PATCH 08/23] more changes --- src/client/unittests/codeLenses/main.ts | 17 +- src/client/unittests/codeLenses/testFiles.ts | 145 ++++++++++-------- .../unittests/common/baseTestManager.ts | 11 +- src/client/unittests/common/contracts.ts | 5 +- src/client/unittests/common/testUtils.ts | 108 ++++++------- src/client/unittests/configuration.ts | 1 + src/client/unittests/display/picker.ts | 19 ++- src/client/unittests/main.ts | 22 +-- src/client/unittests/nosetest/collector.ts | 20 +-- src/client/unittests/nosetest/main.ts | 22 +-- src/client/unittests/nosetest/runner.ts | 32 ++-- src/client/unittests/pytest/collector.ts | 27 ++-- src/client/unittests/pytest/main.ts | 21 +-- src/client/unittests/pytest/runner.ts | 21 ++- src/client/unittests/unittest/collector.ts | 20 ++- src/client/unittests/unittest/main.ts | 26 ++-- src/client/unittests/unittest/runner.ts | 54 ++++--- src/client/unittests/unittest/socketServer.ts | 25 +-- 18 files changed, 315 insertions(+), 281 deletions(-) diff --git a/src/client/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts index 40c4a58754d3..613f86dae062 100644 --- a/src/client/unittests/codeLenses/main.ts +++ b/src/client/unittests/codeLenses/main.ts @@ -1,16 +1,17 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; - -import { TestFileCodeLensProvider } from './testFiles'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; +import { ITestCollectionStorageService } from '../common/testUtils'; +import { TestFileCodeLensProvider } from './testFiles'; + +export function activateCodeLenses(onDidChange: vscode.EventEmitter, + symboldProvider: PythonSymbolProvider, testCollectionStorage: ITestCollectionStorageService): vscode.Disposable { -export function activateCodeLenses(onDidChange: vscode.EventEmitter, symboldProvider: PythonSymbolProvider): vscode.Disposable { const disposables: vscode.Disposable[] = []; - disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, new TestFileCodeLensProvider(onDidChange, symboldProvider))); + const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symboldProvider, testCollectionStorage); + disposables.push(vscode.languages.registerCodeLensProvider(constants.PythonLanguage, codeLensProvider)); return { - dispose: function () { - disposables.forEach(d => d.dispose()); - } + dispose: () => { disposables.forEach(d => d.dispose()); } }; -} \ No newline at end of file +} diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index 98489aedc22d..cfc0ad1bf1eb 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,10 +1,10 @@ 'use strict'; -import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument } from 'vscode'; +import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, workspace } from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; import { TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/contracts'; -import { getDiscoveredTests } from '../common/testUtils'; +import { ITestCollectionStorageService } from '../common/testUtils'; type CodeLensData = { symbolKind: SymbolKind; @@ -18,17 +18,23 @@ type FunctionsAndSuites = { export class TestFileCodeLensProvider implements CodeLensProvider { // tslint:disable-next-line:variable-name - constructor(private _onDidChange: EventEmitter, private symbolProvider: PythonSymbolProvider) { + constructor(private _onDidChange: EventEmitter, + private symbolProvider: PythonSymbolProvider, + private testCollectionStorage: ITestCollectionStorageService) { } get onDidChangeCodeLenses(): Event { return this._onDidChange.event; } - public provideCodeLenses(document: TextDocument, token: CancellationToken): Thenable { - const testItems = getDiscoveredTests(document.uri); + public async provideCodeLenses(document: TextDocument, token: CancellationToken) { + const wkspace = workspace.getWorkspaceFolder(document.uri); + if (!wkspace) { + return []; + } + const testItems = this.testCollectionStorage.getTests(wkspace.uri); if (!testItems || testItems.testFiles.length === 0 || testItems.testFunctions.length === 0) { - return Promise.resolve([]); + return []; } const cancelTokenSrc = new CancellationTokenSource(); @@ -42,79 +48,82 @@ export class TestFileCodeLensProvider implements CodeLensProvider { } }, constants.Delays.MaxUnitTestCodeLensDelay); - return getCodeLenses(document, token, this.symbolProvider); + return this.getCodeLenses(document, token, this.symbolProvider); } public resolveCodeLens(codeLens: CodeLens, token: CancellationToken): CodeLens | Thenable { codeLens.command = { command: 'python.runtests', title: 'Test' }; return Promise.resolve(codeLens); } -} -function getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider): Thenable { - const documentUri = document.uri; - const tests = getDiscoveredTests(document.uri); - if (!tests) { - return null; - } - const file = tests.testFiles.find(item => item.fullPath === documentUri.fsPath); - if (!file) { - return Promise.resolve([]); + private getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider): Thenable { + const wkspace = workspace.getWorkspaceFolder(document.uri); + if (!wkspace) { + return null; + } + const tests = this.testCollectionStorage.getTests(wkspace.uri); + if (!tests) { + return null; + } + const file = tests.testFiles.find(item => item.fullPath === document.uri.fsPath); + if (!file) { + return Promise.resolve([]); + } + const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); + + return symbolProvider.provideDocumentSymbolsForInternalUse(document, token) + .then((symbols: SymbolInformation[]) => { + return symbols.filter(symbol => { + return symbol.kind === SymbolKind.Function || + symbol.kind === SymbolKind.Method || + symbol.kind === SymbolKind.Class; + }).map(symbol => { + // This is bloody crucial, if the start and end columns are the same + // then vscode goes bonkers when ever you edit a line (start scrolling magically) + const range = new Range(symbol.location.range.start, + new Position(symbol.location.range.end.line, + symbol.location.range.end.character + 1)); + + return this.getCodeLens(document.uri.fsPath, allFuncsAndSuites, + range, symbol.name, symbol.kind, symbol.containerName); + }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); + }, reason => { + if (token.isCancellationRequested) { + return []; + } + return Promise.reject(reason); + }); } - const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); - - return symbolProvider.provideDocumentSymbolsForInternalUse(document, token) - .then((symbols: SymbolInformation[]) => { - return symbols.filter(symbol => { - return symbol.kind === SymbolKind.Function || - symbol.kind === SymbolKind.Method || - symbol.kind === SymbolKind.Class; - }).map(symbol => { - // This is bloody crucial, if the start and end columns are the same - // then vscode goes bonkers when ever you edit a line (start scrolling magically) - const range = new Range(symbol.location.range.start, - new Position(symbol.location.range.end.line, - symbol.location.range.end.character + 1)); - - return getCodeLens(documentUri.fsPath, allFuncsAndSuites, - range, symbol.name, symbol.kind, symbol.containerName); - }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); - }, reason => { - if (token.isCancellationRequested) { - return []; - } - return Promise.reject(reason); - }); -} -function getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, - range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { + private getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, + range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { - switch (symbolKind) { - case SymbolKind.Function: - case SymbolKind.Method: { - return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range, symbolContainer); - } - case SymbolKind.Class: { - const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); - if (!cls) { - return null; + switch (symbolKind) { + case SymbolKind.Function: + case SymbolKind.Method: { + return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range, symbolContainer); + } + case SymbolKind.Class: { + const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); + if (!cls) { + return null; + } + return [ + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, + command: constants.Commands.Tests_Run, + arguments: [{ testSuite: [cls] }] + }), + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, + command: constants.Commands.Tests_Debug, + arguments: [{ testSuite: [cls] }] + }) + ]; + } + default: { + return []; } - return [ - new CodeLens(range, { - title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, - command: constants.Commands.Tests_Run, - arguments: [{ testSuite: [cls] }] - }), - new CodeLens(range, { - title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [{ testSuite: [cls] }] - }) - ]; - } - default: { - return []; } } } diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index 0ab133b4c6cb..368f72327d62 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -4,7 +4,7 @@ import { IPythonSettings, PythonSettings } from '../../common/configSettings'; import { isNotInstalledError } from '../../common/helpers'; import { Installer, Product } from '../../common/installer'; import { CANCELLATION_REASON, Tests, TestStatus, TestsToRun } from './contracts'; -import { displayTestErrorMessage, resetTestResults, storeDiscoveredTests } from './testUtils'; +import { displayTestErrorMessage, ITestCollectionStorageService, resetTestResults } from './testUtils'; export abstract class BaseTestManager { protected readonly settings: IPythonSettings; @@ -14,7 +14,8 @@ export abstract class BaseTestManager { private cancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; private discoverTestsPromise: Promise; - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel) { + constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel, + private testCollectionStorage: ITestCollectionStorageService) { this._status = TestStatus.Unknown; this.installer = new Installer(); this.settings = PythonSettings.getInstance(this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); @@ -83,7 +84,8 @@ export abstract class BaseTestManager { if (haveErrorsInDiscovering && !quietMode) { displayTestErrorMessage('There were some errors in disovering unit tests'); } - storeDiscoveredTests(tests, this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; + this.testCollectionStorage.storeTests(wkspace, tests); this.disposeCancellationToken(); return tests; @@ -103,7 +105,8 @@ export abstract class BaseTestManager { // tslint:disable-next-line:prefer-template this.outputChannel.appendLine('' + reason); } - storeDiscoveredTests(null, vscode.Uri.file(this.rootDirectory)); + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; + this.testCollectionStorage.storeTests(wkspace, null); this.disposeCancellationToken(); return Promise.reject(reason); }); diff --git a/src/client/unittests/common/contracts.ts b/src/client/unittests/common/contracts.ts index 0e69ab169873..32945c99d297 100644 --- a/src/client/unittests/common/contracts.ts +++ b/src/client/unittests/common/contracts.ts @@ -1,4 +1,4 @@ -import { Uri, Disposable } from 'vscode'; +import { Disposable, Uri } from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from './baseTestManager'; @@ -49,7 +49,8 @@ export type TestResult = Node & { functionsPassed?: number; functionsFailed?: number; functionsDidNotRun?: number; -} +}; + export type Node = { expanded?: Boolean; }; diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 85122b9a33b3..9dafac503658 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,25 +1,22 @@ -import { ITestVisitor } from './testUtils'; import * as path from 'path'; import * as vscode from 'vscode'; import { Uri, workspace } from 'vscode'; import * as constants from '../../common/constants'; -import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, Tests, TestStatus, TestsToRun, TestSuite, TestFunction } from './contracts'; +import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestsToRun, TestSuite } from './contracts'; +import { ITestVisitor } from './testUtils'; export interface ITestCollectionStorageService { getTests(wkspace: Uri): Tests | undefined; - storeTests(wkspace: Uri, tests: Tests): void; + storeTests(wkspace: Uri, tests: Tests | null | undefined): void; } export class TestCollectionStorageService implements ITestCollectionStorageService { - private testsIndexedByWorkspaceUri: Map; - constructor() { - this.testsIndexedByWorkspaceUri = new Map(); - } + private testsIndexedByWorkspaceUri = new Map(); public getTests(wkspace: Uri): Tests | undefined { const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; } - public storeTests(wkspace: Uri, tests: Tests): void { + public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); } @@ -53,11 +50,12 @@ export function displayTestErrorMessage(message: string) { } -export function parseTestName(name: string, rootDirectory: string): TestsToRun { +export function parseTestName(name: string, rootDirectory: string, testCollectionStorage: ITestCollectionStorageService): TestsToRun { // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. // use to identify a file given the full file name, similary for a folder and function // Perhaps something like a parser or methods like TestFunction.fromString()... something) - const tests = getDiscoveredTests(); + const workspaceUri = workspace.getWorkspaceFolder(Uri.file(rootDirectory)).uri; + const tests = testCollectionStorage.getTests(workspaceUri); if (!tests) { return null; } const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); @@ -231,7 +229,6 @@ export function updateResults(tests: Tests) { export interface ITestsHelper { flattenTestFiles(testFiles: TestFile[]): Tests; - flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite): void; placeTestFilesIntoFolders(tests: Tests): void; } @@ -243,28 +240,54 @@ export interface ITestVisitor { export class TestFlatteningVisitor implements ITestVisitor { // tslint:disable-next-line:variable-name - private _testFunctions: TestFunction[] = []; + private _flattedTestFunctions = new Map(); // tslint:disable-next-line:variable-name - private _testSuites: TestSuite[] = []; - // tslint:disable-next-line:variable-name - private _testFiles: TestFile[] = []; - public get testFunctions(): TestFunction[] { - return [...this._testFunctions]; + private _flattenedTestSuites = new Map(); + public get flattenedTestFunctions(): Readonly { + return [...this._flattedTestFunctions.values()]; } - public get testSuites(): TestFunction[] { - return [...this._testSuites]; + public get flattenedTestSuites(): Readonly { + return [...this._flattenedTestSuites.values()]; } - public get testFiles(): TestFunction[] { - return [...this._testFiles]; + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); + testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); } - public visitTestFunction(testFunction: TestFunction): void { - this._testFunctions.push(testFunction); + private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { + testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); + testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); + this.addTestSuite(testSuite, parentTestFile); } - public visitTestSuite(testSuite: TestSuite): void { - this._testSuites.push(testSuite); + private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { + const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; + this._flattedTestFunctions.set(key, flattenedFunction); } - public visitTestFile(testFile: TestFile): void { - this._testFiles.push(testFile); + private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { + const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; + this._flattenedTestSuites.set(key, flattenedSuite); + } + private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { + const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; + if (this._flattedTestFunctions.has(key)) { + return; + } + const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; + this._flattedTestFunctions.set(key, flattendFunction); } } @@ -323,25 +346,13 @@ export class TestFolderGenerationVisitor implements ITestVisitor { // tslint:disable-next-line:max-classes-per-file export class TestsHelper implements ITestsHelper { public flattenTestFiles(testFiles: TestFile[]): Tests { - const fns: FlattenedTestFunction[] = []; - const suites: FlattenedTestSuite[] = []; - testFiles.forEach(testFile => { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => { - fns.push({ testFunction: fn, xmlClassName: packageName, parentTestFile: testFile }); - }); - - testFile.suites.forEach(suite => { - suites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - this.flattenTestSuites(fns, suites, testFile, suite); - }); - }); + const flatteningVisitor = new TestFlatteningVisitor(); + testFiles.forEach(testFile => flatteningVisitor.visitTestFile(testFile)); const tests = { testFiles: testFiles, - testFunctions: fns, testSuits: suites, + testFunctions: flatteningVisitor.flattenedTestFunctions, + testSuits: flatteningVisitor.flattenedTestSuites, testFolders: [], rootTestFolders: [], summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } @@ -351,17 +362,6 @@ export class TestsHelper implements ITestsHelper { return tests; } - public flattenTestSuites(flattenedFns: FlattenedTestFunction[], flattenedSuites: FlattenedTestSuite[], testFile: TestFile, testSuite: TestSuite) { - testSuite.functions.forEach(fn => { - flattenedFns.push({ testFunction: fn, xmlClassName: testSuite.xmlName, parentTestFile: testFile, parentTestSuite: testSuite }); - }); - - // We may have child classes - testSuite.suites.forEach(suite => { - flattenedSuites.push({ parentTestFile: testFile, testSuite: suite, xmlClassName: suite.xmlName }); - this.flattenTestSuites(flattenedFns, flattenedSuites, testFile, suite); - }); - } public placeTestFilesIntoFolders(tests: Tests): void { // First get all the unique folders const folders: string[] = []; diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 5f24149a888d..1fa5ad31c836 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -14,6 +14,7 @@ import * as nose from './nosetest/testConfigurationManager'; import * as pytest from './pytest/testConfigurationManager'; import * as unittest from './unittest/testConfigurationManager'; +// tslint:disable-next-line:no-any async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { const wkspace = await selectTestWorkspace(); if (!wkspace) { diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index 4a0c91cd5b20..e9f24ec5a451 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -3,9 +3,10 @@ import { QuickPickItem, window } from 'vscode'; import * as vscode from 'vscode'; import * as constants from '../../common/constants'; import { FlattenedTestFunction, TestFile, TestFunction, Tests, TestStatus } from '../common/contracts'; -import { getDiscoveredTests } from '../common/testUtils'; +import { ITestCollectionStorageService } from '../common/testUtils'; export class TestDisplay { + constructor(private testCollectionStorage: ITestCollectionStorageService) { } public displayStopTestUI(message: string) { window.showQuickPick([message]).then(item => { if (item === message) { @@ -14,7 +15,8 @@ export class TestDisplay { }); } public displayTestUI(rootDirectory: string) { - const tests = getDiscoveredTests(rootDirectory ? vscode.Uri.file(rootDirectory) : undefined); + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDirectory)).uri; + const tests = this.testCollectionStorage.getTests(wkspace); window.showQuickPick(buildItems(rootDirectory, tests), { matchOnDescription: true, matchOnDetail: true }).then(onItemSelected); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { @@ -40,7 +42,8 @@ export class TestDisplay { }); } public displayFunctionTestPickerUI(rootDirectory: string, fileName: string, testFunctions: TestFunction[], debug?: boolean) { - const tests = getDiscoveredTests(rootDirectory ? vscode.Uri.file(rootDirectory) : undefined); + const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDirectory)).uri; + const tests = this.testCollectionStorage.getTests(wkspace); if (!tests) { return; } @@ -79,14 +82,16 @@ statusIconMapping.set(TestStatus.Fail, constants.Octicons.Test_Fail); statusIconMapping.set(TestStatus.Error, constants.Octicons.Test_Error); statusIconMapping.set(TestStatus.Skipped, constants.Octicons.Test_Skip); -interface TestItem extends QuickPickItem { +type TestItem = QuickPickItem & { type: Type; fn?: FlattenedTestFunction; -} -interface TestFileItem extends QuickPickItem { +}; + +type TestFileItem = QuickPickItem & { type: Type; testFile?: TestFile; -} +}; + function getSummary(tests?: Tests) { if (!tests || !tests.summary) { return ''; diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index b4c201f30f5b..248882393ffe 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,9 +1,10 @@ 'use strict'; import * as vscode from 'vscode'; -import * as nosetests from './nosetest/main'; -import * as pytest from './pytest/main'; -import * as unittest from './unittest/main'; +import { IUnitTestSettings, PythonSettings } from '../common/configSettings'; import * as constants from '../common/constants'; +import { PythonSymbolProvider } from '../providers/symbolProvider'; +import { activateCodeLenses } from './codeLenses/main'; +import { BaseTestManager } from './common/baseTestManager'; import { CANCELLATION_REASON, FlattenedTestFunction, @@ -12,14 +13,13 @@ import { TestStatus, TestsToRun, } from './common/contracts'; -import { parseTestName, getDiscoveredTests } from './common/testUtils'; -import { BaseTestManager } from './common/baseTestManager'; -import { PythonSettings, IUnitTestSettings } from '../common/configSettings'; +import { getDiscoveredTests, parseTestName } from './common/testUtils'; +import { displayTestFrameworkError } from './configuration'; import { TestResultDisplay } from './display/main'; import { TestDisplay } from './display/picker'; -import { activateCodeLenses } from './codeLenses/main'; -import { displayTestFrameworkError } from './configuration'; -import { PythonSymbolProvider } from '../providers/symbolProvider'; +import * as nosetests from './nosetest/main'; +import * as pytest from './pytest/main'; +import * as unittest from './unittest/main'; let testManager: BaseTestManager | undefined | null; let pyTestManager: pytest.TestManager | undefined | null; @@ -28,7 +28,7 @@ let nosetestManager: nosetests.TestManager | undefined | null; let testResultDisplay: TestResultDisplay; let testDisplay: TestDisplay; let outChannel: vscode.OutputChannel; -let onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); +const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider) { // TODO: Add multi workspace support @@ -36,7 +36,7 @@ export function activate(context: vscode.ExtensionContext, outputChannel: vscode uniTestSettingsString = JSON.stringify(settings.unitTest); context.subscriptions.push({ dispose: dispose }); outChannel = outputChannel; - let disposables = registerCommands(); + const disposables = registerCommands(); context.subscriptions.push(...disposables); if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { diff --git a/src/client/unittests/nosetest/collector.ts b/src/client/unittests/nosetest/collector.ts index c78d1fd86f9f..7c60cb8d3830 100644 --- a/src/client/unittests/nosetest/collector.ts +++ b/src/client/unittests/nosetest/collector.ts @@ -1,12 +1,12 @@ 'use strict'; -import * as path from 'path'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, TestSuite, TestFunction, Tests } from '../common/contracts'; import * as os from 'os'; -import { extractBetweenDelimiters, convertFileToPackage, flattenTestFiles } from '../common/testUtils'; +import * as path from 'path'; import { CancellationToken } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; import { OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { TestFile, TestFunction, Tests, TestSuite } from '../common/contracts'; +import { convertFileToPackage, extractBetweenDelimiters, flattenTestFiles } from '../common/testUtils'; +import { execPythonFile } from './../../common/utils'; const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; const NOSE_WANT_FILE_SUFFIX = '.py? True'; @@ -44,8 +44,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc // and starts with nose.selector: DEBUG: want if (logOutputLines[lastLineIndex].endsWith('? True')) { logOutputLines.push(''); - } - else { + } else { // We don't need this line logOutputLines[lastLineIndex] = ''; } @@ -115,10 +114,10 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin } if (line.startsWith('nose.selector: DEBUG: wantClass ? True'); + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ? True'); const clsName = path.extname(name).substring(1); const testSuite: TestSuite = { - name: clsName, nameToRun: fileName + `:${clsName}`, + name: clsName, nameToRun: `${fileName}:${clsName}`, functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, isInstance: false, functionsFailed: 0, functionsPassed: 0 }; @@ -126,7 +125,7 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin return; } if (line.startsWith('nose.selector: DEBUG: wantClass ')) { - let name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); const testSuite: TestSuite = { name: path.extname(name).substring(1), nameToRun: `${fileName}:.${name}`, functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, @@ -144,6 +143,7 @@ function parseNoseTestModuleCollectionResult(rootDirectory: string, lines: strin time: 0, functionsFailed: 0, functionsPassed: 0 }; + // tslint:disable-next-line:no-non-null-assertion const cls = testFile.suites.find(suite => suite.name === clsName)!; cls.functions.push(fn); return; diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index 12ced38c9a7d..fddf3a8be496 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -1,23 +1,25 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; import { OutputChannel } from 'vscode'; -import { TestsToRun, Tests } from '../common/contracts'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; +import { Tests, TestsToRun } from '../common/contracts'; +import { ITestCollectionStorageService } from '../common/testUtils'; +import { discoverTests } from './collector'; import { runTest } from './runner'; -import { Product } from '../../common/installer'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('nosetest', Product.nosetest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { + super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage); } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = this.settings.unitTest.nosetestArgs.slice(0); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.nosetestArgs.slice(0); return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = this.settings.unitTest.nosetestArgs.slice(0); + // tslint:disable-next-line:no-any + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + const args = this.settings.unitTest.nosetestArgs.slice(0); if (runFailedTests === true && args.indexOf('--failed') === -1) { args.push('--failed'); } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index 3f6f73bb2532..5717fa75b8f2 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -1,17 +1,18 @@ 'use strict'; -import { createTemporaryFile } from '../../common/helpers'; -import { OutputChannel, CancellationToken, Uri } from 'vscode'; -import { TestsToRun, Tests } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; -import { updateResultsFromXmlLogFile, PassCalculationFormulae } from '../common/xUnitParser'; -import { run } from '../common/runner'; -import { PythonSettings } from '../../common/configSettings'; import * as path from 'path'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { createTemporaryFile } from '../../common/helpers'; +import { Tests, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; +import { run } from '../common/runner'; +import { updateResults } from '../common/testUtils'; +import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; +// tslint:disable-next-line:no-any export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { @@ -28,6 +29,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } let xmlLogFile = ''; + // tslint:disable-next-line:no-empty let xmlLogFileCleanup: Function = () => { }; // Check if '--with-xunit' is in args list @@ -37,7 +39,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } // Check if '--xunit-file' exists, if not generate random xml file - let indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); + const indexOfXUnitFile = noseTestArgs.findIndex(value => value.indexOf(XUNIT_FILE) === 0); let promiseToGetXmlLogFile: Promise; if (indexOfXUnitFile === -1) { promiseToGetXmlLogFile = createTemporaryFile('.xml').then(xmlLogResult => { @@ -47,12 +49,10 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes noseTestArgs.push(`${XUNIT_FILE}=${xmlLogFile}`); return xmlLogResult.filePath; }); - } - else { + } else { if (noseTestArgs[indexOfXUnitFile].indexOf('=') === -1) { xmlLogFile = noseTestArgs[indexOfXUnitFile + 1]; - } - else { + } else { xmlLogFile = noseTestArgs[indexOfXUnitFile].substring(noseTestArgs[indexOfXUnitFile].indexOf('=') + 1).trim(); } @@ -64,10 +64,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes if (debug === true) { const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); const nosetestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'nose']; - const args = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); - return launchDebugger(rootDirectory, args, token, outChannel); - } - else { + const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); + return launchDebugger(rootDirectory, debuggerArgs, token, outChannel); + } else { return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel); } }).then(() => { @@ -81,6 +80,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } +// tslint:disable-next-line:no-any export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests).then(() => { updateResults(tests); diff --git a/src/client/unittests/pytest/collector.ts b/src/client/unittests/pytest/collector.ts index 175bf16414d5..d699a2575259 100644 --- a/src/client/unittests/pytest/collector.ts +++ b/src/client/unittests/pytest/collector.ts @@ -1,12 +1,12 @@ 'use strict'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, TestSuite, TestFunction, Tests } from '../common/contracts'; import * as os from 'os'; -import { extractBetweenDelimiters, flattenTestFiles, convertFileToPackage } from '../common/testUtils'; -import * as vscode from 'vscode'; import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; +import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { TestFile, TestFunction, Tests, TestSuite } from '../common/contracts'; +import { convertFileToPackage, extractBetweenDelimiters, flattenTestFiles } from '../common/testUtils'; +import { execPythonFile } from './../../common/utils'; const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--fixtures-per-test', '--pdb', '--runxfail', @@ -18,8 +18,8 @@ const settingsInArgsToExcludeForDiscovery = []; export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { let logOutputLines: string[] = ['']; - let testFiles: TestFile[] = []; - let parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; + const testFiles: TestFile[] = []; + const parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; const errorLine = /==*( *)ERRORS( *)=*/; const errorFileLine = /__*( *)ERROR collecting (.*)/; const lastLineWithErrors = /==*.*/; @@ -104,7 +104,7 @@ function parsePyTestModuleCollectionError(rootDirectory: string, lines: string[] return; } - let errorFileLine = lines[0]; + const errorFileLine = lines[0]; let fileName = errorFileLine.substring(errorFileLine.indexOf('ERROR collecting') + 'ERROR collecting'.length).trim(); fileName = fileName.substr(0, fileName.lastIndexOf(' ')); @@ -144,22 +144,23 @@ function parsePyTestModuleCollectionResult(rootDirectory: string, lines: string[ if (trimmedLine.startsWith(' 0) { - let parentNode = parentNodes[parentNodes.length - 1]; + const parentNode = parentNodes[parentNodes.length - 1]; if (parentNode.indent < indentOfCurrentItem) { return parentNode; } diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 06e86e75fd34..51835007a3fd 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -1,21 +1,22 @@ 'use strict'; -import { TestsToRun, Tests } from '../common/contracts'; -import { runTest } from './runner'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; -import { BaseTestManager } from '../common/baseTestManager'; import { Product } from '../../common/installer'; +import { BaseTestManager } from '../common/baseTestManager'; +import { Tests, TestsToRun } from '../common/contracts'; +import { ITestCollectionStorageService } from '../common/testUtils'; +import { discoverTests } from './collector'; +import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('pytest', Product.pytest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { + super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage); } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = this.settings.unitTest.pyTestArgs.slice(0); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.pyTestArgs.slice(0); return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = this.settings.unitTest.pyTestArgs.slice(0); + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { + const args = this.settings.unitTest.pyTestArgs.slice(0); if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index 963ad150b5c9..37998eed9f86 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -1,15 +1,13 @@ -/// - 'use strict'; -import { createTemporaryFile } from '../../common/helpers'; -import { TestsToRun, Tests } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; +import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; -import { updateResultsFromXmlLogFile, PassCalculationFormulae } from '../common/xUnitParser'; -import { run } from '../common/runner'; import { PythonSettings } from '../../common/configSettings'; -import * as path from 'path'; +import { createTemporaryFile } from '../../common/helpers'; +import { Tests, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; +import { run } from '../common/runner'; +import { updateResults } from '../common/testUtils'; +import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; @@ -41,10 +39,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes if (debug) { const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); const pytestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'pytest']; - const args = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); - return launchDebugger(rootDirectory, args, token, outChannel); - } - else { + const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); + return launchDebugger(rootDirectory, debuggerArgs, token, outChannel); + } else { return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel); } }).then(() => { diff --git a/src/client/unittests/unittest/collector.ts b/src/client/unittests/unittest/collector.ts index 633cbe4bc671..73e724db87e8 100644 --- a/src/client/unittests/unittest/collector.ts +++ b/src/client/unittests/unittest/collector.ts @@ -1,11 +1,11 @@ 'use strict'; -import { execPythonFile } from './../../common/utils'; -import { TestFile, Tests, TestStatus } from '../common/contracts'; -import { flattenTestFiles } from '../common/testUtils'; -import * as vscode from 'vscode'; import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; +import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; +import { TestFile, Tests, TestStatus } from '../common/contracts'; +import { flattenTestFiles } from '../common/testUtils'; +import { execPythonFile } from './../../common/utils'; export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { let startDirectory = '.'; @@ -16,8 +16,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (startDir.trim() === '-s' && args.length >= indexOfStartDir) { // Assume the next items is the directory startDirectory = args[indexOfStartDir + 1]; - } - else { + } else { startDirectory = startDir.substring(2).trim(); if (startDirectory.startsWith('=') || startDirectory.startsWith(' ')) { startDirectory = startDirectory.substring(1); @@ -30,8 +29,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (patternValue.trim() === '-p' && args.length >= indexOfPattern) { // Assume the next items is the directory pattern = args[indexOfPattern + 1]; - } - else { + } else { pattern = patternValue.substring(2).trim(); if (pattern.startsWith('=')) { pattern = pattern.substring(1); @@ -51,7 +49,7 @@ for suite in suites._tests: pass`; let startedCollecting = false; - let testItems: string[] = []; + const testItems: string[] = []; function processOutput(output: string) { output.split(/\r?\n/g).forEach((line, index, lines) => { if (token && token.isCancellationRequested) { @@ -104,7 +102,7 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) } const paths = testIdParts.slice(0, testIdParts.length - 2); - const filePath = path.join(rootDirectory, ...paths) + '.py'; + const filePath = `${path.join(rootDirectory, ...paths)}.py`; const functionName = testIdParts.pop(); const className = testIdParts.pop(); diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 665406886146..207120185e05 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -1,23 +1,25 @@ 'use strict'; -import { PythonSettings } from '../../common/configSettings'; -import { TestsToRun, Tests, TestStatus } from '../common/contracts'; -import { runTest } from './runner'; import * as vscode from 'vscode'; -import { discoverTests } from './collector'; -import { BaseTestManager } from '../common/baseTestManager'; +import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; +import { BaseTestManager } from '../common/baseTestManager'; +import { Tests, TestStatus, TestsToRun } from '../common/contracts'; +import { ITestCollectionStorageService } from '../common/testUtils'; +import { discoverTests } from './collector'; +import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel) { - super('unitest', Product.unittest, rootDirectory, outputChannel); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { + super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage); } - configure() { + // tslint:disable-next-line:no-empty + public configure() { } - discoverTestsImpl(ignoreCache: boolean): Promise { - let args = this.settings.unitTest.unittestArgs.slice(0); + public discoverTestsImpl(ignoreCache: boolean): Promise { + const args = this.settings.unitTest.unittestArgs.slice(0); return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); } - runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args = this.settings.unitTest.unittestArgs.slice(0); + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + const args = this.settings.unitTest.unittestArgs.slice(0); if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; testsToRun.testFunction = tests.testFunctions.filter(fn => { diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index d6f98e8365aa..819fe2db0a72 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -2,23 +2,27 @@ 'use strict'; import * as path from 'path'; -import { TestsToRun, Tests, TestStatus } from '../common/contracts'; -import { updateResults } from '../common/testUtils'; -import { BaseTestManager } from '../common/baseTestManager'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; -import { run } from '../common/runner'; -import { Server } from './socketServer'; import { PythonSettings } from '../../common/configSettings'; +import { BaseTestManager } from '../common/baseTestManager'; +import { Tests, TestStatus, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; -interface TestStatusMap { +import { run } from '../common/runner'; +import { updateResults } from '../common/testUtils'; +import { Server } from './socketServer'; +type TestStatusMap = { status: TestStatus; summaryProperty: string; -} +}; const outcomeMapping = new Map(); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('passed', { status: TestStatus.Pass, summaryProperty: 'passed' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('failed', { status: TestStatus.Fail, summaryProperty: 'failures' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('error', { status: TestStatus.Error, summaryProperty: 'errors' }); +// tslint:disable-next-line:no-backbone-get-set-outside-model outcomeMapping.set('skipped', { status: TestStatus.Skipped, summaryProperty: 'skipped' }); interface ITestData { @@ -28,6 +32,7 @@ interface ITestData { traceback: string; } +// tslint:disable-next-line:max-func-body-length export function runTest(testManager: BaseTestManager, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { tests.summary.errors = 0; tests.summary.failures = 0; @@ -37,12 +42,16 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_testlauncher.py'); const server = new Server(); server.on('error', (message: string, ...data: string[]) => { + // tslint:disable-next-line:no-console console.log(`${message} ${data.join(' ')}`); }); + // tslint:disable-next-line:no-empty server.on('log', (message: string, ...data: string[]) => { }); + // tslint:disable-next-line:no-empty server.on('connect', (data) => { }); + // tslint:disable-next-line:no-empty server.on('start', (data: { test: string }) => { }); server.on('result', (data: ITestData) => { @@ -57,31 +66,31 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes if (failFast && (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors')) { testManager.stop(); } - } - else { + } else { if (statusDetails) { tests.summary[statusDetails.summaryProperty] += 1; } } }); + // tslint:disable-next-line:no-empty server.on('socket.disconnected', (data) => { }); return server.start().then(port => { - let testPaths: string[] = getIdsOfTestsToRun(tests, testsToRun); - for (let counter = 0; counter < testPaths.length; counter++) { - testPaths[counter] = '-t' + testPaths[counter].trim(); + const testPaths: string[] = getIdsOfTestsToRun(tests, testsToRun); + for (let counter = 0; counter < testPaths.length; counter += 1) { + testPaths[counter] = `-t${testPaths[counter].trim()}`; } const startTestDiscoveryDirectory = getStartDirectory(args); - function runTest(testFile: string = '', testId: string = '') { + function runTestInternal(testFile: string = '', testId: string = '') { let testArgs = buildTestArgs(args); failFast = testArgs.indexOf('--uf') >= 0; testArgs = testArgs.filter(arg => arg !== '--uf'); testArgs.push(`--result-port=${port}`); if (debug === true) { - testArgs.push(...[`--secret=my_secret`, `--port=3000`]); + testArgs.push(...['--secret=my_secret', '--port=3000']); } testArgs.push(`--us=${startTestDiscoveryDirectory}`); if (testId.length > 0) { @@ -92,34 +101,33 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes } if (debug === true) { return launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), token, outChannel); - } - else { + } else { return run(PythonSettings.getInstance(Uri.file(rootDirectory)).pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); } } // Test everything if (testPaths.length === 0) { - return runTest(); + return runTestInternal(); } // Ok, the ptvs test runner can only work with one test at a time let promise = Promise.resolve(''); if (Array.isArray(testsToRun.testFile)) { testsToRun.testFile.forEach(testFile => { - promise = promise.then(() => runTest(testFile.fullPath, testFile.nameToRun)); + promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun)); }); } if (Array.isArray(testsToRun.testSuite)) { testsToRun.testSuite.forEach(testSuite => { const testFileName = tests.testSuits.find(t => t.testSuite === testSuite).parentTestFile.fullPath; - promise = promise.then(() => runTest(testFileName, testSuite.nameToRun)); + promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun)); }); } if (Array.isArray(testsToRun.testFunction)) { testsToRun.testFunction.forEach(testFn => { const testFileName = tests.testFunctions.find(t => t.testFunction === testFn).parentTestFile.fullPath; - promise = promise.then(() => runTest(testFileName, testFn.nameToRun)); + promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun)); }); } return promise; @@ -139,8 +147,7 @@ function getStartDirectory(args: string[]): string { if ((startDir.trim() === '-s' || startDir.trim() === '--start-directory') && args.length >= indexOfStartDir) { // Assume the next items is the directory startDirectory = args[indexOfStartDir + 1]; - } - else { + } else { const lenToStartFrom = startDir.startsWith('-s') ? '-s'.length : '--start-directory'.length; startDirectory = startDir.substring(lenToStartFrom).trim(); if (startDirectory.startsWith('=')) { @@ -159,8 +166,7 @@ function buildTestArgs(args: string[]): string[] { if ((patternValue.trim() === '-p' || patternValue.trim() === '--pattern') && args.length >= indexOfPattern) { // Assume the next items is the directory pattern = args[indexOfPattern + 1]; - } - else { + } else { const lenToStartFrom = patternValue.startsWith('-p') ? '-p'.length : '--pattern'.length; pattern = patternValue.substring(lenToStartFrom).trim(); if (pattern.startsWith('=')) { diff --git a/src/client/unittests/unittest/socketServer.ts b/src/client/unittests/unittest/socketServer.ts index 6c4d3bc6166b..334ae6b85c4e 100644 --- a/src/client/unittests/unittest/socketServer.ts +++ b/src/client/unittests/unittest/socketServer.ts @@ -1,16 +1,19 @@ 'use strict'; -import * as net from 'net'; +import { EventEmitter } from 'events'; import * as fs from 'fs'; +import * as net from 'net'; import * as os from 'os'; -import { Disposable } from 'vscode' +import { Disposable } from 'vscode'; import { createDeferred, Deferred } from '../../common/helpers'; -import { EventEmitter } from 'events'; +// tslint:disable-next-line:variable-name const MaxConnections = 100; function getIPType() { const networkInterfaces = os.networkInterfaces(); + // tslint:disable-next-line:variable-name let IPType = ''; + // tslint:disable-next-line:prefer-type-cast no-any if (networkInterfaces && Array.isArray(networkInterfaces) && (networkInterfaces as any).length > 0) { // getting the family of first network interface available IPType = networkInterfaces[Object.keys(networkInterfaces)[0]][0].family; @@ -41,7 +44,7 @@ export class Server extends EventEmitter implements Disposable { } } public start(): Promise { - this.startedDef = createDeferred() + this.startedDef = createDeferred(); fs.unlink(this.path, () => { this.server = net.createServer(this.connectionListener.bind(this)); this.server.maxConnections = MaxConnections; @@ -72,16 +75,17 @@ export class Server extends EventEmitter implements Disposable { this.emit('error', err); }); socket.on('data', (data) => { - let sock = socket; + const sock = socket; // Assume we have just one client socket connection let dataStr = this.ipcBuffer += data; + // tslint:disable-next-line:no-constant-condition while (true) { const startIndex = dataStr.indexOf('{'); if (startIndex === -1) { return; } - const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim()); + const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim(), 10); if (dataStr.length < startIndex + lengthOfMessage) { return; } @@ -97,13 +101,16 @@ export class Server extends EventEmitter implements Disposable { this.emit('log', message, ...data); } private onCloseSocket() { - for (let i = 0, count = this.sockets.length; i < count; i++) { - let socket = this.sockets[i]; + // tslint:disable-next-line:one-variable-per-declaration + for (let i = 0, count = this.sockets.length; i < count; i += 1) { + const socket = this.sockets[i]; let destroyedSocketId = false; if (socket && socket.readable) { continue; } + // tslint:disable-next-line:no-any prefer-type-cast if ((socket as any).id) { + // tslint:disable-next-line:no-any prefer-type-cast destroyedSocketId = (socket as any).id; } this.log('socket disconnected', destroyedSocketId.toString()); @@ -115,4 +122,4 @@ export class Server extends EventEmitter implements Disposable { return; } } -} \ No newline at end of file +} From 7ddbb1804c0f3adc36bcfdbb0b3a64f7cf87a4b4 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Oct 2017 11:49:49 -0700 Subject: [PATCH 09/23] lots of refactoring --- src/client/common/installer.ts | 44 ++- src/client/unittests/codeLenses/main.ts | 2 +- src/client/unittests/codeLenses/testFiles.ts | 28 +- .../unittests/common/baseTestManager.ts | 33 +- .../unittests/common/configSettingService.ts | 2 +- src/client/unittests/common/constants.ts | 1 + src/client/unittests/common/storageService.ts | 24 ++ .../common/testConfigurationManager.ts | 2 +- .../unittests/common/testManagerService.ts | 17 +- .../common/testManagerServiceFactory.ts | 7 +- .../unittests/common/testResultsService.ts | 110 ++++++ src/client/unittests/common/testUtils.ts | 336 +--------------- .../common/testVisitors/flatteningVisitor.ts | 65 ++++ .../testVisitors/folderGenerationVisitor.ts | 55 +++ .../common/testVisitors/resultResetVisitor.ts | 37 ++ .../common/{contracts.ts => types.ts} | 32 +- .../common/workspaceTestManagerService.ts | 28 +- src/client/unittests/common/xUnitParser.ts | 2 +- src/client/unittests/configuration.ts | 6 +- src/client/unittests/display/main.ts | 3 +- src/client/unittests/display/picker.ts | 36 +- src/client/unittests/main.ts | 363 +++++++++--------- src/client/unittests/nosetest/collector.ts | 8 +- src/client/unittests/nosetest/main.ts | 13 +- src/client/unittests/nosetest/runner.ts | 11 +- .../nosetest/testConfigurationManager.ts | 2 +- src/client/unittests/pytest/collector.ts | 8 +- src/client/unittests/pytest/main.ts | 13 +- src/client/unittests/pytest/runner.ts | 11 +- .../pytest/testConfigurationManager.ts | 2 +- src/client/unittests/unittest/collector.ts | 11 +- src/client/unittests/unittest/main.ts | 15 +- src/client/unittests/unittest/runner.ts | 9 +- .../unittest/testConfigurationManager.ts | 2 +- src/test/index.ts | 2 +- src/test/unittests/nosetest.test.ts | 16 +- src/test/unittests/pytest.test.ts | 20 +- src/test/unittests/unittest.test.ts | 20 +- 38 files changed, 736 insertions(+), 660 deletions(-) create mode 100644 src/client/unittests/common/constants.ts create mode 100644 src/client/unittests/common/storageService.ts create mode 100644 src/client/unittests/common/testResultsService.ts create mode 100644 src/client/unittests/common/testVisitors/flatteningVisitor.ts create mode 100644 src/client/unittests/common/testVisitors/folderGenerationVisitor.ts create mode 100644 src/client/unittests/common/testVisitors/resultResetVisitor.ts rename src/client/unittests/common/{contracts.ts => types.ts} (77%) diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index fb8042454455..43a0cb933ef5 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -177,6 +177,7 @@ export class Installer implements vscode.Disposable { // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); return disablePromptForFeatures.indexOf(productName) === -1; } @@ -199,9 +200,9 @@ export class Installer implements vscode.Disposable { return InstallerResponse.Ignore; } - const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : 'Install ' + productName; - const disableOption = 'Disable ' + productTypeName; - const dontShowAgain = `Don't show this prompt again`; + const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : `Install ${productName}`; + const disableOption = `Disable ${productTypeName}`; + const dontShowAgain = 'Don\'t show this prompt again'; const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; const useOtherFormatter = `Use '${alternateFormatter}' formatter`; const options = []; @@ -220,8 +221,8 @@ export class Installer implements vscode.Disposable { case disableOption: { if (Linters.indexOf(product) >= 0) { return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled); - } - else { + } else { + // tslint:disable-next-line:no-non-null-assertion const settingToDisable = SettingToDisableProduct.get(product)!; return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled); } @@ -232,6 +233,7 @@ export class Installer implements vscode.Disposable { } case dontShowAgain: { const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); features.push(productName); return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore); @@ -255,31 +257,32 @@ export class Installer implements vscode.Disposable { this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); this.outputChannel.show(); - } - else { + } else { window.showInformationMessage('Install Universal Ctags and set it in your path or define the path in your python.workspaceSymbols.ctagsPath settings'); } return InstallerResponse.Ignore; } + // tslint:disable-next-line:no-non-null-assertion let installArgs = ProductInstallScripts.get(product)!; - let pipIndex = installArgs.indexOf('pip'); + const pipIndex = installArgs.indexOf('pip'); if (pipIndex > 0) { installArgs = installArgs.slice(); - let proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); + const proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); if (proxy.length > 0) { installArgs.splice(2, 0, proxy); installArgs.splice(2, 0, '--proxy'); } } + // tslint:disable-next-line:no-any let installationPromise: Promise; if (this.outputChannel && installArgs[0] === '-m') { // Errors are just displayed to the user this.outputChannel.show(); installationPromise = execPythonFile(resource, settings.PythonSettings.getInstance(resource).pythonPath, + // tslint:disable-next-line:no-non-null-assertion installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); }); - } - else { + } else { // When using terminal get the fully qualitified path // Cuz people may launch vs code from terminal when they have activated the appropriate virtual env // Problem is terminal doesn't use the currently activated virtual env @@ -291,12 +294,13 @@ export class Installer implements vscode.Disposable { if (installArgs[0] === '-m') { if (pythonPath.indexOf(' ') >= 0) { installScript = `"${pythonPath}" ${installScript}`; - } - else { + } else { installScript = `${pythonPath} ${installScript}`; } } + // tslint:disable-next-line:no-non-null-assertion Installer.terminal!.sendText(installScript); + // tslint:disable-next-line:no-non-null-assertion Installer.terminal!.show(false); }); } @@ -306,30 +310,33 @@ export class Installer implements vscode.Disposable { .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); } + // tslint:disable-next-line:member-ordering public isInstalled(product: Product, resource?: Uri): Promise { return isProductInstalled(product, resource); } + // tslint:disable-next-line:member-ordering no-any public uninstall(product: Product, resource?: Uri): Promise { return uninstallproduct(product, resource); } + // tslint:disable-next-line:member-ordering public disableLinter(product: Product, resource: Uri) { if (resource && !workspace.getWorkspaceFolder(resource)) { + // tslint:disable-next-line:no-non-null-assertion const settingToDisable = SettingToDisableProduct.get(product)!; const pythonConfig = workspace.getConfiguration('python', resource); return pythonConfig.update(settingToDisable, false, ConfigurationTarget.Workspace); - } - else { + } else { const pythonConfig = workspace.getConfiguration('python'); return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); } } + // tslint:disable-next-line:no-any private updateSetting(setting: string, value: any, resource?: Uri) { if (resource && !workspace.getWorkspaceFolder(resource)) { const pythonConfig = workspace.getConfiguration('python', resource); return pythonConfig.update(setting, value, ConfigurationTarget.Workspace); - } - else { + } else { const pythonConfig = workspace.getConfiguration('python'); return pythonConfig.update(setting, value, true); } @@ -351,6 +358,7 @@ async function isProductInstalled(product: Product, resource?: Uri): Promise !isNotInstalledError(reason)); } +// tslint:disable-next-line:no-any function uninstallproduct(product: Product, resource?: Uri): Promise { if (!ProductUninstallScripts.has(product)) { return Promise.resolve(); } + // tslint:disable-next-line:no-non-null-assertion const uninstallArgs = ProductUninstallScripts.get(product)!; return execPythonFile(resource, 'python', uninstallArgs, getCwdForInstallScript(resource), false); } diff --git a/src/client/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts index 613f86dae062..3efd30091227 100644 --- a/src/client/unittests/codeLenses/main.ts +++ b/src/client/unittests/codeLenses/main.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService } from '../common/types'; import { TestFileCodeLensProvider } from './testFiles'; export function activateCodeLenses(onDidChange: vscode.EventEmitter, diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index cfc0ad1bf1eb..f8f9e5b381e1 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,10 +1,10 @@ 'use strict'; import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, workspace } from 'vscode'; +import { Uri } from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; -import { TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/types'; type CodeLensData = { symbolKind: SymbolKind; @@ -84,7 +84,7 @@ export class TestFileCodeLensProvider implements CodeLensProvider { new Position(symbol.location.range.end.line, symbol.location.range.end.character + 1)); - return this.getCodeLens(document.uri.fsPath, allFuncsAndSuites, + return this.getCodeLens(document.uri, allFuncsAndSuites, range, symbol.name, symbol.kind, symbol.containerName); }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); }, reason => { @@ -95,13 +95,13 @@ export class TestFileCodeLensProvider implements CodeLensProvider { }); } - private getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, + private getCodeLens(file: Uri, allFuncsAndSuites: FunctionsAndSuites, range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { switch (symbolKind) { case SymbolKind.Function: case SymbolKind.Method: { - return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range, symbolContainer); + return getFunctionCodeLens(file, allFuncsAndSuites, symbolName, range, symbolContainer); } case SymbolKind.Class: { const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); @@ -112,12 +112,12 @@ export class TestFileCodeLensProvider implements CodeLensProvider { new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testSuite: [cls] }] + arguments: [file, { testSuite: [cls] }] }), new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testSuite: [cls] }] + arguments: [file, { testSuite: [cls] }] }) ]; } @@ -163,7 +163,7 @@ function getTestStatusIcons(fns: TestFunction[]): string { return statuses.join(' '); } -function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndSuites, +function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, symbolName: string, range: Range, symbolContainer: string): CodeLens[] { let fn: TestFunction | undefined; @@ -186,12 +186,12 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: [fn] }] + arguments: [file, { testFunction: [fn] }] }), new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: [fn] }] + arguments: [file, { testFunction: [fn] }] }) ]; } @@ -207,12 +207,12 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS new CodeLens(range, { title: constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: functions }] + arguments: [file, { testFunction: functions }] }), new CodeLens(range, { title: constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: functions }] + arguments: [file, { testFunction: functions }] }) ]; } @@ -222,12 +222,12 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, - arguments: [filePath, functions] + arguments: [file, functions] }), new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [filePath, functions] + arguments: [file, functions] }) ]; } diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index 368f72327d62..c0facb6ac89c 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -1,12 +1,15 @@ // import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestFunction, TestStatus, FlattenedTestFunction, FlattenedTestSuite, CANCELLATION_REASON} from './contracts'; import * as vscode from 'vscode'; +import { Uri, workspace } from 'vscode'; import { IPythonSettings, PythonSettings } from '../../common/configSettings'; import { isNotInstalledError } from '../../common/helpers'; import { Installer, Product } from '../../common/installer'; -import { CANCELLATION_REASON, Tests, TestStatus, TestsToRun } from './contracts'; -import { displayTestErrorMessage, ITestCollectionStorageService, resetTestResults } from './testUtils'; +import { CANCELLATION_REASON } from './constants'; +import { displayTestErrorMessage } from './testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; export abstract class BaseTestManager { + public readonly workspace: Uri; protected readonly settings: IPythonSettings; private tests: Tests; // tslint:disable-next-line:variable-name @@ -14,19 +17,21 @@ export abstract class BaseTestManager { private cancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; private discoverTestsPromise: Promise; - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel, - private testCollectionStorage: ITestCollectionStorageService) { + constructor(private testProvider: string, private product: Product, protected rootDirectory: string, + protected outputChannel: vscode.OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + protected testResultsService: ITestResultsService, protected testsHelper: ITestsHelper) { this._status = TestStatus.Unknown; this.installer = new Installer(); - this.settings = PythonSettings.getInstance(this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); + this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); + this.workspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory)).uri; } protected get cancellationToken(): vscode.CancellationToken { if (this.cancellationTokenSource) { return this.cancellationTokenSource.token; } } - // tslint:disable-next-line:no-empty public dispose() { + this.stop(); } public get status(): TestStatus { return this._status; @@ -49,7 +54,7 @@ export abstract class BaseTestManager { return; } - resetTestResults(this.tests); + this.testResultsService.resetResults(this.tests); } public discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { if (this.discoverTestsPromise) { @@ -111,13 +116,7 @@ export abstract class BaseTestManager { return Promise.reject(reason); }); } - public runTest(testsToRun?: TestsToRun, debug?: boolean): Promise; - // tslint:disable-next-line:unified-signatures - public runTest(runFailedTests?: boolean, debug?: boolean): Promise; - // tslint:disable-next-line:no-any - public runTest(args: any, debug?: boolean): Promise { - let runFailedTests = false; - let testsToRun: TestsToRun = null; + public runTest(testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { const moreInfo = { Test_Provider: this.testProvider, Run_Failed_Tests: 'false', @@ -126,13 +125,11 @@ export abstract class BaseTestManager { Run_Specific_Function: 'false' }; - if (typeof args === 'boolean') { - runFailedTests = args === true; + if (runFailedTests === true) { // tslint:disable-next-line:prefer-template moreInfo.Run_Failed_Tests = runFailedTests + ''; } - if (typeof args === 'object' && args !== null) { - testsToRun = args; + if (testsToRun && typeof testsToRun === 'object') { if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { moreInfo.Run_Specific_File = 'true'; } diff --git a/src/client/unittests/common/configSettingService.ts b/src/client/unittests/common/configSettingService.ts index 1e005c8973bc..6d99bb71a374 100644 --- a/src/client/unittests/common/configSettingService.ts +++ b/src/client/unittests/common/configSettingService.ts @@ -1,6 +1,6 @@ import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; import { Product } from '../../common/installer'; -import { ITestConfigSettingsService, UnitTestProduct } from './contracts'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; export class TestConfigSettingsService implements ITestConfigSettingsService { private static getTestArgSetting(product: UnitTestProduct) { diff --git a/src/client/unittests/common/constants.ts b/src/client/unittests/common/constants.ts new file mode 100644 index 000000000000..3d428cfad97c --- /dev/null +++ b/src/client/unittests/common/constants.ts @@ -0,0 +1 @@ +export const CANCELLATION_REASON = 'cancelled_user_request'; diff --git a/src/client/unittests/common/storageService.ts b/src/client/unittests/common/storageService.ts new file mode 100644 index 000000000000..b58e37f8bc93 --- /dev/null +++ b/src/client/unittests/common/storageService.ts @@ -0,0 +1,24 @@ +import { Uri, workspace } from 'vscode'; +import { ITestCollectionStorageService, Tests } from './types'; + +export class TestCollectionStorageService implements ITestCollectionStorageService { + private testsIndexedByWorkspaceUri = new Map(); + public getTests(wkspace: Uri): Tests | undefined { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; + } + public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); + } + public dispose() { + this.testsIndexedByWorkspaceUri.clear(); + } + private getWorkspaceFolderPath(resource?: Uri): string | undefined { + if (!resource) { + return undefined; + } + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.path : undefined; + } +} diff --git a/src/client/unittests/common/testConfigurationManager.ts b/src/client/unittests/common/testConfigurationManager.ts index 84843f7c69d5..fdf5aa83bee8 100644 --- a/src/client/unittests/common/testConfigurationManager.ts +++ b/src/client/unittests/common/testConfigurationManager.ts @@ -4,7 +4,7 @@ import { Uri } from 'vscode'; import { createDeferred } from '../../common/helpers'; import { Installer } from '../../common/installer'; import { getSubDirectories } from '../../common/utils'; -import { ITestConfigSettingsService, UnitTestProduct } from './contracts'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; export abstract class TestConfigurationManager { constructor(protected workspace: Uri, diff --git a/src/client/unittests/common/testManagerService.ts b/src/client/unittests/common/testManagerService.ts index e07ee373fa33..71f24a9ddf6a 100644 --- a/src/client/unittests/common/testManagerService.ts +++ b/src/client/unittests/common/testManagerService.ts @@ -5,16 +5,23 @@ import { TestManager as NoseTestManager } from '../nosetest/main'; import { TestManager as PyTestTestManager } from '../pytest/main'; import { TestManager as UnitTestTestManager } from '../unittest/main'; import { BaseTestManager } from './baseTestManager'; -import { ITestManagerService, UnitTestProduct } from './contracts'; +import { ITestCollectionStorageService, ITestManagerService, ITestResultsService, ITestsHelper, UnitTestProduct } from './types'; type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; export class TestManagerService implements ITestManagerService { private testManagers = new Map(); - constructor(private wkspace: Uri, private outChannel: OutputChannel) { - this.testManagers.set(Product.nosetest, { create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel) }); - this.testManagers.set(Product.pytest, { create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel) }); - this.testManagers.set(Product.unittest, { create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel) }); + constructor(private wkspace: Uri, private outChannel: OutputChannel, + testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + this.testManagers.set(Product.nosetest, { + create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + }); + this.testManagers.set(Product.pytest, { + create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + }); + this.testManagers.set(Product.unittest, { + create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + }); } public dispose() { this.testManagers.forEach(info => { diff --git a/src/client/unittests/common/testManagerServiceFactory.ts b/src/client/unittests/common/testManagerServiceFactory.ts index d48b60bed15f..416cd713e9d0 100644 --- a/src/client/unittests/common/testManagerServiceFactory.ts +++ b/src/client/unittests/common/testManagerServiceFactory.ts @@ -1,10 +1,11 @@ import { OutputChannel, Uri } from 'vscode'; -import { ITestManagerService, ITestManagerServiceFactory } from './contracts'; import { TestManagerService } from './testManagerService'; +import { ITestCollectionStorageService, ITestManagerService, ITestManagerServiceFactory, ITestResultsService, ITestsHelper } from './types'; export class TestManagerServiceFactory implements ITestManagerServiceFactory { - constructor(private outChannel: OutputChannel) { } + constructor(private outChannel: OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + private testResultsService: ITestResultsService, private testsHelper: ITestsHelper) { } public createTestManagerService(wkspace: Uri): ITestManagerService { - return new TestManagerService(wkspace, this.outChannel); + return new TestManagerService(wkspace, this.outChannel, this.testCollectionStorage, this.testResultsService, this.testsHelper); } } diff --git a/src/client/unittests/common/testResultsService.ts b/src/client/unittests/common/testResultsService.ts new file mode 100644 index 000000000000..58fda744123e --- /dev/null +++ b/src/client/unittests/common/testResultsService.ts @@ -0,0 +1,110 @@ +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { ITestResultsService, TestFile, TestFolder, Tests, TestStatus, TestSuite } from './types'; + +export class TestResultsService implements ITestResultsService { + public resetResults(tests: Tests): void { + const resultResetVisitor = new TestResultResetVisitor(); + tests.testFolders.forEach(f => resultResetVisitor.visitTestFolder(f)); + tests.testFunctions.forEach(fn => resultResetVisitor.visitTestFunction(fn.testFunction)); + tests.testSuits.forEach(suite => resultResetVisitor.visitTestSuite(suite.testSuite)); + tests.testFiles.forEach(testFile => resultResetVisitor.visitTestFile(testFile)); + } + public updateResults(tests: Tests): void { + tests.testFiles.forEach(test => this.updateTestFileResults(test)); + tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); + } + private updateTestSuiteResults(test: TestSuite): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFileResults(test: TestFile): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFolderResults(testFolder: TestFolder): void { + let allFilesPassed = true; + let allFilesRan = true; + + testFolder.testFiles.forEach(fl => { + if (allFilesPassed && typeof fl.passed === 'boolean') { + if (!fl.passed) { + allFilesPassed = false; + } + } else { + allFilesRan = false; + } + + testFolder.functionsFailed += fl.functionsFailed; + testFolder.functionsPassed += fl.functionsPassed; + }); + + let allFoldersPassed = true; + let allFoldersRan = true; + + testFolder.folders.forEach(folder => { + this.updateTestFolderResults(folder); + if (allFoldersPassed && typeof folder.passed === 'boolean') { + if (!folder.passed) { + allFoldersPassed = false; + } + } else { + allFoldersRan = false; + } + + testFolder.functionsFailed += folder.functionsFailed; + testFolder.functionsPassed += folder.functionsPassed; + }); + + if (allFilesRan && allFoldersRan) { + testFolder.passed = allFilesPassed && allFoldersPassed; + testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; + } else { + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } + } + private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { + let totalTime = 0; + let allFunctionsPassed = true; + let allFunctionsRan = true; + + test.functions.forEach(fn => { + totalTime += fn.time; + if (typeof fn.passed === 'boolean') { + if (fn.passed) { + test.functionsPassed += 1; + } else { + test.functionsFailed += 1; + allFunctionsPassed = false; + } + } else { + allFunctionsRan = false; + } + }); + + let allSuitesPassed = true; + let allSuitesRan = true; + + test.suites.forEach(suite => { + this.updateTestSuiteResults(suite); + totalTime += suite.time; + if (allSuitesRan && typeof suite.passed === 'boolean') { + if (!suite.passed) { + allSuitesPassed = false; + } + } else { + allSuitesRan = false; + } + + test.functionsFailed += suite.functionsFailed; + test.functionsPassed += suite.functionsPassed; + }); + + test.time = totalTime; + if (allSuitesRan && allFunctionsRan) { + test.passed = allFunctionsPassed && allSuitesPassed; + test.status = test.passed ? TestStatus.Idle : TestStatus.Error; + } else { + test.passed = null; + test.status = TestStatus.Unknown; + } + } +} diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 9dafac503658..7ad40c9b4361 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -2,33 +2,11 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri, workspace } from 'vscode'; import * as constants from '../../common/constants'; -import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestsToRun, TestSuite } from './contracts'; -import { ITestVisitor } from './testUtils'; +import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { FlattenedTestFunction, FlattenedTestSuite, ITestResultsService, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestsToRun, TestSuite } from './types'; +import { ITestCollectionStorageService, ITestsHelper, ITestVisitor } from './types'; -export interface ITestCollectionStorageService { - getTests(wkspace: Uri): Tests | undefined; - storeTests(wkspace: Uri, tests: Tests | null | undefined): void; -} - -export class TestCollectionStorageService implements ITestCollectionStorageService { - private testsIndexedByWorkspaceUri = new Map(); - public getTests(wkspace: Uri): Tests | undefined { - const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; - return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; - } - public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { - const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; - this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); - } -} - -function getWorkspaceFolderPath(resource?: Uri): string | undefined { - if (!resource) { - return undefined; - } - const folder = workspace.getWorkspaceFolder(resource); - return folder ? folder.uri.path : undefined; -} export async function selectTestWorkspace(): Promise { if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { return undefined; @@ -50,26 +28,6 @@ export function displayTestErrorMessage(message: string) { } -export function parseTestName(name: string, rootDirectory: string, testCollectionStorage: ITestCollectionStorageService): TestsToRun { - // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. - // use to identify a file given the full file name, similary for a folder and function - // Perhaps something like a parser or methods like TestFunction.fromString()... something) - const workspaceUri = workspace.getWorkspaceFolder(Uri.file(rootDirectory)).uri; - const tests = testCollectionStorage.getTests(workspaceUri); - if (!tests) { return null; } - const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); - if (testFolders.length > 0) { return { testFolder: testFolders }; } - - const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); - if (testFiles.length > 0) { return { testFile: testFiles }; } - - const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); - if (testFns.length > 0) { return { testFunction: testFns }; } - - // Just return this as a test file - return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; -} 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)); @@ -80,270 +38,6 @@ export function convertFileToPackage(filePath: string): string { return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); } -export interface ITestResultService { - resetResults(tests: Tests): void; - updateResults(tests: Tests): void; - updateTestSuiteResults(test: TestSuite): void; - updateTestFileResults(test: TestFile): void; - updateTestFolderResults(testFolder: TestFolder): void; -} - -export class TestResultService implements ITestResultService { - public resetResults(tests: Tests): void { - tests.testFolders.forEach(f => { - f.functionsDidNotRun = 0; - f.functionsFailed = 0; - f.functionsPassed = 0; - f.passed = null; - f.status = TestStatus.Unknown; - }); - tests.testFunctions.forEach(fn => { - fn.testFunction.passed = null; - fn.testFunction.time = 0; - fn.testFunction.message = ''; - fn.testFunction.traceback = ''; - fn.testFunction.status = TestStatus.Unknown; - fn.testFunction.functionsFailed = 0; - fn.testFunction.functionsPassed = 0; - fn.testFunction.functionsDidNotRun = 0; - }); - tests.testSuits.forEach(suite => { - suite.testSuite.passed = null; - suite.testSuite.time = 0; - suite.testSuite.status = TestStatus.Unknown; - suite.testSuite.functionsFailed = 0; - suite.testSuite.functionsPassed = 0; - suite.testSuite.functionsDidNotRun = 0; - }); - tests.testFiles.forEach(testFile => { - testFile.passed = null; - testFile.time = 0; - testFile.status = TestStatus.Unknown; - testFile.functionsFailed = 0; - testFile.functionsPassed = 0; - testFile.functionsDidNotRun = 0; - }); - } - public updateResults(tests: Tests): void { - tests.testFiles.forEach(test => this.updateTestFileResults(test)); - tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); - } - public updateTestSuiteResults(test: TestSuite): void { - this.updateTestSuiteAndFileResults(test); - } - public updateTestFileResults(test: TestFile): void { - this.updateTestSuiteAndFileResults(test); - } - public updateTestFolderResults(testFolder: TestFolder): void { - let allFilesPassed = true; - let allFilesRan = true; - - testFolder.testFiles.forEach(fl => { - if (allFilesPassed && typeof fl.passed === 'boolean') { - if (!fl.passed) { - allFilesPassed = false; - } - } else { - allFilesRan = false; - } - - testFolder.functionsFailed += fl.functionsFailed; - testFolder.functionsPassed += fl.functionsPassed; - }); - - let allFoldersPassed = true; - let allFoldersRan = true; - - testFolder.folders.forEach(folder => { - this.updateTestFolderResults(folder); - if (allFoldersPassed && typeof folder.passed === 'boolean') { - if (!folder.passed) { - allFoldersPassed = false; - } - } else { - allFoldersRan = false; - } - - testFolder.functionsFailed += folder.functionsFailed; - testFolder.functionsPassed += folder.functionsPassed; - }); - - if (allFilesRan && allFoldersRan) { - testFolder.passed = allFilesPassed && allFoldersPassed; - testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; - } else { - testFolder.passed = null; - testFolder.status = TestStatus.Unknown; - } - } - private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { - let totalTime = 0; - let allFunctionsPassed = true; - let allFunctionsRan = true; - - test.functions.forEach(fn => { - totalTime += fn.time; - if (typeof fn.passed === 'boolean') { - if (fn.passed) { - test.functionsPassed += 1; - } else { - test.functionsFailed += 1; - allFunctionsPassed = false; - } - } else { - allFunctionsRan = false; - } - }); - - let allSuitesPassed = true; - let allSuitesRan = true; - - test.suites.forEach(suite => { - this.updateTestSuiteResults(suite); - totalTime += suite.time; - if (allSuitesRan && typeof suite.passed === 'boolean') { - if (!suite.passed) { - allSuitesPassed = false; - } - } else { - allSuitesRan = false; - } - - test.functionsFailed += suite.functionsFailed; - test.functionsPassed += suite.functionsPassed; - }); - - test.time = totalTime; - if (allSuitesRan && allFunctionsRan) { - test.passed = allFunctionsPassed && allSuitesPassed; - test.status = test.passed ? TestStatus.Idle : TestStatus.Error; - } else { - test.passed = null; - test.status = TestStatus.Unknown; - } - } -} -export function updateResults(tests: Tests) { - new TestResultService().updateResults(tests); -} - -export interface ITestsHelper { - flattenTestFiles(testFiles: TestFile[]): Tests; - placeTestFilesIntoFolders(tests: Tests): void; -} - -export interface ITestVisitor { - visitTestFunction(testFunction: TestFunction): void; - visitTestSuite(testSuite: TestSuite): void; - visitTestFile(testFile: TestFile): void; -} - -export class TestFlatteningVisitor implements ITestVisitor { - // tslint:disable-next-line:variable-name - private _flattedTestFunctions = new Map(); - // tslint:disable-next-line:variable-name - private _flattenedTestSuites = new Map(); - public get flattenedTestFunctions(): Readonly { - return [...this._flattedTestFunctions.values()]; - } - public get flattenedTestSuites(): Readonly { - return [...this._flattenedTestSuites.values()]; - } - // tslint:disable-next-line:no-empty - public visitTestFunction(testFunction: TestFunction): void { } - // tslint:disable-next-line:no-empty - public visitTestSuite(testSuite: TestSuite): void { } - public visitTestFile(testFile: TestFile): void { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); - testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); - } - private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { - testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); - testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); - this.addTestSuite(testSuite, parentTestFile); - } - private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { - const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; - if (this._flattenedTestSuites.has(key)) { - return; - } - const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; - this._flattedTestFunctions.set(key, flattenedFunction); - } - private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { - const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; - if (this._flattenedTestSuites.has(key)) { - return; - } - const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; - this._flattenedTestSuites.set(key, flattenedSuite); - } - private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { - const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; - if (this._flattedTestFunctions.has(key)) { - return; - } - const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; - this._flattedTestFunctions.set(key, flattendFunction); - } -} - -// tslint:disable-next-line:max-classes-per-file -export class TestFolderGenerationVisitor implements ITestVisitor { - // tslint:disable-next-line:variable-name - private _testFolders: TestFolder[] = []; - // tslint:disable-next-line:variable-name - private _rootTestFolders: TestFolder[] = []; - private folderMap = new Map(); - public get testFolders(): Readonly { - return [...this._testFolders]; - } - public get rootTestFolders(): Readonly { - return [...this._rootTestFolders]; - } - // tslint:disable-next-line:no-empty - public visitTestFunction(testFunction: TestFunction): void { } - // tslint:disable-next-line:no-empty - public visitTestSuite(testSuite: TestSuite): void { } - public visitTestFile(testFile: TestFile): void { - // First get all the unique folders - const folders: string[] = []; - const dir = path.dirname(testFile.name); - if (this.folderMap.has(dir)) { - const folder = this.folderMap.get(dir); - folder.testFiles.push(testFile); - return; - } - - dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { - let newPath = currentName; - let parentFolder: TestFolder; - if (accumulatedPath.length > 0) { - parentFolder = this.folderMap.get(accumulatedPath); - newPath = path.join(accumulatedPath, currentName); - } - if (!this.folderMap.has(newPath)) { - const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - this.folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder.folders.push(testFolder); - } else { - this._rootTestFolders.push(testFolder); - } - this._testFolders.push(testFolder); - } - return newPath; - }, ''); - - // tslint:disable-next-line:no-non-null-assertion - this.folderMap.get(dir)!.testFiles.push(testFile); - } -} - -// tslint:disable-next-line:max-classes-per-file export class TestsHelper implements ITestsHelper { public flattenTestFiles(testFiles: TestFile[]): Tests { const flatteningVisitor = new TestFlatteningVisitor(); @@ -401,12 +95,22 @@ export class TestsHelper implements ITestsHelper { }, ''); }); } -} + public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun { + // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. + // use to identify a file given the full file name, similary for a folder and function + // Perhaps something like a parser or methods like TestFunction.fromString()... something) + if (!tests) { return null; } + const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); + const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); + if (testFolders.length > 0) { return { testFolder: testFolders }; } -export function flattenTestFiles(testFiles: TestFile[]): Tests { - return new TestsHelper().flattenTestFiles(testFiles); -} + const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); + if (testFiles.length > 0) { return { testFile: testFiles }; } -export function resetTestResults(tests: Tests) { - new TestResultService().resetResults(tests); + const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); + if (testFns.length > 0) { return { testFunction: testFns }; } + + // Just return this as a test file + return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; + } } diff --git a/src/client/unittests/common/testVisitors/flatteningVisitor.ts b/src/client/unittests/common/testVisitors/flatteningVisitor.ts new file mode 100644 index 000000000000..a6e36175b9a6 --- /dev/null +++ b/src/client/unittests/common/testVisitors/flatteningVisitor.ts @@ -0,0 +1,65 @@ +import { convertFileToPackage } from '../testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + TestSuite +} from '../types'; + +export class TestFlatteningVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _flattedTestFunctions = new Map(); + // tslint:disable-next-line:variable-name + private _flattenedTestSuites = new Map(); + public get flattenedTestFunctions(): Readonly { + return [...this._flattedTestFunctions.values()]; + } + public get flattenedTestSuites(): Readonly { + return [...this._flattenedTestSuites.values()]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); + testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } + private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { + testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); + testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); + this.addTestSuite(testSuite, parentTestFile); + } + private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { + const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; + this._flattedTestFunctions.set(key, flattenedFunction); + } + private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { + const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; + this._flattenedTestSuites.set(key, flattenedSuite); + } + private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { + const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; + if (this._flattedTestFunctions.has(key)) { + return; + } + const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; + this._flattedTestFunctions.set(key, flattendFunction); + } +} diff --git a/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts new file mode 100644 index 000000000000..46956873edd1 --- /dev/null +++ b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts @@ -0,0 +1,55 @@ +import * as path from 'path'; +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestSuite } from '../types'; + +export class TestFolderGenerationVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _testFolders: TestFolder[] = []; + // tslint:disable-next-line:variable-name + private _rootTestFolders: TestFolder[] = []; + private folderMap = new Map(); + public get testFolders(): Readonly { + return [...this._testFolders]; + } + public get rootTestFolders(): Readonly { + return [...this._rootTestFolders]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // First get all the unique folders + const folders: string[] = []; + const dir = path.dirname(testFile.name); + if (this.folderMap.has(dir)) { + const folder = this.folderMap.get(dir); + folder.testFiles.push(testFile); + return; + } + + dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (accumulatedPath.length > 0) { + parentFolder = this.folderMap.get(accumulatedPath); + newPath = path.join(accumulatedPath, currentName); + } + if (!this.folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + this.folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + this._rootTestFolders.push(testFolder); + } + this._testFolders.push(testFolder); + } + return newPath; + }, ''); + + // tslint:disable-next-line:no-non-null-assertion + this.folderMap.get(dir)!.testFiles.push(testFile); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } +} diff --git a/src/client/unittests/common/testVisitors/resultResetVisitor.ts b/src/client/unittests/common/testVisitors/resultResetVisitor.ts new file mode 100644 index 000000000000..0d58c1076b04 --- /dev/null +++ b/src/client/unittests/common/testVisitors/resultResetVisitor.ts @@ -0,0 +1,37 @@ +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestStatus, TestSuite } from '../types'; + +export class TestResultResetVisitor implements ITestVisitor { + public visitTestFunction(testFunction: TestFunction): void { + testFunction.passed = null; + testFunction.time = 0; + testFunction.message = ''; + testFunction.traceback = ''; + testFunction.status = TestStatus.Unknown; + testFunction.functionsFailed = 0; + testFunction.functionsPassed = 0; + testFunction.functionsDidNotRun = 0; + } + public visitTestSuite(testSuite: TestSuite): void { + testSuite.passed = null; + testSuite.time = 0; + testSuite.status = TestStatus.Unknown; + testSuite.functionsFailed = 0; + testSuite.functionsPassed = 0; + testSuite.functionsDidNotRun = 0; + } + public visitTestFile(testFile: TestFile): void { + testFile.passed = null; + testFile.time = 0; + testFile.status = TestStatus.Unknown; + testFile.functionsFailed = 0; + testFile.functionsPassed = 0; + testFile.functionsDidNotRun = 0; + } + public visitTestFolder(testFolder: TestFolder) { + testFolder.functionsDidNotRun = 0; + testFolder.functionsFailed = 0; + testFolder.functionsPassed = 0; + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } +} diff --git a/src/client/unittests/common/contracts.ts b/src/client/unittests/common/types.ts similarity index 77% rename from src/client/unittests/common/contracts.ts rename to src/client/unittests/common/types.ts index 32945c99d297..9cb84cef8d29 100644 --- a/src/client/unittests/common/contracts.ts +++ b/src/client/unittests/common/types.ts @@ -2,8 +2,6 @@ import { Disposable, Uri } from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from './baseTestManager'; -export const CANCELLATION_REASON = 'cancelled_user_request'; - export type TestFolder = TestResult & { name: string; testFiles: TestFile[]; @@ -116,11 +114,35 @@ export interface ITestManagerService extends Disposable { getTestWorkingDirectory(): string; getPreferredTestManager(): UnitTestProduct; } + export interface ITestManagerServiceFactory { createTestManagerService(wkspace: Uri): ITestManagerService; } + export interface IWorkspaceTestManagerService extends Disposable { - getTestManager(wkspace: Uri): BaseTestManager | undefined; - getTestWorkingDirectory(wkspace: Uri): string; - getPreferredTestManager(wkspace: Uri): UnitTestProduct; + getTestManager(resource: Uri): BaseTestManager | undefined; + getTestWorkingDirectory(resource: Uri): string; + getPreferredTestManager(resource: Uri): UnitTestProduct; +} + +export interface ITestsHelper { + flattenTestFiles(testFiles: TestFile[]): Tests; + placeTestFilesIntoFolders(tests: Tests): void; +} + +export interface ITestVisitor { + visitTestFunction(testFunction: TestFunction): void; + visitTestSuite(testSuite: TestSuite): void; + visitTestFile(testFile: TestFile): void; + visitTestFolder(testFile: TestFolder): void; +} + +export interface ITestCollectionStorageService extends Disposable { + getTests(wkspace: Uri): Tests | undefined; + storeTests(wkspace: Uri, tests: Tests | null | undefined): void; +} + +export interface ITestResultsService { + resetResults(tests: Tests): void; + updateResults(tests: Tests): void; } diff --git a/src/client/unittests/common/workspaceTestManagerService.ts b/src/client/unittests/common/workspaceTestManagerService.ts index 17178d7d3684..e9ff0b9cb9db 100644 --- a/src/client/unittests/common/workspaceTestManagerService.ts +++ b/src/client/unittests/common/workspaceTestManagerService.ts @@ -5,8 +5,8 @@ import { TestManager as NoseTestManager } from '../nosetest/main'; import { TestManager as PyTestTestManager } from '../pytest/main'; import { TestManager as UnitTestTestManager } from '../unittest/main'; import { BaseTestManager } from './baseTestManager'; -import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './contracts'; import { TestManagerService } from './testManagerService'; +import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './types'; type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; @@ -21,18 +21,38 @@ export class WorkspaceTestManagerService implements IWorkspaceTestManagerService public dispose() { this.workspaceTestManagers.forEach(info => info.dispose()); } - public getTestManager(wkspace: Uri): BaseTestManager | undefined { + public getTestManager(resource: Uri): BaseTestManager | undefined { + const wkspace = this.getWorkspace(resource); this.ensureTestManagerService(wkspace); return this.workspaceTestManagers.get(wkspace.fsPath).getTestManager(); } - public getTestWorkingDirectory(wkspace: Uri) { + public getTestWorkingDirectory(resource: Uri) { + const wkspace = this.getWorkspace(resource); this.ensureTestManagerService(wkspace); return this.workspaceTestManagers.get(wkspace.fsPath).getTestWorkingDirectory(); } - public getPreferredTestManager(wkspace: Uri): UnitTestProduct { + public getPreferredTestManager(resource: Uri): UnitTestProduct { + const wkspace = this.getWorkspace(resource); this.ensureTestManagerService(wkspace); return this.workspaceTestManagers.get(wkspace.fsPath).getPreferredTestManager(); } + private getWorkspace(resource: Uri): Uri { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + const noWkspaceMessage = 'Please open a workspace'; + this.outChannel.appendLine(noWkspaceMessage); + throw new Error(noWkspaceMessage); + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const message = `Resource '${resource.fsPath}' does not belong to any workspace`; + this.outChannel.appendLine(message); + throw new Error(message); + } private ensureTestManagerService(wkspace: Uri) { if (!this.workspaceTestManagers.has(wkspace.fsPath)) { this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory.createTestManagerService(wkspace)); diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts index df26a46d0201..6318060a94f7 100644 --- a/src/client/unittests/common/xUnitParser.ts +++ b/src/client/unittests/common/xUnitParser.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as xml2js from 'xml2js'; -import { Tests, TestStatus } from './contracts'; +import { Tests, TestStatus } from './types'; export enum PassCalculationFormulae { pytest, diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 1fa5ad31c836..04c6568a77c7 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,21 +1,21 @@ 'use strict'; import * as path from 'path'; -import { OutputChannel, Uri } from 'vscode'; import * as vscode from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { Installer, Product } from '../common/installer'; import { getSubDirectories } from '../common/utils'; import { TestConfigSettingsService } from './common/configSettingService'; -import { UnitTestProduct } from './common/contracts'; import { TestConfigurationManager } from './common/testConfigurationManager'; import { selectTestWorkspace } from './common/testUtils'; +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'; // tslint:disable-next-line:no-any -async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { +async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { const wkspace = await selectTestWorkspace(); if (!wkspace) { return; diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts index 3d12da43b2bd..8f2468c3fafa 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -2,8 +2,9 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; import { createDeferred, isNotInstalledError } from '../../common/helpers'; -import { CANCELLATION_REASON, Tests } from '../common/contracts'; +import { CANCELLATION_REASON } from '../common/constants'; import { displayTestErrorMessage } from '../common/testUtils'; +import { Tests } from '../common/types'; export class TestResultDisplay { private statusBar: vscode.StatusBarItem; diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index e9f24ec5a451..61c18840ae51 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,23 +1,22 @@ import * as path from 'path'; -import { QuickPickItem, window } from 'vscode'; +import { QuickPickItem, Uri, window } from 'vscode'; import * as vscode from 'vscode'; import * as constants from '../../common/constants'; -import { FlattenedTestFunction, TestFile, TestFunction, Tests, TestStatus } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; export class TestDisplay { constructor(private testCollectionStorage: ITestCollectionStorageService) { } - public displayStopTestUI(message: string) { + public displayStopTestUI(workspace: Uri, message: string) { window.showQuickPick([message]).then(item => { if (item === message) { - vscode.commands.executeCommand(constants.Commands.Tests_Stop); + vscode.commands.executeCommand(constants.Commands.Tests_Stop, workspace); } }); } - public displayTestUI(rootDirectory: string) { - const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDirectory)).uri; + public displayTestUI(wkspace: Uri) { const tests = this.testCollectionStorage.getTests(wkspace); - window.showQuickPick(buildItems(rootDirectory, tests), { matchOnDescription: true, matchOnDetail: true }).then(onItemSelected); + window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then(item => onItemSelected(wkspace, item, false)); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { @@ -41,13 +40,13 @@ export class TestDisplay { }, reject); }); } - public displayFunctionTestPickerUI(rootDirectory: string, fileName: string, testFunctions: TestFunction[], debug?: boolean) { - const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDirectory)).uri; + public displayFunctionTestPickerUI(wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { const tests = this.testCollectionStorage.getTests(wkspace); if (!tests) { return; } - const testFile = tests.testFiles.find(file => file.name === fileName || file.fullPath === fileName); + const fileName = file.fsPath; + const testFile = tests.testFiles.find(item => item.name === fileName || item.fullPath === fileName); if (!testFile) { return; } @@ -58,7 +57,7 @@ export class TestDisplay { window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(testItem, debug); + return onItemSelected(wkspace, testItem, debug); }); } } @@ -112,7 +111,7 @@ function getSummary(tests?: Tests) { } return statusText.join(', ').trim(); } -function buildItems(rootDirectory: string, tests?: Tests): TestItem[] { +function buildItems(tests?: Tests): TestItem[] { const items: TestItem[] = []; items.push({ description: '', label: 'Run All Unit Tests', type: Type.RunAll }); items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); @@ -188,12 +187,13 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T }); return fileItems; } -function onItemSelected(selection: TestItem, debug?: boolean) { +function onItemSelected(wkspace: Uri, selection: TestItem, debug?: boolean) { if (!selection || typeof selection.type !== 'number') { return; } let cmd = ''; - const args = []; + // tslint:disable-next-line:no-any + const args: any[] = [wkspace]; switch (selection.type) { case Type.Null: { return; @@ -220,12 +220,14 @@ function onItemSelected(selection: TestItem, debug?: boolean) { } case Type.RunMethod: { cmd = constants.Commands.Tests_Run; - args.push(selection.fn); + // tslint:disable-next-line:prefer-type-cast + args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); break; } case Type.DebugMethod: { cmd = constants.Commands.Tests_Debug; - args.push(selection.fn); + // tslint:disable-next-line:prefer-type-cast + 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 248882393ffe..69c4b017cec0 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,19 +1,18 @@ 'use strict'; import * as vscode from 'vscode'; +import { Uri, window, workspace } from 'vscode'; import { IUnitTestSettings, PythonSettings } from '../common/configSettings'; import * as constants from '../common/constants'; import { PythonSymbolProvider } from '../providers/symbolProvider'; import { activateCodeLenses } from './codeLenses/main'; import { BaseTestManager } from './common/baseTestManager'; -import { - CANCELLATION_REASON, - FlattenedTestFunction, - TestFile, - TestFunction, - TestStatus, - TestsToRun, -} from './common/contracts'; -import { getDiscoveredTests, parseTestName } from './common/testUtils'; +import { CANCELLATION_REASON } from './common/constants'; +import { TestCollectionStorageService } from './common/storageService'; +import { TestManagerServiceFactory } from './common/testManagerServiceFactory'; +import { TestResultsService } from './common/testResultsService'; +import { selectTestWorkspace, TestsHelper } from './common/testUtils'; +import { FlattenedTestFunction, ITestCollectionStorageService, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; +import { WorkspaceTestManagerService } from './common/workspaceTestManagerService'; import { displayTestFrameworkError } from './configuration'; import { TestResultDisplay } from './display/main'; import { TestDisplay } from './display/picker'; @@ -21,51 +20,58 @@ import * as nosetests from './nosetest/main'; import * as pytest from './pytest/main'; import * as unittest from './unittest/main'; -let testManager: BaseTestManager | undefined | null; -let pyTestManager: pytest.TestManager | undefined | null; -let unittestManager: unittest.TestManager | undefined | null; -let nosetestManager: nosetests.TestManager | undefined | null; +let workspaceTestManagerService: IWorkspaceTestManagerService; let testResultDisplay: TestResultDisplay; let testDisplay: TestDisplay; let outChannel: vscode.OutputChannel; const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); +let testCollectionStorage: ITestCollectionStorageService; export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider) { - // TODO: Add multi workspace support - const settings = PythonSettings.getInstance(); - uniTestSettingsString = JSON.stringify(settings.unitTest); context.subscriptions.push({ dispose: dispose }); outChannel = outputChannel; const disposables = registerCommands(); context.subscriptions.push(...disposables); - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - // Ignore the exceptions returned - // This function is invoked via a command which will be invoked else where in the extension - discoverTests(true).catch(() => { - // Ignore the errors - }); - } + testCollectionStorage = new TestCollectionStorageService(); + const testResultsService = new TestResultsService(); + const testsHelper = new TestsHelper(); + const testManagerServiceFactory = new TestManagerServiceFactory(outChannel, testCollectionStorage, testResultsService, testsHelper); + workspaceTestManagerService = new WorkspaceTestManagerService(outChannel, testManagerServiceFactory); - settings.addListener('change', onConfigChanged); - context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider)); + context.subscriptions.push(autoResetTests()); + context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider, testCollectionStorage)); context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(onDocumentSaved)); -} -function getTestWorkingDirectory() { - // TODO: Add multi workspace support - const settings = PythonSettings.getInstance(); - return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : vscode.workspace.rootPath!; + autoDiscoverTests(); } +async function getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { + let wkspace: Uri; + 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); + } +} let timeoutId: number; async function onDocumentSaved(doc: vscode.TextDocument): Promise { - let testManager = getTestRunner(); + const testManager = await getTestManager(false, doc.uri); if (!testManager) { return; } - - let tests = await testManager.discoverTests(false, true); + const tests = await testManager.discoverTests(false, true); if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { return; } @@ -76,125 +82,157 @@ async function onDocumentSaved(doc: vscode.TextDocument): Promise { if (timeoutId) { clearTimeout(timeoutId); } - timeoutId = setTimeout(() => { discoverTests(true); }, 1000); + timeoutId = setTimeout(() => discoverTests(doc.uri, true), 1000); } function dispose() { - if (pyTestManager) { - pyTestManager.dispose(); - } - if (nosetestManager) { - nosetestManager.dispose(); - } - if (unittestManager) { - unittestManager.dispose(); - } + workspaceTestManagerService.dispose(); + testCollectionStorage.dispose(); } function registerCommands(): vscode.Disposable[] { const disposables = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, () => { - // Ignore the exceptions returned - // This command will be invoked else where in the extension - discoverTests(true).catch(() => { return null; }); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked else where in the extension. + // tslint:disable-next-line:no-empty + discoverTests(resource, true).catch(() => { }); })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (testId) => runTestsImpl(testId))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (testId) => runTestsImpl(testId, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(undefined, undefined, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (file: Uri, testToRun?: TestsToRun) => runTestsImpl(file, testToRun))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (file: Uri, testToRun: TestsToRun) => runTestsImpl(file, testToRun, false, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file, testFunctions) => displayPickerUI(file, testFunctions))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file: Uri, testFunctions: TestFunction[]) => displayPickerUI(file, testFunctions))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (file, testFunctions) => displayPickerUI(file, testFunctions, true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, () => stopTests())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (resource: Uri) => stopTests(resource))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, () => outChannel.show())); 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, () => selectAndRunTestMethod())); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, () => selectAndRunTestMethod(true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, () => selectAndRunTestFile())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, () => runCurrentTestFile())); return disposables; } -function displayUI() { - let testManager = getTestRunner(); +async function displayUI() { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayTestUI(getTestWorkingDirectory()); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayTestUI(testManager.workspace); } -function displayPickerUI(file: string, testFunctions: TestFunction[], debug?: boolean) { - let testManager = getTestRunner(); +async function displayPickerUI(file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await getTestManager(true, file); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayFunctionTestPickerUI(getTestWorkingDirectory(), file, testFunctions, debug); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayFunctionTestPickerUI(testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); } -function selectAndRunTestMethod(debug?: boolean) { - let testManager = getTestRunner(); +async function selectAndRunTestMethod(debug?: boolean) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFunction(getTestWorkingDirectory(), tests).then(testFn => { - runTestsImpl(testFn, debug); - }).catch(() => { }); - }); + return; + } + try { + await testManager.discoverTests(true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspace.fsPath, tests); + if (!selectedTestFn) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, debug); } -function selectAndRunTestFile() { - let testManager = getTestRunner(); +async function selectAndRunTestFile() { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFile(getTestWorkingDirectory(), tests).then(testFile => { - runTestsImpl({ testFile: [testFile] }); - }).catch(() => { }); - }); + return; + } + try { + await testManager.discoverTests(true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedFile = await testDisplay.selectTestFile(testManager.workspace.fsPath, tests); + if (!selectedFile) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFile: [selectedFile] } as TestsToRun); } -function runCurrentTestFile() { +async function runCurrentTestFile() { if (!vscode.window.activeTextEditor) { return; } - const currentFilePath = vscode.window.activeTextEditor.document.fileName; - let testManager = getTestRunner(); + const testManager = await getTestManager(true, window.activeTextEditor.document.uri); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - const testFiles = tests.testFiles.filter(testFile => { - return testFile.fullPath === currentFilePath; - }); - if (testFiles.length < 1) { - return; - } - runTestsImpl({ testFile: [testFiles[0]] }); + try { + await testManager.discoverTests(true, true); + } catch (ex) { + return; + } + const tests = testCollectionStorage.getTests(testManager.workspace); + 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 + await runTestsImpl(testManager.workspace, { testFile: [testFiles[0]] } as TestsToRun); } -function displayStopUI(message: string) { - let testManager = getTestRunner(); +async function displayStopUI(message: string) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayStopTestUI(message); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayStopTestUI(testManager.workspace, message); } + let uniTestSettingsString: string; +function autoResetTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; + } + const settings = PythonSettings.getInstance(); + uniTestSettingsString = JSON.stringify(settings.unitTest); + return workspace.onDidChangeConfiguration(() => setTimeout(onConfigChanged, 1000)); +} function onConfigChanged() { - // TODO: Add multi workspace support + // 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 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) + + // 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; @@ -205,71 +243,47 @@ function onConfigChanged() { if (testResultDisplay) { testResultDisplay.enabled = false; } - - if (testManager) { - testManager.stop(); - testManager = null; - } - if (pyTestManager) { - pyTestManager.dispose(); - pyTestManager = null; - } - if (nosetestManager) { - nosetestManager.dispose(); - nosetestManager = null; - } - if (unittestManager) { - unittestManager.dispose(); - unittestManager = null; - } + workspaceTestManagerService.dispose(); return; } - if (testResultDisplay) { testResultDisplay.enabled = true; } - - // No need to display errors - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - discoverTests(true); - } + autoDiscoverTests(); } -function getTestRunner() { - const rootDirectory = getTestWorkingDirectory(); - const settings = PythonSettings.getInstance(vscode.Uri.file(rootDirectory)); - if (settings.unitTest.nosetestsEnabled) { - return nosetestManager = nosetestManager ? nosetestManager : new nosetests.TestManager(rootDirectory, outChannel); - } - else if (settings.unitTest.pyTestEnabled) { - return pyTestManager = pyTestManager ? pyTestManager : new pytest.TestManager(rootDirectory, outChannel); +function autoDiscoverTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; } - else if (settings.unitTest.unittestEnabled) { - return unittestManager = unittestManager ? unittestManager : new unittest.TestManager(rootDirectory, outChannel); + const settings = PythonSettings.getInstance(); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + return; } - return null; -} -function stopTests() { - let testManager = getTestRunner(); + // No need to display errors. + // tslint:disable-next-line:no-empty + discoverTests(workspace.workspaceFolders[0].uri, true).catch(() => { }); +} +async function stopTests(resource: Uri) { + const testManager = await getTestManager(true, resource); if (testManager) { testManager.stop(); } } -function discoverTests(ignoreCache?: boolean) { - let testManager = getTestRunner(); +async function discoverTests(resource?: Uri, ignoreCache?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { - displayTestFrameworkError(outChannel); - return Promise.resolve(null); + return; } if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - return testResultDisplay.DisplayDiscoverStatus(testManager.discoverTests(ignoreCache)); - } - else { - return Promise.resolve(null); + const discoveryPromise = testManager.discoverTests(ignoreCache); + testResultDisplay.displayDiscoverStatus(discoveryPromise); + await discoveryPromise; } } +// tslint:disable-next-line:no-any function isTestsToRun(arg: any): arg is TestsToRun { if (arg && arg.testFunction && Array.isArray(arg.testFunction)) { return true; @@ -282,46 +296,21 @@ function isTestsToRun(arg: any): arg is TestsToRun { } return false; } -function isUri(arg: any): arg is vscode.Uri { - return arg && arg.fsPath && typeof arg.fsPath === 'string'; -} -function isFlattenedTestFunction(arg: any): arg is FlattenedTestFunction { - return arg && arg.testFunction && typeof arg.xmlClassName === 'string' && - arg.parentTestFile && typeof arg.testFunction.name === 'string'; -} -function identifyTestType(rootDirectory: string, arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction): TestsToRun | boolean | null | undefined { - if (typeof arg === 'boolean') { - return arg === true; - } - if (isTestsToRun(arg)) { - return arg; - } - if (isFlattenedTestFunction(arg)) { - return { testFunction: [arg.testFunction] }; - } - if (isUri(arg)) { - return parseTestName(arg.fsPath, rootDirectory); - } - return null; -} -function runTestsImpl(arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction, debug: boolean = false) { - let testManager = getTestRunner(); +async function runTestsImpl(resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { + const testManager = await getTestManager(true, resource); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - // lastRanTests = testsToRun; - const runInfo = identifyTestType(getTestWorkingDirectory(), arg); - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); + const promise = testManager.runTest(testsToRun, runFailedTests, debug) + .catch(reason => { + if (reason !== CANCELLATION_REASON) { + outChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); - const ret = typeof runInfo === 'boolean' ? testManager.runTest(runInfo, debug) : testManager.runTest(runInfo as TestsToRun, debug); - let runPromise = ret.catch(reason => { - if (reason !== CANCELLATION_REASON) { - outChannel.appendLine('Error: ' + reason); - } - return Promise.reject(reason); - }); - - testResultDisplay.DisplayProgressStatus(runPromise, debug); + testResultDisplay.displayProgressStatus(promise, debug); + await promise; } diff --git a/src/client/unittests/nosetest/collector.ts b/src/client/unittests/nosetest/collector.ts index 7c60cb8d3830..3a202654e6fd 100644 --- a/src/client/unittests/nosetest/collector.ts +++ b/src/client/unittests/nosetest/collector.ts @@ -4,8 +4,8 @@ import * as path from 'path'; import { CancellationToken } from 'vscode'; import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { TestFile, TestFunction, Tests, TestSuite } from '../common/contracts'; -import { convertFileToPackage, extractBetweenDelimiters, flattenTestFiles } from '../common/testUtils'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; import { execPythonFile } from './../../common/utils'; const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; @@ -20,7 +20,7 @@ const argsToExcludeForDiscovery = ['-v', '--verbose', '--failed', '--process-restartworker', '--with-xunit']; const settingsInArgsToExcludeForDiscovery = ['--verbosity']; -export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; let testFiles: TestFile[] = []; @@ -83,7 +83,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc // Exclude tests that don't have any functions or test suites testFiles = testFiles.filter(testFile => testFile.suites.length > 0 || testFile.functions.length > 0); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index fddf3a8be496..ab5c329282f6 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -4,18 +4,19 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestsToRun } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { - super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.nosetestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } // tslint:disable-next-line:no-any public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { @@ -26,6 +27,6 @@ export class TestManager extends BaseTestManager { if (!runFailedTests && args.indexOf('--with-id') === -1) { args.push('--with-id'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index 5717fa75b8f2..69dc425f6b96 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -3,17 +3,16 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { Tests, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { updateResults } from '../common/testUtils'; +import { ITestResultsService, Tests, TestsToRun } from '../common/types'; import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; // tslint:disable-next-line:no-any -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -70,7 +69,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel); } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -81,9 +80,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } // tslint:disable-next-line:no-any -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); } diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index ff6738f124ff..436c6fac8bd8 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; -import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { constructor(workspace: Uri, outputChannel: vscode.OutputChannel, diff --git a/src/client/unittests/pytest/collector.ts b/src/client/unittests/pytest/collector.ts index d699a2575259..4507f1fdb64c 100644 --- a/src/client/unittests/pytest/collector.ts +++ b/src/client/unittests/pytest/collector.ts @@ -4,8 +4,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { TestFile, TestFunction, Tests, TestSuite } from '../common/contracts'; -import { convertFileToPackage, extractBetweenDelimiters, flattenTestFiles } from '../common/testUtils'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; import { execPythonFile } from './../../common/utils'; const argsToExcludeForDiscovery = ['-x', '--exitfirst', @@ -16,7 +16,7 @@ const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--disable-pytest-warnings', '-l', '--showlocals']; const settingsInArgsToExcludeForDiscovery = []; -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; const testFiles: TestFile[] = []; const parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; @@ -90,7 +90,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (token && token.isCancellationRequested) { return Promise.reject('cancelled'); } - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 51835007a3fd..f463765fe441 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -2,24 +2,25 @@ import * as vscode from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestsToRun } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { - super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.pyTestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.pyTestArgs.slice(0); if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index 37998eed9f86..f8c48d08c9ef 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -3,13 +3,12 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { Tests, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { updateResults } from '../common/testUtils'; +import { ITestResultsService, Tests, TestsToRun } from '../common/types'; import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -45,7 +44,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel); } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -55,9 +54,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); } diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index a1a0d3b898d5..addda7dd802e 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; -import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { constructor(workspace: Uri, outputChannel: vscode.OutputChannel, diff --git a/src/client/unittests/unittest/collector.ts b/src/client/unittests/unittest/collector.ts index 73e724db87e8..8180641c009e 100644 --- a/src/client/unittests/unittest/collector.ts +++ b/src/client/unittests/unittest/collector.ts @@ -3,11 +3,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { TestFile, Tests, TestStatus } from '../common/contracts'; -import { flattenTestFiles } from '../common/testUtils'; +import { ITestsHelper, TestFile, Tests, TestStatus } from '../common/types'; import { execPythonFile } from './../../common/utils'; -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let startDirectory = '.'; let pattern = 'test*.py'; const indexOfStartDir = args.findIndex(arg => arg.indexOf('-s') === 0); @@ -81,17 +80,17 @@ for suite in suites._tests: if (startDirectory.length > 1) { testsDirectory = path.isAbsolute(startDirectory) ? startDirectory : path.resolve(rootDirectory, startDirectory); } - return parseTestIds(testsDirectory, testItems); + return parseTestIds(testsDirectory, testItems, testsHelper); }); } -function parseTestIds(rootDirectory: string, testIds: string[]): Tests { +function parseTestIds(rootDirectory: string, testIds: string[], testsHelper: ITestsHelper): Tests { const testFiles: TestFile[] = []; testIds.forEach(testId => { addTestId(rootDirectory, testId, testFiles); }); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); } function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) { diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 207120185e05..ab20b11d5338 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -3,22 +3,23 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestStatus, TestsToRun } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { - super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } // tslint:disable-next-line:no-empty public configure() { } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.unittestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.unittestArgs.slice(0); if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; @@ -26,6 +27,6 @@ export class TestManager extends BaseTestManager { return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; }).map(fn => fn.testFunction); } - return runTest(this, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this, this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index 819fe2db0a72..d6c13c9c2d76 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -1,14 +1,11 @@ -/// - 'use strict'; import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestStatus, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { updateResults } from '../common/testUtils'; +import { ITestResultsService, Tests, TestStatus, TestsToRun } from '../common/types'; import { Server } from './socketServer'; type TestStatusMap = { status: TestStatus; @@ -33,7 +30,7 @@ interface ITestData { } // tslint:disable-next-line:max-func-body-length -export function runTest(testManager: BaseTestManager, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testManager: BaseTestManager, testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { tests.summary.errors = 0; tests.summary.failures = 0; tests.summary.passed = 0; @@ -132,7 +129,7 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes } return promise; }).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }).catch(reason => { return Promise.reject(reason); diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 2b3c12bba765..2113f874f4c1 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,8 +1,8 @@ import * as path from 'path'; import { OutputChannel, Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; -import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { constructor(workspace: Uri, outputChannel: OutputChannel, diff --git a/src/test/index.ts b/src/test/index.ts index c704c5684564..6a4979bc9302 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -9,6 +9,6 @@ testRunner.configure({ useColors: true, timeout: 25000, retries: 3, - grep: 'Python Path Settings Updater' + grep: 'Unit Tests' }); module.exports = testRunner; diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index d61e4e0d9944..9e531a393d85 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -2,7 +2,10 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { Tests, TestsToRun } from '../../client/unittests/common/contracts'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import * as nose from '../../client/unittests/nosetest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; @@ -23,6 +26,10 @@ suite('Unit Tests (nosetest)', () => { let testManager: nose.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + suiteSetup(async () => { filesToDelete.forEach(file => { if (fs.existsSync(file)) { @@ -51,7 +58,10 @@ suite('Unit Tests (nosetest)', () => { await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new nose.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new nose.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); } test('Discover Tests (single test file)', async () => { @@ -130,7 +140,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.passed, 6, 'Passed'); assert.equal(results.summary.skipped, 2, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 1, 'Errors again'); assert.equal(results.summary.failures, 7, 'Failures again'); assert.equal(results.summary.passed, 0, 'Passed again'); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index c0fb8dc64540..e2b9ffde4123 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -1,7 +1,10 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { TestFile, TestsToRun } from '../../client/unittests/common/contracts'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestFile, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import * as pytest from '../../client/unittests/pytest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; @@ -19,6 +22,9 @@ suite('Unit Tests (PyTest)', () => { let testManager: pytest.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; suiteSetup(async () => { await initialize(); @@ -37,11 +43,17 @@ suite('Unit Tests (PyTest)', () => { await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new pytest.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); } test('Discover Tests (single test file)', async () => { - testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); @@ -106,7 +118,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.passed, 17, 'Passed'); assert.equal(results.summary.skipped, 3, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 0, 'Failed Errors'); assert.equal(results.summary.failures, 9, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index 478111c1d620..99397a7855cc 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -2,7 +2,10 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; -import { TestsToRun } from '../../client/unittests/common/contracts'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import * as unittest from '../../client/unittests/unittest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; @@ -27,6 +30,9 @@ suite('Unit Tests (unittest)', () => { let testManager: unittest.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; const rootDirectory = UNITTEST_TEST_FILES_PATH; const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suiteSetup(async () => { @@ -48,12 +54,18 @@ suite('Unit Tests (unittest)', () => { testResultDisplay.dispose(); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new unittest.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); } test('Discover Tests (single test file)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); @@ -101,7 +113,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.passed, 3, 'Passed'); assert.equal(results.summary.skipped, 1, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 1, 'Failed Errors'); assert.equal(results.summary.failures, 4, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); From dfb86e6c2e4ead0fad9ac7b7281ff88818ea5262 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Oct 2017 12:49:00 -0700 Subject: [PATCH 10/23] adding support for multiroot workspaces --- package.json | 5 +- src/client/common/installer.ts | 49 ++- src/client/linters/baseLinter.ts | 2 +- src/client/linters/errorHandlers/main.ts | 9 +- src/client/linters/errorHandlers/standard.ts | 4 +- src/client/unittests/codeLenses/main.ts | 2 +- src/client/unittests/codeLenses/testFiles.ts | 48 ++- .../unittests/common/baseTestManager.ts | 44 +-- .../unittests/common/configSettingService.ts | 6 +- src/client/unittests/common/constants.ts | 1 + src/client/unittests/common/storageService.ts | 21 + .../common/testConfigurationManager.ts | 8 +- .../unittests/common/testManagerService.ts | 24 +- .../common/testManagerServiceFactory.ts | 7 +- .../unittests/common/testResultsService.ts | 110 ++++++ src/client/unittests/common/testUtils.ts | 337 +--------------- .../common/testVisitors/flatteningVisitor.ts | 65 ++++ .../testVisitors/folderGenerationVisitor.ts | 55 +++ .../common/testVisitors/resultResetVisitor.ts | 37 ++ .../common/{contracts.ts => types.ts} | 32 +- .../common/workspaceTestManagerService.ts | 69 ++-- src/client/unittests/common/xUnitParser.ts | 2 +- src/client/unittests/configuration.ts | 8 +- src/client/unittests/display/main.ts | 4 +- src/client/unittests/display/picker.ts | 36 +- src/client/unittests/main.ts | 364 +++++++++--------- src/client/unittests/nosetest/collector.ts | 8 +- src/client/unittests/nosetest/main.ts | 13 +- src/client/unittests/nosetest/runner.ts | 11 +- .../nosetest/testConfigurationManager.ts | 2 +- src/client/unittests/pytest/collector.ts | 8 +- src/client/unittests/pytest/main.ts | 13 +- src/client/unittests/pytest/runner.ts | 11 +- .../pytest/testConfigurationManager.ts | 2 +- src/client/unittests/unittest/collector.ts | 25 +- src/client/unittests/unittest/main.ts | 15 +- src/client/unittests/unittest/runner.ts | 9 +- .../unittest/testConfigurationManager.ts | 2 +- src/client/workspaceSymbols/main.ts | 3 +- src/test/unittests/nosetest.test.ts | 16 +- src/test/unittests/pytest.test.ts | 20 +- src/test/unittests/unittest.test.ts | 20 +- typings.json | 6 - typings/globals/xml2js/index.d.ts | 96 ----- typings/globals/xml2js/typings.json | 8 - typings/index.d.ts | 1 - typings/node.d.ts | 1 - typings/vscode-typings.d.ts | 1 - 48 files changed, 783 insertions(+), 857 deletions(-) create mode 100644 src/client/unittests/common/constants.ts create mode 100644 src/client/unittests/common/storageService.ts create mode 100644 src/client/unittests/common/testResultsService.ts create mode 100644 src/client/unittests/common/testVisitors/flatteningVisitor.ts create mode 100644 src/client/unittests/common/testVisitors/folderGenerationVisitor.ts create mode 100644 src/client/unittests/common/testVisitors/resultResetVisitor.ts rename src/client/unittests/common/{contracts.ts => types.ts} (77%) delete mode 100644 typings.json delete mode 100644 typings/globals/xml2js/index.d.ts delete mode 100644 typings/globals/xml2js/typings.json delete mode 100644 typings/index.d.ts delete mode 100644 typings/node.d.ts delete mode 100644 typings/vscode-typings.d.ts diff --git a/package.json b/package.json index 887c63a47f5b..b1736078a793 100644 --- a/package.json +++ b/package.json @@ -1641,6 +1641,7 @@ "@types/socket.io-client": "^1.4.27", "@types/uuid": "^3.3.27", "@types/winreg": "^1.2.30", + "@types/xml2js": "^0.4.0", "babel-core": "^6.14.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.14.0", @@ -1650,10 +1651,10 @@ "gulp-typescript": "^3.2.2", "husky": "^0.14.3", "ignore-loader": "^0.1.1", - "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", - "sinon": "^2.3.6", "mocha": "^2.3.3", "relative": "^3.0.2", + "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", + "sinon": "^2.3.6", "ts-loader": "^2.3.4", "tslint": "^5.7.0", "tslint-eslint-rules": "^4.1.1", diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index fb8042454455..cd0cf9bc20a8 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -1,6 +1,6 @@ import * as os from 'os'; import * as vscode from 'vscode'; -import { commands, ConfigurationTarget, Disposable, OutputChannel, Terminal, Uri, window, workspace } from 'vscode'; +import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; import * as settings from './configSettings'; import { isNotInstalledError } from './helpers'; import { error } from './logger'; @@ -177,6 +177,7 @@ export class Installer implements vscode.Disposable { // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); return disablePromptForFeatures.indexOf(productName) === -1; } @@ -185,7 +186,8 @@ export class Installer implements vscode.Disposable { public async promptToInstall(product: Product, resource?: Uri): Promise { // tslint:disable-next-line:no-non-null-assertion const productType = ProductTypes.get(product)!; - const productTypeName = ProductTypeNames.get(productType); + // tslint:disable-next-line:no-non-null-assertion + const productTypeName = ProductTypeNames.get(productType)!; // tslint:disable-next-line:no-non-null-assertion const productName = ProductNames.get(product)!; @@ -199,9 +201,9 @@ export class Installer implements vscode.Disposable { return InstallerResponse.Ignore; } - const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : 'Install ' + productName; - const disableOption = 'Disable ' + productTypeName; - const dontShowAgain = `Don't show this prompt again`; + const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : `Install ${productName}`; + const disableOption = `Disable ${productTypeName}`; + const dontShowAgain = 'Don\'t show this prompt again'; const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; const useOtherFormatter = `Use '${alternateFormatter}' formatter`; const options = []; @@ -220,8 +222,8 @@ export class Installer implements vscode.Disposable { case disableOption: { if (Linters.indexOf(product) >= 0) { return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled); - } - else { + } else { + // tslint:disable-next-line:no-non-null-assertion const settingToDisable = SettingToDisableProduct.get(product)!; return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled); } @@ -232,6 +234,7 @@ export class Installer implements vscode.Disposable { } case dontShowAgain: { const pythonConfig = workspace.getConfiguration('python'); + // tslint:disable-next-line:prefer-type-cast const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); features.push(productName); return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore); @@ -255,31 +258,32 @@ export class Installer implements vscode.Disposable { this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); this.outputChannel.show(); - } - else { + } else { window.showInformationMessage('Install Universal Ctags and set it in your path or define the path in your python.workspaceSymbols.ctagsPath settings'); } return InstallerResponse.Ignore; } + // tslint:disable-next-line:no-non-null-assertion let installArgs = ProductInstallScripts.get(product)!; - let pipIndex = installArgs.indexOf('pip'); + const pipIndex = installArgs.indexOf('pip'); if (pipIndex > 0) { installArgs = installArgs.slice(); - let proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); + const proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); if (proxy.length > 0) { installArgs.splice(2, 0, proxy); installArgs.splice(2, 0, '--proxy'); } } + // tslint:disable-next-line:no-any let installationPromise: Promise; if (this.outputChannel && installArgs[0] === '-m') { // Errors are just displayed to the user this.outputChannel.show(); installationPromise = execPythonFile(resource, settings.PythonSettings.getInstance(resource).pythonPath, + // tslint:disable-next-line:no-non-null-assertion installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); }); - } - else { + } else { // When using terminal get the fully qualitified path // Cuz people may launch vs code from terminal when they have activated the appropriate virtual env // Problem is terminal doesn't use the currently activated virtual env @@ -291,12 +295,13 @@ export class Installer implements vscode.Disposable { if (installArgs[0] === '-m') { if (pythonPath.indexOf(' ') >= 0) { installScript = `"${pythonPath}" ${installScript}`; - } - else { + } else { installScript = `${pythonPath} ${installScript}`; } } + // tslint:disable-next-line:no-non-null-assertion Installer.terminal!.sendText(installScript); + // tslint:disable-next-line:no-non-null-assertion Installer.terminal!.show(false); }); } @@ -306,30 +311,33 @@ export class Installer implements vscode.Disposable { .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); } + // tslint:disable-next-line:member-ordering public isInstalled(product: Product, resource?: Uri): Promise { return isProductInstalled(product, resource); } + // tslint:disable-next-line:member-ordering no-any public uninstall(product: Product, resource?: Uri): Promise { return uninstallproduct(product, resource); } + // tslint:disable-next-line:member-ordering public disableLinter(product: Product, resource: Uri) { if (resource && !workspace.getWorkspaceFolder(resource)) { + // tslint:disable-next-line:no-non-null-assertion const settingToDisable = SettingToDisableProduct.get(product)!; const pythonConfig = workspace.getConfiguration('python', resource); return pythonConfig.update(settingToDisable, false, ConfigurationTarget.Workspace); - } - else { + } else { const pythonConfig = workspace.getConfiguration('python'); return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); } } + // tslint:disable-next-line:no-any private updateSetting(setting: string, value: any, resource?: Uri) { if (resource && !workspace.getWorkspaceFolder(resource)) { const pythonConfig = workspace.getConfiguration('python', resource); return pythonConfig.update(setting, value, ConfigurationTarget.Workspace); - } - else { + } else { const pythonConfig = workspace.getConfiguration('python'); return pythonConfig.update(setting, value, true); } @@ -351,6 +359,7 @@ async function isProductInstalled(product: Product, resource?: Uri): Promise !isNotInstalledError(reason)); } +// tslint:disable-next-line:no-any function uninstallproduct(product: Product, resource?: Uri): Promise { if (!ProductUninstallScripts.has(product)) { return Promise.resolve(); } + // tslint:disable-next-line:no-non-null-assertion const uninstallArgs = ProductUninstallScripts.get(product)!; return execPythonFile(resource, 'python', uninstallArgs, getCwdForInstallScript(resource), false); } diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 551b6bc84622..46c89d16dff9 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -144,7 +144,7 @@ export abstract class BaseLinter { }); } - protected handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri) { + protected handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { this._errorHandler.handleError(expectedFileName, fileName, error, resource); } } diff --git a/src/client/linters/errorHandlers/main.ts b/src/client/linters/errorHandlers/main.ts index cc9120627929..e8b9c4d82b35 100644 --- a/src/client/linters/errorHandlers/main.ts +++ b/src/client/linters/errorHandlers/main.ts @@ -2,10 +2,11 @@ import { OutputChannel, Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; import { InvalidArgumentsErrorHandler } from './invalidArgs'; -import { StandardErrorHandler } from './standard'; import { NotInstalledErrorHandler } from './notInstalled'; +import { StandardErrorHandler } from './standard'; export class ErrorHandler { + // tslint:disable-next-line:variable-name private _errorHandlers: StandardErrorHandler[] = []; constructor(protected id: string, protected product: Product, protected installer: Installer, protected outputChannel: OutputChannel) { this._errorHandlers = [ @@ -15,7 +16,7 @@ export class ErrorHandler { ]; } - public handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri) { - this._errorHandlers.some(handler => handler.handleError(expectedFileName, fileName, error)); + public handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri) { + this._errorHandlers.some(handler => handler.handleError(expectedFileName, fileName, error, resource)); } -} \ No newline at end of file +} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index e9f665a0d09c..966f8afa41a9 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -6,7 +6,7 @@ export class StandardErrorHandler { constructor(protected id: string, protected product: Product, protected installer: Installer, protected outputChannel: OutputChannel) { } - private displayLinterError(resource?: Uri) { + private displayLinterError(resource: Uri) { const message = `There was an error in running the linter '${this.id}'`; window.showErrorMessage(message, 'Disable linter', 'View Errors').then(item => { switch (item) { @@ -22,7 +22,7 @@ export class StandardErrorHandler { }); } - public handleError(expectedFileName: string, fileName: string, error: Error, resource?: Uri): boolean { + public handleError(expectedFileName: string, fileName: string, error: Error, resource: Uri): boolean { if (typeof error === 'string' && (error as string).indexOf("OSError: [Errno 2] No such file or directory: '/") > 0) { return false; } diff --git a/src/client/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts index 613f86dae062..3efd30091227 100644 --- a/src/client/unittests/codeLenses/main.ts +++ b/src/client/unittests/codeLenses/main.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService } from '../common/types'; import { TestFileCodeLensProvider } from './testFiles'; export function activateCodeLenses(onDidChange: vscode.EventEmitter, diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index cfc0ad1bf1eb..9962f0615095 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -1,16 +1,11 @@ 'use strict'; import { CancellationToken, CancellationTokenSource, CodeLens, CodeLensProvider, Event, EventEmitter, Position, Range, SymbolInformation, SymbolKind, TextDocument, workspace } from 'vscode'; +import { Uri } from 'vscode'; import * as constants from '../../common/constants'; import { PythonSymbolProvider } from '../../providers/symbolProvider'; -import { TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, TestFile, TestFunction, TestStatus, TestsToRun, TestSuite } from '../common/types'; -type CodeLensData = { - symbolKind: SymbolKind; - symbolName: string; - fileName: string; -}; type FunctionsAndSuites = { functions: TestFunction[]; suites: TestSuite[]; @@ -56,18 +51,18 @@ export class TestFileCodeLensProvider implements CodeLensProvider { return Promise.resolve(codeLens); } - private getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider): Thenable { + private async getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: PythonSymbolProvider) { const wkspace = workspace.getWorkspaceFolder(document.uri); if (!wkspace) { - return null; + return []; } const tests = this.testCollectionStorage.getTests(wkspace.uri); if (!tests) { - return null; + return []; } const file = tests.testFiles.find(item => item.fullPath === document.uri.fsPath); if (!file) { - return Promise.resolve([]); + return []; } const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); @@ -84,7 +79,7 @@ export class TestFileCodeLensProvider implements CodeLensProvider { new Position(symbol.location.range.end.line, symbol.location.range.end.character + 1)); - return this.getCodeLens(document.uri.fsPath, allFuncsAndSuites, + return this.getCodeLens(document.uri, allFuncsAndSuites, range, symbol.name, symbol.kind, symbol.containerName); }).reduce((previous, current) => previous.concat(current), []).filter(codeLens => codeLens !== null); }, reason => { @@ -95,13 +90,13 @@ export class TestFileCodeLensProvider implements CodeLensProvider { }); } - private getCodeLens(fileName: string, allFuncsAndSuites: FunctionsAndSuites, + private getCodeLens(file: Uri, allFuncsAndSuites: FunctionsAndSuites, range: Range, symbolName: string, symbolKind: SymbolKind, symbolContainer: string): CodeLens[] { switch (symbolKind) { case SymbolKind.Function: case SymbolKind.Method: { - return getFunctionCodeLens(fileName, allFuncsAndSuites, symbolName, range, symbolContainer); + return getFunctionCodeLens(file, allFuncsAndSuites, symbolName, range, symbolContainer); } case SymbolKind.Class: { const cls = allFuncsAndSuites.suites.find(item => item.name === symbolName); @@ -112,12 +107,12 @@ export class TestFileCodeLensProvider implements CodeLensProvider { new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testSuite: [cls] }] + arguments: [file, { testSuite: [cls] }] }), new CodeLens(range, { title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testSuite: [cls] }] + arguments: [file, { testSuite: [cls] }] }) ]; } @@ -128,7 +123,7 @@ export class TestFileCodeLensProvider implements CodeLensProvider { } } -function getTestStatusIcon(status: TestStatus): string { +function getTestStatusIcon(status?: TestStatus): string { switch (status) { case TestStatus.Pass: { return '✔ '; @@ -163,7 +158,7 @@ function getTestStatusIcons(fns: TestFunction[]): string { return statuses.join(' '); } -function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndSuites, +function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, symbolName: string, range: Range, symbolContainer: string): CodeLens[] { let fn: TestFunction | undefined; @@ -186,12 +181,12 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: [fn] }] + arguments: [file, { testFunction: [fn] }] }), new CodeLens(range, { title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: [fn] }] + arguments: [file, { testFunction: [fn] }] }) ]; } @@ -200,19 +195,19 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS // If we have [ in the name, then this is a parameterized function const functions = functionsAndSuites.functions.filter(func => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']')); if (functions.length === 0) { - return null; + return []; } if (functions.length === 0) { return [ new CodeLens(range, { title: constants.Text.CodeLensRunUnitTest, command: constants.Commands.Tests_Run, - arguments: [{ testFunction: functions }] + arguments: [file, { testFunction: functions }] }), new CodeLens(range, { title: constants.Text.CodeLensDebugUnitTest, command: constants.Commands.Tests_Debug, - arguments: [{ testFunction: functions }] + arguments: [file, { testFunction: functions }] }) ]; } @@ -222,18 +217,19 @@ function getFunctionCodeLens(filePath: string, functionsAndSuites: FunctionsAndS new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI, - arguments: [filePath, functions] + arguments: [file, functions] }), new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [filePath, functions] + arguments: [file, functions] }) ]; } function getAllTestSuitesAndFunctionsPerFile(testFile: TestFile): FunctionsAndSuites { - const all = { functions: testFile.functions, suites: [] }; + // tslint:disable-next-line:prefer-type-cast + const all = { functions: testFile.functions, suites: [] as TestSuite[] }; testFile.suites.forEach(suite => { all.suites.push(suite); diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index 368f72327d62..b949dd6dec12 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -1,12 +1,15 @@ // import {TestFolder, TestsToRun, Tests, TestFile, TestSuite, TestFunction, TestStatus, FlattenedTestFunction, FlattenedTestSuite, CANCELLATION_REASON} from './contracts'; import * as vscode from 'vscode'; +import { Uri, workspace } from 'vscode'; import { IPythonSettings, PythonSettings } from '../../common/configSettings'; import { isNotInstalledError } from '../../common/helpers'; import { Installer, Product } from '../../common/installer'; -import { CANCELLATION_REASON, Tests, TestStatus, TestsToRun } from './contracts'; -import { displayTestErrorMessage, ITestCollectionStorageService, resetTestResults } from './testUtils'; +import { CANCELLATION_REASON } from './constants'; +import { displayTestErrorMessage } from './testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; export abstract class BaseTestManager { + public readonly workspace: Uri; protected readonly settings: IPythonSettings; private tests: Tests; // tslint:disable-next-line:variable-name @@ -14,19 +17,19 @@ export abstract class BaseTestManager { private cancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; private discoverTestsPromise: Promise; - constructor(private testProvider: string, private product: Product, protected rootDirectory: string, protected outputChannel: vscode.OutputChannel, - private testCollectionStorage: ITestCollectionStorageService) { + constructor(private testProvider: string, private product: Product, protected rootDirectory: string, + protected outputChannel: vscode.OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + protected testResultsService: ITestResultsService, protected testsHelper: ITestsHelper) { this._status = TestStatus.Unknown; this.installer = new Installer(); - this.settings = PythonSettings.getInstance(this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); + this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); + this.workspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory)).uri; } - protected get cancellationToken(): vscode.CancellationToken { - if (this.cancellationTokenSource) { - return this.cancellationTokenSource.token; - } + protected get cancellationToken(): vscode.CancellationToken | undefined { + return this.cancellationTokenSource ? this.cancellationTokenSource.token : undefined; } - // tslint:disable-next-line:no-empty public dispose() { + this.stop(); } public get status(): TestStatus { return this._status; @@ -49,9 +52,9 @@ export abstract class BaseTestManager { return; } - resetTestResults(this.tests); + this.testResultsService.resetResults(this.tests); } - public discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { + public async discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { if (this.discoverTestsPromise) { return this.discoverTestsPromise; } @@ -91,7 +94,8 @@ export abstract class BaseTestManager { return tests; }).catch(reason => { if (isNotInstalledError(reason) && !quietMode) { - this.installer.promptToInstall(this.product, this.rootDirectory ? vscode.Uri.file(this.rootDirectory) : undefined); + // tslint:disable-next-line:no-floating-promises + this.installer.promptToInstall(this.product, this.workspace); } this.tests = null; @@ -111,13 +115,7 @@ export abstract class BaseTestManager { return Promise.reject(reason); }); } - public runTest(testsToRun?: TestsToRun, debug?: boolean): Promise; - // tslint:disable-next-line:unified-signatures - public runTest(runFailedTests?: boolean, debug?: boolean): Promise; - // tslint:disable-next-line:no-any - public runTest(args: any, debug?: boolean): Promise { - let runFailedTests = false; - let testsToRun: TestsToRun = null; + public runTest(testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { const moreInfo = { Test_Provider: this.testProvider, Run_Failed_Tests: 'false', @@ -126,13 +124,11 @@ export abstract class BaseTestManager { Run_Specific_Function: 'false' }; - if (typeof args === 'boolean') { - runFailedTests = args === true; + if (runFailedTests === true) { // tslint:disable-next-line:prefer-template moreInfo.Run_Failed_Tests = runFailedTests + ''; } - if (typeof args === 'object' && args !== null) { - testsToRun = args; + if (testsToRun && typeof testsToRun === 'object') { if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { moreInfo.Run_Specific_File = 'true'; } diff --git a/src/client/unittests/common/configSettingService.ts b/src/client/unittests/common/configSettingService.ts index 1e005c8973bc..f6a90619f9fe 100644 --- a/src/client/unittests/common/configSettingService.ts +++ b/src/client/unittests/common/configSettingService.ts @@ -1,6 +1,6 @@ import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; import { Product } from '../../common/installer'; -import { ITestConfigSettingsService, UnitTestProduct } from './contracts'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; export class TestConfigSettingsService implements ITestConfigSettingsService { private static getTestArgSetting(product: UnitTestProduct) { @@ -55,12 +55,12 @@ export class TestConfigSettingsService implements ITestConfigSettingsService { return TestConfigSettingsService.updateSetting(testDirectory, setting, args); } - public enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { const setting = TestConfigSettingsService.getTestEnablingSetting(product); return TestConfigSettingsService.updateSetting(testDirectory, setting, true); } - public disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + 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/constants.ts b/src/client/unittests/common/constants.ts new file mode 100644 index 000000000000..3d428cfad97c --- /dev/null +++ b/src/client/unittests/common/constants.ts @@ -0,0 +1 @@ +export const CANCELLATION_REASON = 'cancelled_user_request'; diff --git a/src/client/unittests/common/storageService.ts b/src/client/unittests/common/storageService.ts new file mode 100644 index 000000000000..775750de1fe9 --- /dev/null +++ b/src/client/unittests/common/storageService.ts @@ -0,0 +1,21 @@ +import { Uri, workspace } from 'vscode'; +import { ITestCollectionStorageService, Tests } from './types'; + +export class TestCollectionStorageService implements ITestCollectionStorageService { + private testsIndexedByWorkspaceUri = new Map(); + public getTests(wkspace: Uri): Tests | undefined { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; + } + public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { + const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; + this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); + } + public dispose() { + this.testsIndexedByWorkspaceUri.clear(); + } + private getWorkspaceFolderPath(resource: Uri): string | undefined { + const folder = workspace.getWorkspaceFolder(resource); + return folder ? folder.uri.path : undefined; + } +} diff --git a/src/client/unittests/common/testConfigurationManager.ts b/src/client/unittests/common/testConfigurationManager.ts index 84843f7c69d5..672c138aceb9 100644 --- a/src/client/unittests/common/testConfigurationManager.ts +++ b/src/client/unittests/common/testConfigurationManager.ts @@ -4,7 +4,7 @@ import { Uri } from 'vscode'; import { createDeferred } from '../../common/helpers'; import { Installer } from '../../common/installer'; import { getSubDirectories } from '../../common/utils'; -import { ITestConfigSettingsService, UnitTestProduct } from './contracts'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; export abstract class TestConfigurationManager { constructor(protected workspace: Uri, @@ -14,11 +14,11 @@ export abstract class TestConfigurationManager { protected testConfigSettingsService: ITestConfigSettingsService) { } // tslint:disable-next-line:no-any public abstract configure(rootDir: string): Promise; - public enable() { + public async enable() { return this.testConfigSettingsService.enable(this.workspace, this.product); } // tslint:disable-next-line:no-any - public disable(): Thenable { + public async disable() { return this.testConfigSettingsService.enable(this.workspace, this.product); } protected selectTestDir(rootDir: string, subDirs: string[], customOptions: vscode.QuickPickItem[] = []): Promise { @@ -77,7 +77,7 @@ export abstract class TestConfigurationManager { return def.promise; } - protected getTestDirs(rootDir): Promise { + protected getTestDirs(rootDir: string): Promise { return getSubDirectories(rootDir).then(subDirs => { subDirs.sort(); diff --git a/src/client/unittests/common/testManagerService.ts b/src/client/unittests/common/testManagerService.ts index e07ee373fa33..a4d7b32850d2 100644 --- a/src/client/unittests/common/testManagerService.ts +++ b/src/client/unittests/common/testManagerService.ts @@ -1,20 +1,27 @@ -import { Disposable, OutputChannel, Uri } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { TestManager as NoseTestManager } from '../nosetest/main'; import { TestManager as PyTestTestManager } from '../pytest/main'; import { TestManager as UnitTestTestManager } from '../unittest/main'; import { BaseTestManager } from './baseTestManager'; -import { ITestManagerService, UnitTestProduct } from './contracts'; +import { ITestCollectionStorageService, ITestManagerService, ITestResultsService, ITestsHelper, UnitTestProduct } from './types'; type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; export class TestManagerService implements ITestManagerService { private testManagers = new Map(); - constructor(private wkspace: Uri, private outChannel: OutputChannel) { - this.testManagers.set(Product.nosetest, { create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel) }); - this.testManagers.set(Product.pytest, { create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel) }); - this.testManagers.set(Product.unittest, { create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel) }); + constructor(private wkspace: Uri, private outChannel: OutputChannel, + testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + this.testManagers.set(Product.nosetest, { + create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + }); + this.testManagers.set(Product.pytest, { + create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + }); + this.testManagers.set(Product.unittest, { + create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + }); } public dispose() { this.testManagers.forEach(info => { @@ -25,7 +32,7 @@ export class TestManagerService implements ITestManagerService { } public getTestManager(): BaseTestManager | undefined { const preferredTestManager = this.getPreferredTestManager(); - if (!preferredTestManager) { + if (typeof preferredTestManager !== 'number') { return; } @@ -40,7 +47,7 @@ export class TestManagerService implements ITestManagerService { const settings = PythonSettings.getInstance(this.wkspace); return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.wkspace.fsPath; } - public getPreferredTestManager(): UnitTestProduct { + public getPreferredTestManager(): UnitTestProduct | undefined { const settings = PythonSettings.getInstance(this.wkspace); if (settings.unitTest.nosetestsEnabled) { return Product.nosetest; @@ -49,5 +56,6 @@ export class TestManagerService implements ITestManagerService { } else if (settings.unitTest.unittestEnabled) { return Product.unittest; } + return undefined; } } diff --git a/src/client/unittests/common/testManagerServiceFactory.ts b/src/client/unittests/common/testManagerServiceFactory.ts index d48b60bed15f..416cd713e9d0 100644 --- a/src/client/unittests/common/testManagerServiceFactory.ts +++ b/src/client/unittests/common/testManagerServiceFactory.ts @@ -1,10 +1,11 @@ import { OutputChannel, Uri } from 'vscode'; -import { ITestManagerService, ITestManagerServiceFactory } from './contracts'; import { TestManagerService } from './testManagerService'; +import { ITestCollectionStorageService, ITestManagerService, ITestManagerServiceFactory, ITestResultsService, ITestsHelper } from './types'; export class TestManagerServiceFactory implements ITestManagerServiceFactory { - constructor(private outChannel: OutputChannel) { } + constructor(private outChannel: OutputChannel, private testCollectionStorage: ITestCollectionStorageService, + private testResultsService: ITestResultsService, private testsHelper: ITestsHelper) { } public createTestManagerService(wkspace: Uri): ITestManagerService { - return new TestManagerService(wkspace, this.outChannel); + return new TestManagerService(wkspace, this.outChannel, this.testCollectionStorage, this.testResultsService, this.testsHelper); } } diff --git a/src/client/unittests/common/testResultsService.ts b/src/client/unittests/common/testResultsService.ts new file mode 100644 index 000000000000..58fda744123e --- /dev/null +++ b/src/client/unittests/common/testResultsService.ts @@ -0,0 +1,110 @@ +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { ITestResultsService, TestFile, TestFolder, Tests, TestStatus, TestSuite } from './types'; + +export class TestResultsService implements ITestResultsService { + public resetResults(tests: Tests): void { + const resultResetVisitor = new TestResultResetVisitor(); + tests.testFolders.forEach(f => resultResetVisitor.visitTestFolder(f)); + tests.testFunctions.forEach(fn => resultResetVisitor.visitTestFunction(fn.testFunction)); + tests.testSuits.forEach(suite => resultResetVisitor.visitTestSuite(suite.testSuite)); + tests.testFiles.forEach(testFile => resultResetVisitor.visitTestFile(testFile)); + } + public updateResults(tests: Tests): void { + tests.testFiles.forEach(test => this.updateTestFileResults(test)); + tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); + } + private updateTestSuiteResults(test: TestSuite): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFileResults(test: TestFile): void { + this.updateTestSuiteAndFileResults(test); + } + private updateTestFolderResults(testFolder: TestFolder): void { + let allFilesPassed = true; + let allFilesRan = true; + + testFolder.testFiles.forEach(fl => { + if (allFilesPassed && typeof fl.passed === 'boolean') { + if (!fl.passed) { + allFilesPassed = false; + } + } else { + allFilesRan = false; + } + + testFolder.functionsFailed += fl.functionsFailed; + testFolder.functionsPassed += fl.functionsPassed; + }); + + let allFoldersPassed = true; + let allFoldersRan = true; + + testFolder.folders.forEach(folder => { + this.updateTestFolderResults(folder); + if (allFoldersPassed && typeof folder.passed === 'boolean') { + if (!folder.passed) { + allFoldersPassed = false; + } + } else { + allFoldersRan = false; + } + + testFolder.functionsFailed += folder.functionsFailed; + testFolder.functionsPassed += folder.functionsPassed; + }); + + if (allFilesRan && allFoldersRan) { + testFolder.passed = allFilesPassed && allFoldersPassed; + testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; + } else { + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } + } + private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { + let totalTime = 0; + let allFunctionsPassed = true; + let allFunctionsRan = true; + + test.functions.forEach(fn => { + totalTime += fn.time; + if (typeof fn.passed === 'boolean') { + if (fn.passed) { + test.functionsPassed += 1; + } else { + test.functionsFailed += 1; + allFunctionsPassed = false; + } + } else { + allFunctionsRan = false; + } + }); + + let allSuitesPassed = true; + let allSuitesRan = true; + + test.suites.forEach(suite => { + this.updateTestSuiteResults(suite); + totalTime += suite.time; + if (allSuitesRan && typeof suite.passed === 'boolean') { + if (!suite.passed) { + allSuitesPassed = false; + } + } else { + allSuitesRan = false; + } + + test.functionsFailed += suite.functionsFailed; + test.functionsPassed += suite.functionsPassed; + }); + + test.time = totalTime; + if (allSuitesRan && allFunctionsRan) { + test.passed = allFunctionsPassed && allSuitesPassed; + test.status = test.passed ? TestStatus.Idle : TestStatus.Error; + } else { + test.passed = null; + test.status = TestStatus.Unknown; + } + } +} diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 9dafac503658..88ac405835a5 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,34 +1,13 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri, workspace } from 'vscode'; +import { window } from 'vscode'; import * as constants from '../../common/constants'; -import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestsToRun, TestSuite } from './contracts'; -import { ITestVisitor } from './testUtils'; +import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; +import { TestResultResetVisitor } from './testVisitors/resultResetVisitor'; +import { TestFile, TestFolder, Tests, TestsToRun } from './types'; +import { ITestsHelper } from './types'; -export interface ITestCollectionStorageService { - getTests(wkspace: Uri): Tests | undefined; - storeTests(wkspace: Uri, tests: Tests | null | undefined): void; -} - -export class TestCollectionStorageService implements ITestCollectionStorageService { - private testsIndexedByWorkspaceUri = new Map(); - public getTests(wkspace: Uri): Tests | undefined { - const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; - return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; - } - public storeTests(wkspace: Uri, tests: Tests | null | undefined): void { - const workspaceFolder = getWorkspaceFolderPath(wkspace) || ''; - this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); - } -} - -function getWorkspaceFolderPath(resource?: Uri): string | undefined { - if (!resource) { - return undefined; - } - const folder = workspace.getWorkspaceFolder(resource); - return folder ? folder.uri.path : undefined; -} export async function selectTestWorkspace(): Promise { if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { return undefined; @@ -50,26 +29,6 @@ export function displayTestErrorMessage(message: string) { } -export function parseTestName(name: string, rootDirectory: string, testCollectionStorage: ITestCollectionStorageService): TestsToRun { - // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. - // use to identify a file given the full file name, similary for a folder and function - // Perhaps something like a parser or methods like TestFunction.fromString()... something) - const workspaceUri = workspace.getWorkspaceFolder(Uri.file(rootDirectory)).uri; - const tests = testCollectionStorage.getTests(workspaceUri); - if (!tests) { return null; } - const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); - if (testFolders.length > 0) { return { testFolder: testFolders }; } - - const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); - if (testFiles.length > 0) { return { testFile: testFiles }; } - - const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); - if (testFns.length > 0) { return { testFunction: testFns }; } - - // Just return this as a test file - return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; -} 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)); @@ -80,270 +39,6 @@ export function convertFileToPackage(filePath: string): string { return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); } -export interface ITestResultService { - resetResults(tests: Tests): void; - updateResults(tests: Tests): void; - updateTestSuiteResults(test: TestSuite): void; - updateTestFileResults(test: TestFile): void; - updateTestFolderResults(testFolder: TestFolder): void; -} - -export class TestResultService implements ITestResultService { - public resetResults(tests: Tests): void { - tests.testFolders.forEach(f => { - f.functionsDidNotRun = 0; - f.functionsFailed = 0; - f.functionsPassed = 0; - f.passed = null; - f.status = TestStatus.Unknown; - }); - tests.testFunctions.forEach(fn => { - fn.testFunction.passed = null; - fn.testFunction.time = 0; - fn.testFunction.message = ''; - fn.testFunction.traceback = ''; - fn.testFunction.status = TestStatus.Unknown; - fn.testFunction.functionsFailed = 0; - fn.testFunction.functionsPassed = 0; - fn.testFunction.functionsDidNotRun = 0; - }); - tests.testSuits.forEach(suite => { - suite.testSuite.passed = null; - suite.testSuite.time = 0; - suite.testSuite.status = TestStatus.Unknown; - suite.testSuite.functionsFailed = 0; - suite.testSuite.functionsPassed = 0; - suite.testSuite.functionsDidNotRun = 0; - }); - tests.testFiles.forEach(testFile => { - testFile.passed = null; - testFile.time = 0; - testFile.status = TestStatus.Unknown; - testFile.functionsFailed = 0; - testFile.functionsPassed = 0; - testFile.functionsDidNotRun = 0; - }); - } - public updateResults(tests: Tests): void { - tests.testFiles.forEach(test => this.updateTestFileResults(test)); - tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); - } - public updateTestSuiteResults(test: TestSuite): void { - this.updateTestSuiteAndFileResults(test); - } - public updateTestFileResults(test: TestFile): void { - this.updateTestSuiteAndFileResults(test); - } - public updateTestFolderResults(testFolder: TestFolder): void { - let allFilesPassed = true; - let allFilesRan = true; - - testFolder.testFiles.forEach(fl => { - if (allFilesPassed && typeof fl.passed === 'boolean') { - if (!fl.passed) { - allFilesPassed = false; - } - } else { - allFilesRan = false; - } - - testFolder.functionsFailed += fl.functionsFailed; - testFolder.functionsPassed += fl.functionsPassed; - }); - - let allFoldersPassed = true; - let allFoldersRan = true; - - testFolder.folders.forEach(folder => { - this.updateTestFolderResults(folder); - if (allFoldersPassed && typeof folder.passed === 'boolean') { - if (!folder.passed) { - allFoldersPassed = false; - } - } else { - allFoldersRan = false; - } - - testFolder.functionsFailed += folder.functionsFailed; - testFolder.functionsPassed += folder.functionsPassed; - }); - - if (allFilesRan && allFoldersRan) { - testFolder.passed = allFilesPassed && allFoldersPassed; - testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; - } else { - testFolder.passed = null; - testFolder.status = TestStatus.Unknown; - } - } - private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { - let totalTime = 0; - let allFunctionsPassed = true; - let allFunctionsRan = true; - - test.functions.forEach(fn => { - totalTime += fn.time; - if (typeof fn.passed === 'boolean') { - if (fn.passed) { - test.functionsPassed += 1; - } else { - test.functionsFailed += 1; - allFunctionsPassed = false; - } - } else { - allFunctionsRan = false; - } - }); - - let allSuitesPassed = true; - let allSuitesRan = true; - - test.suites.forEach(suite => { - this.updateTestSuiteResults(suite); - totalTime += suite.time; - if (allSuitesRan && typeof suite.passed === 'boolean') { - if (!suite.passed) { - allSuitesPassed = false; - } - } else { - allSuitesRan = false; - } - - test.functionsFailed += suite.functionsFailed; - test.functionsPassed += suite.functionsPassed; - }); - - test.time = totalTime; - if (allSuitesRan && allFunctionsRan) { - test.passed = allFunctionsPassed && allSuitesPassed; - test.status = test.passed ? TestStatus.Idle : TestStatus.Error; - } else { - test.passed = null; - test.status = TestStatus.Unknown; - } - } -} -export function updateResults(tests: Tests) { - new TestResultService().updateResults(tests); -} - -export interface ITestsHelper { - flattenTestFiles(testFiles: TestFile[]): Tests; - placeTestFilesIntoFolders(tests: Tests): void; -} - -export interface ITestVisitor { - visitTestFunction(testFunction: TestFunction): void; - visitTestSuite(testSuite: TestSuite): void; - visitTestFile(testFile: TestFile): void; -} - -export class TestFlatteningVisitor implements ITestVisitor { - // tslint:disable-next-line:variable-name - private _flattedTestFunctions = new Map(); - // tslint:disable-next-line:variable-name - private _flattenedTestSuites = new Map(); - public get flattenedTestFunctions(): Readonly { - return [...this._flattedTestFunctions.values()]; - } - public get flattenedTestSuites(): Readonly { - return [...this._flattenedTestSuites.values()]; - } - // tslint:disable-next-line:no-empty - public visitTestFunction(testFunction: TestFunction): void { } - // tslint:disable-next-line:no-empty - public visitTestSuite(testSuite: TestSuite): void { } - public visitTestFile(testFile: TestFile): void { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); - testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); - } - private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { - testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); - testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); - this.addTestSuite(testSuite, parentTestFile); - } - private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { - const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; - if (this._flattenedTestSuites.has(key)) { - return; - } - const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; - this._flattedTestFunctions.set(key, flattenedFunction); - } - private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { - const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; - if (this._flattenedTestSuites.has(key)) { - return; - } - const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; - this._flattenedTestSuites.set(key, flattenedSuite); - } - private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { - const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; - if (this._flattedTestFunctions.has(key)) { - return; - } - const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; - this._flattedTestFunctions.set(key, flattendFunction); - } -} - -// tslint:disable-next-line:max-classes-per-file -export class TestFolderGenerationVisitor implements ITestVisitor { - // tslint:disable-next-line:variable-name - private _testFolders: TestFolder[] = []; - // tslint:disable-next-line:variable-name - private _rootTestFolders: TestFolder[] = []; - private folderMap = new Map(); - public get testFolders(): Readonly { - return [...this._testFolders]; - } - public get rootTestFolders(): Readonly { - return [...this._rootTestFolders]; - } - // tslint:disable-next-line:no-empty - public visitTestFunction(testFunction: TestFunction): void { } - // tslint:disable-next-line:no-empty - public visitTestSuite(testSuite: TestSuite): void { } - public visitTestFile(testFile: TestFile): void { - // First get all the unique folders - const folders: string[] = []; - const dir = path.dirname(testFile.name); - if (this.folderMap.has(dir)) { - const folder = this.folderMap.get(dir); - folder.testFiles.push(testFile); - return; - } - - dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { - let newPath = currentName; - let parentFolder: TestFolder; - if (accumulatedPath.length > 0) { - parentFolder = this.folderMap.get(accumulatedPath); - newPath = path.join(accumulatedPath, currentName); - } - if (!this.folderMap.has(newPath)) { - const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - this.folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder.folders.push(testFolder); - } else { - this._rootTestFolders.push(testFolder); - } - this._testFolders.push(testFolder); - } - return newPath; - }, ''); - - // tslint:disable-next-line:no-non-null-assertion - this.folderMap.get(dir)!.testFiles.push(testFile); - } -} - -// tslint:disable-next-line:max-classes-per-file export class TestsHelper implements ITestsHelper { public flattenTestFiles(testFiles: TestFile[]): Tests { const flatteningVisitor = new TestFlatteningVisitor(); @@ -401,12 +96,22 @@ export class TestsHelper implements ITestsHelper { }, ''); }); } -} + public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun { + // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. + // use to identify a file given the full file name, similary for a folder and function + // Perhaps something like a parser or methods like TestFunction.fromString()... something) + if (!tests) { return null; } + const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); + const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); + if (testFolders.length > 0) { return { testFolder: testFolders }; } -export function flattenTestFiles(testFiles: TestFile[]): Tests { - return new TestsHelper().flattenTestFiles(testFiles); -} + const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); + if (testFiles.length > 0) { return { testFile: testFiles }; } -export function resetTestResults(tests: Tests) { - new TestResultService().resetResults(tests); + const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); + if (testFns.length > 0) { return { testFunction: testFns }; } + + // Just return this as a test file + return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; + } } diff --git a/src/client/unittests/common/testVisitors/flatteningVisitor.ts b/src/client/unittests/common/testVisitors/flatteningVisitor.ts new file mode 100644 index 000000000000..4a8ed96babbc --- /dev/null +++ b/src/client/unittests/common/testVisitors/flatteningVisitor.ts @@ -0,0 +1,65 @@ +import { convertFileToPackage } from '../testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + TestSuite +} from '../types'; + +export class TestFlatteningVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _flattedTestFunctions = new Map(); + // tslint:disable-next-line:variable-name + private _flattenedTestSuites = new Map(); + public get flattenedTestFunctions(): FlattenedTestFunction[] { + return [...this._flattedTestFunctions.values()]; + } + public get flattenedTestSuites(): FlattenedTestSuite[] { + return [...this._flattenedTestSuites.values()]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); + testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } + private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { + testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); + testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); + this.addTestSuite(testSuite, parentTestFile); + } + private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { + const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; + this._flattedTestFunctions.set(key, flattenedFunction); + } + private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { + const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; + this._flattenedTestSuites.set(key, flattenedSuite); + } + private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { + const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; + if (this._flattedTestFunctions.has(key)) { + return; + } + const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; + this._flattedTestFunctions.set(key, flattendFunction); + } +} diff --git a/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts new file mode 100644 index 000000000000..46956873edd1 --- /dev/null +++ b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts @@ -0,0 +1,55 @@ +import * as path from 'path'; +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestSuite } from '../types'; + +export class TestFolderGenerationVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _testFolders: TestFolder[] = []; + // tslint:disable-next-line:variable-name + private _rootTestFolders: TestFolder[] = []; + private folderMap = new Map(); + public get testFolders(): Readonly { + return [...this._testFolders]; + } + public get rootTestFolders(): Readonly { + return [...this._rootTestFolders]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(testFunction: TestFunction): void { } + // tslint:disable-next-line:no-empty + public visitTestSuite(testSuite: TestSuite): void { } + public visitTestFile(testFile: TestFile): void { + // First get all the unique folders + const folders: string[] = []; + const dir = path.dirname(testFile.name); + if (this.folderMap.has(dir)) { + const folder = this.folderMap.get(dir); + folder.testFiles.push(testFile); + return; + } + + dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { + let newPath = currentName; + let parentFolder: TestFolder; + if (accumulatedPath.length > 0) { + parentFolder = this.folderMap.get(accumulatedPath); + newPath = path.join(accumulatedPath, currentName); + } + if (!this.folderMap.has(newPath)) { + const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; + this.folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder.folders.push(testFolder); + } else { + this._rootTestFolders.push(testFolder); + } + this._testFolders.push(testFolder); + } + return newPath; + }, ''); + + // tslint:disable-next-line:no-non-null-assertion + this.folderMap.get(dir)!.testFiles.push(testFile); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(testFile: TestFolder) { } +} diff --git a/src/client/unittests/common/testVisitors/resultResetVisitor.ts b/src/client/unittests/common/testVisitors/resultResetVisitor.ts new file mode 100644 index 000000000000..0d58c1076b04 --- /dev/null +++ b/src/client/unittests/common/testVisitors/resultResetVisitor.ts @@ -0,0 +1,37 @@ +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestStatus, TestSuite } from '../types'; + +export class TestResultResetVisitor implements ITestVisitor { + public visitTestFunction(testFunction: TestFunction): void { + testFunction.passed = null; + testFunction.time = 0; + testFunction.message = ''; + testFunction.traceback = ''; + testFunction.status = TestStatus.Unknown; + testFunction.functionsFailed = 0; + testFunction.functionsPassed = 0; + testFunction.functionsDidNotRun = 0; + } + public visitTestSuite(testSuite: TestSuite): void { + testSuite.passed = null; + testSuite.time = 0; + testSuite.status = TestStatus.Unknown; + testSuite.functionsFailed = 0; + testSuite.functionsPassed = 0; + testSuite.functionsDidNotRun = 0; + } + public visitTestFile(testFile: TestFile): void { + testFile.passed = null; + testFile.time = 0; + testFile.status = TestStatus.Unknown; + testFile.functionsFailed = 0; + testFile.functionsPassed = 0; + testFile.functionsDidNotRun = 0; + } + public visitTestFolder(testFolder: TestFolder) { + testFolder.functionsDidNotRun = 0; + testFolder.functionsFailed = 0; + testFolder.functionsPassed = 0; + testFolder.passed = null; + testFolder.status = TestStatus.Unknown; + } +} diff --git a/src/client/unittests/common/contracts.ts b/src/client/unittests/common/types.ts similarity index 77% rename from src/client/unittests/common/contracts.ts rename to src/client/unittests/common/types.ts index 32945c99d297..9cb84cef8d29 100644 --- a/src/client/unittests/common/contracts.ts +++ b/src/client/unittests/common/types.ts @@ -2,8 +2,6 @@ import { Disposable, Uri } from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from './baseTestManager'; -export const CANCELLATION_REASON = 'cancelled_user_request'; - export type TestFolder = TestResult & { name: string; testFiles: TestFile[]; @@ -116,11 +114,35 @@ export interface ITestManagerService extends Disposable { getTestWorkingDirectory(): string; getPreferredTestManager(): UnitTestProduct; } + export interface ITestManagerServiceFactory { createTestManagerService(wkspace: Uri): ITestManagerService; } + export interface IWorkspaceTestManagerService extends Disposable { - getTestManager(wkspace: Uri): BaseTestManager | undefined; - getTestWorkingDirectory(wkspace: Uri): string; - getPreferredTestManager(wkspace: Uri): UnitTestProduct; + getTestManager(resource: Uri): BaseTestManager | undefined; + getTestWorkingDirectory(resource: Uri): string; + getPreferredTestManager(resource: Uri): UnitTestProduct; +} + +export interface ITestsHelper { + flattenTestFiles(testFiles: TestFile[]): Tests; + placeTestFilesIntoFolders(tests: Tests): void; +} + +export interface ITestVisitor { + visitTestFunction(testFunction: TestFunction): void; + visitTestSuite(testSuite: TestSuite): void; + visitTestFile(testFile: TestFile): void; + visitTestFolder(testFile: TestFolder): void; +} + +export interface ITestCollectionStorageService extends Disposable { + getTests(wkspace: Uri): Tests | undefined; + storeTests(wkspace: Uri, tests: Tests | null | undefined): void; +} + +export interface ITestResultsService { + resetResults(tests: Tests): void; + updateResults(tests: Tests): void; } diff --git a/src/client/unittests/common/workspaceTestManagerService.ts b/src/client/unittests/common/workspaceTestManagerService.ts index 17178d7d3684..ff7e198cb423 100644 --- a/src/client/unittests/common/workspaceTestManagerService.ts +++ b/src/client/unittests/common/workspaceTestManagerService.ts @@ -1,74 +1,53 @@ import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; -import { TestManager as NoseTestManager } from '../nosetest/main'; -import { TestManager as PyTestTestManager } from '../pytest/main'; -import { TestManager as UnitTestTestManager } from '../unittest/main'; import { BaseTestManager } from './baseTestManager'; -import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './contracts'; import { TestManagerService } from './testManagerService'; - -type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; +import { ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './types'; export class WorkspaceTestManagerService implements IWorkspaceTestManagerService, Disposable { private workspaceTestManagers = new Map(); - private workspaceTestSettings = new Map(); private disposables: Disposable[] = []; constructor(private outChannel: OutputChannel, private testManagerServiceFactory: ITestManagerServiceFactory) { - // workspace.onDidChangeConfiguration(this.monitorChangesToTestSettings, this, this.disposables); } public dispose() { this.workspaceTestManagers.forEach(info => info.dispose()); } - public getTestManager(wkspace: Uri): BaseTestManager | undefined { + public getTestManager(resource: Uri): BaseTestManager | undefined { + const wkspace = this.getWorkspace(resource); this.ensureTestManagerService(wkspace); return this.workspaceTestManagers.get(wkspace.fsPath).getTestManager(); } - public getTestWorkingDirectory(wkspace: Uri) { + public getTestWorkingDirectory(resource: Uri) { + const wkspace = this.getWorkspace(resource); this.ensureTestManagerService(wkspace); return this.workspaceTestManagers.get(wkspace.fsPath).getTestWorkingDirectory(); } - public getPreferredTestManager(wkspace: Uri): UnitTestProduct { + public getPreferredTestManager(resource: Uri): UnitTestProduct { + const wkspace = this.getWorkspace(resource); this.ensureTestManagerService(wkspace); return this.workspaceTestManagers.get(wkspace.fsPath).getPreferredTestManager(); } + private getWorkspace(resource: Uri): Uri { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + const noWkspaceMessage = 'Please open a workspace'; + this.outChannel.appendLine(noWkspaceMessage); + throw new Error(noWkspaceMessage); + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const message = `Resource '${resource.fsPath}' does not belong to any workspace`; + this.outChannel.appendLine(message); + throw new Error(message); + } private ensureTestManagerService(wkspace: Uri) { if (!this.workspaceTestManagers.has(wkspace.fsPath)) { this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory.createTestManagerService(wkspace)); - // this.trackTestSettings(wkspace); } } - // private trackTestSettings(wkspace: Uri) { - // const pythonConfig = workspace.getConfiguration('python', wkspace); - // // tslint:disable-next-line:no-backbone-get-set-outside-model - // const unitTestSettings = pythonConfig.get<{}>('unitTest'); - // this.workspaceTestSettings.set(wkspace.fsPath, JSON.stringify(unitTestSettings)); - // } - // private monitorChangesToTestSettings() { - // this.workspaceTestSettings.forEach((_, workspacePath) => { - // const testSettingsChanged = this.checkForChangesInSettings(Uri.file(workspacePath)); - // if (testSettingsChanged) { - // this.rebuildTestManagers(workspace); - // } - // }); - // } - // private rebuildTestManagers(wkspace: Uri) { - // if (!this.workspaceTestManagers.get(wkspace.fsPath)) { - // return; - // } - // const service = this.workspaceTestManagers.get(wkspace.fsPath); - // const mgr = service.getTestManager(); - // mgr.stop(); - // mgr.dispose(); - // service.dispose(); - // this.ensureTestManagerService(wkspace); - // service.getTestManager(); - // } - // private checkForChangesInSettings(wkspace: Uri) { - // const currentSettings = this.workspaceTestSettings.get(wkspace.fsPath); - // this.trackTestSettings(wkspace); - // const newSettings = this.workspaceTestSettings.get(wkspace.fsPath); - // return currentSettings !== newSettings; - // } } diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts index df26a46d0201..6318060a94f7 100644 --- a/src/client/unittests/common/xUnitParser.ts +++ b/src/client/unittests/common/xUnitParser.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as xml2js from 'xml2js'; -import { Tests, TestStatus } from './contracts'; +import { Tests, TestStatus } from './types'; export enum PassCalculationFormulae { pytest, diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 1fa5ad31c836..65d817085acc 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -1,27 +1,27 @@ 'use strict'; import * as path from 'path'; -import { OutputChannel, Uri } from 'vscode'; import * as vscode from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { Installer, Product } from '../common/installer'; import { getSubDirectories } from '../common/utils'; import { TestConfigSettingsService } from './common/configSettingService'; -import { UnitTestProduct } from './common/contracts'; import { TestConfigurationManager } from './common/testConfigurationManager'; import { selectTestWorkspace } from './common/testUtils'; +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'; // tslint:disable-next-line:no-any -async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false): Thenable { +async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { const wkspace = await selectTestWorkspace(); if (!wkspace) { return; } const selectedTestRunner = await selectTestRunner(messageToDisplay); - if (!selectedTestRunner) { + if (typeof selectedTestRunner !== 'number') { return Promise.reject(null); } const configMgr: TestConfigurationManager = createTestConfigurationManager(wkspace, selectedTestRunner, outputChannel); diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts index 3d12da43b2bd..ab232152e295 100644 --- a/src/client/unittests/display/main.ts +++ b/src/client/unittests/display/main.ts @@ -2,8 +2,9 @@ import * as vscode from 'vscode'; import * as constants from '../../common/constants'; import { createDeferred, isNotInstalledError } from '../../common/helpers'; -import { CANCELLATION_REASON, Tests } from '../common/contracts'; +import { CANCELLATION_REASON } from '../common/constants'; import { displayTestErrorMessage } from '../common/testUtils'; +import { Tests } from '../common/types'; export class TestResultDisplay { private statusBar: vscode.StatusBarItem; @@ -156,6 +157,7 @@ export class TestResultDisplay { if (!haveTests) { vscode.window.showInformationMessage('No tests discovered, please check the configuration settings for the tests.', 'Disable Tests').then(item => { if (item === 'Disable Tests') { + // tslint:disable-next-line:no-floating-promises this.disableTests(); } }); diff --git a/src/client/unittests/display/picker.ts b/src/client/unittests/display/picker.ts index e9f24ec5a451..61c18840ae51 100644 --- a/src/client/unittests/display/picker.ts +++ b/src/client/unittests/display/picker.ts @@ -1,23 +1,22 @@ import * as path from 'path'; -import { QuickPickItem, window } from 'vscode'; +import { QuickPickItem, Uri, window } from 'vscode'; import * as vscode from 'vscode'; import * as constants from '../../common/constants'; -import { FlattenedTestFunction, TestFile, TestFunction, Tests, TestStatus } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { FlattenedTestFunction, ITestCollectionStorageService, TestFile, TestFunction, Tests, TestStatus, TestsToRun } from '../common/types'; export class TestDisplay { constructor(private testCollectionStorage: ITestCollectionStorageService) { } - public displayStopTestUI(message: string) { + public displayStopTestUI(workspace: Uri, message: string) { window.showQuickPick([message]).then(item => { if (item === message) { - vscode.commands.executeCommand(constants.Commands.Tests_Stop); + vscode.commands.executeCommand(constants.Commands.Tests_Stop, workspace); } }); } - public displayTestUI(rootDirectory: string) { - const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDirectory)).uri; + public displayTestUI(wkspace: Uri) { const tests = this.testCollectionStorage.getTests(wkspace); - window.showQuickPick(buildItems(rootDirectory, tests), { matchOnDescription: true, matchOnDetail: true }).then(onItemSelected); + window.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then(item => onItemSelected(wkspace, item, false)); } public selectTestFunction(rootDirectory: string, tests: Tests): Promise { return new Promise((resolve, reject) => { @@ -41,13 +40,13 @@ export class TestDisplay { }, reject); }); } - public displayFunctionTestPickerUI(rootDirectory: string, fileName: string, testFunctions: TestFunction[], debug?: boolean) { - const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDirectory)).uri; + public displayFunctionTestPickerUI(wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { const tests = this.testCollectionStorage.getTests(wkspace); if (!tests) { return; } - const testFile = tests.testFiles.find(file => file.name === fileName || file.fullPath === fileName); + const fileName = file.fsPath; + const testFile = tests.testFiles.find(item => item.name === fileName || item.fullPath === fileName); if (!testFile) { return; } @@ -58,7 +57,7 @@ export class TestDisplay { window.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), { matchOnDescription: true, matchOnDetail: true }).then(testItem => { - return onItemSelected(testItem, debug); + return onItemSelected(wkspace, testItem, debug); }); } } @@ -112,7 +111,7 @@ function getSummary(tests?: Tests) { } return statusText.join(', ').trim(); } -function buildItems(rootDirectory: string, tests?: Tests): TestItem[] { +function buildItems(tests?: Tests): TestItem[] { const items: TestItem[] = []; items.push({ description: '', label: 'Run All Unit Tests', type: Type.RunAll }); items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); @@ -188,12 +187,13 @@ function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): T }); return fileItems; } -function onItemSelected(selection: TestItem, debug?: boolean) { +function onItemSelected(wkspace: Uri, selection: TestItem, debug?: boolean) { if (!selection || typeof selection.type !== 'number') { return; } let cmd = ''; - const args = []; + // tslint:disable-next-line:no-any + const args: any[] = [wkspace]; switch (selection.type) { case Type.Null: { return; @@ -220,12 +220,14 @@ function onItemSelected(selection: TestItem, debug?: boolean) { } case Type.RunMethod: { cmd = constants.Commands.Tests_Run; - args.push(selection.fn); + // tslint:disable-next-line:prefer-type-cast + args.push({ testFunction: [selection.fn.testFunction] } as TestsToRun); break; } case Type.DebugMethod: { cmd = constants.Commands.Tests_Debug; - args.push(selection.fn); + // tslint:disable-next-line:prefer-type-cast + 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 248882393ffe..d62bdcaa5857 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,19 +1,18 @@ 'use strict'; import * as vscode from 'vscode'; +import { Uri, window, workspace } from 'vscode'; import { IUnitTestSettings, PythonSettings } from '../common/configSettings'; import * as constants from '../common/constants'; import { PythonSymbolProvider } from '../providers/symbolProvider'; import { activateCodeLenses } from './codeLenses/main'; import { BaseTestManager } from './common/baseTestManager'; -import { - CANCELLATION_REASON, - FlattenedTestFunction, - TestFile, - TestFunction, - TestStatus, - TestsToRun, -} from './common/contracts'; -import { getDiscoveredTests, parseTestName } from './common/testUtils'; +import { CANCELLATION_REASON } from './common/constants'; +import { TestCollectionStorageService } from './common/storageService'; +import { TestManagerServiceFactory } from './common/testManagerServiceFactory'; +import { TestResultsService } from './common/testResultsService'; +import { selectTestWorkspace, TestsHelper } from './common/testUtils'; +import { FlattenedTestFunction, ITestCollectionStorageService, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; +import { WorkspaceTestManagerService } from './common/workspaceTestManagerService'; import { displayTestFrameworkError } from './configuration'; import { TestResultDisplay } from './display/main'; import { TestDisplay } from './display/picker'; @@ -21,51 +20,58 @@ import * as nosetests from './nosetest/main'; import * as pytest from './pytest/main'; import * as unittest from './unittest/main'; -let testManager: BaseTestManager | undefined | null; -let pyTestManager: pytest.TestManager | undefined | null; -let unittestManager: unittest.TestManager | undefined | null; -let nosetestManager: nosetests.TestManager | undefined | null; +let workspaceTestManagerService: IWorkspaceTestManagerService; let testResultDisplay: TestResultDisplay; let testDisplay: TestDisplay; let outChannel: vscode.OutputChannel; const onDidChange: vscode.EventEmitter = new vscode.EventEmitter(); +let testCollectionStorage: ITestCollectionStorageService; export function activate(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, symboldProvider: PythonSymbolProvider) { - // TODO: Add multi workspace support - const settings = PythonSettings.getInstance(); - uniTestSettingsString = JSON.stringify(settings.unitTest); context.subscriptions.push({ dispose: dispose }); outChannel = outputChannel; const disposables = registerCommands(); context.subscriptions.push(...disposables); - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - // Ignore the exceptions returned - // This function is invoked via a command which will be invoked else where in the extension - discoverTests(true).catch(() => { - // Ignore the errors - }); - } + testCollectionStorage = new TestCollectionStorageService(); + const testResultsService = new TestResultsService(); + const testsHelper = new TestsHelper(); + const testManagerServiceFactory = new TestManagerServiceFactory(outChannel, testCollectionStorage, testResultsService, testsHelper); + workspaceTestManagerService = new WorkspaceTestManagerService(outChannel, testManagerServiceFactory); - settings.addListener('change', onConfigChanged); - context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider)); + context.subscriptions.push(autoResetTests()); + context.subscriptions.push(activateCodeLenses(onDidChange, symboldProvider, testCollectionStorage)); context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(onDocumentSaved)); -} -function getTestWorkingDirectory() { - // TODO: Add multi workspace support - const settings = PythonSettings.getInstance(); - return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : vscode.workspace.rootPath!; + autoDiscoverTests(); } +async function getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { + let wkspace: Uri; + 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); + } +} let timeoutId: number; async function onDocumentSaved(doc: vscode.TextDocument): Promise { - let testManager = getTestRunner(); + const testManager = await getTestManager(false, doc.uri); if (!testManager) { return; } - - let tests = await testManager.discoverTests(false, true); + const tests = await testManager.discoverTests(false, true); if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { return; } @@ -76,125 +82,158 @@ async function onDocumentSaved(doc: vscode.TextDocument): Promise { if (timeoutId) { clearTimeout(timeoutId); } - timeoutId = setTimeout(() => { discoverTests(true); }, 1000); + timeoutId = setTimeout(() => discoverTests(doc.uri, true), 1000); } function dispose() { - if (pyTestManager) { - pyTestManager.dispose(); - } - if (nosetestManager) { - nosetestManager.dispose(); - } - if (unittestManager) { - unittestManager.dispose(); - } + workspaceTestManagerService.dispose(); + testCollectionStorage.dispose(); } function registerCommands(): vscode.Disposable[] { const disposables = []; - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, () => { - // Ignore the exceptions returned - // This command will be invoked else where in the extension - discoverTests(true).catch(() => { return null; }); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Discover, (resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked else where in the extension. + // tslint:disable-next-line:no-empty + discoverTests(resource, true).catch(() => { }); })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (testId) => runTestsImpl(testId))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (testId) => runTestsImpl(testId, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(undefined, undefined, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (file: Uri, testToRun?: TestsToRun) => runTestsImpl(file, testToRun))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (file: Uri, testToRun: TestsToRun) => runTestsImpl(file, testToRun, false, true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_View_UI, () => displayUI())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file, testFunctions) => displayPickerUI(file, testFunctions))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI, (file: Uri, testFunctions: TestFunction[]) => displayPickerUI(file, testFunctions))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Picker_UI_Debug, (file, testFunctions) => displayPickerUI(file, testFunctions, true))); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, () => stopTests())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Stop, (resource: Uri) => stopTests(resource))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_ViewOutput, () => outChannel.show())); 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, () => selectAndRunTestMethod())); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, () => selectAndRunTestMethod(true))); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, () => selectAndRunTestFile())); + // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Current_File, () => runCurrentTestFile())); return disposables; } -function displayUI() { - let testManager = getTestRunner(); +async function displayUI() { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayTestUI(getTestWorkingDirectory()); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayTestUI(testManager.workspace); } -function displayPickerUI(file: string, testFunctions: TestFunction[], debug?: boolean) { - let testManager = getTestRunner(); +async function displayPickerUI(file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await getTestManager(true, file); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayFunctionTestPickerUI(getTestWorkingDirectory(), file, testFunctions, debug); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayFunctionTestPickerUI(testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); } -function selectAndRunTestMethod(debug?: boolean) { - let testManager = getTestRunner(); +async function selectAndRunTestMethod(debug?: boolean) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFunction(getTestWorkingDirectory(), tests).then(testFn => { - runTestsImpl(testFn, debug); - }).catch(() => { }); - }); + return; + } + try { + await testManager.discoverTests(true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspace.fsPath, tests); + if (!selectedTestFn) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, debug); } -function selectAndRunTestFile() { - let testManager = getTestRunner(); +async function selectAndRunTestFile() { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); - } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.selectTestFile(getTestWorkingDirectory(), tests).then(testFile => { - runTestsImpl({ testFile: [testFile] }); - }).catch(() => { }); - }); + return; + } + try { + await testManager.discoverTests(true, true); + } catch (ex) { + return; + } + + const tests = testCollectionStorage.getTests(testManager.workspace); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + const selectedFile = await testDisplay.selectTestFile(testManager.workspace.fsPath, tests); + if (!selectedFile) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await runTestsImpl(testManager.workspace, { testFile: [selectedFile] } as TestsToRun); } -function runCurrentTestFile() { +async function runCurrentTestFile() { if (!vscode.window.activeTextEditor) { return; } - const currentFilePath = vscode.window.activeTextEditor.document.fileName; - let testManager = getTestRunner(); + const testManager = await getTestManager(true, window.activeTextEditor.document.uri); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testManager.discoverTests(true, true).then(() => { - const tests = getDiscoveredTests(); - const testFiles = tests.testFiles.filter(testFile => { - return testFile.fullPath === currentFilePath; - }); - if (testFiles.length < 1) { - return; - } - runTestsImpl({ testFile: [testFiles[0]] }); + try { + await testManager.discoverTests(true, true); + } catch (ex) { + return; + } + const tests = testCollectionStorage.getTests(testManager.workspace); + 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 + await runTestsImpl(testManager.workspace, { testFile: [testFiles[0]] } as TestsToRun); } -function displayStopUI(message: string) { - let testManager = getTestRunner(); +async function displayStopUI(message: string) { + const testManager = await getTestManager(true); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - testDisplay = testDisplay ? testDisplay : new TestDisplay(); - testDisplay.displayStopTestUI(message); + testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); + testDisplay.displayStopTestUI(testManager.workspace, message); } + let uniTestSettingsString: string; +function autoResetTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + // tslint:disable-next-line:no-empty + return { dispose: () => { } }; + } + const settings = PythonSettings.getInstance(); + uniTestSettingsString = JSON.stringify(settings.unitTest); + return workspace.onDidChangeConfiguration(() => setTimeout(onConfigChanged, 1000)); +} function onConfigChanged() { - // TODO: Add multi workspace support + // 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 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) + + // 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; @@ -205,71 +244,47 @@ function onConfigChanged() { if (testResultDisplay) { testResultDisplay.enabled = false; } - - if (testManager) { - testManager.stop(); - testManager = null; - } - if (pyTestManager) { - pyTestManager.dispose(); - pyTestManager = null; - } - if (nosetestManager) { - nosetestManager.dispose(); - nosetestManager = null; - } - if (unittestManager) { - unittestManager.dispose(); - unittestManager = null; - } + workspaceTestManagerService.dispose(); return; } - if (testResultDisplay) { testResultDisplay.enabled = true; } - - // No need to display errors - if (settings.unitTest.nosetestsEnabled || settings.unitTest.pyTestEnabled || settings.unitTest.unittestEnabled) { - discoverTests(true); - } + autoDiscoverTests(); } -function getTestRunner() { - const rootDirectory = getTestWorkingDirectory(); - const settings = PythonSettings.getInstance(vscode.Uri.file(rootDirectory)); - if (settings.unitTest.nosetestsEnabled) { - return nosetestManager = nosetestManager ? nosetestManager : new nosetests.TestManager(rootDirectory, outChannel); - } - else if (settings.unitTest.pyTestEnabled) { - return pyTestManager = pyTestManager ? pyTestManager : new pytest.TestManager(rootDirectory, outChannel); +function autoDiscoverTests() { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { + return; } - else if (settings.unitTest.unittestEnabled) { - return unittestManager = unittestManager ? unittestManager : new unittest.TestManager(rootDirectory, outChannel); + const settings = PythonSettings.getInstance(); + if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { + return; } - return null; -} -function stopTests() { - let testManager = getTestRunner(); + // No need to display errors. + // tslint:disable-next-line:no-empty + discoverTests(workspace.workspaceFolders[0].uri, true).catch(() => { }); +} +async function stopTests(resource: Uri) { + const testManager = await getTestManager(true, resource); if (testManager) { testManager.stop(); } } -function discoverTests(ignoreCache?: boolean) { - let testManager = getTestRunner(); +async function discoverTests(resource?: Uri, ignoreCache?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { - displayTestFrameworkError(outChannel); - return Promise.resolve(null); + return; } if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - return testResultDisplay.DisplayDiscoverStatus(testManager.discoverTests(ignoreCache)); - } - else { - return Promise.resolve(null); + const discoveryPromise = testManager.discoverTests(ignoreCache); + testResultDisplay.displayDiscoverStatus(discoveryPromise); + await discoveryPromise; } } +// tslint:disable-next-line:no-any function isTestsToRun(arg: any): arg is TestsToRun { if (arg && arg.testFunction && Array.isArray(arg.testFunction)) { return true; @@ -282,46 +297,21 @@ function isTestsToRun(arg: any): arg is TestsToRun { } return false; } -function isUri(arg: any): arg is vscode.Uri { - return arg && arg.fsPath && typeof arg.fsPath === 'string'; -} -function isFlattenedTestFunction(arg: any): arg is FlattenedTestFunction { - return arg && arg.testFunction && typeof arg.xmlClassName === 'string' && - arg.parentTestFile && typeof arg.testFunction.name === 'string'; -} -function identifyTestType(rootDirectory: string, arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction): TestsToRun | boolean | null | undefined { - if (typeof arg === 'boolean') { - return arg === true; - } - if (isTestsToRun(arg)) { - return arg; - } - if (isFlattenedTestFunction(arg)) { - return { testFunction: [arg.testFunction] }; - } - if (isUri(arg)) { - return parseTestName(arg.fsPath, rootDirectory); - } - return null; -} -function runTestsImpl(arg?: vscode.Uri | TestsToRun | boolean | FlattenedTestFunction, debug: boolean = false) { - let testManager = getTestRunner(); +async function runTestsImpl(resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { + const testManager = await getTestManager(true, resource); if (!testManager) { - return displayTestFrameworkError(outChannel); + return; } - // lastRanTests = testsToRun; - const runInfo = identifyTestType(getTestWorkingDirectory(), arg); - testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); + const promise = testManager.runTest(testsToRun, runFailedTests, debug) + .catch(reason => { + if (reason !== CANCELLATION_REASON) { + outChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); - const ret = typeof runInfo === 'boolean' ? testManager.runTest(runInfo, debug) : testManager.runTest(runInfo as TestsToRun, debug); - let runPromise = ret.catch(reason => { - if (reason !== CANCELLATION_REASON) { - outChannel.appendLine('Error: ' + reason); - } - return Promise.reject(reason); - }); - - testResultDisplay.DisplayProgressStatus(runPromise, debug); + testResultDisplay.displayProgressStatus(promise, debug); + await promise; } diff --git a/src/client/unittests/nosetest/collector.ts b/src/client/unittests/nosetest/collector.ts index 7c60cb8d3830..3a202654e6fd 100644 --- a/src/client/unittests/nosetest/collector.ts +++ b/src/client/unittests/nosetest/collector.ts @@ -4,8 +4,8 @@ import * as path from 'path'; import { CancellationToken } from 'vscode'; import { OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { TestFile, TestFunction, Tests, TestSuite } from '../common/contracts'; -import { convertFileToPackage, extractBetweenDelimiters, flattenTestFiles } from '../common/testUtils'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; import { execPythonFile } from './../../common/utils'; const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; @@ -20,7 +20,7 @@ const argsToExcludeForDiscovery = ['-v', '--verbose', '--failed', '--process-restartworker', '--with-xunit']; const settingsInArgsToExcludeForDiscovery = ['--verbosity']; -export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; let testFiles: TestFile[] = []; @@ -83,7 +83,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: Canc // Exclude tests that don't have any functions or test suites testFiles = testFiles.filter(testFile => testFile.suites.length > 0 || testFile.functions.length > 0); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index fddf3a8be496..ab5c329282f6 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -4,18 +4,19 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestsToRun } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { - super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.nosetestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } // tslint:disable-next-line:no-any public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { @@ -26,6 +27,6 @@ export class TestManager extends BaseTestManager { if (!runFailedTests && args.indexOf('--with-id') === -1) { args.push('--with-id'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index 5717fa75b8f2..69dc425f6b96 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -3,17 +3,16 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { Tests, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { updateResults } from '../common/testUtils'; +import { ITestResultsService, Tests, TestsToRun } from '../common/types'; import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; // tslint:disable-next-line:no-any -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -70,7 +69,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel); } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -81,9 +80,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes } // tslint:disable-next-line:no-any -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); } diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index ff6738f124ff..436c6fac8bd8 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; -import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { constructor(workspace: Uri, outputChannel: vscode.OutputChannel, diff --git a/src/client/unittests/pytest/collector.ts b/src/client/unittests/pytest/collector.ts index d699a2575259..4507f1fdb64c 100644 --- a/src/client/unittests/pytest/collector.ts +++ b/src/client/unittests/pytest/collector.ts @@ -4,8 +4,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { TestFile, TestFunction, Tests, TestSuite } from '../common/contracts'; -import { convertFileToPackage, extractBetweenDelimiters, flattenTestFiles } from '../common/testUtils'; +import { convertFileToPackage, extractBetweenDelimiters } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestSuite } from '../common/types'; import { execPythonFile } from './../../common/utils'; const argsToExcludeForDiscovery = ['-x', '--exitfirst', @@ -16,7 +16,7 @@ const argsToExcludeForDiscovery = ['-x', '--exitfirst', '--disable-pytest-warnings', '-l', '--showlocals']; const settingsInArgsToExcludeForDiscovery = []; -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let logOutputLines: string[] = ['']; const testFiles: TestFile[] = []; const parentNodes: { indent: number, item: TestFile | TestSuite }[] = []; @@ -90,7 +90,7 @@ export function discoverTests(rootDirectory: string, args: string[], token: vsco if (token && token.isCancellationRequested) { return Promise.reject('cancelled'); } - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); }); } diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 51835007a3fd..f463765fe441 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -2,24 +2,25 @@ import * as vscode from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestsToRun } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { - super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.pyTestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.pyTestArgs.slice(0); if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } - return runTest(this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index 37998eed9f86..f8c48d08c9ef 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -3,13 +3,12 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { Tests, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { updateResults } from '../common/testUtils'; +import { ITestResultsService, Tests, TestsToRun } from '../common/types'; import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -export function runTest(rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -45,7 +44,7 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel); } }).then(() => { - return updateResultsFromLogFiles(tests, xmlLogFile); + return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); }).then(result => { xmlLogFileCleanup(); return result; @@ -55,9 +54,9 @@ export function runTest(rootDirectory: string, tests: Tests, args: string[], tes }); } -export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string): Promise { +export function updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { return updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }); } diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index a1a0d3b898d5..addda7dd802e 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; -import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { constructor(workspace: Uri, outputChannel: vscode.OutputChannel, diff --git a/src/client/unittests/unittest/collector.ts b/src/client/unittests/unittest/collector.ts index 73e724db87e8..50c6edc19dec 100644 --- a/src/client/unittests/unittest/collector.ts +++ b/src/client/unittests/unittest/collector.ts @@ -3,11 +3,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { OutputChannel } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; -import { TestFile, Tests, TestStatus } from '../common/contracts'; -import { flattenTestFiles } from '../common/testUtils'; +import { ITestsHelper, TestFile, TestFunction, Tests, TestStatus, TestSuite } from '../common/types'; import { execPythonFile } from './../../common/utils'; -export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel): Promise { +export function discoverTests(rootDirectory: string, args: string[], token: vscode.CancellationToken, ignoreCache: boolean, outChannel: OutputChannel, testsHelper: ITestsHelper): Promise { let startDirectory = '.'; let pattern = 'test*.py'; const indexOfStartDir = args.findIndex(arg => arg.indexOf('-s') === 0); @@ -81,17 +80,17 @@ for suite in suites._tests: if (startDirectory.length > 1) { testsDirectory = path.isAbsolute(startDirectory) ? startDirectory : path.resolve(rootDirectory, startDirectory); } - return parseTestIds(testsDirectory, testItems); + return parseTestIds(testsDirectory, testItems, testsHelper); }); } -function parseTestIds(rootDirectory: string, testIds: string[]): Tests { +function parseTestIds(rootDirectory: string, testIds: string[], testsHelper: ITestsHelper): Tests { const testFiles: TestFile[] = []; testIds.forEach(testId => { addTestId(rootDirectory, testId, testFiles); }); - return flattenTestFiles(testFiles); + return testsHelper.flattenTestFiles(testFiles); } function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) { @@ -112,8 +111,10 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) testFile = { name: path.basename(filePath), fullPath: filePath, - functions: [], - suites: [], + // tslint:disable-next-line:prefer-type-cast + functions: [] as TestFunction[], + // tslint:disable-next-line:prefer-type-cast + suites: [] as TestSuite[], nameToRun: `${className}.${functionName}`, xmlName: '', status: TestStatus.Idle, @@ -128,8 +129,10 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) if (!testSuite) { testSuite = { name: className, - functions: [], - suites: [], + // tslint:disable-next-line:prefer-type-cast + functions: [] as TestFunction[], + // tslint:disable-next-line:prefer-type-cast + suites: [] as TestSuite[], isUnitTest: true, isInstance: false, nameToRun: classNameToRun, @@ -140,7 +143,7 @@ function addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) testFile.suites.push(testSuite); } - const testFunction = { + const testFunction: TestFunction = { name: functionName, nameToRun: testId, status: TestStatus.Idle, diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 207120185e05..ab20b11d5338 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -3,22 +3,23 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestStatus, TestsToRun } from '../common/contracts'; -import { ITestCollectionStorageService } from '../common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { - constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService) { - super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage); + constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, + testCollectionStorage: ITestCollectionStorageService, + testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } // tslint:disable-next-line:no-empty public configure() { } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.unittestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel); + return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } - public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { + public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.unittestArgs.slice(0); if (runFailedTests === true) { testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; @@ -26,6 +27,6 @@ export class TestManager extends BaseTestManager { return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; }).map(fn => fn.testFunction); } - return runTest(this, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this, this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index 819fe2db0a72..d6c13c9c2d76 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -1,14 +1,11 @@ -/// - 'use strict'; import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { BaseTestManager } from '../common/baseTestManager'; -import { Tests, TestStatus, TestsToRun } from '../common/contracts'; import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { updateResults } from '../common/testUtils'; +import { ITestResultsService, Tests, TestStatus, TestsToRun } from '../common/types'; import { Server } from './socketServer'; type TestStatusMap = { status: TestStatus; @@ -33,7 +30,7 @@ interface ITestData { } // tslint:disable-next-line:max-func-body-length -export function runTest(testManager: BaseTestManager, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testManager: BaseTestManager, testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { tests.summary.errors = 0; tests.summary.failures = 0; tests.summary.passed = 0; @@ -132,7 +129,7 @@ export function runTest(testManager: BaseTestManager, rootDirectory: string, tes } return promise; }).then(() => { - updateResults(tests); + testResultsService.updateResults(tests); return tests; }).catch(reason => { return Promise.reject(reason); diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 2b3c12bba765..2113f874f4c1 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -1,8 +1,8 @@ import * as path from 'path'; import { OutputChannel, Uri } from 'vscode'; import { Installer, Product } from '../../common/installer'; -import { ITestConfigSettingsService } from '../common/contracts'; import { TestConfigurationManager } from '../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../common/types'; export class ConfigurationManager extends TestConfigurationManager { constructor(workspace: Uri, outputChannel: OutputChannel, diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts index 3a5c64e55b4e..1be92e608df4 100644 --- a/src/client/workspaceSymbols/main.ts +++ b/src/client/workspaceSymbols/main.ts @@ -5,6 +5,7 @@ import { fsExistsAsync } from '../common/utils'; import { isNotInstalledError } from '../common/helpers'; import { PythonLanguage, Commands } from '../common/constants'; import { WorkspaceSymbolProvider } from './provider'; +import { workspace } from 'vscode'; const MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD = 2; @@ -101,7 +102,7 @@ export class WorkspaceSymbols implements vscode.Disposable { continue; } else { - promptPromise = this.installer.promptToInstall(Product.ctags); + promptPromise = this.installer.promptToInstall(Product.ctags, workspace.workspaceFolders[0].uri); promptResponse = await promptPromise; } if (promptResponse !== InstallerResponse.Installed || (!token || token.isCancellationRequested)) { diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index d61e4e0d9944..9e531a393d85 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -2,7 +2,10 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { Tests, TestsToRun } from '../../client/unittests/common/contracts'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import * as nose from '../../client/unittests/nosetest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; @@ -23,6 +26,10 @@ suite('Unit Tests (nosetest)', () => { let testManager: nose.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; + suiteSetup(async () => { filesToDelete.forEach(file => { if (fs.existsSync(file)) { @@ -51,7 +58,10 @@ suite('Unit Tests (nosetest)', () => { await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new nose.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new nose.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); } test('Discover Tests (single test file)', async () => { @@ -130,7 +140,7 @@ suite('Unit Tests (nosetest)', () => { assert.equal(results.summary.passed, 6, 'Passed'); assert.equal(results.summary.skipped, 2, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 1, 'Errors again'); assert.equal(results.summary.failures, 7, 'Failures again'); assert.equal(results.summary.passed, 0, 'Passed again'); diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index c0fb8dc64540..e2b9ffde4123 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -1,7 +1,10 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { TestFile, TestsToRun } from '../../client/unittests/common/contracts'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestFile, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import * as pytest from '../../client/unittests/pytest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; @@ -19,6 +22,9 @@ suite('Unit Tests (PyTest)', () => { let testManager: pytest.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: vscode.OutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; suiteSetup(async () => { await initialize(); @@ -37,11 +43,17 @@ suite('Unit Tests (PyTest)', () => { await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new pytest.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); } test('Discover Tests (single test file)', async () => { - testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); @@ -106,7 +118,7 @@ suite('Unit Tests (PyTest)', () => { assert.equal(results.summary.passed, 17, 'Passed'); assert.equal(results.summary.skipped, 3, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 0, 'Failed Errors'); assert.equal(results.summary.failures, 9, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index 478111c1d620..99397a7855cc 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -2,7 +2,10 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget } from 'vscode'; -import { TestsToRun } from '../../client/unittests/common/contracts'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; import { TestResultDisplay } from '../../client/unittests/display/main'; import * as unittest from '../../client/unittests/unittest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; @@ -27,6 +30,9 @@ suite('Unit Tests (unittest)', () => { let testManager: unittest.TestManager; let testResultDisplay: TestResultDisplay; let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let testsHelper: ITestsHelper; const rootDirectory = UNITTEST_TEST_FILES_PATH; const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suiteSetup(async () => { @@ -48,12 +54,18 @@ suite('Unit Tests (unittest)', () => { testResultDisplay.dispose(); }); function createTestManager(rootDir: string = rootDirectory) { - testManager = new unittest.TestManager(rootDir, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); } test('Discover Tests (single test file)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel); + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); @@ -101,7 +113,7 @@ suite('Unit Tests (unittest)', () => { assert.equal(results.summary.passed, 3, 'Passed'); assert.equal(results.summary.skipped, 1, 'skipped'); - results = await testManager.runTest(true); + results = await testManager.runTest(undefined, true); assert.equal(results.summary.errors, 1, 'Failed Errors'); assert.equal(results.summary.failures, 4, 'Failed Failures'); assert.equal(results.summary.passed, 0, 'Failed Passed'); diff --git a/typings.json b/typings.json deleted file mode 100644 index 9f6c5ea9c4a9..000000000000 --- a/typings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "globalDependencies": { - "fs-extra": "registry:dt/fs-extra#0.0.0+20160319124112", - "xml2js": "registry:dt/xml2js#0.0.0+20160317120654" - } -} diff --git a/typings/globals/xml2js/index.d.ts b/typings/globals/xml2js/index.d.ts deleted file mode 100644 index a3feea4d5ab8..000000000000 --- a/typings/globals/xml2js/index.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Generated by typings -// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts -declare module 'xml2js' { - - export = xml2js; - - namespace xml2js { - function parseString(xml: string, callback: (err: any, result: any) => void): void; - function parseString(xml: string, options: Options, callback: (err: any, result: any) => void): void; - - var defaults: { - '0.1': Options; - '0.2': OptionsV2; - } - - class Builder { - constructor(options?: BuilderOptions); - buildObject(rootObj: any): string; - } - - class Parser { - constructor(options?: Options); - processAsync(): any; - assignOrPush(obj: any, key: string, newValue: any): any; - reset(): any; - parseString(str: string , cb?: Function): void; - } - - interface RenderOptions { - indent?: string; - newline?: string; - pretty?: boolean; - } - - interface XMLDeclarationOptions { - encoding?: string; - standalone?: boolean; - version?: string; - } - - interface BuilderOptions { - doctype?: any; - headless?: boolean; - indent?: string; - newline?: string; - pretty?: boolean; - renderOpts?: RenderOptions; - rootName?: string; - xmldec?: XMLDeclarationOptions; - } - - interface Options { - async?: boolean; - attrkey?: string; - attrNameProcessors?: [(name: string) => string]; - attrValueProcessors?: [(name: string) => string]; - charkey?: string; - charsAsChildren?: boolean; - childkey?: string; - emptyTag?: any; - explicitArray?: boolean; - explicitCharkey?: boolean; - explicitChildren?: boolean; - explicitRoot?: boolean; - ignoreAttrs?: boolean; - mergeAttrs?: boolean; - normalize?: boolean; - normalizeTags?: boolean; - strict?: boolean; - tagNameProcessors?: [(name: string) => string]; - trim?: boolean; - validator?: Function; - valueProcessors?: [(name: string) => string]; - xmlns?: boolean; - } - - interface OptionsV2 extends Options { - preserveChildrenOrder?: boolean; - rootName?: string; - xmldec?: { - version: string; - encoding?: string; - standalone?: boolean; - }; - doctype?: any; - renderOpts?: { - pretty?: boolean; - indent?: string; - newline?: string; - }; - headless?: boolean; - chunkSize?: number; - cdata?: boolean; - } - } -} diff --git a/typings/globals/xml2js/typings.json b/typings/globals/xml2js/typings.json deleted file mode 100644 index 4f70efbc7f27..000000000000 --- a/typings/globals/xml2js/typings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "resolution": "main", - "tree": { - "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts", - "raw": "registry:dt/xml2js#0.0.0+20160317120654", - "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/xml2js/xml2js.d.ts" - } -} diff --git a/typings/index.d.ts b/typings/index.d.ts deleted file mode 100644 index 3e9f3c454ee6..000000000000 --- a/typings/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/typings/node.d.ts b/typings/node.d.ts deleted file mode 100644 index 457fd135e6f6..000000000000 --- a/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts deleted file mode 100644 index e9d47fd5a066..000000000000 --- a/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file From 6c1e20785b90e94dcbdd7b165e4b47ffed4426a9 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 27 Oct 2017 12:56:58 -0700 Subject: [PATCH 11/23] removed grep --- src/test/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/index.ts b/src/test/index.ts index c704c5684564..64de9103f775 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -8,7 +8,6 @@ testRunner.configure({ ui: 'tdd', useColors: true, timeout: 25000, - retries: 3, - grep: 'Python Path Settings Updater' + retries: 3 }); module.exports = testRunner; From c753dbe8b959f29e42f96c3029cda2cef4a38058 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Sun, 29 Oct 2017 05:19:40 -0700 Subject: [PATCH 12/23] copy changes for #948 and #1353 into multroot --- src/client/unittests/unittest/runner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index d6c13c9c2d76..5654a5dab64d 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -87,7 +87,8 @@ export function runTest(testManager: BaseTestManager, testResultsService: ITestR testArgs.push(`--result-port=${port}`); if (debug === true) { - testArgs.push(...['--secret=my_secret', '--port=3000']); + const debugPort = PythonSettings.getInstance(Uri.file(rootDirectory)).unitTest.debugPort; + testArgs.push(...['--secret=my_secret', `--port=${debugPort}`]); } testArgs.push(`--us=${startTestDiscoveryDirectory}`); if (testId.length > 0) { From 57821f9338b290fa57894c63526de22e72df56eb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Sun, 29 Oct 2017 07:15:58 -0700 Subject: [PATCH 13/23] replicate solution for #1041 and #1354 --- src/client/common/utils.ts | 4 +- .../unittests/common/baseTestManager.ts | 70 +++++++++++++------ src/client/unittests/main.ts | 12 ++-- src/client/unittests/nosetest/main.ts | 4 +- src/client/unittests/pytest/main.ts | 4 +- src/client/unittests/unittest/main.ts | 4 +- 6 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/client/common/utils.ts b/src/client/common/utils.ts index 2d171de81d19..85930a837a8c 100644 --- a/src/client/common/utils.ts +++ b/src/client/common/utils.ts @@ -165,7 +165,7 @@ function handleResponse(file: string, includeErrorAsResponse: boolean, error: Er // pylint: // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr - // These error messages are useless when using pylint + // These error messages are useless when using pylint if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { return Promise.resolve(stdout + '\n' + stderr); } @@ -189,7 +189,7 @@ function handlePythonModuleResponse(includeErrorAsResponse: boolean, error: Erro // pylint: // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr - // These error messages are useless when using pylint + // These error messages are useless when using pylint if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { return Promise.resolve(stdout + '\n' + stderr); } diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index b949dd6dec12..1e631f83125e 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -14,7 +14,8 @@ export abstract class BaseTestManager { private tests: Tests; // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; - private cancellationTokenSource: vscode.CancellationTokenSource; + private discoveryCancellationTokenSource: vscode.CancellationTokenSource; + private testRunnerCancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; private discoverTestsPromise: Promise; constructor(private testProvider: string, private product: Product, protected rootDirectory: string, @@ -25,8 +26,11 @@ export abstract class BaseTestManager { this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); this.workspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory)).uri; } - protected get cancellationToken(): vscode.CancellationToken | undefined { - return this.cancellationTokenSource ? this.cancellationTokenSource.token : undefined; + protected get testDiscoveryCancellationToken(): vscode.CancellationToken | undefined { + return this.discoveryCancellationTokenSource ? this.discoveryCancellationTokenSource.token : undefined; + } + protected get testRunnerCancellationToken(): vscode.CancellationToken | undefined { + return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; } public dispose() { this.stop(); @@ -39,8 +43,11 @@ export abstract class BaseTestManager { return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.rootDirectory; } public stop() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.cancel(); + if (this.discoveryCancellationTokenSource) { + this.discoveryCancellationTokenSource.cancel(); + } + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); } } public reset() { @@ -54,7 +61,7 @@ export abstract class BaseTestManager { this.testResultsService.resetResults(this.tests); } - public async discoverTests(ignoreCache: boolean = false, quietMode: boolean = false): Promise { + public async discoverTests(ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise { if (this.discoverTestsPromise) { return this.discoverTestsPromise; } @@ -65,7 +72,13 @@ export abstract class BaseTestManager { } this._status = TestStatus.Discovering; - this.createCancellationToken(); + // If ignoreCache is true, its an indication of the fact that its a user invoked operation. + // Hence we can stop the debugger. + if (userInitiated) { + this.stop(); + } + + this.createCancellationTokens(); return this.discoverTestsPromise = this.discoverTestsImpl(ignoreCache) .then(tests => { this.tests = tests; @@ -89,7 +102,7 @@ export abstract class BaseTestManager { } const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, tests); - this.disposeCancellationToken(); + this.disposeCancellationTokens(); return tests; }).catch(reason => { @@ -100,7 +113,7 @@ export abstract class BaseTestManager { this.tests = null; this.discoverTestsPromise = null; - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; } else { @@ -111,7 +124,7 @@ export abstract class BaseTestManager { } const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, null); - this.disposeCancellationToken(); + this.disposeCancellationTokens(); return Promise.reject(reason); }); } @@ -144,14 +157,16 @@ export abstract class BaseTestManager { } this._status = TestStatus.Running; - this.createCancellationToken(); + this.stop(); + this.createCancellationTokens(true); + // If running failed tests, then don't clear the previously build UnitTests // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) const clearDiscoveredTestCache = runFailedTests || moreInfo.Run_Specific_File || moreInfo.Run_Specific_Class || moreInfo.Run_Specific_Function ? false : true; - return this.discoverTests(clearDiscoveredTestCache, true) + return this.discoverTests(clearDiscoveredTestCache, true, true) .catch(reason => { - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { return Promise.reject(reason); } displayTestErrorMessage('Errors in discovering tests, continuing with tests'); @@ -164,30 +179,39 @@ export abstract class BaseTestManager { return this.runTestImpl(tests, testsToRun, runFailedTests, debug); }).then(() => { this._status = TestStatus.Idle; - this.disposeCancellationToken(); + this.disposeCancellationTokens(true); return this.tests; }).catch(reason => { - if (this.cancellationToken && this.cancellationToken.isCancellationRequested) { + if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { reason = CANCELLATION_REASON; this._status = TestStatus.Idle; } else { this._status = TestStatus.Error; } - this.disposeCancellationToken(); + this.disposeCancellationTokens(true); return Promise.reject(reason); }); } // tslint:disable-next-line:no-any protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; protected abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; - private createCancellationToken() { - this.disposeCancellationToken(); - this.cancellationTokenSource = new vscode.CancellationTokenSource(); + private createCancellationTokens(createTestRunnerToken: boolean = false) { + this.disposeCancellationTokens(createTestRunnerToken); + this.discoveryCancellationTokenSource = new vscode.CancellationTokenSource(); + if (createTestRunnerToken) { + this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); + } } - private disposeCancellationToken() { - if (this.cancellationTokenSource) { - this.cancellationTokenSource.dispose(); + private disposeCancellationTokens(disposeTestRunnerToken: boolean = false) { + if (this.discoveryCancellationTokenSource) { + this.discoveryCancellationTokenSource.dispose(); + } + this.discoveryCancellationTokenSource = null; + if (disposeTestRunnerToken) { + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.dispose(); + } + this.testRunnerCancellationTokenSource = null; } - this.cancellationTokenSource = null; } } diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index d62bdcaa5857..b570e9bd923d 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -95,7 +95,7 @@ function registerCommands(): vscode.Disposable[] { // Ignore the exceptions returned. // This command will be invoked else where in the extension. // tslint:disable-next-line:no-empty - discoverTests(resource, true).catch(() => { }); + discoverTests(resource, true, true).catch(() => { }); })); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(undefined, undefined, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper @@ -146,7 +146,7 @@ async function selectAndRunTestMethod(debug?: boolean) { return; } try { - await testManager.discoverTests(true, true); + await testManager.discoverTests(true, true, true); } catch (ex) { return; } @@ -166,7 +166,7 @@ async function selectAndRunTestFile() { return; } try { - await testManager.discoverTests(true, true); + await testManager.discoverTests(true, true, true); } catch (ex) { return; } @@ -189,7 +189,7 @@ async function runCurrentTestFile() { return; } try { - await testManager.discoverTests(true, true); + await testManager.discoverTests(true, true, true); } catch (ex) { return; } @@ -271,7 +271,7 @@ async function stopTests(resource: Uri) { testManager.stop(); } } -async function discoverTests(resource?: Uri, ignoreCache?: boolean) { +async function discoverTests(resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean) { const testManager = await getTestManager(true, resource); if (!testManager) { return; @@ -279,7 +279,7 @@ async function discoverTests(resource?: Uri, ignoreCache?: boolean) { if (testManager && (testManager.status !== TestStatus.Discovering && testManager.status !== TestStatus.Running)) { testResultDisplay = testResultDisplay ? testResultDisplay : new TestResultDisplay(outChannel, onDidChange); - const discoveryPromise = testManager.discoverTests(ignoreCache); + const discoveryPromise = testManager.discoverTests(ignoreCache, false, userInitiated); testResultDisplay.displayDiscoverStatus(discoveryPromise); await discoveryPromise; } diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index ab5c329282f6..9687da404418 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -16,7 +16,7 @@ export class TestManager extends BaseTestManager { } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.nosetestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } // tslint:disable-next-line:no-any public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { @@ -27,6 +27,6 @@ export class TestManager extends BaseTestManager { if (!runFailedTests && args.indexOf('--with-id') === -1) { args.push('--with-id'); } - return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index f463765fe441..74f968a00f05 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -14,13 +14,13 @@ export class TestManager extends BaseTestManager { } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.pyTestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.pyTestArgs.slice(0); if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } - return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index ab20b11d5338..6b91cf0c119d 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -17,7 +17,7 @@ export class TestManager extends BaseTestManager { } public discoverTestsImpl(ignoreCache: boolean): Promise { const args = this.settings.unitTest.unittestArgs.slice(0); - return discoverTests(this.rootDirectory, args, this.cancellationToken, ignoreCache, this.outputChannel, this.testsHelper); + return discoverTests(this.rootDirectory, args, this.testDiscoveryCancellationToken, ignoreCache, this.outputChannel, this.testsHelper); } public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { const args = this.settings.unitTest.unittestArgs.slice(0); @@ -27,6 +27,6 @@ export class TestManager extends BaseTestManager { return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; }).map(fn => fn.testFunction); } - return runTest(this, this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.cancellationToken, this.outputChannel, debug); + return runTest(this, this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } From 042dce1314a7443e26ea52d2f27bee063207eefb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Sun, 29 Oct 2017 21:00:15 -0700 Subject: [PATCH 14/23] #1041 create enum for creation of cancellation token --- .../unittests/common/baseTestManager.ts | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index 1e631f83125e..da0a356a7519 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -8,13 +8,18 @@ import { CANCELLATION_REASON } from './constants'; import { displayTestErrorMessage } from './testUtils'; import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; +enum CancellationTokenType { + testDicovery, + testRunner +} + export abstract class BaseTestManager { public readonly workspace: Uri; protected readonly settings: IPythonSettings; private tests: Tests; // tslint:disable-next-line:variable-name private _status: TestStatus = TestStatus.Unknown; - private discoveryCancellationTokenSource: vscode.CancellationTokenSource; + private testDiscoveryCancellationTokenSource: vscode.CancellationTokenSource; private testRunnerCancellationTokenSource: vscode.CancellationTokenSource; private installer: Installer; private discoverTestsPromise: Promise; @@ -27,7 +32,7 @@ export abstract class BaseTestManager { this.workspace = workspace.getWorkspaceFolder(Uri.file(this.rootDirectory)).uri; } protected get testDiscoveryCancellationToken(): vscode.CancellationToken | undefined { - return this.discoveryCancellationTokenSource ? this.discoveryCancellationTokenSource.token : undefined; + return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; } protected get testRunnerCancellationToken(): vscode.CancellationToken | undefined { return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; @@ -43,8 +48,8 @@ export abstract class BaseTestManager { return settings.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.rootDirectory; } public stop() { - if (this.discoveryCancellationTokenSource) { - this.discoveryCancellationTokenSource.cancel(); + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.cancel(); } if (this.testRunnerCancellationTokenSource) { this.testRunnerCancellationTokenSource.cancel(); @@ -78,7 +83,7 @@ export abstract class BaseTestManager { this.stop(); } - this.createCancellationTokens(); + this.createCancellationToken(CancellationTokenType.testDicovery); return this.discoverTestsPromise = this.discoverTestsImpl(ignoreCache) .then(tests => { this.tests = tests; @@ -102,7 +107,7 @@ export abstract class BaseTestManager { } const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, tests); - this.disposeCancellationTokens(); + this.disposeCancellationToken(CancellationTokenType.testDicovery); return tests; }).catch(reason => { @@ -124,7 +129,7 @@ export abstract class BaseTestManager { } const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, null); - this.disposeCancellationTokens(); + this.disposeCancellationToken(CancellationTokenType.testDicovery); return Promise.reject(reason); }); } @@ -158,7 +163,6 @@ export abstract class BaseTestManager { this._status = TestStatus.Running; this.stop(); - this.createCancellationTokens(true); // If running failed tests, then don't clear the previously build UnitTests // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests @@ -166,7 +170,7 @@ export abstract class BaseTestManager { const clearDiscoveredTestCache = runFailedTests || moreInfo.Run_Specific_File || moreInfo.Run_Specific_Class || moreInfo.Run_Specific_Function ? false : true; return this.discoverTests(clearDiscoveredTestCache, true, true) .catch(reason => { - if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { + if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { return Promise.reject(reason); } displayTestErrorMessage('Errors in discovering tests, continuing with tests'); @@ -176,10 +180,11 @@ export abstract class BaseTestManager { }; }) .then(tests => { + this.createCancellationToken(CancellationTokenType.testRunner); return this.runTestImpl(tests, testsToRun, runFailedTests, debug); }).then(() => { this._status = TestStatus.Idle; - this.disposeCancellationTokens(true); + this.disposeCancellationToken(CancellationTokenType.testRunner); return this.tests; }).catch(reason => { if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { @@ -188,26 +193,28 @@ export abstract class BaseTestManager { } else { this._status = TestStatus.Error; } - this.disposeCancellationTokens(true); + this.disposeCancellationToken(CancellationTokenType.testRunner); return Promise.reject(reason); }); } // tslint:disable-next-line:no-any protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; protected abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; - private createCancellationTokens(createTestRunnerToken: boolean = false) { - this.disposeCancellationTokens(createTestRunnerToken); - this.discoveryCancellationTokenSource = new vscode.CancellationTokenSource(); - if (createTestRunnerToken) { + private createCancellationToken(tokenType: CancellationTokenType) { + this.disposeCancellationToken(tokenType); + if (tokenType === CancellationTokenType.testDicovery) { + this.testDiscoveryCancellationTokenSource = new vscode.CancellationTokenSource(); + } else { this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); } } - private disposeCancellationTokens(disposeTestRunnerToken: boolean = false) { - if (this.discoveryCancellationTokenSource) { - this.discoveryCancellationTokenSource.dispose(); - } - this.discoveryCancellationTokenSource = null; - if (disposeTestRunnerToken) { + private disposeCancellationToken(tokenType: CancellationTokenType) { + if (tokenType === CancellationTokenType.testDicovery) { + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.dispose(); + } + this.testDiscoveryCancellationTokenSource = null; + } else { if (this.testRunnerCancellationTokenSource) { this.testRunnerCancellationTokenSource.dispose(); } From 3cf8a9ccf7587e1a5ba8950e792c72bfb936ced0 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 30 Oct 2017 14:25:10 -0700 Subject: [PATCH 15/23] multiroot support for unit tests --- package.json | 7 +- src/client/unittests/common/debugLauncher.ts | 111 +++++----- .../unittests/common/testManagerService.ts | 14 +- .../common/testManagerServiceFactory.ts | 7 +- src/client/unittests/common/types.ts | 10 +- src/client/unittests/main.ts | 10 +- src/client/unittests/nosetest/main.ts | 6 +- src/client/unittests/nosetest/runner.ts | 11 +- src/client/unittests/pytest/main.ts | 6 +- src/client/unittests/pytest/runner.ts | 11 +- src/client/unittests/unittest/main.ts | 6 +- src/client/unittests/unittest/runner.ts | 21 +- src/test/.vscode/settings.json | 8 +- src/test/common.ts | 11 +- src/test/index.ts | 4 +- .../debuggerTest/tests/test_debugger_one.py | 8 + .../debuggerTest/tests/test_debugger_two.py | 8 + .../debuggerTest/tests/test_debugger_two.txt | 8 + .../tests/test_debugger_two.updated.txt | 14 ++ src/test/unittests/debugger.test.ts | 197 ++++++++++++++++++ src/test/unittests/mocks.ts | 40 ++++ src/test/unittests/nosetest.test.ts | 6 +- src/test/unittests/pytest.test.ts | 5 +- src/test/unittests/rediscover.test.ts | 108 ++++++++++ src/test/unittests/unittest.test.ts | 8 +- 25 files changed, 534 insertions(+), 111 deletions(-) create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt create mode 100644 src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt create mode 100644 src/test/unittests/debugger.test.ts create mode 100644 src/test/unittests/mocks.ts create mode 100644 src/test/unittests/rediscover.test.ts diff --git a/package.json b/package.json index b1736078a793..63f80480c85c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "jupyter", "django", "debugger", - "unittest" + "unittest", + "multi-root ready" ], "categories": [ "Languages", @@ -1629,6 +1630,8 @@ "vscode": "^1.1.5" }, "devDependencies": { + "@types/chai": "^4.0.4", + "@types/chai-as-promised": "^7.1.0", "@types/fs-extra": "^4.0.2", "@types/jquery": "^1.10.31", "@types/lodash": "^4.14.74", @@ -1645,6 +1648,8 @@ "babel-core": "^6.14.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.14.0", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", "event-stream": "^3.3.4", "gulp": "^3.9.1", "gulp-filter": "^5.0.1", diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts index 67d741018674..783dbc24fd7c 100644 --- a/src/client/unittests/common/debugLauncher.ts +++ b/src/client/unittests/common/debugLauncher.ts @@ -3,63 +3,66 @@ import { CancellationToken, debug, OutputChannel, Uri, workspace } from 'vscode' import { PythonSettings } from '../../common/configSettings'; import { createDeferred } from './../../common/helpers'; import { execPythonFile } from './../../common/utils'; +import { ITestDebugLauncher } from './types'; -export function launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel) { - const pythonSettings = PythonSettings.getInstance(rootDirectory ? Uri.file(rootDirectory) : undefined); - // tslint:disable-next-line:no-any - const def = createDeferred(); - // tslint:disable-next-line:no-any - const launchDef = createDeferred(); - let outputChannelShown = false; - execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { - if (data.startsWith(`READY${os.EOL}`)) { - // debug socket server has started. - launchDef.resolve(); - data = data.substring((`READY${os.EOL}`).length); - } +export class DebugLauncher implements ITestDebugLauncher { + public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel) { + const pythonSettings = PythonSettings.getInstance(rootDirectory ? Uri.file(rootDirectory) : undefined); + // tslint:disable-next-line:no-any + const def = createDeferred(); + // tslint:disable-next-line:no-any + const launchDef = createDeferred(); + let outputChannelShown = false; + execPythonFile(rootDirectory, pythonSettings.pythonPath, testArgs, rootDirectory, true, (data: string) => { + if (data.startsWith(`READY${os.EOL}`)) { + // debug socket server has started. + launchDef.resolve(); + data = data.substring((`READY${os.EOL}`).length); + } - if (!outputChannelShown) { - outputChannelShown = true; - outChannel.show(); - } - outChannel.append(data); - }, token).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }).then(() => { - if (!def.rejected && !def.resolved) { - def.resolve(); - } - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); + if (!outputChannelShown) { + outputChannelShown = true; + outChannel.show(); + } + outChannel.append(data); + }, token).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }).then(() => { + if (!def.rejected && !def.resolved) { + def.resolve(); + } + }).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } + }); - launchDef.promise.then(() => { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - throw new Error('Please open a workspace'); - } - let workspaceFolder = workspace.getWorkspaceFolder(Uri.file(rootDirectory)); - if (!workspaceFolder) { - workspaceFolder = workspace.workspaceFolders[0]; - } - return debug.startDebugging(workspaceFolder, { - name: 'Debug Unit Test', - type: 'python', - request: 'attach', - localRoot: rootDirectory, - remoteRoot: rootDirectory, - port: pythonSettings.unitTest.debugPort, - secret: 'my_secret', - host: 'localhost' + launchDef.promise.then(() => { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + throw new Error('Please open a workspace'); + } + let workspaceFolder = workspace.getWorkspaceFolder(Uri.file(rootDirectory)); + if (!workspaceFolder) { + workspaceFolder = workspace.workspaceFolders[0]; + } + return debug.startDebugging(workspaceFolder, { + name: 'Debug Unit Test', + type: 'python', + request: 'attach', + localRoot: rootDirectory, + remoteRoot: rootDirectory, + port: pythonSettings.unitTest.debugPort, + secret: 'my_secret', + host: 'localhost' + }); + }).catch(reason => { + if (!def.rejected && !def.resolved) { + def.reject(reason); + } }); - }).catch(reason => { - if (!def.rejected && !def.resolved) { - def.reject(reason); - } - }); - return def.promise; + return def.promise; + } } diff --git a/src/client/unittests/common/testManagerService.ts b/src/client/unittests/common/testManagerService.ts index a4d7b32850d2..87cfa5d2c1e4 100644 --- a/src/client/unittests/common/testManagerService.ts +++ b/src/client/unittests/common/testManagerService.ts @@ -5,22 +5,23 @@ import { TestManager as NoseTestManager } from '../nosetest/main'; import { TestManager as PyTestTestManager } from '../pytest/main'; import { TestManager as UnitTestTestManager } from '../unittest/main'; import { BaseTestManager } from './baseTestManager'; -import { ITestCollectionStorageService, ITestManagerService, ITestResultsService, ITestsHelper, UnitTestProduct } from './types'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestManagerService, ITestResultsService, ITestsHelper, UnitTestProduct } from './types'; type TestManagerInstanceInfo = { instance?: BaseTestManager, create(rootDirectory: string): BaseTestManager }; export class TestManagerService implements ITestManagerService { private testManagers = new Map(); constructor(private wkspace: Uri, private outChannel: OutputChannel, - testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + testCollectionStorage: ITestCollectionStorageService, testResultsService: ITestResultsService, + testsHelper: ITestsHelper, debugLauncher: ITestDebugLauncher) { this.testManagers.set(Product.nosetest, { - create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + create: (rootDirectory: string) => new NoseTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) }); this.testManagers.set(Product.pytest, { - create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + create: (rootDirectory: string) => new PyTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) }); this.testManagers.set(Product.unittest, { - create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper) + create: (rootDirectory: string) => new UnitTestTestManager(rootDirectory, this.outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher) }); } public dispose() { @@ -36,7 +37,8 @@ export class TestManagerService implements ITestManagerService { return; } - const info = this.testManagers.get(preferredTestManager); + // tslint:disable-next-line:no-non-null-assertion + const info = this.testManagers.get(preferredTestManager)!; if (!info.instance) { const testDirectory = this.getTestWorkingDirectory(); info.instance = info.create(testDirectory); diff --git a/src/client/unittests/common/testManagerServiceFactory.ts b/src/client/unittests/common/testManagerServiceFactory.ts index 416cd713e9d0..f10600ef8b45 100644 --- a/src/client/unittests/common/testManagerServiceFactory.ts +++ b/src/client/unittests/common/testManagerServiceFactory.ts @@ -1,11 +1,12 @@ import { OutputChannel, Uri } from 'vscode'; import { TestManagerService } from './testManagerService'; -import { ITestCollectionStorageService, ITestManagerService, ITestManagerServiceFactory, ITestResultsService, ITestsHelper } from './types'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestManagerService, ITestManagerServiceFactory, ITestResultsService, ITestsHelper } from './types'; export class TestManagerServiceFactory implements ITestManagerServiceFactory { constructor(private outChannel: OutputChannel, private testCollectionStorage: ITestCollectionStorageService, - private testResultsService: ITestResultsService, private testsHelper: ITestsHelper) { } + private testResultsService: ITestResultsService, private testsHelper: ITestsHelper, + private debugLauncher: ITestDebugLauncher) { } public createTestManagerService(wkspace: Uri): ITestManagerService { - return new TestManagerService(wkspace, this.outChannel, this.testCollectionStorage, this.testResultsService, this.testsHelper); + return new TestManagerService(wkspace, this.outChannel, this.testCollectionStorage, this.testResultsService, this.testsHelper, this.debugLauncher); } } diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index 9cb84cef8d29..7477456b41e4 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -1,4 +1,4 @@ -import { Disposable, Uri } from 'vscode'; +import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from './baseTestManager'; @@ -112,7 +112,7 @@ export interface ITestConfigSettingsService { export interface ITestManagerService extends Disposable { getTestManager(): BaseTestManager | undefined; getTestWorkingDirectory(): string; - getPreferredTestManager(): UnitTestProduct; + getPreferredTestManager(): UnitTestProduct | undefined; } export interface ITestManagerServiceFactory { @@ -122,7 +122,7 @@ export interface ITestManagerServiceFactory { export interface IWorkspaceTestManagerService extends Disposable { getTestManager(resource: Uri): BaseTestManager | undefined; getTestWorkingDirectory(resource: Uri): string; - getPreferredTestManager(resource: Uri): UnitTestProduct; + getPreferredTestManager(resource: Uri): UnitTestProduct | undefined; } export interface ITestsHelper { @@ -146,3 +146,7 @@ export interface ITestResultsService { resetResults(tests: Tests): void; updateResults(tests: Tests): void; } + +export interface ITestDebugLauncher { + launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise; +} diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index b570e9bd923d..c3d50bc3ff59 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -1,12 +1,13 @@ 'use strict'; -import * as vscode from 'vscode'; import { Uri, window, workspace } from 'vscode'; +import * as vscode from 'vscode'; import { IUnitTestSettings, PythonSettings } from '../common/configSettings'; import * as constants from '../common/constants'; import { PythonSymbolProvider } from '../providers/symbolProvider'; import { activateCodeLenses } from './codeLenses/main'; import { BaseTestManager } from './common/baseTestManager'; import { CANCELLATION_REASON } from './common/constants'; +import { DebugLauncher } from './common/debugLauncher'; import { TestCollectionStorageService } from './common/storageService'; import { TestManagerServiceFactory } from './common/testManagerServiceFactory'; import { TestResultsService } from './common/testResultsService'; @@ -36,7 +37,8 @@ export function activate(context: vscode.ExtensionContext, outputChannel: vscode testCollectionStorage = new TestCollectionStorageService(); const testResultsService = new TestResultsService(); const testsHelper = new TestsHelper(); - const testManagerServiceFactory = new TestManagerServiceFactory(outChannel, testCollectionStorage, testResultsService, testsHelper); + const debugLauncher = new DebugLauncher(); + const testManagerServiceFactory = new TestManagerServiceFactory(outChannel, testCollectionStorage, testResultsService, testsHelper, debugLauncher); workspaceTestManagerService = new WorkspaceTestManagerService(outChannel, testManagerServiceFactory); context.subscriptions.push(autoResetTests()); @@ -47,7 +49,7 @@ export function activate(context: vscode.ExtensionContext, outputChannel: vscode } async function getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { - let wkspace: Uri; + let wkspace: Uri | undefined; if (resource) { const wkspaceFolder = workspace.getWorkspaceFolder(resource); wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; @@ -181,7 +183,7 @@ async function selectAndRunTestFile() { await runTestsImpl(testManager.workspace, { testFile: [selectedFile] } as TestsToRun); } async function runCurrentTestFile() { - if (!vscode.window.activeTextEditor) { + if (!window.activeTextEditor) { return; } const testManager = await getTestManager(true, window.activeTextEditor.document.uri); diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index 9687da404418..f670fb2333a3 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -4,14 +4,14 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService, - testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { super('nosetest', Product.nosetest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } public discoverTestsImpl(ignoreCache: boolean): Promise { @@ -27,6 +27,6 @@ export class TestManager extends BaseTestManager { if (!runFailedTests && args.indexOf('--with-id') === -1) { args.push('--with-id'); } - return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts index 69dc425f6b96..ec67b2adc656 100644 --- a/src/client/unittests/nosetest/runner.ts +++ b/src/client/unittests/nosetest/runner.ts @@ -3,16 +3,15 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { ITestResultsService, Tests, TestsToRun } from '../common/types'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestsToRun } from '../common/types'; import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; const WITH_XUNIT = '--with-xunit'; const XUNIT_FILE = '--xunit-file'; // tslint:disable-next-line:no-any -export function runTest(testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -64,9 +63,11 @@ export function runTest(testResultsService: ITestResultsService, rootDirectory: const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); const nosetestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'nose']; const debuggerArgs = [testLauncherFile].concat(nosetestlauncherargs).concat(noseTestArgs.concat(testPaths)); - return launchDebugger(rootDirectory, debuggerArgs, token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; } else { - return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return run(pythonSettings.unitTest.nosetestPath, noseTestArgs.concat(testPaths), rootDirectory, token, outChannel) as Promise; } }).then(() => { return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 74f968a00f05..ee53c93de207 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -2,14 +2,14 @@ import * as vscode from 'vscode'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService, - testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { super('pytest', Product.pytest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } public discoverTestsImpl(ignoreCache: boolean): Promise { @@ -21,6 +21,6 @@ export class TestManager extends BaseTestManager { if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { args.push('--last-failed'); } - return runTest(this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); + return runTest(this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts index f8c48d08c9ef..3e314b305cab 100644 --- a/src/client/unittests/pytest/runner.ts +++ b/src/client/unittests/pytest/runner.ts @@ -3,12 +3,11 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { createTemporaryFile } from '../../common/helpers'; -import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { ITestResultsService, Tests, TestsToRun } from '../common/types'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestsToRun } from '../common/types'; import { PassCalculationFormulae, updateResultsFromXmlLogFile } from '../common/xUnitParser'; -export function runTest(testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { let testPaths = []; if (testsToRun && testsToRun.testFolder) { testPaths = testPaths.concat(testsToRun.testFolder.map(f => f.nameToRun)); @@ -39,9 +38,11 @@ export function runTest(testResultsService: ITestResultsService, rootDirectory: const testLauncherFile = path.join(__dirname, '..', '..', '..', '..', 'pythonFiles', 'PythonTools', 'testlauncher.py'); const pytestlauncherargs = [rootDirectory, 'my_secret', pythonSettings.unitTest.debugPort.toString(), 'pytest']; const debuggerArgs = [testLauncherFile].concat(pytestlauncherargs).concat(testArgs); - return launchDebugger(rootDirectory, debuggerArgs, token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, debuggerArgs, token, outChannel) as Promise; } else { - return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return run(pythonSettings.unitTest.pyTestPath, testArgs, rootDirectory, token, outChannel) as Promise; } }).then(() => { return updateResultsFromLogFiles(tests, xmlLogFile, testResultsService); diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 6b91cf0c119d..162ec396b6ef 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -3,13 +3,13 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { Product } from '../../common/installer'; import { BaseTestManager } from '../common/baseTestManager'; -import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; +import { ITestCollectionStorageService, ITestDebugLauncher, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from '../common/types'; import { discoverTests } from './collector'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { constructor(rootDirectory: string, outputChannel: vscode.OutputChannel, testCollectionStorage: ITestCollectionStorageService, - testResultsService: ITestResultsService, testsHelper: ITestsHelper) { + testResultsService: ITestResultsService, testsHelper: ITestsHelper, private debugLauncher: ITestDebugLauncher) { super('unitest', Product.unittest, rootDirectory, outputChannel, testCollectionStorage, testResultsService, testsHelper); } // tslint:disable-next-line:no-empty @@ -27,6 +27,6 @@ export class TestManager extends BaseTestManager { return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; }).map(fn => fn.testFunction); } - return runTest(this, this.testResultsService, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); + return runTest(this, this.testResultsService, this.debugLauncher, this.rootDirectory, tests, args, testsToRun, this.testRunnerCancellationToken, this.outputChannel, debug); } } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index 5654a5dab64d..9413970d3fdf 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -3,9 +3,8 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import { BaseTestManager } from '../common/baseTestManager'; -import { launchDebugger } from '../common/debugLauncher'; import { run } from '../common/runner'; -import { ITestResultsService, Tests, TestStatus, TestsToRun } from '../common/types'; +import { ITestDebugLauncher, ITestResultsService, Tests, TestStatus, TestsToRun } from '../common/types'; import { Server } from './socketServer'; type TestStatusMap = { status: TestStatus; @@ -30,7 +29,7 @@ interface ITestData { } // tslint:disable-next-line:max-func-body-length -export function runTest(testManager: BaseTestManager, testResultsService: ITestResultsService, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { +export function runTest(testManager: BaseTestManager, testResultsService: ITestResultsService, debugLauncher: ITestDebugLauncher, rootDirectory: string, tests: Tests, args: string[], testsToRun?: TestsToRun, token?: CancellationToken, outChannel?: OutputChannel, debug?: boolean): Promise { tests.summary.errors = 0; tests.summary.failures = 0; tests.summary.passed = 0; @@ -98,8 +97,10 @@ export function runTest(testManager: BaseTestManager, testResultsService: ITestR testArgs.push(`--testFile=${testFile}`); } if (debug === true) { - return launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), token, outChannel); + // tslint:disable-next-line:prefer-type-cast no-any + return debugLauncher.launchDebugger(rootDirectory, [testLauncherFile].concat(testArgs), token, outChannel); } else { + // tslint:disable-next-line:prefer-type-cast no-any return run(PythonSettings.getInstance(Uri.file(rootDirectory)).pythonPath, [testLauncherFile].concat(testArgs), rootDirectory, token, outChannel); } } @@ -113,22 +114,26 @@ export function runTest(testManager: BaseTestManager, testResultsService: ITestR let promise = Promise.resolve(''); if (Array.isArray(testsToRun.testFile)) { testsToRun.testFile.forEach(testFile => { - promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun) as Promise); }); } if (Array.isArray(testsToRun.testSuite)) { testsToRun.testSuite.forEach(testSuite => { const testFileName = tests.testSuits.find(t => t.testSuite === testSuite).parentTestFile.fullPath; - promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun) as Promise); }); } if (Array.isArray(testsToRun.testFunction)) { testsToRun.testFunction.forEach(testFn => { const testFileName = tests.testFunctions.find(t => t.testFunction === testFn).parentTestFile.fullPath; - promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun)); + // tslint:disable-next-line:prefer-type-cast no-any + promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun) as Promise); }); } - return promise; + // tslint:disable-next-line:prefer-type-cast no-any + return promise as Promise; }).then(() => { testResultsService.updateResults(tests); return tests; diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index 1d54a0da6b81..2218e2cecd87 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -5,10 +5,12 @@ "python.unitTest.nosetestArgs": [], "python.unitTest.pyTestArgs": [], "python.unitTest.unittestArgs": [ - "-s=./tests", - "-p=test_*.py" + "-v", + "-s", + ".", + "-p", + "*test*.py" ], - "python.pythonPath": "python", "python.formatting.formatOnSave": false, "python.sortImports.args": [], "python.linting.lintOnSave": false, diff --git a/src/test/common.ts b/src/test/common.ts index 00ef1caa14f2..446fa1b7fef3 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs-extra'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { PythonSettings } from '../client/common/configSettings'; @@ -12,7 +13,8 @@ export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' | 'linting.flake8Enabled' | 'linting.pep8Enabled' | 'linting.pylamaEnabled' | 'linting.prospectorEnabled' | 'linting.pydocstyleEnabled' | 'linting.mypyEnabled' | 'unitTest.nosetestArgs' | 'unitTest.pyTestArgs' | 'unitTest.unittestArgs' | - 'formatting.formatOnSave' | 'formatting.provider' | 'sortImports.args'; + 'formatting.formatOnSave' | 'formatting.provider' | 'sortImports.args' | + 'unitTest.nosetestsEnabled' | 'unitTest.pyTestEnabled' | 'unitTest.unittestEnabled'; export async function updateSetting(setting: PythonSettingKeys, value: {}, resource: Uri, configTarget: ConfigurationTarget) { const settings = workspace.getConfiguration('python', resource); @@ -91,6 +93,13 @@ async function restoreGlobalPythonPathSetting(): Promise { PythonSettings.dispose(); } +export async function deleteDirectory(dir: string) { + const exists = await fs.pathExists(dir); + if (exists) { + await fs.remove(dir); + } +} + const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath').globalValue; export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); export const setPythonPathInWorkspaceRoot = async (pythonPath: string) => retryAsync(setPythonPathInWorkspace)(undefined, ConfigurationTarget.Workspace, pythonPath); diff --git a/src/test/index.ts b/src/test/index.ts index 64de9103f775..f14ba247bb92 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,6 +1,6 @@ +import * as testRunner from 'vscode/lib/testrunner'; import { initializePython, IS_MULTI_ROOT_TEST } from './initialize'; -const testRunner = require('vscode/lib/testrunner'); -process.env['VSC_PYTHON_CI_TEST'] = '1'; +process.env.VSC_PYTHON_CI_TEST = '1'; // You can directly control Mocha options by uncommenting the following lines. // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info. diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py new file mode 100644 index 000000000000..db18d3885488 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_one_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt new file mode 100644 index 000000000000..b70c80df1619 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt @@ -0,0 +1,14 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_2(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_3(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts new file mode 100644 index 000000000000..866d991c3923 --- /dev/null +++ b/src/test/unittests/debugger.test.ts @@ -0,0 +1,197 @@ +import { assert, expect, should, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget } from 'vscode'; +import { createDeferred } from '../../client/common/helpers'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; +import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; +import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; +import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Debugging', () => { + let testManager: BaseTestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let mockDebugLauncher: MockDebugLauncher; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async function () { + // Test disvovery is where the delay is, hence give 10 seconds (as we discover tests at least twice in each test). + // tslint:disable-next-line:no-invalid-this + this.timeout(10000); + await initialize(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await initializeTest(); + }); + teardown(async () => { + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + outChannel.dispose(); + mockDebugLauncher.dispose(); + if (testManager) { + testManager.dispose(); + } + testResultDisplay.dispose(); + }); + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + mockDebugLauncher = new MockDebugLauncher(); + } + + async function testStartingDebugger() { + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + testManager.runTest({ testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + } + + test('Debugger should start (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + test('Debugger should start (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + test('Debugger should start (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStartingDebugger(); + }); + + async function testStoppingDebugger() { + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest({ testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(true, true, true); + + expect(runningPromise).eventually.throws(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); + } + + test('Debugger should stop when user invokes a test discovery (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + test('Debugger should stop when user invokes a test discovery (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + test('Debugger should stop when user invokes a test discovery (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testStoppingDebugger(); + }); + + async function testDebuggerWhenRediscoveringTests() { + const tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest({ testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(false, true); + const deferred = createDeferred(); + + discoveryPromise + .then(() => deferred.resolve('')) + .catch(ex => deferred.reject(ex)); + + // This promise should never resolve nor reject. + runningPromise + .then(() => 'Debugger stopped when it shouldn\'t have') + .catch(() => 'Debugger crashed when it shouldn\'t have') + .then(error => { + deferred.reject(error); + }); + + // Should complete without any errors + await deferred.promise; + } + + test('Debugger should not stop when test discovery is invoked automatically by extension (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await testDebuggerWhenRediscoveringTests(); + }); +}); diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts new file mode 100644 index 000000000000..036c0ffa14d1 --- /dev/null +++ b/src/test/unittests/mocks.ts @@ -0,0 +1,40 @@ +import { CancellationToken, Disposable, OutputChannel } from 'vscode'; +import { createDeferred, Deferred } from '../../client/common/helpers'; +import { ITestDebugLauncher, Tests } from '../../client/unittests/common/types'; + +export class MockDebugLauncher implements ITestDebugLauncher, Disposable { + public get launched(): Promise { + return this._launched.promise; + } + public get debuggerPromise(): Deferred { + return this._promise; + } + public get cancellationToken(): CancellationToken { + return this._token; + } + // tslint:disable-next-line:variable-name + private _launched: Deferred; + // tslint:disable-next-line:variable-name + private _promise?: Deferred; + // tslint:disable-next-line:variable-name + private _token: CancellationToken; + constructor() { + this._launched = createDeferred(); + } + public async launchDebugger(rootDirectory: string, testArgs: string[], token?: CancellationToken, outChannel?: OutputChannel): Promise { + this._launched.resolve(true); + // tslint:disable-next-line:no-non-null-assertion + this._token = token!; + this._promise = createDeferred(); + // tslint:disable-next-line:no-non-null-assertion + token!.onCancellationRequested(() => { + if (this._promise) { + this._promise.reject('Mock-User Cancelled'); + } + }); + return this._promise.promise; + } + public dispose() { + this._promise = undefined; + } +} diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index 9e531a393d85..81ffed6f8819 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -11,6 +11,7 @@ import * as nose from '../../client/unittests/nosetest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); @@ -39,7 +40,8 @@ suite('Unit Tests (nosetest)', () => { await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); await initialize(); }); - suiteTeardown(() => { + suiteTeardown(async () => { + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); filesToDelete.forEach(file => { if (fs.existsSync(file)) { fs.unlinkSync(file); @@ -61,7 +63,7 @@ suite('Unit Tests (nosetest)', () => { storageService = new TestCollectionStorageService(); resultsService = new TestResultsService(); testsHelper = new TestsHelper(); - testManager = new nose.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); + testManager = new nose.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); } test('Discover Tests (single test file)', async () => { diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index e2b9ffde4123..9d67fa85390c 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -10,6 +10,7 @@ import * as pytest from '../../client/unittests/pytest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; const UNITTEST_TEST_FILES_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'single'); @@ -46,14 +47,14 @@ suite('Unit Tests (PyTest)', () => { storageService = new TestCollectionStorageService(); resultsService = new TestResultsService(); testsHelper = new TestsHelper(); - testManager = new pytest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); + testManager = new pytest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); } test('Discover Tests (single test file)', async () => { storageService = new TestCollectionStorageService(); resultsService = new TestResultsService(); testsHelper = new TestsHelper(); - testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper); + testManager = new pytest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts new file mode 100644 index 000000000000..e3b27acc858f --- /dev/null +++ b/src/test/unittests/rediscover.test.ts @@ -0,0 +1,108 @@ +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ConfigurationTarget, Position, Range, window, workspace } from 'vscode'; +import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; +import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; +import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; +import { TestResultsService } from '../../client/unittests/common/testResultsService'; +import { TestsHelper } from '../../client/unittests/common/testUtils'; +import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, TestsToRun } from '../../client/unittests/common/types'; +import { TestResultDisplay } from '../../client/unittests/display/main'; +import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; +import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; +import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; +import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const testFile = path.join(testFilesPath, 'tests', 'test_debugger_two.py'); +const testFileWithFewTests = path.join(testFilesPath, 'tests', 'test_debugger_two.txt'); +const testFileWithMoreTests = path.join(testFilesPath, 'tests', 'test_debugger_two.updated.txt'); +const defaultUnitTestArgs = [ + '-v', + '-s', + '.', + '-p', + '*test*.py' +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Discovery', () => { + let testManager: BaseTestManager; + let testResultDisplay: TestResultDisplay; + let outChannel: MockOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let mockDebugLauncher: MockDebugLauncher; + let testsHelper: ITestsHelper; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + }); + setup(async () => { + outChannel = new MockOutputChannel('Python Test Log'); + testResultDisplay = new TestResultDisplay(outChannel); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await resetSettings(); + await initializeTest(); + }); + teardown(async () => { + await resetSettings(); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + outChannel.dispose(); + if (testManager) { + testManager.dispose(); + } + testResultDisplay.dispose(); + }); + + async function resetSettings() { + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); + } + + function createTestManagerDepedencies() { + storageService = new TestCollectionStorageService(); + resultsService = new TestResultsService(); + testsHelper = new TestsHelper(); + mockDebugLauncher = new MockDebugLauncher(); + } + + async function discoverUnitTests() { + let tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + + await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); + tests = await testManager.discoverTests(true, true); + assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); + } + + + test('Re-discover tests (unittest)', async () => { + await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new UnitTestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); + + test('Re-discover tests (pytest)', async () => { + await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new PytestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); + + test('Re-discover tests (nosetest)', async () => { + await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + createTestManagerDepedencies(); + testManager = new NosetestManager(testFilesPath, outChannel, storageService, resultsService, testsHelper, mockDebugLauncher); + await discoverUnitTests(); + }); +}); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index 99397a7855cc..f504ffb1e73f 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -11,6 +11,7 @@ import * as unittest from '../../client/unittests/unittest/main'; import { rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; +import { MockDebugLauncher } from './mocks'; const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles'); const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); @@ -48,16 +49,17 @@ suite('Unit Tests (unittest)', () => { } await initializeTest(); }); - teardown(() => { + teardown(async () => { outChannel.dispose(); testManager.dispose(); testResultDisplay.dispose(); + await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); }); function createTestManager(rootDir: string = rootDirectory) { storageService = new TestCollectionStorageService(); resultsService = new TestResultsService(); testsHelper = new TestsHelper(); - testManager = new unittest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper); + testManager = new unittest.TestManager(rootDir, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); } test('Discover Tests (single test file)', async () => { @@ -65,7 +67,7 @@ suite('Unit Tests (unittest)', () => { storageService = new TestCollectionStorageService(); resultsService = new TestResultsService(); testsHelper = new TestsHelper(); - testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper); + testManager = new unittest.TestManager(UNITTEST_SINGLE_TEST_FILE_PATH, outChannel, storageService, resultsService, testsHelper, new MockDebugLauncher()); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); From dff9f207754042a2f7603d97125438fc1df12387 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 30 Oct 2017 14:42:59 -0700 Subject: [PATCH 16/23] remove empty line (linter warning) --- src/test/unittests/rediscover.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts index e3b27acc858f..9630e6c36197 100644 --- a/src/test/unittests/rediscover.test.ts +++ b/src/test/unittests/rediscover.test.ts @@ -84,7 +84,6 @@ suite('Unit Tests Discovery', () => { assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); } - test('Re-discover tests (unittest)', async () => { await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); createTestManagerDepedencies(); From e53fa97cbe90a671f7e83142f13de0a12dcea15a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 30 Oct 2017 15:05:39 -0700 Subject: [PATCH 17/23] delete pyc before making changes to py file --- src/test/common.ts | 6 ++++++ src/test/unittests/rediscover.test.ts | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/test/common.ts b/src/test/common.ts index 446fa1b7fef3..fe8bc2b35547 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -99,6 +99,12 @@ export async function deleteDirectory(dir: string) { await fs.remove(dir); } } +export async function deleteFile(file: string) { + const exists = await fs.pathExists(file); + if (exists) { + await fs.remove(file); + } +} 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/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts index 9630e6c36197..7a7e88c74bcc 100644 --- a/src/test/unittests/rediscover.test.ts +++ b/src/test/unittests/rediscover.test.ts @@ -1,7 +1,7 @@ import { assert } from 'chai'; import * as fs from 'fs-extra'; import * as path from 'path'; -import { ConfigurationTarget, Position, Range, window, workspace } from 'vscode'; +import { ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; import { BaseTestManager } from '../../client/unittests/common/baseTestManager'; import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; import { TestCollectionStorageService } from '../../client/unittests/common/storageService'; @@ -12,7 +12,7 @@ import { TestResultDisplay } from '../../client/unittests/display/main'; import { TestManager as NosetestManager } from '../../client/unittests/nosetest/main'; import { TestManager as PytestManager } from '../../client/unittests/pytest/main'; import { TestManager as UnitTestManager } from '../../client/unittests/unittest/main'; -import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; +import { deleteDirectory, deleteFile, rootWorkspaceUri, updateSetting } from '../common'; import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; import { MockOutputChannel } from './../mockClasses'; import { MockDebugLauncher } from './mocks'; @@ -78,7 +78,7 @@ suite('Unit Tests Discovery', () => { assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); tests = await testManager.discoverTests(true, true); assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); From 59f1d3fcfc5f2447f733f4c4a304669f7c99e2dd Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 30 Oct 2017 15:07:01 -0700 Subject: [PATCH 18/23] delete pyc file in teardown --- src/test/unittests/rediscover.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts index 7a7e88c74bcc..0db008148bc6 100644 --- a/src/test/unittests/rediscover.test.ts +++ b/src/test/unittests/rediscover.test.ts @@ -53,6 +53,7 @@ suite('Unit Tests Discovery', () => { teardown(async () => { await resetSettings(); await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); outChannel.dispose(); if (testManager) { testManager.dispose(); From 277c5353d9c22c8130f89f8a453a25ecb47f2e76 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 30 Oct 2017 15:35:05 -0700 Subject: [PATCH 19/23] merged multiroot master --- src/client/common/installer.ts | 2 +- .../configuration/pythonPathUpdaterService.ts | 1 - .../unittests/common/baseTestManager.ts | 1 - src/test/definitions/parallel.test.ts | 40 +++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/test/definitions/parallel.test.ts diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts index cd0cf9bc20a8..c78d6bf410be 100644 --- a/src/client/common/installer.ts +++ b/src/client/common/installer.ts @@ -1,6 +1,6 @@ import * as os from 'os'; import * as vscode from 'vscode'; -import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; +import { commands, ConfigurationTarget, Disposable, OutputChannel, Terminal, Uri, window, workspace } from 'vscode'; import * as settings from './configSettings'; import { isNotInstalledError } from './helpers'; import { error } from './logger'; diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index adeca205553b..b81963f9d8fe 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -9,7 +9,6 @@ export class PythonPathUpdaterService { const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); try { - await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); } catch (reason) { // tslint:disable-next-line:no-unsafe-any prefer-type-cast diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index da0a356a7519..e54c80b63440 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -163,7 +163,6 @@ export abstract class BaseTestManager { this._status = TestStatus.Running; this.stop(); - // If running failed tests, then don't clear the previously build UnitTests // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) diff --git a/src/test/definitions/parallel.test.ts b/src/test/definitions/parallel.test.ts new file mode 100644 index 000000000000..f70edb4a9910 --- /dev/null +++ b/src/test/definitions/parallel.test.ts @@ -0,0 +1,40 @@ +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as vscode from 'vscode'; +import * as path from 'path'; +import { initialize, closeActiveWindows } from '../initialize'; +import { normalizeMarkedString } from '../textUtils'; + +const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); + +suite('Code, Hover Definition and Intellisense', () => { + suiteSetup(() => initialize()); + suiteTeardown(() => closeActiveWindows()); + teardown(() => closeActiveWindows()); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + const editor = await vscode.window.showTextDocument(textDocument); + + let position = new vscode.Position(30, 5); + const hoverDef = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + const codeDef = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); + position = new vscode.Position(3, 10); + const list = await vscode.commands.executeCommand('vscode.executeCompletionItemProvider', textDocument.uri, position); + + assert.equal(list.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); + + assert.equal(codeDef.length, 1, 'Definition length is incorrect'); + assert.equal(codeDef[0].uri.fsPath, fileOne, 'Incorrect file'); + assert.equal(`${codeDef[0].range.start.line},${codeDef[0].range.start.character}`, '17,4', 'Start position is incorrect'); + assert.equal(`${codeDef[0].range.end.line},${codeDef[0].range.end.character}`, '21,11', 'End position is incorrect'); + + assert.equal(hoverDef.length, 1, 'Definition length is incorrect'); + assert.equal(`${hoverDef[0].range.start.line},${hoverDef[0].range.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${hoverDef[0].range.end.line},${hoverDef[0].range.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(hoverDef[0].contents.length, 1, 'Invalid content items'); + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(hoverDef[0].contents[0]), expectedContent, 'function signature incorrect'); + }); +}); From f102cfb894a0ae96c073907a29e1468154957766 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 30 Oct 2017 17:21:28 -0700 Subject: [PATCH 20/23] pass uri of workspace when displaing prompt for configuration --- src/client/common/constants.ts | 2 +- .../unittests/common/testConfigurationManager.ts | 2 +- src/client/unittests/common/testUtils.ts | 2 +- src/client/unittests/configuration.ts | 14 +++++--------- .../unittests/nosetest/testConfigurationManager.ts | 10 +++++----- .../unittests/pytest/testConfigurationManager.ts | 10 +++++----- .../unittests/unittest/testConfigurationManager.ts | 8 ++++---- 7 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 91f6e5af31b4..5ab6c470e79d 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -96,4 +96,4 @@ export namespace Documentation { export const Home = '/docs/workspaceSymbols/'; export const InstallOnWindows = '/docs/workspaceSymbols/#Install-Windows'; } -} \ No newline at end of file +} diff --git a/src/client/unittests/common/testConfigurationManager.ts b/src/client/unittests/common/testConfigurationManager.ts index 672c138aceb9..afdad0192c87 100644 --- a/src/client/unittests/common/testConfigurationManager.ts +++ b/src/client/unittests/common/testConfigurationManager.ts @@ -13,7 +13,7 @@ export abstract class TestConfigurationManager { protected installer: Installer, protected testConfigSettingsService: ITestConfigSettingsService) { } // tslint:disable-next-line:no-any - public abstract configure(rootDir: string): Promise; + public abstract configure(wkspace: Uri): Promise; public async enable() { return this.testConfigSettingsService.enable(this.workspace, this.product); } diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 88ac405835a5..61eb5eb89060 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -16,7 +16,7 @@ export async function selectTestWorkspace(): Promise { } else { // tslint:disable-next-line:no-any prefer-type-cast const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); - return workspace ? workspaceFolder.uri : undefined; + return workspaceFolder ? workspaceFolder.uri : undefined; } } diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts index 65d817085acc..ecbefeae8d4d 100644 --- a/src/client/unittests/configuration.ts +++ b/src/client/unittests/configuration.ts @@ -15,11 +15,7 @@ import * as pytest from './pytest/testConfigurationManager'; import * as unittest from './unittest/testConfigurationManager'; // tslint:disable-next-line:no-any -async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { - const wkspace = await selectTestWorkspace(); - if (!wkspace) { - return; - } +async function promptToEnableAndConfigureTestFramework(wkspace: Uri, 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); @@ -39,7 +35,7 @@ async function promptToEnableAndConfigureTestFramework(outputChannel: vscode.Out return configMgr.enable(); } - return configMgr.configure(vscode.workspace.rootPath).then(() => { + return configMgr.configure(wkspace).then(() => { return enableTest(wkspace, configMgr); }).catch(reason => { return enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); @@ -51,12 +47,12 @@ export function displayTestFrameworkError(wkspace: Uri, outputChannel: vscode.Ou enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; if (enabledCount > 1) { - return promptToEnableAndConfigureTestFramework(outputChannel, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); + return promptToEnableAndConfigureTestFramework(wkspace, 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 => { if (item === option) { - return promptToEnableAndConfigureTestFramework(outputChannel); + return promptToEnableAndConfigureTestFramework(wkspace, outputChannel); } return Promise.reject(null); }); @@ -87,7 +83,7 @@ export async function displayPromptToEnableTests(rootDir: string, outputChannel: return; } if (item === yes) { - await promptToEnableAndConfigureTestFramework(outputChannel); + await promptToEnableAndConfigureTestFramework(vscode.workspace.getWorkspaceFolder(vscode.Uri.file(rootDir)).uri, outputChannel); } else { const pythonConfig = vscode.workspace.getConfiguration('python'); await pythonConfig.update('unitTest.promptToConfigure', false); diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts index 436c6fac8bd8..7a8b0bedf152 100644 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ b/src/client/unittests/nosetest/testConfigurationManager.ts @@ -21,17 +21,17 @@ export class ConfigurationManager extends TestConfigurationManager { return values.filter(exists => exists.length > 0); } // tslint:disable-next-line:no-any - public async configure(rootDir: string): Promise { + public async configure(wkspace: Uri): Promise { const args: string[] = []; const configFileOptionLabel = 'Use existing config file'; - const configFiles = await ConfigurationManager.configFilesExist(rootDir); + const configFiles = await ConfigurationManager.configFilesExist(wkspace.fsPath); // If a config file exits, there's nothing to be configured. if (configFiles.length > 0) { return; } - const subDirs = await this.getTestDirs(rootDir); - const testDir = await this.selectTestDir(rootDir, subDirs); + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { args.push(testDir); } @@ -39,6 +39,6 @@ export class ConfigurationManager extends TestConfigurationManager { if (!installed) { await this.installer.install(Product.nosetest); } - await this.testConfigSettingsService.updateTestArgs(rootDir, Product.nosetest, args); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.nosetest, args); } } diff --git a/src/client/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts index addda7dd802e..8017835d7b95 100644 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ b/src/client/unittests/pytest/testConfigurationManager.ts @@ -21,11 +21,11 @@ export class ConfigurationManager extends TestConfigurationManager { return values.filter(exists => exists.length > 0); } // tslint:disable-next-line:no-any - public async configure(rootDir: string) { + public async configure(wkspace: Uri) { const args = []; const configFileOptionLabel = 'Use existing config file'; const options: vscode.QuickPickItem[] = []; - const configFiles = await ConfigurationManager.configFilesExist(rootDir); + const configFiles = await ConfigurationManager.configFilesExist(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; @@ -37,8 +37,8 @@ export class ConfigurationManager extends TestConfigurationManager { description: 'setup.cfg' }); } - const subDirs = await this.getTestDirs(rootDir); - const testDir = await this.selectTestDir(rootDir, subDirs, options); + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs, options); if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { args.push(testDir); } @@ -46,6 +46,6 @@ export class ConfigurationManager extends TestConfigurationManager { if (!installed) { await this.installer.install(Product.pytest); } - await this.testConfigSettingsService.updateTestArgs(rootDir, Product.pytest, args); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } } diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index 2113f874f4c1..c10e0c406fd3 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -10,10 +10,10 @@ export class ConfigurationManager extends TestConfigurationManager { super(workspace, Product.unittest, outputChannel, installer, testConfigSettingsService); } // tslint:disable-next-line:no-any - public async configure(rootDir: string) { + public async configure(wkspace: Uri) { const args = ['-v']; - const subDirs = await this.getTestDirs(rootDir); - const testDir = await this.selectTestDir(rootDir, subDirs); + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); args.push('-s'); if (typeof testDir === 'string' && testDir !== '.') { args.push(`.${path.sep}${testDir}`); @@ -28,6 +28,6 @@ export class ConfigurationManager extends TestConfigurationManager { } else { args.push('test*.py'); } - await this.testConfigSettingsService.updateTestArgs(rootDir, Product.unittest, args); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.unittest, args); } } From d8dfd0a3ae3956304fe32e2dd1bd5a50a415fa6f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 31 Oct 2017 09:43:16 -0700 Subject: [PATCH 21/23] pass uri to commands --- src/client/unittests/main.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index c3d50bc3ff59..3f4dae31ee01 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -99,7 +99,7 @@ function registerCommands(): vscode.Disposable[] { // tslint:disable-next-line:no-empty discoverTests(resource, true, true).catch(() => { }); })); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, () => runTestsImpl(undefined, undefined, true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run_Failed, (resource: Uri) => runTestsImpl(resource, undefined, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Run, (file: Uri, testToRun?: TestsToRun) => runTestsImpl(file, testToRun))); disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Debug, (file: Uri, testToRun: TestsToRun) => runTestsImpl(file, testToRun, false, true))); @@ -114,8 +114,8 @@ function registerCommands(): vscode.Disposable[] { 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, () => selectAndRunTestMethod())); - disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, () => selectAndRunTestMethod(true))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (resource: Uri) => selectAndRunTestMethod(resource))); + disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (resource: Uri) => selectAndRunTestMethod(resource, true))); // tslint:disable-next-line:no-unnecessary-callback-wrapper disposables.push(vscode.commands.registerCommand(constants.Commands.Tests_Select_And_Run_File, () => selectAndRunTestFile())); // tslint:disable-next-line:no-unnecessary-callback-wrapper @@ -142,8 +142,8 @@ async function displayPickerUI(file: Uri, testFunctions: TestFunction[], debug?: testDisplay = testDisplay ? testDisplay : new TestDisplay(testCollectionStorage); testDisplay.displayFunctionTestPickerUI(testManager.workspace, testManager.workingDirectory, file, testFunctions, debug); } -async function selectAndRunTestMethod(debug?: boolean) { - const testManager = await getTestManager(true); +async function selectAndRunTestMethod(resource: Uri, debug?: boolean) { + const testManager = await getTestManager(true, resource); if (!testManager) { return; } From 118e89914ac69b37b6fbdb61a67fb3c36bf69f8d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 31 Oct 2017 12:23:24 -0700 Subject: [PATCH 22/23] fixed typos based on code review --- src/client/unittests/codeLenses/testFiles.ts | 12 ++++++------ src/client/unittests/common/baseTestManager.ts | 14 +++++++------- .../unittests/common/testResultsService.ts | 2 +- src/client/unittests/common/testUtils.ts | 8 ++++---- src/client/unittests/common/types.ts | 2 +- src/client/unittests/main.ts | 2 +- src/client/unittests/unittest/runner.ts | 2 +- src/test/unittests/debugger.test.ts | 6 +++--- src/test/unittests/nosetest.test.ts | 16 ++++++++-------- src/test/unittests/pytest.test.ts | 12 ++++++------ src/test/unittests/rediscover.test.ts | 2 +- src/test/unittests/unittest.test.ts | 10 +++++----- 12 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts index 9962f0615095..bfb3fd550ffe 100644 --- a/src/client/unittests/codeLenses/testFiles.ts +++ b/src/client/unittests/codeLenses/testFiles.ts @@ -36,7 +36,7 @@ export class TestFileCodeLensProvider implements CodeLensProvider { token.onCancellationRequested(() => { cancelTokenSrc.cancel(); }); // Strop trying to build the code lenses if unable to get a list of - // symbols in this file afrer x time + // symbols in this file afrer x time. setTimeout(() => { if (!cancelTokenSrc.token.isCancellationRequested) { cancelTokenSrc.cancel(); @@ -74,7 +74,7 @@ export class TestFileCodeLensProvider implements CodeLensProvider { symbol.kind === SymbolKind.Class; }).map(symbol => { // This is bloody crucial, if the start and end columns are the same - // then vscode goes bonkers when ever you edit a line (start scrolling magically) + // then vscode goes bonkers when ever you edit a line (start scrolling magically). const range = new Range(symbol.location.range.start, new Position(symbol.location.range.end.line, symbol.location.range.end.character + 1)); @@ -165,7 +165,7 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, if (symbolContainer.length === 0) { fn = functionsAndSuites.functions.find(func => func.name === symbolName); } else { - // Assume single levels for now + // Assume single levels for now. functionsAndSuites.suites .filter(s => s.name === symbolContainer) .forEach(s => { @@ -191,8 +191,8 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, ]; } - // Ok, possible we're dealing with parameterized unit tests - // If we have [ in the name, then this is a parameterized function + // Ok, possible we're dealing with parameterized unit tests. + // If we have [ in the name, then this is a parameterized function. const functions = functionsAndSuites.functions.filter(func => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']')); if (functions.length === 0) { return []; @@ -212,7 +212,7 @@ function getFunctionCodeLens(file: Uri, functionsAndSuites: FunctionsAndSuites, ]; } - // Find all flattened functions + // Find all flattened functions. return [ new CodeLens(range, { title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, diff --git a/src/client/unittests/common/baseTestManager.ts b/src/client/unittests/common/baseTestManager.ts index e54c80b63440..4b4612c7da85 100644 --- a/src/client/unittests/common/baseTestManager.ts +++ b/src/client/unittests/common/baseTestManager.ts @@ -9,7 +9,7 @@ import { displayTestErrorMessage } from './testUtils'; import { ITestCollectionStorageService, ITestResultsService, ITestsHelper, Tests, TestStatus, TestsToRun } from './types'; enum CancellationTokenType { - testDicovery, + testDiscovery, testRunner } @@ -83,7 +83,7 @@ export abstract class BaseTestManager { this.stop(); } - this.createCancellationToken(CancellationTokenType.testDicovery); + this.createCancellationToken(CancellationTokenType.testDiscovery); return this.discoverTestsPromise = this.discoverTestsImpl(ignoreCache) .then(tests => { this.tests = tests; @@ -107,7 +107,7 @@ export abstract class BaseTestManager { } const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, tests); - this.disposeCancellationToken(CancellationTokenType.testDicovery); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); return tests; }).catch(reason => { @@ -129,7 +129,7 @@ export abstract class BaseTestManager { } const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(this.rootDirectory)).uri; this.testCollectionStorage.storeTests(wkspace, null); - this.disposeCancellationToken(CancellationTokenType.testDicovery); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); return Promise.reject(reason); }); } @@ -174,7 +174,7 @@ export abstract class BaseTestManager { } displayTestErrorMessage('Errors in discovering tests, continuing with tests'); return { - rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuits: [], + rootTestFolders: [], testFiles: [], testFolders: [], testFunctions: [], testSuites: [], summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } }; }) @@ -201,14 +201,14 @@ export abstract class BaseTestManager { protected abstract discoverTestsImpl(ignoreCache: boolean, debug?: boolean): Promise; private createCancellationToken(tokenType: CancellationTokenType) { this.disposeCancellationToken(tokenType); - if (tokenType === CancellationTokenType.testDicovery) { + if (tokenType === CancellationTokenType.testDiscovery) { this.testDiscoveryCancellationTokenSource = new vscode.CancellationTokenSource(); } else { this.testRunnerCancellationTokenSource = new vscode.CancellationTokenSource(); } } private disposeCancellationToken(tokenType: CancellationTokenType) { - if (tokenType === CancellationTokenType.testDicovery) { + if (tokenType === CancellationTokenType.testDiscovery) { if (this.testDiscoveryCancellationTokenSource) { this.testDiscoveryCancellationTokenSource.dispose(); } diff --git a/src/client/unittests/common/testResultsService.ts b/src/client/unittests/common/testResultsService.ts index 58fda744123e..8743b579d879 100644 --- a/src/client/unittests/common/testResultsService.ts +++ b/src/client/unittests/common/testResultsService.ts @@ -6,7 +6,7 @@ export class TestResultsService implements ITestResultsService { const resultResetVisitor = new TestResultResetVisitor(); tests.testFolders.forEach(f => resultResetVisitor.visitTestFolder(f)); tests.testFunctions.forEach(fn => resultResetVisitor.visitTestFunction(fn.testFunction)); - tests.testSuits.forEach(suite => resultResetVisitor.visitTestSuite(suite.testSuite)); + tests.testSuites.forEach(suite => resultResetVisitor.visitTestSuite(suite.testSuite)); tests.testFiles.forEach(testFile => resultResetVisitor.visitTestFile(testFile)); } public updateResults(tests: Tests): void { diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 61eb5eb89060..7ce389bdf66f 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -47,7 +47,7 @@ export class TestsHelper implements ITestsHelper { const tests = { testFiles: testFiles, testFunctions: flatteningVisitor.flattenedTestFunctions, - testSuits: flatteningVisitor.flattenedTestSuites, + testSuites: flatteningVisitor.flattenedTestSuites, testFolders: [], rootTestFolders: [], summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } @@ -98,8 +98,8 @@ export class TestsHelper implements ITestsHelper { } public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun { // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. - // use to identify a file given the full file name, similary for a folder and function - // Perhaps something like a parser or methods like TestFunction.fromString()... something) + // Use to identify a file given the full file name, similarly for a folder and function. + // Perhaps something like a parser or methods like TestFunction.fromString()... something). if (!tests) { return null; } const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); @@ -111,7 +111,7 @@ export class TestsHelper implements ITestsHelper { const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); if (testFns.length > 0) { return { testFunction: testFns }; } - // Just return this as a test file + // Just return this as a test file. return { testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0 }] }; } } diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index 7477456b41e4..5125b5a4c17c 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -78,7 +78,7 @@ export type Tests = { summary: TestSummary; testFiles: TestFile[]; testFunctions: FlattenedTestFunction[]; - testSuits: FlattenedTestSuite[]; + testSuites: FlattenedTestSuite[]; testFolders: TestFolder[]; rootTestFolders: TestFolder[]; }; diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts index 3f4dae31ee01..adb86808d695 100644 --- a/src/client/unittests/main.ts +++ b/src/client/unittests/main.ts @@ -228,7 +228,7 @@ function autoResetTests() { } function onConfigChanged() { // If there's one workspace, then stop the tests and restart, - // Else let the user do this manually. + // else let the user do this manually. if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length > 1) { return; } diff --git a/src/client/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts index 9413970d3fdf..db72c77b14ef 100644 --- a/src/client/unittests/unittest/runner.ts +++ b/src/client/unittests/unittest/runner.ts @@ -120,7 +120,7 @@ export function runTest(testManager: BaseTestManager, testResultsService: ITestR } if (Array.isArray(testsToRun.testSuite)) { testsToRun.testSuite.forEach(testSuite => { - const testFileName = tests.testSuits.find(t => t.testSuite === testSuite).parentTestFile.fullPath; + const testFileName = tests.testSuites.find(t => t.testSuite === testSuite).parentTestFile.fullPath; // tslint:disable-next-line:prefer-type-cast no-any promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun) as Promise); }); diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts index 866d991c3923..c908a7f7080e 100644 --- a/src/test/unittests/debugger.test.ts +++ b/src/test/unittests/debugger.test.ts @@ -78,7 +78,7 @@ suite('Unit Tests Debugging', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); const testFunction = [tests.testFunctions[0].testFunction]; testManager.runTest({ testFunction }, false, true); @@ -111,7 +111,7 @@ suite('Unit Tests Debugging', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); const testFunction = [tests.testFunctions[0].testFunction]; const runningPromise = testManager.runTest({ testFunction }, false, true); @@ -148,7 +148,7 @@ suite('Unit Tests Debugging', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); const testFunction = [tests.testFunctions[0].testFunction]; const runningPromise = testManager.runTest({ testFunction }, false, true); diff --git a/src/test/unittests/nosetest.test.ts b/src/test/unittests/nosetest.test.ts index 81ffed6f8819..9105c483fec3 100644 --- a/src/test/unittests/nosetest.test.ts +++ b/src/test/unittests/nosetest.test.ts @@ -71,17 +71,17 @@ suite('Unit Tests (nosetest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === path.join('tests', 'test_one.py') && t.nameToRun === t.name), true, 'Test File not found'); }); - test('Check that nameToRun in testSuits has class name after : (single test file)', async () => { + test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { createTestManager(UNITTEST_SINGLE_TEST_FILE_PATH); const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testSuits.every(t => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), true, 'Suite name does not match class name'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.every(t => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), true, 'Suite name does not match class name'); }); function lookForTestFile(tests: Tests, testFile: string) { @@ -94,7 +94,7 @@ suite('Unit Tests (nosetest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 6, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 6, 'Incorrect number of test suites'); lookForTestFile(tests, path.join('tests', 'test_unittest_one.py')); lookForTestFile(tests, path.join('tests', 'test_unittest_two.py')); lookForTestFile(tests, path.join('tests', 'unittest_three_test.py')); @@ -108,7 +108,7 @@ suite('Unit Tests (nosetest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); lookForTestFile(tests, path.join('specific', 'tst_unittest_one.py')); lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); }); @@ -119,7 +119,7 @@ suite('Unit Tests (nosetest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); lookForTestFile(tests, 'test_root.py'); }); @@ -168,7 +168,7 @@ suite('Unit Tests (nosetest)', () => { await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testSuiteToRun = tests.testSuits.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); + const testSuiteToRun = tests.testSuites.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); assert.ok(testSuiteToRun, 'Test suite not found'); // tslint:disable-next-line:no-non-null-assertion const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToRun!.testSuite] }; diff --git a/src/test/unittests/pytest.test.ts b/src/test/unittests/pytest.test.ts index 9d67fa85390c..cf0ba4130ca8 100644 --- a/src/test/unittests/pytest.test.ts +++ b/src/test/unittests/pytest.test.ts @@ -58,7 +58,7 @@ suite('Unit Tests (PyTest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); }); @@ -69,7 +69,7 @@ suite('Unit Tests (PyTest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 6, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 29, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 8, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 8, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); @@ -84,7 +84,7 @@ suite('Unit Tests (PyTest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); }); @@ -95,7 +95,7 @@ suite('Unit Tests (PyTest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 4, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 4, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'other/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); }); @@ -151,7 +151,7 @@ suite('Unit Tests (PyTest)', () => { await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); createTestManager(); const tests = await testManager.discoverTests(true, true); - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuits[0].testSuite] }; + const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [tests.testSuites[0].testSuite] }; const results = await testManager.runTest(testSuite); assert.equal(results.summary.errors, 0, 'Errors'); assert.equal(results.summary.failures, 1, 'Failures'); @@ -179,6 +179,6 @@ suite('Unit Tests (PyTest)', () => { assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); }); }); diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts index 0db008148bc6..8478b9b8e14b 100644 --- a/src/test/unittests/rediscover.test.ts +++ b/src/test/unittests/rediscover.test.ts @@ -77,7 +77,7 @@ suite('Unit Tests Discovery', () => { async function discoverUnitTests() { let tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testSuits.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); diff --git a/src/test/unittests/unittest.test.ts b/src/test/unittests/unittest.test.ts index f504ffb1e73f..bcf201bcc3dc 100644 --- a/src/test/unittests/unittest.test.ts +++ b/src/test/unittests/unittest.test.ts @@ -71,7 +71,7 @@ suite('Unit Tests (unittest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); }); @@ -81,7 +81,7 @@ suite('Unit Tests (unittest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 3, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'Test_test1.test_A'), true, 'Test File not found'); assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'Test_test2.test_A2'), true, 'Test File not found'); }); @@ -92,7 +92,7 @@ suite('Unit Tests (unittest)', () => { const tests = await testManager.discoverTests(true, true); assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'Test_test3.test_A'), true, 'Test File not found'); }); @@ -144,7 +144,7 @@ suite('Unit Tests (unittest)', () => { const tests = await testManager.discoverTests(true, true); // tslint:disable-next-line:no-non-null-assertion - const testSuiteToTest = tests.testSuits.find(s => s.testSuite.name === 'Test_test_one_1')!.testSuite; + const testSuiteToTest = tests.testSuites.find(s => s.testSuite.name === 'Test_test_one_1')!.testSuite; const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToTest] }; const results = await testManager.runTest(testSuite); @@ -174,6 +174,6 @@ suite('Unit Tests (unittest)', () => { assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuits.length, 1, 'Incorrect number of test suites'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); }); }); From 456ce9284410fa2022bab57c9db5e39a2f3a3102 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 31 Oct 2017 13:06:25 -0700 Subject: [PATCH 23/23] prefix path with forward slash, as is done in the extension unit tests --- src/client/unittests/unittest/testConfigurationManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts index c10e0c406fd3..87334cd72746 100644 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ b/src/client/unittests/unittest/testConfigurationManager.ts @@ -16,7 +16,7 @@ export class ConfigurationManager extends TestConfigurationManager { const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); args.push('-s'); if (typeof testDir === 'string' && testDir !== '.') { - args.push(`.${path.sep}${testDir}`); + args.push(`./${testDir}`); } else { args.push('.'); }