From 13743703d896df4310df1e2cf82995c461f346b6 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 7 Apr 2023 14:33:29 -0700 Subject: [PATCH 01/10] hel --- .github/workflows/triage-info-needed.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml index 3b4165c74c55..07b20b0aec3f 100644 --- a/.github/workflows/triage-info-needed.yml +++ b/.github/workflows/triage-info-needed.yml @@ -21,7 +21,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const issue = await github.issues.get({ + const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number @@ -36,7 +36,7 @@ jobs: const shouldAddLabel = isTeamMember && commentAuthor !== issue.data.user.login && isRequestForInfo; if (shouldAddLabel) { - await github.issues.addLabels({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, @@ -55,7 +55,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const issue = await github.issues.get({ + const issue = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number @@ -63,7 +63,7 @@ jobs: const commentAuthor = context.payload.comment.user.login; const issueAuthor = issue.data.user.login; if (commentAuthor === issueAuthor) { - await github.issues.removeLabel({ + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, @@ -77,7 +77,7 @@ jobs: } // Loop through all the comments on the issue in reverse order and find the last username that a TRIAGER mentioned // If the comment author is the last mentioned username, remove the "info-needed" label - const comments = await github.issues.listComments({ + const comments = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number @@ -89,7 +89,7 @@ jobs: const matches = comment.body.match(/@\w+/g) || []; const mentionedUsernames = matches.map(match => match.replace('@', '')); if (mentionedUsernames.includes(commentAuthor)) { - await github.issues.removeLabel({ + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, From 6403d20d5c18ecb8fe6c1cdc65e10218a619a3ab Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Apr 2023 11:41:41 -0700 Subject: [PATCH 02/10] Attempt to fix pre-release build (#21071) ![image](https://user-images.githubusercontent.com/13199757/232574996-0b772fb3-59cf-40f1-a9b7-8e356dc83a81.png) --- build/webpack/webpack.extension.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js index b1b3922126d6..f496aa32ee26 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -61,6 +61,9 @@ const config = { // See: https://github.com/microsoft/vscode-extension-telemetry/issues/41#issuecomment-598852991 'applicationinsights-native-metrics', '@opentelemetry/tracing', + '@azure/opentelemetry-instrumentation-azure-sdk', + '@opentelemetry/instrumentation', + '@azure/functions-core', ], plugins: [...common.getDefaultPlugins('extension')], resolve: { From a1f50418a6e41ad3ddbddffcedd8ca8a909f798f Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Mon, 17 Apr 2023 15:05:21 -0700 Subject: [PATCH 03/10] Use new logging API for python extension logger and LS logger (#21062) In this PR: 1. Changes the python extension logging to use LogOutputChannel 2. Changes the language server logger with LogOutputChannel 3. Test output channel uses OutputChannel as it needs to show test output and not really logging. Also, using logging test output makes it pretty much unreadable. 4. Simplifies logging channel and output channel registration. We need to do this now to make it easier for new test work to integrate with output logging. For #20844 This still doesn't get rid of the log level setting. --- .../activation/common/analysisOptions.ts | 4 +- src/client/activation/common/outputChannel.ts | 6 +-- src/client/activation/types.ts | 6 +-- .../common/application/applicationShell.ts | 6 +-- src/client/common/application/types.ts | 4 +- src/client/common/constants.ts | 2 - .../common/installer/moduleInstaller.ts | 5 +- src/client/common/process/rawProcessApis.ts | 6 ++- src/client/common/process/types.ts | 3 +- src/client/common/types.ts | 9 ++-- src/client/extensionActivation.ts | 15 ++---- src/client/extensionInit.ts | 15 +++--- src/client/linters/errorHandlers/standard.ts | 5 +- src/client/logging/outputChannelLogger.ts | 17 +++--- src/client/testing/constants.ts | 4 -- .../testing/testController/common/server.ts | 1 + .../testing/testController/controller.ts | 27 ++++++++-- .../pytest/pytestDiscoveryAdapter.ts | 9 +++- .../pytest/pytestExecutionAdapter.ts | 11 +++- .../testing/testController/pytest/runner.ts | 7 ++- .../testing/testController/unittest/runner.ts | 7 ++- .../unittest/testDiscoveryAdapter.ts | 9 +++- .../unittest/testExecutionAdapter.ts | 9 +++- .../node/analysisOptions.unit.test.ts | 6 +-- .../activation/outputChannel.unit.test.ts | 6 +-- .../installer/moduleInstaller.unit.test.ts | 9 ++-- src/test/format/formatter.unit.test.ts | 9 ++-- src/test/linters/common.ts | 8 +-- src/test/linters/lintengine.test.ts | 12 ++--- src/test/mockClasses.ts | 22 +++++++- src/test/mocks/vsc/index.ts | 32 +++++++++++ src/test/serviceRegistry.ts | 15 ++---- .../testConfigurationManager.unit.test.ts | 5 +- src/test/testing/configuration.unit.test.ts | 11 ++-- .../testing/configurationFactory.unit.test.ts | 5 +- .../testDiscoveryAdapter.unit.test.ts | 12 +++-- .../testExecutionAdapter.unit.test.ts | 12 +++-- .../workspaceTestAdapter.unit.test.ts | 53 +++++++++++++++---- src/test/vscode-mock.ts | 1 + 39 files changed, 259 insertions(+), 146 deletions(-) delete mode 100644 src/client/testing/constants.ts diff --git a/src/client/activation/common/analysisOptions.ts b/src/client/activation/common/analysisOptions.ts index 18de19384fbf..75d0aabef9d2 100644 --- a/src/client/activation/common/analysisOptions.ts +++ b/src/client/activation/common/analysisOptions.ts @@ -5,7 +5,7 @@ import { DocumentFilter, LanguageClientOptions, RevealOutputChannelOn } from 'vs import { IWorkspaceService } from '../../common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../common/constants'; -import { IOutputChannel, Resource } from '../../common/types'; +import { ILogOutputChannel, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { traceDecoratorError } from '../../logging'; @@ -14,7 +14,7 @@ import { ILanguageServerAnalysisOptions, ILanguageServerOutputChannel } from '.. export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServerAnalysisOptions { protected readonly didChange = new EventEmitter(); - private readonly output: IOutputChannel; + private readonly output: ILogOutputChannel; protected constructor( lsOutputChannel: ILanguageServerOutputChannel, diff --git a/src/client/activation/common/outputChannel.ts b/src/client/activation/common/outputChannel.ts index 830bfbfdf55b..60a99687793e 100644 --- a/src/client/activation/common/outputChannel.ts +++ b/src/client/activation/common/outputChannel.ts @@ -6,13 +6,13 @@ import { inject, injectable } from 'inversify'; import { IApplicationShell, ICommandManager } from '../../common/application/types'; import '../../common/extensions'; -import { IDisposableRegistry, IOutputChannel } from '../../common/types'; +import { IDisposableRegistry, ILogOutputChannel } from '../../common/types'; import { OutputChannelNames } from '../../common/utils/localize'; import { ILanguageServerOutputChannel } from '../types'; @injectable() export class LanguageServerOutputChannel implements ILanguageServerOutputChannel { - public output: IOutputChannel | undefined; + public output: ILogOutputChannel | undefined; private registered = false; @@ -22,7 +22,7 @@ export class LanguageServerOutputChannel implements ILanguageServerOutputChannel @inject(IDisposableRegistry) private readonly disposable: IDisposableRegistry, ) {} - public get channel(): IOutputChannel { + public get channel(): ILogOutputChannel { if (!this.output) { this.output = this.appShell.createOutputChannel(OutputChannelNames.languageServer); this.disposable.push(this.output); diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 873d608f0bd0..2a177bb570b8 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -5,7 +5,7 @@ import { Event } from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; -import type { IDisposable, IOutputChannel, Resource } from '../common/types'; +import type { IDisposable, ILogOutputChannel, Resource } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); @@ -110,10 +110,10 @@ export interface ILanguageServerOutputChannel { /** * Creates output channel if necessary and returns it * - * @type {IOutputChannel} + * @type {ILogOutputChannel} * @memberof ILanguageServerOutputChannel */ - readonly channel: IOutputChannel; + readonly channel: ILogOutputChannel; } export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivationService'); diff --git a/src/client/common/application/applicationShell.ts b/src/client/common/application/applicationShell.ts index c1a5de51b7f6..454662472010 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -14,10 +14,10 @@ import { InputBoxOptions, languages, LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, - OutputChannel, Progress, ProgressOptions, QuickPick, @@ -166,8 +166,8 @@ export class ApplicationShell implements IApplicationShell { public createTreeView(viewId: string, options: TreeViewOptions): TreeView { return window.createTreeView(viewId, options); } - public createOutputChannel(name: string): OutputChannel { - return window.createOutputChannel(name); + public createOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); } public createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem { return languages.createLanguageStatusItem(id, selector); diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 1b054eda687c..77d7b5af3279 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -25,10 +25,10 @@ import { InputBox, InputBoxOptions, LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, - OutputChannel, Progress, ProgressOptions, QuickPick, @@ -429,7 +429,7 @@ export interface IApplicationShell { * * @param name Human-readable string which will be used to represent the channel in the UI. */ - createOutputChannel(name: string): OutputChannel; + createOutputChannel(name: string): LogOutputChannel; createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index da44a7bfe677..f53923b3e1b4 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -93,8 +93,6 @@ export namespace ThemeIcons { export const DEFAULT_INTERPRETER_SETTING = 'python'; -export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; - export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; export function isTestExecution(): boolean { diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 4049edb8ec0d..62160b7e25c9 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -12,12 +12,11 @@ import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { IApplicationShell } from '../application/types'; import { wrapCancellationTokens } from '../cancellation'; -import { STANDARD_OUTPUT_CHANNEL } from '../constants'; import { IFileSystem } from '../platform/types'; import * as internalPython from '../process/internal/python'; import { IProcessServiceFactory } from '../process/types'; import { ITerminalServiceFactory, TerminalCreationOptions } from '../terminal/types'; -import { ExecutionInfo, IConfigurationService, IOutputChannel, Product } from '../types'; +import { ExecutionInfo, IConfigurationService, ILogOutputChannel, Product } from '../types'; import { isResource } from '../utils/misc'; import { ProductNames } from './productNames'; import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; @@ -152,7 +151,7 @@ export abstract class ModuleInstaller implements IModuleInstaller { const options = { name: 'VS Code Python', }; - const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get(ILogOutputChannel); const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; traceLog(`[Elevated] ${command}`); diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts index 59b5fe69c9cd..fe54b3a8be87 100644 --- a/src/client/common/process/rawProcessApis.ts +++ b/src/client/common/process/rawProcessApis.ts @@ -121,7 +121,10 @@ export function plainExec( } const stdoutBuffers: Buffer[] = []; - on(proc.stdout, 'data', (data: Buffer) => stdoutBuffers.push(data)); + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + options.outputChannel?.append(data.toString()); + }); const stderrBuffers: Buffer[] = []; on(proc.stderr, 'data', (data: Buffer) => { if (options.mergeStdOutErr) { @@ -130,6 +133,7 @@ export function plainExec( } else { stderrBuffers.push(data); } + options.outputChannel?.append(data.toString()); }); proc.once('close', () => { diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index bcab76e66b09..8298957285e8 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -3,7 +3,7 @@ import { ChildProcess, ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; import { Observable } from 'rxjs/Observable'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { PythonExecInfo } from '../../pythonEnvironments/exec'; import { InterpreterInformation, PythonEnvironment } from '../../pythonEnvironments/info'; import { ExecutionInfo, IDisposable } from '../types'; @@ -24,6 +24,7 @@ export type SpawnOptions = ChildProcessSpawnOptions & { mergeStdOutErr?: boolean; throwOnStdErr?: boolean; extraVariables?: NodeJS.ProcessEnv; + outputChannel?: OutputChannel; }; export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 3fac5e7e0044..3359854f89b7 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -15,9 +15,10 @@ import { Extension, ExtensionContext, Memento, - OutputChannel, + LogOutputChannel, Uri, WorkspaceEdit, + OutputChannel, } from 'vscode'; import { LanguageServerType } from '../activation/types'; import type { InstallOptions, InterpreterUri, ModuleInstallFlags } from './installer/types'; @@ -29,8 +30,10 @@ export interface IDisposable { dispose(): void | undefined | Promise; } -export const IOutputChannel = Symbol('IOutputChannel'); -export interface IOutputChannel extends OutputChannel {} +export const ILogOutputChannel = Symbol('ILogOutputChannel'); +export interface ILogOutputChannel extends LogOutputChannel {} +export const ITestOutputChannel = Symbol('ITestOutputChannel'); +export interface ITestOutputChannel extends OutputChannel {} export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export interface IDocumentSymbolProvider extends DocumentSymbolProvider {} export const IsWindows = Symbol('IS_WINDOWS'); diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 4b2a41105d77..a93ae562c97d 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -3,21 +3,14 @@ 'use strict'; -import { - debug, - DebugConfigurationProvider, - DebugConfigurationProviderTriggerKind, - languages, - OutputChannel, - window, -} from 'vscode'; +import { debug, DebugConfigurationProvider, DebugConfigurationProviderTriggerKind, languages, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationManager } from './activation/types'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL, UseProposedApi } from './common/constants'; +import { Commands, PYTHON, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; import { IFileSystem } from './common/platform/types'; import { @@ -25,7 +18,7 @@ import { IDisposableRegistry, IExtensions, IInterpreterPathService, - IOutputChannel, + ILogOutputChannel, IPathUtils, } from './common/types'; import { noop } from './common/utils/misc'; @@ -173,7 +166,7 @@ async function activateLegacy(ext: ExtensionState): Promise { const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); dispatcher.registerEventHandlers(); - const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = serviceManager.get(ILogOutputChannel); disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts index 4ee5f099f15d..851bc943cb8d 100644 --- a/src/client/extensionInit.ts +++ b/src/client/extensionInit.ts @@ -4,9 +4,8 @@ 'use strict'; import { Container } from 'inversify'; -import { Disposable, Memento, OutputChannel, window } from 'vscode'; +import { Disposable, Memento, window } from 'vscode'; import { instance, mock } from 'ts-mockito'; -import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; @@ -16,7 +15,8 @@ import { IDisposableRegistry, IExtensionContext, IMemento, - IOutputChannel, + ILogOutputChannel, + ITestOutputChannel, WORKSPACE_MEMENTO, } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; @@ -26,7 +26,6 @@ import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; import { IServiceContainer, IServiceManager } from './ioc/types'; import * as pythonEnvironments from './pythonEnvironments'; -import { TEST_OUTPUT_CHANNEL } from './testing/constants'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { registerLogger } from './logging'; import { OutputChannelLogger } from './logging/outputChannelLogger'; @@ -54,7 +53,7 @@ export function initializeGlobals( serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); serviceManager.addSingletonInstance(IExtensionContext, context); - const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python); + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python, { log: true }); disposables.push(standardOutputChannel); disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); @@ -62,12 +61,12 @@ export function initializeGlobals( const unitTestOutChannel = workspaceService.isVirtualWorkspace || !workspaceService.isTrusted ? // Do not create any test related output UI when using virtual workspaces. - instance(mock()) + instance(mock()) : window.createOutputChannel(OutputChannelNames.pythonTest); disposables.push(unitTestOutChannel); - serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(ILogOutputChannel, standardOutputChannel); + serviceManager.addSingletonInstance(ITestOutputChannel, unitTestOutChannel); return { context, diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index 6bd2d3c8e115..f6e04b50ff19 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -1,7 +1,6 @@ import { l10n, Uri } from 'vscode'; import { IApplicationShell } from '../../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { ExecutionInfo, IOutputChannel } from '../../common/types'; +import { ExecutionInfo, ILogOutputChannel } from '../../common/types'; import { traceError, traceLog } from '../../logging'; import { ILinterManager, LinterId } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; @@ -29,7 +28,7 @@ export class StandardErrorHandler extends BaseErrorHandler { private async displayLinterError(linterId: LinterId) { const message = l10n.t("There was an error in running the linter '{0}'", linterId); const appShell = this.serviceContainer.get(IApplicationShell); - const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get(ILogOutputChannel); const action = await appShell.showErrorMessage(message, 'View Errors'); if (action === 'View Errors') { outputChannel.show(); diff --git a/src/client/logging/outputChannelLogger.ts b/src/client/logging/outputChannelLogger.ts index 27ea0031c017..40505d33a735 100644 --- a/src/client/logging/outputChannelLogger.ts +++ b/src/client/logging/outputChannelLogger.ts @@ -2,34 +2,29 @@ // Licensed under the MIT License. import * as util from 'util'; -import { OutputChannel } from 'vscode'; +import { LogOutputChannel } from 'vscode'; import { Arguments, ILogging } from './types'; -import { getTimeForLogging } from './util'; - -function formatMessage(level?: string, ...data: Arguments): string { - return level ? `[${level.toUpperCase()} ${getTimeForLogging()}]: ${util.format(...data)}` : util.format(...data); -} export class OutputChannelLogger implements ILogging { - constructor(private readonly channel: OutputChannel) {} + constructor(private readonly channel: LogOutputChannel) {} public traceLog(...data: Arguments): void { this.channel.appendLine(util.format(...data)); } public traceError(...data: Arguments): void { - this.channel.appendLine(formatMessage('error', ...data)); + this.channel.error(util.format(...data)); } public traceWarn(...data: Arguments): void { - this.channel.appendLine(formatMessage('warn', ...data)); + this.channel.warn(util.format(...data)); } public traceInfo(...data: Arguments): void { - this.channel.appendLine(formatMessage('info', ...data)); + this.channel.info(util.format(...data)); } public traceVerbose(...data: Arguments): void { - this.channel.appendLine(formatMessage('debug', ...data)); + this.channel.debug(util.format(...data)); } } diff --git a/src/client/testing/constants.ts b/src/client/testing/constants.ts deleted file mode 100644 index f8c4bb749498..000000000000 --- a/src/client/testing/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -export const TEST_OUTPUT_CHANNEL = 'TEST_OUTPUT_CHANNEL'; diff --git a/src/client/testing/testController/common/server.ts b/src/client/testing/testController/common/server.ts index 1b7d46c995e7..6849f0f8969a 100644 --- a/src/client/testing/testController/common/server.ts +++ b/src/client/testing/testController/common/server.ts @@ -99,6 +99,7 @@ export class PythonTestServer implements ITestServer, Disposable { token: options.token, cwd: options.cwd, throwOnStdErr: true, + outputChannel: options.outChannel, }; // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index b2be2d9c3054..e745adb019b2 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -20,7 +20,7 @@ import { IExtensionSingleActivationService } from '../../activation/types'; import { ICommandManager, IWorkspaceService } from '../../common/application/types'; import * as constants from '../../common/constants'; import { IPythonExecutionFactory } from '../../common/process/types'; -import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, ITestOutputChannel, Resource } from '../../common/types'; import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; @@ -92,6 +92,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc @inject(ICommandManager) private readonly commandManager: ICommandManager, @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, + @inject(ITestOutputChannel) private readonly testOutputChannel: ITestOutputChannel, ) { this.refreshCancellation = new CancellationTokenSource(); @@ -158,12 +159,28 @@ export class PythonTestController implements ITestController, IExtensionSingleAc let executionAdapter: ITestExecutionAdapter; let testProvider: TestProvider; if (settings.testing.unittestEnabled) { - discoveryAdapter = new UnittestTestDiscoveryAdapter(this.pythonTestServer, this.configSettings); - executionAdapter = new UnittestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new UnittestTestDiscoveryAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); + executionAdapter = new UnittestTestExecutionAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); testProvider = UNITTEST_PROVIDER; } else { - discoveryAdapter = new PytestTestDiscoveryAdapter(this.pythonTestServer, this.configSettings); - executionAdapter = new PytestTestExecutionAdapter(this.pythonTestServer, this.configSettings); + discoveryAdapter = new PytestTestDiscoveryAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); + executionAdapter = new PytestTestExecutionAdapter( + this.pythonTestServer, + this.configSettings, + this.testOutputChannel, + ); testProvider = PYTEST_PROVIDER; } diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 379f1ea9d12c..d2f6f6448777 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -7,7 +7,7 @@ import { IPythonExecutionFactory, SpawnOptions, } from '../../../common/process/types'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { traceVerbose } from '../../../logging'; @@ -21,7 +21,11 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private deferred: Deferred | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -68,6 +72,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { TEST_UUID: uuid.toString(), TEST_PORT: this.testServer.getPort().toString(), }, + outputChannel: this.outputChannel, }; // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 70aa2698c0d1..1f6b504074cd 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; @@ -16,7 +16,11 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { private deferred: Deferred | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -31,6 +35,8 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // ** Old version of discover tests. async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { traceVerbose(uri, testIds, debugBool); + // TODO:Remove this line after enabling runs + this.outputChannel.appendLine('Running tests.'); this.deferred = createDeferred(); return this.deferred.promise; } @@ -62,6 +68,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { // TEST_UUID: uuid.toString(), // TEST_PORT: this.testServer.getPort().toString(), // }, +// outputChannel: this.outputChannel, // }; // // Create the Python environment in which to execute the command. diff --git a/src/client/testing/testController/pytest/runner.ts b/src/client/testing/testController/pytest/runner.ts index 7bc045b34687..2c6cff724398 100644 --- a/src/client/testing/testController/pytest/runner.ts +++ b/src/client/testing/testController/pytest/runner.ts @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } from 'inversify'; import { Disposable, TestItem, TestRun, TestRunProfileKind } from 'vscode'; -import { IOutputChannel } from '../../../common/types'; +import { ITestOutputChannel } from '../../../common/types'; import { PYTEST_PROVIDER } from '../../common/constants'; import { ITestDebugLauncher, ITestRunner, LaunchOptions, Options } from '../../common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../constants'; import { filterArguments, getOptionValues } from '../common/argumentsHelper'; import { createTemporaryFile } from '../common/externalDependencies'; import { updateResultFromJunitXml } from '../common/resultsHelper'; @@ -32,7 +31,7 @@ export class PytestRunner implements ITestsRunner { constructor( @inject(ITestRunner) private readonly runner: ITestRunner, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, + @inject(ITestOutputChannel) private readonly outputChannel: ITestOutputChannel, ) {} public async runTests( diff --git a/src/client/testing/testController/unittest/runner.ts b/src/client/testing/testController/unittest/runner.ts index d6bbb59ee640..e0897f554cea 100644 --- a/src/client/testing/testController/unittest/runner.ts +++ b/src/client/testing/testController/unittest/runner.ts @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable, inject, named } from 'inversify'; +import { injectable, inject } from 'inversify'; import { Location, TestController, TestItem, TestMessage, TestRun, TestRunProfileKind } from 'vscode'; import * as internalScripts from '../../../common/process/internal/scripts'; import { splitLines } from '../../../common/stringUtils'; -import { IOutputChannel } from '../../../common/types'; +import { ITestOutputChannel } from '../../../common/types'; import { noop } from '../../../common/utils/misc'; import { traceError, traceInfo } from '../../../logging'; import { UNITTEST_PROVIDER } from '../../common/constants'; import { ITestRunner, ITestDebugLauncher, IUnitTestSocketServer, LaunchOptions, Options } from '../../common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../constants'; import { clearAllChildren, getTestCaseNodes } from '../common/testItemUtilities'; import { ITestRun, ITestsRunner, TestData, TestRunInstanceOptions, TestRunOptions } from '../common/types'; import { fixLogLines } from '../common/utils'; @@ -33,7 +32,7 @@ export class UnittestRunner implements ITestsRunner { constructor( @inject(ITestRunner) private readonly runner: ITestRunner, @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, - @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, + @inject(ITestOutputChannel) private readonly outputChannel: ITestOutputChannel, @inject(IUnitTestSocketServer) private readonly server: IUnitTestSocketServer, ) {} diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts index fa411b50216f..3f8ecb5797d3 100644 --- a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { @@ -23,7 +23,11 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { private cwd: string | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -50,6 +54,7 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { command, cwd: this.cwd, uuid, + outChannel: this.outputChannel, }; this.promiseMap.set(uuid, deferred); diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts index 0bca2778ef75..b39e0cd29560 100644 --- a/src/client/testing/testController/unittest/testExecutionAdapter.ts +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { @@ -24,7 +24,11 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { private cwd: string | undefined; - constructor(public testServer: ITestServer, public configSettings: IConfigurationService) { + constructor( + public testServer: ITestServer, + public configSettings: IConfigurationService, + private readonly outputChannel: ITestOutputChannel, + ) { testServer.onDataReceived(this.onDataReceivedHandler, this); } @@ -51,6 +55,7 @@ export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { uuid, debugBool, testIds, + outChannel: this.outputChannel, }; const deferred = createDeferred(); diff --git a/src/test/activation/node/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts index d4781f7e03e5..47c0cb0cc838 100644 --- a/src/test/activation/node/analysisOptions.unit.test.ts +++ b/src/test/activation/node/analysisOptions.unit.test.ts @@ -9,7 +9,7 @@ import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/no import { ILanguageServerOutputChannel } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { IExperimentService, IOutputChannel } from '../../../client/common/types'; +import { IExperimentService, ILogOutputChannel } from '../../../client/common/types'; suite('Pylance Language Server - Analysis Options', () => { class TestClass extends NodeLanguageServerAnalysisOptions { @@ -28,13 +28,13 @@ suite('Pylance Language Server - Analysis Options', () => { } let analysisOptions: TestClass; - let outputChannel: IOutputChannel; + let outputChannel: ILogOutputChannel; let lsOutputChannel: typemoq.IMock; let workspace: typemoq.IMock; let experimentService: IExperimentService; setup(() => { - outputChannel = typemoq.Mock.ofType().object; + outputChannel = typemoq.Mock.ofType().object; workspace = typemoq.Mock.ofType(); workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); const workspaceConfig = typemoq.Mock.ofType(); diff --git a/src/test/activation/outputChannel.unit.test.ts b/src/test/activation/outputChannel.unit.test.ts index f7d827b4782a..f8f38783bb0e 100644 --- a/src/test/activation/outputChannel.unit.test.ts +++ b/src/test/activation/outputChannel.unit.test.ts @@ -7,7 +7,7 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IOutputChannel } from '../../client/common/types'; +import { ILogOutputChannel } from '../../client/common/types'; import { sleep } from '../../client/common/utils/async'; import { OutputChannelNames } from '../../client/common/utils/localize'; @@ -15,10 +15,10 @@ suite('Language Server Output Channel', () => { let appShell: TypeMoq.IMock; let languageServerOutputChannel: LanguageServerOutputChannel; let commandManager: TypeMoq.IMock; - let output: TypeMoq.IMock; + let output: TypeMoq.IMock; setup(() => { appShell = TypeMoq.Mock.ofType(); - output = TypeMoq.Mock.ofType(); + output = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object, []); }); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index 5c77f8165492..01ac0e315555 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -13,7 +13,6 @@ import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { CancellationTokenSource, Disposable, ProgressLocation, Uri, WorkspaceConfiguration } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; import { ModuleInstaller } from '../../../client/common/installer/moduleInstaller'; import { PipEnvInstaller, pipenvName } from '../../../client/common/installer/pipEnvInstaller'; @@ -32,7 +31,7 @@ import { IConfigurationService, IDisposableRegistry, IInstaller, - IOutputChannel, + ILogOutputChannel, IPythonSettings, Product, } from '../../../client/common/types'; @@ -89,7 +88,7 @@ suite('Module Installer', () => { return super.elevatedInstall(execPath, args); } } - let outputChannel: TypeMoq.IMock; + let outputChannel: TypeMoq.IMock; let appShell: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; @@ -104,9 +103,9 @@ suite('Module Installer', () => { traceLogStub = sinon.stub(logging, 'traceLog'); serviceContainer = TypeMoq.Mock.ofType(); - outputChannel = TypeMoq.Mock.ofType(); + outputChannel = TypeMoq.Mock.ofType(); serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => outputChannel.object); appShell = TypeMoq.Mock.ofType(); serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts index cf0297628a83..05970d0c71f6 100644 --- a/src/test/format/formatter.unit.test.ts +++ b/src/test/format/formatter.unit.test.ts @@ -13,7 +13,6 @@ import { IApplicationShell, IWorkspaceService } from '../../client/common/applic import { WorkspaceService } from '../../client/common/application/workspace'; import { PythonSettings } from '../../client/common/configSettings'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; import { IPythonToolExecutionService } from '../../client/common/process/types'; import { @@ -21,7 +20,7 @@ import { IConfigurationService, IDisposableRegistry, IFormattingSettings, - IOutputChannel, + ILogOutputChannel, IPythonSettings, } from '../../client/common/types'; import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; @@ -37,7 +36,7 @@ import { MockOutputChannel } from '../mockClasses'; suite('Formatting - Test Arguments', () => { let container: IServiceContainer; - let outputChannel: IOutputChannel; + let outputChannel: ILogOutputChannel; let workspace: IWorkspaceService; let settings: IPythonSettings; const workspaceUri = Uri.file(__dirname); @@ -85,9 +84,7 @@ suite('Formatting - Test Arguments', () => { when(configService.getSettings(anything())).thenReturn(instance(settings)); when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).thenReturn( - instance(outputChannel), - ); + when(container.get(ILogOutputChannel)).thenReturn(instance(outputChannel)); when(container.get(IApplicationShell)).thenReturn(instance(appShell)); when(container.get(IFormatterHelper)).thenReturn(formatterHelper); when(container.get(IWorkspaceService)).thenReturn(instance(workspace)); diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts index c602492ccd67..3c8f72a8d710 100644 --- a/src/test/linters/common.ts +++ b/src/test/linters/common.ts @@ -18,7 +18,7 @@ import { IConfigurationService, IInstaller, IMypyCategorySeverity, - IOutputChannel, + ILogOutputChannel, IPycodestyleCategorySeverity, IPylintCategorySeverity, IPythonSettings, @@ -243,7 +243,7 @@ export class BaseTestFixture { public lintingSettings: LintingSettings; // data - public outputChannel: TypeMoq.IMock; + public outputChannel: TypeMoq.IMock; // artifacts public output: string; @@ -309,10 +309,10 @@ export class BaseTestFixture { // data - this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + this.outputChannel = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); this.serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))) .returns(() => this.outputChannel.object); this.initData(); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts index 20599d7fb713..1bf77c502af5 100644 --- a/src/test/linters/lintengine.test.ts +++ b/src/test/linters/lintengine.test.ts @@ -4,12 +4,12 @@ 'use strict'; import * as TypeMoq from 'typemoq'; -import { OutputChannel, TextDocument, Uri } from 'vscode'; +import { TextDocument, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; import '../../client/common/extensions'; import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, ILintingSettings, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IConfigurationService, ILintingSettings, ILogOutputChannel, IPythonSettings } from '../../client/common/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; import { LintingEngine } from '../../client/linters/lintingEngine'; @@ -54,10 +54,8 @@ suite('Linting - LintingEngine', () => { .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) .returns(() => configService.object); - const outputChannel = TypeMoq.Mock.ofType(); - serviceContainer - .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) - .returns(() => outputChannel.object); + const outputChannel = TypeMoq.Mock.ofType(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); lintManager = TypeMoq.Mock.ofType(); lintManager.setup((x) => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index 0273cf27fdb9..c962c4d67ca4 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -1,4 +1,5 @@ import * as vscode from 'vscode'; +import * as util from 'util'; import { Flake8CategorySeverity, ILintingSettings, @@ -7,13 +8,32 @@ import { IPylintCategorySeverity, } from '../client/common/types'; -export class MockOutputChannel implements vscode.OutputChannel { +export class MockOutputChannel implements vscode.LogOutputChannel { public name: string; public output: string; public isShown!: boolean; + private _eventEmitter = new vscode.EventEmitter(); + public onDidChangeLogLevel: vscode.Event = this._eventEmitter.event; constructor(name: string) { this.name = name; this.output = ''; + this.logLevel = vscode.LogLevel.Debug; + } + public logLevel: vscode.LogLevel; + trace(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + debug(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + info(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + warn(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + error(error: string | Error, ...args: any[]): void { + this.appendLine(util.format(error, ...args)); } public append(value: string) { this.output += value; diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index e6ea57a88673..89f4ab1a2d07 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -411,3 +411,35 @@ export class InlayHint { public kind?: vscode.InlayHintKind, ) {} } + +export enum LogLevel { + /** + * No messages are logged with this level. + */ + Off = 0, + + /** + * All messages are logged with this level. + */ + Trace = 1, + + /** + * Messages with debug and higher log level are logged with this level. + */ + Debug = 2, + + /** + * Messages with info and higher log level are logged with this level. + */ + Info = 3, + + /** + * Messages with warning and higher log level are logged with this level. + */ + Warning = 4, + + /** + * Only error messages are logged with this level. + */ + Error = 5, +} diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index e3c6763eace7..1b8a9d78d580 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -4,8 +4,7 @@ import { Container } from 'inversify'; import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { Disposable, Memento, OutputChannel } from 'vscode'; -import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; +import { Disposable, Memento } from 'vscode'; import { IS_WINDOWS } from '../client/common/platform/constants'; import { FileSystem } from '../client/common/platform/fileSystem'; import { PathUtils } from '../client/common/platform/pathUtils'; @@ -28,10 +27,11 @@ import { ICurrentProcess, IDisposableRegistry, IMemento, - IOutputChannel, + ILogOutputChannel, IPathUtils, IsWindows, WORKSPACE_MEMENTO, + ITestOutputChannel, } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; @@ -48,7 +48,6 @@ import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; -import { TEST_OUTPUT_CHANNEL } from '../client/testing/constants'; import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; import { LegacyFileSystem } from './legacyFileSystem'; import { MockOutputChannel } from './mockClasses'; @@ -83,14 +82,10 @@ export class IocContainer { const stdOutputChannel = new MockOutputChannel('Python'); this.disposables.push(stdOutputChannel); - this.serviceManager.addSingletonInstance( - IOutputChannel, - stdOutputChannel, - STANDARD_OUTPUT_CHANNEL, - ); + this.serviceManager.addSingletonInstance(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance(ITestOutputChannel, testOutputChannel); this.serviceManager.addSingleton( IInterpreterAutoSelectionService, diff --git a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts index 02a7193172af..c8b6085e599d 100644 --- a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -5,13 +5,12 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; +import { IInstaller, ITestOutputChannel, Product } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { IServiceContainer } from '../../../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; import { TestConfigurationManager } from '../../../../client/testing/common/testConfigurationManager'; import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../../../client/testing/constants'; class MockTestConfigurationManager extends TestConfigurationManager { // The workspace arg is ignored. @@ -42,7 +41,7 @@ suite('Unit Test Configuration Manager (unit)', () => { const installer = TypeMoq.Mock.ofType().object; const serviceContainer = TypeMoq.Mock.ofType(); serviceContainer - .setup((s) => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((s) => s.get(TypeMoq.It.isValue(ITestOutputChannel))) .returns(() => outputChannel); serviceContainer .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts index fec936a2a21a..abb57aac2309 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -7,7 +7,13 @@ import { expect } from 'chai'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { IConfigurationService, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../client/common/types'; +import { + IConfigurationService, + IInstaller, + ITestOutputChannel, + IPythonSettings, + Product, +} from '../../client/common/types'; import { getNamesAndValues } from '../../client/common/utils/enum'; import { IServiceContainer } from '../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; @@ -18,7 +24,6 @@ import { ITestConfigurationManagerFactory, ITestsHelper, } from '../../client/testing/common/types'; -import { TEST_OUTPUT_CHANNEL } from '../../client/testing/constants'; import { ITestingSettings } from '../../client/testing/configuration/types'; import { NONE_SELECTED, UnitTestConfigurationService } from '../../client/testing/configuration'; @@ -56,7 +61,7 @@ suite('Unit Tests - ConfigurationService', () => { configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts index 1418147d615c..74f7dd0da19b 100644 --- a/src/test/testing/configurationFactory.unit.test.ts +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -7,11 +7,10 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; +import { IInstaller, ITestOutputChannel, Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; -import { TEST_OUTPUT_CHANNEL } from '../../client/testing/constants'; import * as pytest from '../../client/testing/configuration/pytest/testConfigurationManager'; import * as unittest from '../../client/testing/configuration/unittest/testConfigurationManager'; @@ -26,7 +25,7 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { const testConfigService = typeMoq.Mock.ofType(); serviceContainer - .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .setup((c) => c.get(typeMoq.It.isValue(ITestOutputChannel))) .returns(() => outputChannel.object); serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); serviceContainer diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts index 113e4175fe69..3d3521291f74 100644 --- a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -3,14 +3,16 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../../client/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; suite('Unittest test discovery adapter', () => { let stubConfigSettings: IConfigurationService; + let outputChannel: typemoq.IMock; setup(() => { stubConfigSettings = ({ @@ -18,6 +20,7 @@ suite('Unittest test discovery adapter', () => { testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; + outputChannel = typemoq.Mock.ofType(); }); test('discoverTests should send the discovery command to the test server', async () => { @@ -25,6 +28,7 @@ suite('Unittest test discovery adapter', () => { const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { + delete opt.outChannel; options = opt; return Promise.resolve(); }, @@ -37,7 +41,7 @@ suite('Unittest test discovery adapter', () => { const uri = Uri.file('/foo/bar'); const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'discovery.py'); - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.discoverTests(uri); assert.deepStrictEqual(options, { @@ -62,7 +66,7 @@ suite('Unittest test discovery adapter', () => { const uri = Uri.file('/foo/bar'); const data = { status: 'success' }; const uuid = '123456789'; - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); const promise = adapter.discoverTests(uri); adapter.onDataReceivedHandler({ uuid, data: JSON.stringify(data) }); @@ -87,7 +91,7 @@ suite('Unittest test discovery adapter', () => { const uri = Uri.file('/foo/bar'); - const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings, outputChannel.object); const promise = adapter.discoverTests(uri); const data = { status: 'success' }; diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts index f8648c8963fa..d88f033d39a4 100644 --- a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -3,14 +3,16 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../../client/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; import { ITestServer, TestCommandOptions } from '../../../../client/testing/testController/common/types'; import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; suite('Unittest test execution adapter', () => { let stubConfigSettings: IConfigurationService; + let outputChannel: typemoq.IMock; setup(() => { stubConfigSettings = ({ @@ -18,6 +20,7 @@ suite('Unittest test execution adapter', () => { testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, }), } as unknown) as IConfigurationService; + outputChannel = typemoq.Mock.ofType(); }); test('runTests should send the run command to the test server', async () => { @@ -25,6 +28,7 @@ suite('Unittest test execution adapter', () => { const stubTestServer = ({ sendCommand(opt: TestCommandOptions): Promise { + delete opt.outChannel; options = opt; return Promise.resolve(); }, @@ -37,7 +41,7 @@ suite('Unittest test execution adapter', () => { const uri = Uri.file('/foo/bar'); const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); adapter.runTests(uri, [], false); const expectedOptions: TestCommandOptions = { @@ -66,7 +70,7 @@ suite('Unittest test execution adapter', () => { const data = { status: 'success' }; const uuid = '123456789'; - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); // triggers runTests flow which will run onDataReceivedHandler and the // promise resolves into the parsed data. @@ -93,7 +97,7 @@ suite('Unittest test execution adapter', () => { const uri = Uri.file('/foo/bar'); - const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const adapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings, outputChannel.object); // triggers runTests flow which will run onDataReceivedHandler and the // promise resolves into the parsed data. diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts index b6be8d6081de..539647aece9f 100644 --- a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -3,9 +3,10 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; import { TestController, TestItem, Uri } from 'vscode'; -import { IConfigurationService } from '../../../client/common/types'; +import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types'; import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; @@ -20,6 +21,7 @@ suite('Workspace test adapter', () => { let discoverTestsStub: sinon.SinonStub; let sendTelemetryStub: sinon.SinonStub; + let outputChannel: typemoq.IMock; let telemetryEvent: { eventName: EventName; properties: Record }[] = []; @@ -97,6 +99,7 @@ suite('Workspace test adapter', () => { discoverTestsStub = sandbox.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + outputChannel = typemoq.Mock.ofType(); }); teardown(() => { @@ -109,8 +112,16 @@ suite('Workspace test adapter', () => { test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { discoverTestsStub.resolves(); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); // 7/7 + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -134,8 +145,16 @@ suite('Workspace test adapter', () => { }), ); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); // 7/7 + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', testDiscoveryAdapter, @@ -155,8 +174,16 @@ suite('Workspace test adapter', () => { test('If discovery succeeds, send a telemetry event with the "failed" key set to false', async () => { discoverTestsStub.resolves({ status: 'success' }); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', @@ -177,8 +204,16 @@ suite('Workspace test adapter', () => { test('If discovery failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { discoverTestsStub.rejects(new Error('foo')); - const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubTestServer, stubConfigSettings); - const testExecutionAdapter = new UnittestTestExecutionAdapter(stubTestServer, stubConfigSettings); + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); + const testExecutionAdapter = new UnittestTestExecutionAdapter( + stubTestServer, + stubConfigSettings, + outputChannel.object, + ); const workspaceTestAdapter = new WorkspaceTestAdapter( 'unittest', diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 9fb4bbec329b..ebbe7ca59e72 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -112,6 +112,7 @@ mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError mockedVSCode.LanguageStatusSeverity = vscodeMocks.LanguageStatusSeverity; mockedVSCode.QuickPickItemKind = vscodeMocks.QuickPickItemKind; mockedVSCode.InlayHint = vscodeMocks.InlayHint; +mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).NotebookCellKind = vscodeMocks.vscMockExtHostedTypes.NotebookCellKind; (mockedVSCode as any).CellOutputKind = vscodeMocks.vscMockExtHostedTypes.CellOutputKind; (mockedVSCode as any).NotebookCellRunState = vscodeMocks.vscMockExtHostedTypes.NotebookCellRunState; From 32174205437c7a79095cd8ad703a7ab8f42dfce9 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 17 Apr 2023 19:34:24 -0700 Subject: [PATCH 04/10] Direct users to the Jupyter extension when using Run in Interactive window (#21072) Closes https://github.com/microsoft/vscode-python/issues/20576 --- package.json | 23 ++++++- package.nls.json | 2 + src/client/common/application/commands.ts | 1 + src/client/common/application/contextKeys.ts | 1 + src/client/common/constants.ts | 1 + src/client/common/serviceRegistry.ts | 5 ++ src/client/common/utils/localize.ts | 3 + src/client/jupyter/jupyterIntegration.ts | 5 +- src/client/jupyter/requireJupyterPrompt.ts | 45 ++++++++++++++ src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 16 +++++ ...eractiveWindowMiddlewareAddon.unit.test.ts | 3 +- .../jupyter/requireJupyterPrompt.unit.test.ts | 60 +++++++++++++++++++ 13 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/client/jupyter/requireJupyterPrompt.ts create mode 100644 src/test/jupyter/requireJupyterPrompt.unit.test.ts diff --git a/package.json b/package.json index 893eb2299abd..62bd7f89b700 100644 --- a/package.json +++ b/package.json @@ -374,6 +374,11 @@ "light": "resources/light/repl.svg" }, "title": "%python.command.python.viewOutput.title%" + }, + { + "category": "Python", + "command": "python.installJupyter", + "title": "%python.command.python.installJupyter.title%" } ], "configuration": { @@ -1705,13 +1710,25 @@ { "submenu": "python.run", "group": "Python", - "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported" + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && isWorkspaceTrusted" }, { "command": "python.sortImports", "group": "Refactor", "title": "%python.command.python.sortImports.title%", "when": "editorLangId == python && !notebookEditorFocused && !virtualWorkspace && shellExecutionSupported" + }, + { + "submenu": "python.runFileInteractive", + "group": "Jupyter2", + "when": "editorLangId == python && !virtualWorkspace && shellExecutionSupported && !isJupyterInstalled && isWorkspaceTrusted" + } + ], + "python.runFileInteractive": [ + { + "command": "python.installJupyter", + "group": "Jupyter2", + "when": "resourceLangId == python && !virtualWorkspace && shellExecutionSupported" } ], "python.run": [ @@ -1779,6 +1796,10 @@ "id": "python.run", "label": "%python.editor.context.submenu.runPython%", "icon": "$(play)" + }, + { + "id": "python.runFileInteractive", + "label": "%python.editor.context.submenu.runPythonInteractive%" } ], "viewsWelcome": [ diff --git a/package.nls.json b/package.nls.json index cb777d95c214..0bf0ac2a8426 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,6 +10,7 @@ "python.command.python.setInterpreter.title": "Select Interpreter", "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", + "python.command.python.installJupyter.title": "Install the Jupyter extension", "python.command.python.viewLanguageServerOutput.title": "Show Language Server Output", "python.command.python.configureTests.title": "Configure Tests", "python.command.testing.rerunFailedTests.title": "Rerun Failed Tests", @@ -26,6 +27,7 @@ "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.menu.createNewFile.title": "Python File", "python.editor.context.submenu.runPython": "Run Python", + "python.editor.context.submenu.runPythonInteractive": "Run in Interactive window", "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 277baffd19a4..2a4404440101 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -17,6 +17,7 @@ export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping; */ interface ICommandNameWithoutArgumentTypeMapping { [Commands.InstallPythonOnMac]: []; + [Commands.InstallJupyter]: []; [Commands.InstallPythonOnLinux]: []; [Commands.InstallPython]: []; [Commands.ClearWorkspaceInterpreter]: []; diff --git a/src/client/common/application/contextKeys.ts b/src/client/common/application/contextKeys.ts index 2f791ae66846..d6249f05eaec 100644 --- a/src/client/common/application/contextKeys.ts +++ b/src/client/common/application/contextKeys.ts @@ -5,4 +5,5 @@ export enum ExtensionContextKey { showInstallPythonTile = 'showInstallPythonTile', HasFailedTests = 'hasFailedTests', RefreshingTests = 'refreshingTests', + IsJupyterInstalled = 'isJupyterInstalled', } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index f53923b3e1b4..104504650aed 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -46,6 +46,7 @@ export namespace Commands { export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; export const GetSelectedInterpreterPath = 'python.interpreterPath'; + export const InstallJupyter = 'python.installJupyter'; export const InstallPython = 'python.installPython'; export const InstallPythonOnLinux = 'python.installPythonOnLinux'; export const InstallPythonOnMac = 'python.installPythonOnMac'; diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 8e2c4d4d3ebe..5b527499460a 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -90,6 +90,7 @@ import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStep import { Random } from './utils/random'; import { ContextKeyManager } from './application/contextKeyManager'; import { CreatePythonFileCommandHandler } from './application/commands/createPythonFile'; +import { RequireJupyterPrompt } from '../jupyter/requireJupyterPrompt'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); @@ -110,6 +111,10 @@ export function registerTypes(serviceManager: IServiceManager): void { IJupyterExtensionDependencyManager, JupyterExtensionDependencyManager, ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequireJupyterPrompt, + ); serviceManager.addSingleton( IExtensionSingleActivationService, CreatePythonFileCommandHandler, diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 10c70c8c6cd0..e67aad586764 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -188,6 +188,9 @@ export namespace LanguageService { ); } export namespace Interpreters { + export const requireJupyter = l10n.t( + 'Running in Interactive window requires Jupyter Extension. Would you like to install it? [Learn more](https://aka.ms/pythonJupyterSupport).', + ); export const installingPython = l10n.t('Installing Python into Environment...'); export const discovering = l10n.t('Discovering Python Interpreters'); export const refreshing = l10n.t('Refreshing Python Interpreters'); diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts index 16da174f3178..a0fa0fedb63f 100644 --- a/src/client/jupyter/jupyterIntegration.ts +++ b/src/client/jupyter/jupyterIntegration.ts @@ -8,7 +8,7 @@ import { inject, injectable, named } from 'inversify'; import { dirname } from 'path'; import { CancellationToken, Event, Extension, Memento, Uri } from 'vscode'; import type { SemVer } from 'semver'; -import { IWorkspaceService } from '../common/application/types'; +import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; import { InterpreterUri, ModuleInstallFlags } from '../common/installer/types'; import { @@ -35,6 +35,7 @@ import { import { PythonEnvironment } from '../pythonEnvironments/info'; import { IDataViewerDataProvider, IJupyterUriProvider } from './types'; import { PylanceApi } from '../activation/node/pylanceApi'; +import { ExtensionContextKey } from '../common/application/contextKeys'; /** * This allows Python extension to update Product enum without breaking Jupyter. * I.e. we have a strict contract, else using numbers (in enums) is bound to break across products. @@ -201,9 +202,11 @@ export class JupyterExtensionIntegration { @inject(IComponentAdapter) private pyenvs: IComponentAdapter, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, ) {} public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { + this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true); if (!this.workspaceService.isTrusted) { this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(jupyterExtensionApi)); return undefined; diff --git a/src/client/jupyter/requireJupyterPrompt.ts b/src/client/jupyter/requireJupyterPrompt.ts new file mode 100644 index 000000000000..3e6878ba4269 --- /dev/null +++ b/src/client/jupyter/requireJupyterPrompt.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { Common, Interpreters } from '../common/utils/localize'; +import { Commands, JUPYTER_EXTENSION_ID } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +@injectable() +export class RequireJupyterPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallJupyter, () => this._showPrompt())); + } + + public async _showPrompt(): Promise { + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.requireJupyter, ...prompts); + sendTelemetryEvent(EventName.REQUIRE_JUPYTER_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.commandManager.executeCommand( + 'workbench.extensions.installExtension', + JUPYTER_EXTENSION_ID, + undefined, + ); + } + } +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index d30a2683562c..778eb7cc39a0 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -30,6 +30,7 @@ export enum EventName { PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', + REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index df2b454cbe27..9e86f29201ee 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1315,6 +1315,22 @@ export interface IEventNamePropertyMapping { */ selection: 'Allow' | 'Close' | undefined; }; + /** + * Telemetry event sent with details when user attempts to run in interactive window when Jupyter is not installed. + */ + /* __GDPR__ + "conda_inherit_env_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karrtikr" } + } + */ + [EventName.REQUIRE_JUPYTER_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `undefined` When 'x' is selected + */ + selection: 'Yes' | 'No' | undefined; + }; /** * Telemetry event sent with details when user clicks the prompt with the following message: * diff --git a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts index 472dea147503..256e57a5d724 100644 --- a/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts +++ b/src/test/activation/node/lspInteractiveWindowMiddlewareAddon.unit.test.ts @@ -19,7 +19,7 @@ import { } from '../../../client/interpreter/contracts'; import { IInterpreterSelector } from '../../../client/interpreter/configuration/types'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { IWorkspaceService } from '../../../client/common/application/types'; +import { IContextKeyManager, IWorkspaceService } from '../../../client/common/application/types'; import { MockMemento } from '../../mocks/mementos'; suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { @@ -41,6 +41,7 @@ suite('Pylance Language Server - Interactive Window LSP Notebooks', () => { mock(), mock(), mock(), + mock(), ); jupyterApi.registerGetNotebookUriForTextDocumentUriFunction(getNotebookUriFunction); }); diff --git a/src/test/jupyter/requireJupyterPrompt.unit.test.ts b/src/test/jupyter/requireJupyterPrompt.unit.test.ts new file mode 100644 index 000000000000..0eb6c9e06958 --- /dev/null +++ b/src/test/jupyter/requireJupyterPrompt.unit.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { mock, instance, verify, anything, when } from 'ts-mockito'; +import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; +import { Commands, JUPYTER_EXTENSION_ID } from '../../client/common/constants'; +import { IDisposableRegistry } from '../../client/common/types'; +import { Common, Interpreters } from '../../client/common/utils/localize'; +import { RequireJupyterPrompt } from '../../client/jupyter/requireJupyterPrompt'; + +suite('RequireJupyterPrompt Unit Tests', () => { + let requireJupyterPrompt: RequireJupyterPrompt; + let appShell: IApplicationShell; + let commandManager: ICommandManager; + let disposables: IDisposableRegistry; + + setup(() => { + appShell = mock(); + commandManager = mock(); + disposables = mock(); + + requireJupyterPrompt = new RequireJupyterPrompt( + instance(appShell), + instance(commandManager), + instance(disposables), + ); + }); + + test('Activation registers command', async () => { + await requireJupyterPrompt.activate(); + + verify(commandManager.registerCommand(Commands.InstallJupyter, anything())).once(); + }); + + test('Show prompt with Yes selection installs Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelYes)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).once(); + }); + + test('Show prompt with No selection does not install Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelNo)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).never(); + }); +}); From 540658b7efa15542ae6b4e5ce10d996993c5516b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 18 Apr 2023 08:03:14 -0700 Subject: [PATCH 05/10] Add quick pick hover support to explain conda environment lacking a Python interpreter (#21073) Closes https://github.com/microsoft/vscode-python/issues/20786 --- package.json | 3 ++- src/client/common/utils/localize.ts | 3 +++ .../commands/setInterpreter.ts | 1 + src/client/interpreter/configuration/types.ts | 6 +----- .../vscode.proposed.quickPickItemTooltip.d.ts | 16 ++++++++++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts diff --git a/package.json b/package.json index 62bd7f89b700..60c80693f36e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "contribEditorContentMenu", "quickPickSortByLabel", "envShellEvent", - "testObserver" + "testObserver", + "quickPickItemTooltip" ], "author": { "name": "Microsoft Corporation" diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e67aad586764..4c3cd7a45d0d 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -221,6 +221,9 @@ export namespace Interpreters { } export namespace InterpreterQuickPickList { + export const condaEnvWithoutPythonTooltip = l10n.t( + 'Python is not available in this environment, it will automatically be installed upon selecting it', + ); export const noPythonInstalled = l10n.t('Python is not installed, please download and install it'); export const clickForInstructions = l10n.t('Click for instructions...'); export const globalGroupName = l10n.t('Global'); diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 9a1d643269ef..c0876ff518dd 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -415,6 +415,7 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem if (isInterpreterQuickPickItem(item) && isProblematicCondaEnvironment(item.interpreter)) { if (!items[i].label.includes(Octicons.Warning)) { items[i].label = `${Octicons.Warning} ${items[i].label}`; + items[i].tooltip = InterpreterQuickPickList.condaEnvWithoutPythonTooltip; } } }); diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts index 90facb7fe640..2f3882e1246e 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -52,11 +52,7 @@ export interface IInterpreterQuickPickItem extends QuickPickItem { interpreter: PythonEnvironment; } -export interface ISpecialQuickPickItem { - label: string; - description?: string; - detail?: string; - alwaysShow: boolean; +export interface ISpecialQuickPickItem extends QuickPickItem { path?: string; } diff --git a/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts b/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts new file mode 100644 index 000000000000..4e7d00fa5edf --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.quickPickItemTooltip.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/73904 + + export interface QuickPickItem { + /** + * An optional flag to sort the final results by index of first query match in label. Defaults to true. + */ + tooltip?: string | MarkdownString; + } +} From b377503eb7225d87fd5575e8fefd54875e184fc6 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Tue, 18 Apr 2023 08:28:56 -0700 Subject: [PATCH 06/10] Python test execution simple (#21053) closes https://github.com/microsoft/vscode-python/issues/20897 closes https://github.com/microsoft/vscode-python/issues/20084 closes https://github.com/microsoft/vscode-python/issues/20081 --------- Co-authored-by: Karthik Nadig --- .../.data/unittest_folder/test_subtract.py | 3 +- .../expected_discovery_test_output.py | 2 + .../expected_execution_test_output.py | 328 ++++++++++++++++++ pythonFiles/tests/pytestadapter/helpers.py | 6 +- .../tests/pytestadapter/test_discovery.py | 9 +- .../tests/pytestadapter/test_execution.py | 165 +++++++++ pythonFiles/vscode_pytest/__init__.py | 210 +++++++++-- 7 files changed, 688 insertions(+), 35 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/expected_execution_test_output.py create mode 100644 pythonFiles/tests/pytestadapter/test_execution.py diff --git a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py index 80087fed0f3c..087e5140def4 100644 --- a/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py +++ b/pythonFiles/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -22,4 +22,5 @@ def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbe self, ): result = subtract(-2, -3) - self.assertEqual(result, 1) + # This is intentional to test assertion failures + self.assertEqual(result, 100000) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index e1422a81c979..8e96d109ba78 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -3,6 +3,8 @@ from .helpers import TEST_DATA_PATH, find_test_line_number +# This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. + # This is the expected output for the empty_discovery.py file. # └── TEST_DATA_PATH_STR = os.fspath(TEST_DATA_PATH) diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py new file mode 100644 index 000000000000..a894403c7d71 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -0,0 +1,328 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" +TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" +SUCCESS = "success" +FAILURE = "failure" +TEST_SUBTRACT_FUNCTION_NEGATIVE_NUMBERS_ERROR = "self = \n\n def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers\n self,\n ):\n result = subtract(-2, -3)\n> self.assertEqual(result, 100000)\nE AssertionError: 1 != 100000\n\nunittest_folder/test_subtract.py:25: AssertionError" + +# This is the expected output for the unittest_folder execute tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers: success +# │ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers: failure +# └── test_subtract_positive_numbers: success +uf_execution_expected_output = { + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + "outcome": FAILURE, + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers": { + "test": f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the unittest_folder only execute add.py tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers: success +# │ └── test_add_positive_numbers: success +uf_single_file_expected_output = { + f"{TEST_ADD_FUNCTION}test_add_negative_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_negative_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the unittest_folder execute only signle method +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ └── test_add_positive_numbers: success +uf_single_method_execution_expected_output = { + f"{TEST_ADD_FUNCTION}test_add_positive_numbers": { + "test": f"{TEST_ADD_FUNCTION}test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + } +} + +# This is the expected output for the unittest_folder tests run where two tests +# run are in different files. +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# └── test_subtract_positive_numbers: success +uf_non_adjacent_tests_execution_expected_output = { + TEST_SUBTRACT_FUNCTION + + "test_subtract_positive_numbers": { + "test": TEST_SUBTRACT_FUNCTION + "test_subtract_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + TEST_ADD_FUNCTION + + "test_add_positive_numbers": { + "test": TEST_ADD_FUNCTION + "test_add_positive_numbers", + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function: success +simple_execution_pytest_expected_output = { + "simple_pytest.py::test_function": { + "test": "simple_pytest.py::test_function", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# This is the expected output for the unittest_pytest_same_file.py file. +# ├── unittest_pytest_same_file.py +# ├── TestExample +# │ └── test_true_unittest: success +# └── test_true_pytest: success +unit_pytest_same_file_execution_expected_output = { + "unittest_pytest_same_file.py::TestExample::test_true_unittest": { + "test": "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "unittest_pytest_same_file.py::test_true_pytest": { + "test": "unittest_pytest_same_file.py::test_true_pytest", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the dual_level_nested_folder.py tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t: success +# └── test_top_function_f: failure +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t: success +# └── test_bottom_function_f: failure +dual_level_nested_folder_execution_expected_output = { + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the nested_folder tests. +# └── nested_folder_one +# └── nested_folder_two +# └── test_nest.py +# └── test_function: success +double_nested_folder_expected_execution_output = { + "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { + "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── test_adding[3+5-8]: success +# └── test_adding[2+4-6]: success +# └── test_adding[6+9-16]: failure +parametrize_tests_expected_execution_output = { + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "parametrize_tests.py::test_adding[2+4-6]": { + "test": "parametrize_tests.py::test_adding[2+4-6]", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "parametrize_tests.py::test_adding[6+9-16]": { + "test": "parametrize_tests.py::test_adding[6+9-16]", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── parametrize_tests.py +# └── test_adding[3+5-8]: success +single_parametrize_tests_expected_execution_output = { + "parametrize_tests.py::test_adding[3+5-8]": { + "test": "parametrize_tests.py::test_adding[3+5-8]", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── text_docstring.txt +# └── text_docstring: success +doctest_pytest_expected_execution_output = { + "text_docstring.txt::text_docstring.txt": { + "test": "text_docstring.txt::text_docstring.txt", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# Will run all tests in the cwd that fit the test file naming pattern. +no_test_ids_pytest_execution_expected_output = { + "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function": { + "test": "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/test_top_folder.py::test_top_function_t": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/test_top_folder.py::test_top_function_f": { + "test": "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f": { + "test": "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers": { + "test": "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers": { + "test": "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} diff --git a/pythonFiles/tests/pytestadapter/helpers.py b/pythonFiles/tests/pytestadapter/helpers.py index 8d485456c145..b078439f6eac 100644 --- a/pythonFiles/tests/pytestadapter/helpers.py +++ b/pythonFiles/tests/pytestadapter/helpers.py @@ -11,7 +11,7 @@ import subprocess import sys import uuid -from typing import Dict, List, Union +from typing import Any, Dict, List, Optional, Union TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" from typing_extensions import TypedDict @@ -83,7 +83,7 @@ def _new_sock() -> socket.socket: ) -def process_rpc_json(data: str) -> Dict[str, str]: +def process_rpc_json(data: str) -> Dict[str, Any]: """Process the JSON data which comes from the server which runs the pytest discovery.""" str_stream: io.StringIO = io.StringIO(data) @@ -107,7 +107,7 @@ def process_rpc_json(data: str) -> Dict[str, str]: return json.loads(raw_json) -def runner(args: List[str]) -> Union[Dict[str, str], None]: +def runner(args: List[str]) -> Optional[Dict[str, Any]]: """Run the pytest discovery and return the JSON data from the server.""" process_args: List[str] = [ sys.executable, diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 57fa9d624bd6..bb6e7255704e 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import os import shutil -import signal import pytest @@ -31,10 +30,10 @@ def test_syntax_error(tmp_path): shutil.copyfile(file_path, p) actual = runner(["--collect-only", os.fspath(p)]) assert actual - assert all(item in actual for item in ("status", "cwd", "errors")) + assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["errors"]) == 2 + assert len(actual["error"]) == 2 def test_parameterized_error_collect(): @@ -45,10 +44,10 @@ def test_parameterized_error_collect(): file_path_str = "error_parametrize_discovery.py" actual = runner(["--collect-only", file_path_str]) assert actual - assert all(item in actual for item in ("status", "cwd", "errors")) + assert all(item in actual for item in ("status", "cwd", "error")) assert actual["status"] == "error" assert actual["cwd"] == os.fspath(TEST_DATA_PATH) - assert len(actual["errors"]) == 2 + assert len(actual["error"]) == 2 @pytest.mark.parametrize( diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py new file mode 100644 index 000000000000..8613deb96098 --- /dev/null +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -0,0 +1,165 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import shutil + +import pytest +from tests.pytestadapter import expected_execution_test_output + +from .helpers import TEST_DATA_PATH, runner + + +def test_syntax_error_execution(tmp_path): + """Test pytest execution on a file that has a syntax error. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest exeuction on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = TEST_DATA_PATH / "error_syntax_discovery.txt" + temp_dir = tmp_path / "temp_data" + temp_dir.mkdir() + p = temp_dir / "error_syntax_discovery.py" + shutil.copyfile(file_path, p) + actual = runner(["error_syntax_discover.py::test_function"]) + assert actual + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 + + +def test_bad_id_error_execution(): + """Test pytest discovery with a non-existent test_id. + + The json should still be returned but the errors list should be present. + """ + actual = runner(["not/a/real::test_id"]) + assert actual + assert all(item in actual for item in ("status", "cwd", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert len(actual["error"]) == 1 + + +@pytest.mark.parametrize( + "test_ids, expected_const", + [ + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + ], + expected_execution_test_output.uf_execution_expected_output, + ), + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + ], + expected_execution_test_output.uf_single_file_expected_output, + ), + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + ], + expected_execution_test_output.uf_single_method_execution_expected_output, + ), + ( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + ], + expected_execution_test_output.uf_non_adjacent_tests_execution_expected_output, + ), + ( + [ + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "unittest_pytest_same_file.py::test_true_pytest", + ], + expected_execution_test_output.unit_pytest_same_file_execution_expected_output, + ), + ( + [ + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + ], + expected_execution_test_output.dual_level_nested_folder_execution_expected_output, + ), + ( + [ + "double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function" + ], + expected_execution_test_output.double_nested_folder_expected_execution_output, + ), + ( + [ + "parametrize_tests.py::test_adding[3+5-8]", + "parametrize_tests.py::test_adding[2+4-6]", + "parametrize_tests.py::test_adding[6+9-16]", + ], + expected_execution_test_output.parametrize_tests_expected_execution_output, + ), + ( + [ + "parametrize_tests.py::test_adding[3+5-8]", + ], + expected_execution_test_output.single_parametrize_tests_expected_execution_output, + ), + ( + [ + "text_docstring.txt::text_docstring.txt", + ], + expected_execution_test_output.doctest_pytest_expected_execution_output, + ), + ( + [ + "", + ], + expected_execution_test_output.no_test_ids_pytest_execution_expected_output, + ), + ], +) +def test_pytest_execution(test_ids, expected_const): + """ + Test that pytest discovery works as expected where run pytest is always successful + but the actual test results are both successes and failures.: + 1. uf_execution_expected_output: unittest tests run on multiple files. + 2. uf_single_file_expected_output: test run on a single file. + 3. uf_single_method_execution_expected_output: test run on a single method in a file. + 4. uf_non_adjacent_tests_execution_expected_output: test run on unittests in two files with single selection in test explorer. + 5. unit_pytest_same_file_execution_expected_output: test run on a file with both unittest and pytest tests. + 6. dual_level_nested_folder_execution_expected_output: test run on a file with one test file at the top level and one test file in a nested folder. + 7. double_nested_folder_expected_execution_output: test run on a double nested folder. + 8. parametrize_tests_expected_execution_output: test run on a parametrize test with 3 inputs. + 9. single_parametrize_tests_expected_execution_output: test run on single parametrize test. + 10. doctest_pytest_expected_execution_output: test run on doctest file. + 11. no_test_ids_pytest_execution_expected_output: test run with no inputted test ids. + + + Keyword arguments: + test_ids -- an array of test_ids to run. + expected_const -- a dictionary of the expected output from running pytest discovery on the files. + """ + args = test_ids + actual = runner(args) + assert actual + assert all(item in actual for item in ("status", "cwd", "result")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + result_data = actual["result"] + for key in result_data: + if result_data[key]["outcome"] == "failure": + result_data[key]["message"] = "ERROR MESSAGE" + assert result_data == expected_const diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 22acaab57953..6063e4113d55 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -69,7 +69,8 @@ def pytest_exception_interact(node, call, report): """ # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. # call.excinfo.exconly() returns the exception as a string. - ERRORS.append(call.excinfo.exconly()) + if call.excinfo and call.excinfo.typename != "AssertionError": + ERRORS.append(call.excinfo.exconly()) def pytest_keyboard_interrupt(excinfo): @@ -82,34 +83,138 @@ def pytest_keyboard_interrupt(excinfo): ERRORS.append(excinfo.exconly()) +class TestOutcome(Dict): + """A class that handles outcome for a single test. + + for pytest the outcome for a test is only 'passed', 'skipped' or 'failed' + """ + + test: str + outcome: Literal["success", "failure", "skipped"] + message: Union[str, None] + traceback: Union[str, None] + subtest: Optional[str] + + +def create_test_outcome( + test: str, + outcome: str, + message: Union[str, None], + traceback: Union[str, None], + subtype: Optional[str] = None, +) -> TestOutcome: + """A function that creates a TestOutcome object.""" + return TestOutcome( + test=test, + outcome=outcome, + message=message, + traceback=traceback, # TODO: traceback + subtest=None, + ) + + +class testRunResultDict(Dict[str, Dict[str, TestOutcome]]): + """A class that stores all test run results.""" + + outcome: str + tests: Dict[str, TestOutcome] + + +collected_tests = testRunResultDict() +IS_DISCOVERY = False + + +def pytest_load_initial_conftests(early_config, parser, args): + if "--collect-only" in args: + global IS_DISCOVERY + IS_DISCOVERY = True + + +def pytest_report_teststatus(report, config): + """ + A pytest hook that is called when a test is called. It is called 3 times per test, + during setup, call, and teardown. + Keyword arguments: + report -- the report on the test setup, call, and teardown. + config -- configuration object. + """ + + if report.when == "call": + traceback = None + message = None + report_value = "skipped" + if report.passed: + report_value = "success" + elif report.failed: + report_value = "failure" + message = report.longreprtext + item_result = create_test_outcome( + report.nodeid, + report_value, + message, + traceback, + ) + collected_tests[report.nodeid] = item_result + + +ERROR_MESSAGE_CONST = { + 2: "Pytest was unable to start or run any tests due to issues with test discovery or test collection.", + 3: "Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution.", + 4: "Pytest encountered an internal error or exception during test execution.", + 5: "Pytest was unable to find any tests to run.", +} + + def pytest_sessionfinish(session, exitstatus): """A pytest hook that is called after pytest has fulled finished. Keyword arguments: session -- the pytest session object. exitstatus -- the status code of the session. + + 0: All tests passed successfully. + 1: One or more tests failed. + 2: Pytest was unable to start or run any tests due to issues with test discovery or test collection. + 3: Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution. + 4: Pytest encountered an internal error or exception during test execution. + 5: Pytest was unable to find any tests to run. """ cwd = pathlib.Path.cwd() - try: - session_node: Union[TestNode, None] = build_test_tree(session) - if not session_node: - raise VSCodePytestError( - "Something went wrong following pytest finish, \ - no session node was created" + if IS_DISCOVERY: + try: + session_node: Union[TestNode, None] = build_test_tree(session) + if not session_node: + raise VSCodePytestError( + "Something went wrong following pytest finish, \ + no session node was created" + ) + post_response(os.fsdecode(cwd), session_node) + except Exception as e: + ERRORS.append( + f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" ) - post_response(os.fsdecode(cwd), session_node) - except Exception as e: - ERRORS.append( - f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + errorNode: TestNode = { + "name": "", + "path": "", + "type_": "error", + "children": [], + "id_": "", + } + post_response(os.fsdecode(cwd), errorNode) + else: + if exitstatus == 0 or exitstatus == 1: + exitstatus_bool = "success" + else: + ERRORS.append( + f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" + ) + exitstatus_bool = "error" + + execution_post( + os.fsdecode(cwd), + exitstatus_bool, + collected_tests if collected_tests else None, ) - errorNode: TestNode = { - "name": "", - "path": "", - "type_": "error", - "children": [], - "id_": "", - } - post_response(os.fsdecode(cwd), errorNode) def build_test_tree(session: pytest.Session) -> TestNode: @@ -284,13 +389,67 @@ def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode } -class PayloadDict(TypedDict): +class DiscoveryPayloadDict(TypedDict): """A dictionary that is used to send a post request to the server.""" cwd: str status: Literal["success", "error"] tests: Optional[TestNode] - errors: Optional[List[str]] + error: Optional[List[str]] + + +class ExecutionPayloadDict(Dict): + """ + A dictionary that is used to send a execution post request to the server. + """ + + cwd: str + status: Literal["success", "error"] + result: Union[testRunResultDict, None] + not_found: Union[List[str], None] # Currently unused need to check + error: Union[str, None] # Currently unused need to check + + +def execution_post( + cwd: str, + status: Literal["success", "error"], + tests: Union[testRunResultDict, None], +): + """ + Sends a post request to the server after the tests have been executed. + Keyword arguments: + cwd -- the current working directory. + session_node -- the status of running the tests + tests -- the tests that were run and their status. + """ + testPort = os.getenv("TEST_PORT", 45454) + testuuid = os.getenv("TEST_UUID") + payload: ExecutionPayloadDict = ExecutionPayloadDict( + cwd=cwd, status=status, result=tests, not_found=None, error=None + ) + if ERRORS: + payload["error"] = ERRORS + + addr = ("localhost", int(testPort)) + data = json.dumps(payload) + request = f"""Content-Length: {len(data)} +Content-Type: application/json +Request-uuid: {testuuid} + +{data}""" + test_output_file: Optional[str] = os.getenv("TEST_OUTPUT_FILE", None) + if test_output_file == "stdout": + print(request) + elif test_output_file: + pathlib.Path(test_output_file).write_text(request, encoding="utf-8") + else: + try: + with socket_manager.SocketManager(addr) as s: + if s.socket is not None: + s.socket.sendall(request.encode("utf-8")) + except Exception as e: + print(f"Plugin error connection error[vscode-pytest]: {e}") + print(f"[vscode-pytest] data: {request}") def post_response(cwd: str, session_node: TestNode) -> None: @@ -301,15 +460,14 @@ def post_response(cwd: str, session_node: TestNode) -> None: session_node -- the session node, which is the top of the testing tree. errors -- a list of errors that occurred during test collection. """ - payload: PayloadDict = { + payload: DiscoveryPayloadDict = { "cwd": cwd, "status": "success" if not ERRORS else "error", "tests": session_node, - "errors": [], + "error": [], } - if ERRORS: - payload["errors"] = ERRORS - + if ERRORS is not None: + payload["error"] = ERRORS testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) testuuid: Union[str, None] = os.getenv("TEST_UUID") addr = "localhost", int(testPort) From 8223362b0a9b8c1e945058ec90e55cd25bf7f103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Pi=C3=B1a=20Martinez?= Date: Tue, 18 Apr 2023 19:08:42 +0200 Subject: [PATCH 07/10] Add black to `extensions.json` (#20912) Solves #20855 --- .vscode/extensions.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 045d61d31678..5ade8dec4885 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-python.python", + "ms-python.black-formatter", "ms-python.vscode-pylance" ] } From c3c655077f7d3b7ad53b2d61278fa56cb44efef9 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Tue, 18 Apr 2023 10:51:48 -0700 Subject: [PATCH 08/10] uncommenting and overloading --- .../testing/testController/common/types.ts | 19 +- .../pytest/pytestDiscoveryAdapter.ts | 17 +- .../pytest/pytestExecutionAdapter.ts | 114 +++++------ .../testController/workspaceTestAdapter.ts | 44 ++--- .../pytestDiscoveryAdapter.unit.test.ts | 179 +++++++++--------- 5 files changed, 191 insertions(+), 182 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 1fe480c45f26..1b5bf5c96cb9 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -14,6 +14,7 @@ import { } from 'vscode'; // ** import { IPythonExecutionFactory } from '../../../common/process/types'; import { TestDiscoveryOptions } from '../../common/types'; +import { IPythonExecutionFactory } from '../../../common/process/types'; export type TestRunInstanceOptions = TestRunOptions & { exclude?: readonly TestItem[]; @@ -179,21 +180,21 @@ export interface ITestServer { } export interface ITestDiscoveryAdapter { - // ** Uncomment second line and comment out first line to use the new discovery method. + // ** first line old method signature, second line new method signature discoverTests(uri: Uri): Promise; - // discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; + discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise; } // interface for execution/runner adapter export interface ITestExecutionAdapter { - // ** Uncomment second line and comment out first line to use the new execution method. + // ** first line old method signature, second line new method signature runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise; - // runTests( - // uri: Uri, - // testIds: string[], - // debugBool?: boolean, - // executionFactory?: IPythonExecutionFactory, - // ): Promise; + runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise; } // Same types as in pythonFiles/unittestadapter/utils.py diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index d2f6f6448777..3c2e549dda35 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -38,19 +38,18 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } // ** Old version of discover tests. - discoverTests(uri: Uri): Promise { + discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { + if (executionFactory !== undefined) { + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + traceVerbose(pytestArgs); + return this.runPytestDiscovery(uri, executionFactory); + } + // if executionFactory is undefined, we are using the old version of discover tests. traceVerbose(uri); this.deferred = createDeferred(); return this.deferred.promise; } - // Uncomment this version of the function discoverTests to use the new discovery method. - // public async discoverTests(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { - // const settings = this.configSettings.getSettings(uri); - // const { pytestArgs } = settings.testing; - // traceVerbose(pytestArgs); - - // return this.runPytestDiscovery(uri, executionFactory); - // } async runPytestDiscovery(uri: Uri, executionFactory: IPythonExecutionFactory): Promise { const deferred = createDeferred(); diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 1f6b504074cd..18375e610fcf 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -2,10 +2,17 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; +import path from 'path'; import { IConfigurationService, ITestOutputChannel } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; import { traceVerbose } from '../../../logging'; import { DataReceivedEvent, ExecutionTestPayload, ITestExecutionAdapter, ITestServer } from '../common/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; /** * Wrapper Class for pytest test execution. This is where we call `runTestCommand`? @@ -33,69 +40,68 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } // ** Old version of discover tests. - async runTests(uri: Uri, testIds: string[], debugBool?: boolean): Promise { + async runTests( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise { traceVerbose(uri, testIds, debugBool); + if (executionFactory !== undefined) { + return this.runTestsNew(uri, testIds, debugBool, executionFactory); + } // TODO:Remove this line after enabling runs + // if executionFactory is undefined, we are using the old version of discover tests. this.outputChannel.appendLine('Running tests.'); this.deferred = createDeferred(); return this.deferred.promise; } -} - -// public async runTests( -// uri: Uri, -// testIds: string[], -// debugBool?: boolean, -// executionFactory?: IPythonExecutionFactory, -// ): Promise { -// const deferred = createDeferred(); -// const relativePathToPytest = 'pythonFiles'; -// const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); -// this.configSettings.isTestExecution(); -// const uuid = this.testServer.createUUID(uri.fsPath); -// this.promiseMap.set(uuid, deferred); -// const settings = this.configSettings.getSettings(uri); -// const { pytestArgs } = settings.testing; - -// const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; -// const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); -// const spawnOptions: SpawnOptions = { -// cwd: uri.fsPath, -// throwOnStdErr: true, -// extraVariables: { -// PYTHONPATH: pythonPathCommand, -// TEST_UUID: uuid.toString(), -// TEST_PORT: this.testServer.getPort().toString(), -// }, -// outputChannel: this.outputChannel, -// }; + private async runTestsNew( + uri: Uri, + testIds: string[], + debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, + ): Promise { + const deferred = createDeferred(); + const relativePathToPytest = 'pythonFiles'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + this.configSettings.isTestExecution(); + const uuid = this.testServer.createUUID(uri.fsPath); + this.promiseMap.set(uuid, deferred); + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; -// // Create the Python environment in which to execute the command. -// const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { -// allowEnvironmentFetchExceptions: false, -// resource: uri, -// }; -// // need to check what will happen in the exec service is NOT defined and is null -// const execService = await executionFactory?.createActivatedEnvironment(creationOptions); + const pythonPathParts: string[] = process.env.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); -// const testIdsString = testIds.join(' '); -// console.debug('what to do with debug bool?', debugBool); -// try { -// execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); -// } catch (ex) { -// console.error(ex); -// } + const spawnOptions: SpawnOptions = { + cwd: uri.fsPath, + throwOnStdErr: true, + extraVariables: { + PYTHONPATH: pythonPathCommand, + TEST_UUID: uuid.toString(), + TEST_PORT: this.testServer.getPort().toString(), + }, + outputChannel: this.outputChannel, + }; -// return deferred.promise; -// } -// } + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + }; + // need to check what will happen in the exec service is NOT defined and is null + const execService = await executionFactory?.createActivatedEnvironment(creationOptions); -// function buildExecutionCommand(args: string[]): TestExecutionCommand { -// const executionScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'unittestadapter', 'execution.py'); + const testIdsString = testIds.join(' '); + console.debug('what to do with debug bool?', debugBool); + try { + execService?.exec(['-m', 'pytest', '-p', 'vscode_pytest', testIdsString].concat(pytestArgs), spawnOptions); + } catch (ex) { + console.error(ex); + } -// return { -// script: executionScript, -// args: ['--udiscovery', ...args], -// }; -// } + return deferred.promise; + } +} diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts index 8ce51e5dab56..b339256c3e08 100644 --- a/src/client/testing/testController/workspaceTestAdapter.ts +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -17,7 +17,7 @@ import { import { splitLines } from '../../common/stringUtils'; import { createDeferred, Deferred } from '../../common/utils/async'; import { Testing } from '../../common/utils/localize'; -import { traceError } from '../../logging'; +import { traceError, traceVerbose } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { TestProvider } from '../types'; @@ -37,6 +37,7 @@ import { ITestExecutionAdapter, } from './common/types'; import { fixLogLines } from './common/utils'; +import { IPythonExecutionFactory } from '../../common/process/types'; /** * This class exposes a test-provider-agnostic way of discovering tests. @@ -69,13 +70,13 @@ export class WorkspaceTestAdapter { this.vsIdToRunId = new Map(); } - // ** add executionFactory?: IPythonExecutionFactory, to the parameters public async executeTests( testController: TestController, runInstance: TestRun, includes: TestItem[], token?: CancellationToken, debugBool?: boolean, + executionFactory?: IPythonExecutionFactory, ): Promise { if (this.executing) { return this.executing.promise; @@ -102,18 +103,18 @@ export class WorkspaceTestAdapter { } }); - // ** First line is old way, section with if statement below is new way. - rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); - // if (executionFactory !== undefined) { - // rawTestExecData = await this.executionAdapter.runTests( - // this.workspaceUri, - // testCaseIds, - // debugBool, - // executionFactory, - // ); - // } else { - // traceVerbose('executionFactory is undefined'); - // } + // ** execution factory only defined for new rewrite way + if (executionFactory !== undefined) { + rawTestExecData = await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + debugBool, + executionFactory, + ); + traceVerbose('executionFactory defined'); + } else { + rawTestExecData = await this.executionAdapter.runTests(this.workspaceUri, testCaseIds, debugBool); + } deferred.resolve(); } catch (ex) { // handle token and telemetry here @@ -278,12 +279,12 @@ export class WorkspaceTestAdapter { return Promise.resolve(); } - // add `executionFactory?: IPythonExecutionFactory,` to the function for new pytest method public async discoverTests( testController: TestController, token?: CancellationToken, isMultiroot?: boolean, workspaceFilePath?: string, + executionFactory?: IPythonExecutionFactory, ): Promise { sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); @@ -299,13 +300,12 @@ export class WorkspaceTestAdapter { let rawTestData; try { - // ** First line is old way, section with if statement below is new way. - rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); - // if (executionFactory !== undefined) { - // rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); - // } else { - // traceVerbose('executionFactory is undefined'); - // } + // ** execution factory only defined for new rewrite way + if (executionFactory !== undefined) { + rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory); + } else { + rawTestData = await this.discoveryAdapter.discoverTests(this.workspaceUri); + } deferred.resolve(); } catch (ex) { sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts index f5a2203dbd9a..12c79a23c7fd 100644 --- a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -1,91 +1,94 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// // Copyright (c) Microsoft Corporation. All rights reserved. -// // Licensed under the MIT License. -// import * as assert from 'assert'; -// import { Uri } from 'vscode'; -// import * as typeMoq from 'typemoq'; -// import { IConfigurationService } from '../../../../client/common/types'; -// import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; -// import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; -// import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; -// import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import { IConfigurationService, ITestOutputChannel } from '../../../../client/common/types'; +import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { DataReceivedEvent, ITestServer } from '../../../../client/testing/testController/common/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; -// suite('pytest test discovery adapter', () => { -// let testServer: typeMoq.IMock; -// let configService: IConfigurationService; -// let execFactory = typeMoq.Mock.ofType(); -// let adapter: PytestTestDiscoveryAdapter; -// let execService: typeMoq.IMock; -// let deferred: Deferred; -// setup(() => { -// testServer = typeMoq.Mock.ofType(); -// testServer.setup((t) => t.getPort()).returns(() => 12345); -// testServer -// .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => ({ -// dispose: () => { -// /* no-body */ -// }, -// })); -// configService = ({ -// getSettings: () => ({ -// testing: { pytestArgs: ['.'] }, -// }), -// } as unknown) as IConfigurationService; -// execFactory = typeMoq.Mock.ofType(); -// execService = typeMoq.Mock.ofType(); -// execFactory -// .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) -// .returns(() => Promise.resolve(execService.object)); -// deferred = createDeferred(); -// execService -// .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) -// .returns(() => { -// deferred.resolve(); -// return Promise.resolve({ stdout: '{}' }); -// }); -// execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); -// }); -// test('onDataReceivedHandler should parse only if known UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid123'; -// const data = { status: 'success' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const eventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; +suite('pytest test discovery adapter', () => { + let testServer: typeMoq.IMock; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType(); + let adapter: PytestTestDiscoveryAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let outputChannel: typeMoq.IMock; -// adapter = new PytestTestDiscoveryAdapter(testServer.object, configService); -// // ** const promise = adapter.discoverTests(uri, execFactory.object); -// const promise = adapter.discoverTests(uri); -// await deferred.promise; -// adapter.onDataReceivedHandler(eventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { -// const uri = Uri.file('/my/test/path/'); -// const uuid = 'uuid456'; -// let data = { status: 'error' }; -// testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); -// const wrongUriEventData: DataReceivedEvent = { -// uuid: 'incorrect-uuid456', -// data: JSON.stringify(data), -// }; -// adapter = new PytestTestDiscoveryAdapter(testServer.object, configService); -// // ** const promise = adapter.discoverTests(uri, execFactory.object); -// const promise = adapter.discoverTests(uri); -// adapter.onDataReceivedHandler(wrongUriEventData); + setup(() => { + testServer = typeMoq.Mock.ofType(); + testServer.setup((t) => t.getPort()).returns(() => 12345); + testServer + .setup((t) => t.onDataReceived(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* no-body */ + }, + })); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + } as unknown) as IConfigurationService; + execFactory = typeMoq.Mock.ofType(); + execService = typeMoq.Mock.ofType(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + outputChannel = typeMoq.Mock.ofType(); + }); + test('onDataReceivedHandler should parse only if known UUID', async () => { + const uri = Uri.file('/my/test/path/'); + const uuid = 'uuid123'; + const data = { status: 'success' }; + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const eventData: DataReceivedEvent = { + uuid, + data: JSON.stringify(data), + }; -// data = { status: 'success' }; -// const correctUriEventData: DataReceivedEvent = { -// uuid, -// data: JSON.stringify(data), -// }; -// adapter.onDataReceivedHandler(correctUriEventData); -// const result = await promise; -// assert.deepStrictEqual(result, data); -// }); -// }); + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); + const promise = adapter.discoverTests(uri, execFactory.object); + // const promise = adapter.discoverTests(uri); + await deferred.promise; + adapter.onDataReceivedHandler(eventData); + const result = await promise; + assert.deepStrictEqual(result, data); + }); + test('onDataReceivedHandler should not parse if it is unknown UUID', async () => { + const uri = Uri.file('/my/test/path/'); + const uuid = 'uuid456'; + let data = { status: 'error' }; + testServer.setup((t) => t.createUUID(typeMoq.It.isAny())).returns(() => uuid); + const wrongUriEventData: DataReceivedEvent = { + uuid: 'incorrect-uuid456', + data: JSON.stringify(data), + }; + adapter = new PytestTestDiscoveryAdapter(testServer.object, configService, outputChannel.object); + const promise = adapter.discoverTests(uri, execFactory.object); + // const promise = adapter.discoverTests(uri); + adapter.onDataReceivedHandler(wrongUriEventData); + + data = { status: 'success' }; + const correctUriEventData: DataReceivedEvent = { + uuid, + data: JSON.stringify(data), + }; + adapter.onDataReceivedHandler(correctUriEventData); + const result = await promise; + assert.deepStrictEqual(result, data); + }); +}); From 5a8460617393e8783686ecbccc9dd1091c5b19bc Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Tue, 18 Apr 2023 15:43:15 -0700 Subject: [PATCH 09/10] fix comments --- src/client/testing/testController/common/types.ts | 1 - src/client/testing/testController/controller.ts | 5 ++--- .../testing/testController/pytest/pytestDiscoveryAdapter.ts | 4 ++-- .../testing/testController/pytest/pytestExecutionAdapter.ts | 5 ++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts index 1b5bf5c96cb9..52c6c787040c 100644 --- a/src/client/testing/testController/common/types.ts +++ b/src/client/testing/testController/common/types.ts @@ -12,7 +12,6 @@ import { Uri, WorkspaceFolder, } from 'vscode'; -// ** import { IPythonExecutionFactory } from '../../../common/process/types'; import { TestDiscoveryOptions } from '../../common/types'; import { IPythonExecutionFactory } from '../../../common/process/types'; diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index e745adb019b2..65556cd60519 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -389,7 +389,7 @@ export class PythonTestController implements ITestController, IExtensionSingleAc tool: 'pytest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** new execution runner/adapter + // ** uncomment for NEW execution runner/adapter // const testAdapter = // this.testAdapters.get(workspace.uri) || // (this.testAdapters.values().next().value as WorkspaceTestAdapter); @@ -415,12 +415,11 @@ export class PythonTestController implements ITestController, IExtensionSingleAc ); } if (settings.testing.unittestEnabled) { - // potentially squeeze in the new execution way here? sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'unittest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // new execution runner/adapter + // uncomment for NEW execution runner/adapter // const testAdapter = // this.testAdapters.get(workspace.uri) || // (this.testAdapters.values().next().value as WorkspaceTestAdapter); diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts index 3c2e549dda35..b9df9bb30725 100644 --- a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -37,15 +37,15 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { } } - // ** Old version of discover tests. discoverTests(uri: Uri, executionFactory?: IPythonExecutionFactory): Promise { if (executionFactory !== undefined) { + // ** new version of discover tests. const settings = this.configSettings.getSettings(uri); const { pytestArgs } = settings.testing; traceVerbose(pytestArgs); return this.runPytestDiscovery(uri, executionFactory); } - // if executionFactory is undefined, we are using the old version of discover tests. + // if executionFactory is undefined, we are using the old method signature of discover tests. traceVerbose(uri); this.deferred = createDeferred(); return this.deferred.promise; diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts index 18375e610fcf..af97dad0526d 100644 --- a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -39,7 +39,6 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { } } - // ** Old version of discover tests. async runTests( uri: Uri, testIds: string[], @@ -48,10 +47,10 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter { ): Promise { traceVerbose(uri, testIds, debugBool); if (executionFactory !== undefined) { + // ** new version of run tests. return this.runTestsNew(uri, testIds, debugBool, executionFactory); } - // TODO:Remove this line after enabling runs - // if executionFactory is undefined, we are using the old version of discover tests. + // if executionFactory is undefined, we are using the old method signature of run tests. this.outputChannel.appendLine('Running tests.'); this.deferred = createDeferred(); return this.deferred.promise; From ab689f6506dd996db97dc9a88b07b5738eb150bf Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Thu, 20 Apr 2023 10:33:30 -0700 Subject: [PATCH 10/10] add env var for testing --- .../testing/testController/controller.ts | 115 ++++++++++-------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts index 65556cd60519..fb176a30af88 100644 --- a/src/client/testing/testController/controller.ts +++ b/src/client/testing/testController/controller.ts @@ -241,39 +241,46 @@ export class PythonTestController implements ITestController, IExtensionSingleAc if (uri) { const settings = this.configSettings.getSettings(uri); traceVerbose(`Testing: Refreshing test data for ${uri.fsPath}`); + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { // Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - // ** uncomment ~231 - 241 to NEW new test discovery mechanism - // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - // const testAdapter = - // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // testAdapter.discoverTests( - // this.testController, - // this.refreshCancellation.token, - // this.testAdapters.size > 1, - // this.workspaceService.workspaceFile?.fsPath, - // this.pythonExecFactory, - // ); - // uncomment ~243 to use OLD test discovery mechanism - await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + if (rewriteTestingEnabled) { + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + const testAdapter = + this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.testAdapters.size > 1, + this.workspaceService.workspaceFile?.fsPath, + this.pythonExecFactory, + ); + } else { + // else use OLD test discovery mechanism + await this.pytest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + } } else if (settings.testing.unittestEnabled) { // ** Ensure we send test telemetry if it gets disabled again this.sendTestDisabledTelemetry = true; - // uncomment ~248 - 258 to NEW new test discovery mechanism - // const workspace = this.workspaceService.getWorkspaceFolder(uri); - // traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); - // const testAdapter = - // this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // testAdapter.discoverTests( - // this.testController, - // this.refreshCancellation.token, - // this.testAdapters.size > 1, - // this.workspaceService.workspaceFile?.fsPath, - // ); - // uncomment ~260 to use OLD test discovery mechanism - await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + if (rewriteTestingEnabled) { + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + const workspace = this.workspaceService.getWorkspaceFolder(uri); + traceVerbose(`Discover tests for workspace name: ${workspace?.name} - uri: ${uri.fsPath}`); + const testAdapter = + this.testAdapters.get(uri) || (this.testAdapters.values().next().value as WorkspaceTestAdapter); + testAdapter.discoverTests( + this.testController, + this.refreshCancellation.token, + this.testAdapters.size > 1, + this.workspaceService.workspaceFile?.fsPath, + ); + } else { + // else use OLD test discovery mechanism + await this.unittest.refreshTestData(this.testController, uri, this.refreshCancellation.token); + } } else { if (this.sendTestDisabledTelemetry) { this.sendTestDisabledTelemetry = false; @@ -384,25 +391,26 @@ export class PythonTestController implements ITestController, IExtensionSingleAc const settings = this.configSettings.getSettings(workspace.uri); if (testItems.length > 0) { + const rewriteTestingEnabled = process.env.ENABLE_PYTHON_TESTING_REWRITE; if (settings.testing.pytestEnabled) { sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { tool: 'pytest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // ** uncomment for NEW execution runner/adapter - // const testAdapter = - // this.testAdapters.get(workspace.uri) || - // (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // return testAdapter.executeTests( - // this.testController, - // runInstance, - // testItems, - // token, - // request.profile?.kind === TestRunProfileKind.Debug, - // this.pythonExecFactory, - // ); - - // below is old way of running pytest execution + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + if (rewriteTestingEnabled) { + const testAdapter = + this.testAdapters.get(workspace.uri) || + (this.testAdapters.values().next().value as WorkspaceTestAdapter); + return testAdapter.executeTests( + this.testController, + runInstance, + testItems, + token, + request.profile?.kind === TestRunProfileKind.Debug, + this.pythonExecFactory, + ); + } return this.pytest.runTests( { includes: testItems, @@ -419,18 +427,19 @@ export class PythonTestController implements ITestController, IExtensionSingleAc tool: 'unittest', debugging: request.profile?.kind === TestRunProfileKind.Debug, }); - // uncomment for NEW execution runner/adapter - // const testAdapter = - // this.testAdapters.get(workspace.uri) || - // (this.testAdapters.values().next().value as WorkspaceTestAdapter); - // return testAdapter.executeTests( - // this.testController, - // runInstance, - // testItems, - // token, - // request.profile?.kind === TestRunProfileKind.Debug, - // ); - + // ** rewriteTestingEnabled set to true to use NEW test discovery mechanism + if (rewriteTestingEnabled) { + const testAdapter = + this.testAdapters.get(workspace.uri) || + (this.testAdapters.values().next().value as WorkspaceTestAdapter); + return testAdapter.executeTests( + this.testController, + runInstance, + testItems, + token, + request.profile?.kind === TestRunProfileKind.Debug, + ); + } // below is old way of running unittest execution return this.unittest.runTests( {