From bfb5b66ef3d1494b3d4767a90259b43df2c7e30a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:15:34 +0000 Subject: [PATCH 1/9] Initial plan From 01210fd876f9c31c86689ea9c986b5564b1a4967 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:40:20 +0000 Subject: [PATCH 2/9] Implement enhanced pytest installation flow and improved error messages Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../pytest/testConfigurationManager.ts | 21 +++- .../configuration/pytestInstallationHelper.ts | 98 +++++++++++++++ .../testing/testController/common/utils.ts | 23 +++- .../pytestInstallationHelper.unit.test.ts | 112 ++++++++++++++++++ .../common/buildErrorNodeOptions.unit.test.ts | 46 +++++++ 5 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 src/client/testing/configuration/pytestInstallationHelper.ts create mode 100644 src/test/testing/configuration/pytestInstallationHelper.unit.test.ts create mode 100644 src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts diff --git a/src/client/testing/configuration/pytest/testConfigurationManager.ts b/src/client/testing/configuration/pytest/testConfigurationManager.ts index 89f4246346ef..8c0df997012f 100644 --- a/src/client/testing/configuration/pytest/testConfigurationManager.ts +++ b/src/client/testing/configuration/pytest/testConfigurationManager.ts @@ -3,12 +3,19 @@ import { QuickPickItem, Uri } from 'vscode'; import { IFileSystem } from '../../../common/platform/types'; import { Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; +import { IApplicationShell } from '../../../common/application/types'; import { TestConfigurationManager } from '../../common/testConfigurationManager'; import { ITestConfigSettingsService } from '../../common/types'; +import { PytestInstallationHelper } from '../pytestInstallationHelper'; +import { traceInfo } from '../../../logging'; export class ConfigurationManager extends TestConfigurationManager { + private readonly pytestInstallationHelper: PytestInstallationHelper; + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { super(workspace, Product.pytest, serviceContainer, cfg); + const appShell = serviceContainer.get(IApplicationShell); + this.pytestInstallationHelper = new PytestInstallationHelper(appShell); } public async requiresUserToConfigure(wkspace: Uri): Promise { @@ -43,7 +50,19 @@ export class ConfigurationManager extends TestConfigurationManager { } const installed = await this.installer.isInstalled(Product.pytest); if (!installed) { - await this.installer.install(Product.pytest); + // Check if Python Environments extension is available for enhanced installation flow + if (this.pytestInstallationHelper.isEnvExtensionAvailable()) { + traceInfo('pytest not installed, prompting user with environment extension integration'); + const installAttempted = await this.pytestInstallationHelper.promptToInstallPytest(wkspace); + if (!installAttempted) { + // User chose to ignore or installation failed + return; + } + } else { + // Fall back to traditional installer + traceInfo('pytest not installed, falling back to traditional installer'); + await this.installer.install(Product.pytest); + } } await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts new file mode 100644 index 000000000000..b9f6707b05f3 --- /dev/null +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, l10n } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceInfo, traceError } from '../../logging'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { getEnvironment } from '../../envExt/api.internal'; + +/** + * Helper class to handle pytest installation using the appropriate method + * based on whether the Python Environments extension is available. + */ +export class PytestInstallationHelper { + constructor(private readonly appShell: IApplicationShell) {} + + /** + * Prompts the user to install pytest with appropriate installation method. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was attempted, false otherwise + */ + async promptToInstallPytest(workspaceUri: Uri): Promise { + const message = l10n.t('pytest selected but not installed. Would you like to install pytest?'); + const installOption = l10n.t('Install pytest'); + const ignoreOption = l10n.t('Ignore'); + + const selection = await this.appShell.showInformationMessage(message, installOption, ignoreOption); + + if (selection === installOption) { + return this.installPytest(workspaceUri); + } + + return false; + } + + /** + * Installs pytest using the appropriate method based on available extensions. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytest(workspaceUri: Uri): Promise { + try { + if (useEnvExtension()) { + return this.installPytestWithEnvExtension(workspaceUri); + } else { + // Fall back to traditional installer if environments extension is not available + traceInfo('Python Environments extension not available, installation cannot proceed via environment extension'); + return false; + } + } catch (error) { + traceError('Error installing pytest:', error); + return false; + } + } + + /** + * Installs pytest using the Python Environments extension. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytestWithEnvExtension(workspaceUri: Uri): Promise { + try { + const envExtApi = await getEnvExtApi(); + const environment = await getEnvironment(workspaceUri); + + if (!environment) { + traceError('No Python environment found for workspace:', workspaceUri.fsPath); + await this.appShell.showErrorMessage( + l10n.t('No Python environment found. Please set up a Python environment first.') + ); + return false; + } + + traceInfo('Installing pytest using Python Environments extension...'); + await envExtApi.managePackages(environment, { + install: ['pytest'], + }); + + traceInfo('pytest installation completed successfully'); + await this.appShell.showInformationMessage(l10n.t('pytest has been installed successfully.')); + return true; + } catch (error) { + traceError('Failed to install pytest using Python Environments extension:', error); + await this.appShell.showErrorMessage( + l10n.t('Failed to install pytest. Please install it manually or check your Python environment.') + ); + return false; + } + } + + /** + * Checks if the Python Environments extension is available for package management. + * @returns True if the extension is available, false otherwise + */ + isEnvExtensionAvailable(): boolean { + return useEnvExtension(); + } +} \ No newline at end of file diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index c624ef034cf1..af4d9cad2ac8 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -174,12 +174,31 @@ export async function startDiscoveryNamedPipe( return pipeName; } +/** + * Detects if an error message indicates that pytest is not installed. + * @param message The error message to check + * @returns True if the error indicates pytest is not installed + */ +function isPytestNotInstalledError(message: string): boolean { + return message.includes('ModuleNotFoundError') && message.includes('pytest') || + message.includes('No module named') && message.includes('pytest') || + message.includes('ImportError') && message.includes('pytest'); +} + export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { - const labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let errorMessage = message; + + // Provide more specific error message if pytest is not installed + if (testType === 'pytest' && isPytestNotInstalledError(message)) { + labelText = 'pytest Not Installed'; + errorMessage = 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'; + } + return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, - error: message, + error: errorMessage, }; } diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts new file mode 100644 index 000000000000..74ca681cbd8f --- /dev/null +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { PytestInstallationHelper } from '../../../client/testing/configuration/pytestInstallationHelper'; +import * as envExtApi from '../../../client/envExt/api.internal'; + +suite('PytestInstallationHelper', () => { + let appShell: TypeMoq.IMock; + let helper: PytestInstallationHelper; + let useEnvExtensionStub: sinon.SinonStub; + let getEnvExtApiStub: sinon.SinonStub; + let getEnvironmentStub: sinon.SinonStub; + + const workspaceUri = Uri.file('/test/workspace'); + + setup(() => { + appShell = TypeMoq.Mock.ofType(); + helper = new PytestInstallationHelper(appShell.object); + + useEnvExtensionStub = sinon.stub(envExtApi, 'useEnvExtension'); + getEnvExtApiStub = sinon.stub(envExtApi, 'getEnvExtApi'); + getEnvironmentStub = sinon.stub(envExtApi, 'getEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('promptToInstallPytest should return false if user selects ignore', async () => { + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('Ignore')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should return false if user cancels', async () => { + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('isEnvExtensionAvailable should return result from useEnvExtension', () => { + useEnvExtensionStub.returns(true); + + const result = helper.isEnvExtensionAvailable(); + + expect(result).to.be.true; + expect(useEnvExtensionStub.calledOnce).to.be.true; + }); + + test('promptToInstallPytest should return false if env extension not available', async () => { + useEnvExtensionStub.returns(false); + + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should attempt installation when env extension is available', async () => { + useEnvExtensionStub.returns(true); + + const mockEnvironment = { envId: { id: 'test-env', managerId: 'test-manager' } }; + const mockEnvExtApi = { + managePackages: sinon.stub().resolves() + }; + + getEnvExtApiStub.resolves(mockEnvExtApi); + getEnvironmentStub.resolves(mockEnvironment); + + appShell + .setup((a) => a.showInformationMessage( + TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.is((msg: string) => msg.includes('successfully')))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.true; + expect(mockEnvExtApi.managePackages.calledOnceWithExactly(mockEnvironment, { install: ['pytest'] })).to.be.true; + appShell.verifyAll(); + }); +}); \ No newline at end of file diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts new file mode 100644 index 000000000000..6d78676a1ff5 --- /dev/null +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { buildErrorNodeOptions } from '../../../../client/testing/testController/common/utils'; + +suite('buildErrorNodeOptions - pytest not installed detection', () => { + const workspaceUri = Uri.file('/test/workspace'); + + test('Should detect pytest ModuleNotFoundError and provide specific message', () => { + const errorMessage = 'Traceback (most recent call last):\n File "", line 1, in \n import pytest\nModuleNotFoundError: No module named \'pytest\''; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Not Installed [workspace]'); + expect(result.error).to.equal('pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'); + }); + + test('Should detect pytest ImportError and provide specific message', () => { + const errorMessage = 'ImportError: No module named pytest'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Not Installed [workspace]'); + expect(result.error).to.equal('pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'); + }); + + test('Should use generic error for non-pytest-related errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should use generic error for unittest errors', () => { + const errorMessage = 'ModuleNotFoundError: No module named \'pytest\''; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + expect(result.error).to.equal('ModuleNotFoundError: No module named \'pytest\''); + }); +}); \ No newline at end of file From 1345d61fb09e5114e6f4c2520726c8eeb843e3c1 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:58:34 -0700 Subject: [PATCH 3/9] refine messaging etc --- .../configuration/pytest/testConfigurationManager.ts | 2 +- .../configuration/pytestInstallationHelper.ts | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/client/testing/configuration/pytest/testConfigurationManager.ts b/src/client/testing/configuration/pytest/testConfigurationManager.ts index 8c0df997012f..08f88f8564c7 100644 --- a/src/client/testing/configuration/pytest/testConfigurationManager.ts +++ b/src/client/testing/configuration/pytest/testConfigurationManager.ts @@ -49,6 +49,7 @@ export class ConfigurationManager extends TestConfigurationManager { args.push(testDir); } const installed = await this.installer.isInstalled(Product.pytest); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); if (!installed) { // Check if Python Environments extension is available for enhanced installation flow if (this.pytestInstallationHelper.isEnvExtensionAvailable()) { @@ -64,7 +65,6 @@ export class ConfigurationManager extends TestConfigurationManager { await this.installer.install(Product.pytest); } } - await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); } private async getConfigFiles(rootDir: string): Promise { diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts index b9f6707b05f3..3a66a55f028e 100644 --- a/src/client/testing/configuration/pytestInstallationHelper.ts +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -44,7 +44,9 @@ export class PytestInstallationHelper { return this.installPytestWithEnvExtension(workspaceUri); } else { // Fall back to traditional installer if environments extension is not available - traceInfo('Python Environments extension not available, installation cannot proceed via environment extension'); + traceInfo( + 'Python Environments extension not available, installation cannot proceed via environment extension', + ); return false; } } catch (error) { @@ -66,7 +68,7 @@ export class PytestInstallationHelper { if (!environment) { traceError('No Python environment found for workspace:', workspaceUri.fsPath); await this.appShell.showErrorMessage( - l10n.t('No Python environment found. Please set up a Python environment first.') + l10n.t('No Python environment found. Please set up a Python environment first.'), ); return false; } @@ -77,13 +79,9 @@ export class PytestInstallationHelper { }); traceInfo('pytest installation completed successfully'); - await this.appShell.showInformationMessage(l10n.t('pytest has been installed successfully.')); return true; } catch (error) { traceError('Failed to install pytest using Python Environments extension:', error); - await this.appShell.showErrorMessage( - l10n.t('Failed to install pytest. Please install it manually or check your Python environment.') - ); return false; } } @@ -95,4 +93,4 @@ export class PytestInstallationHelper { isEnvExtensionAvailable(): boolean { return useEnvExtension(); } -} \ No newline at end of file +} From 95443321d07e2a887f78af03a4f900c5060f9333 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:00:44 -0700 Subject: [PATCH 4/9] formatting --- .../pytestInstallationHelper.unit.test.ts | 32 +++++++++--------- .../common/buildErrorNodeOptions.unit.test.ts | 33 +++++++++++-------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts index 74ca681cbd8f..18f974dddbef 100644 --- a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -21,7 +21,7 @@ suite('PytestInstallationHelper', () => { setup(() => { appShell = TypeMoq.Mock.ofType(); helper = new PytestInstallationHelper(appShell.object); - + useEnvExtensionStub = sinon.stub(envExtApi, 'useEnvExtension'); getEnvExtApiStub = sinon.stub(envExtApi, 'getEnvExtApi'); getEnvironmentStub = sinon.stub(envExtApi, 'getEnvironment'); @@ -57,16 +57,16 @@ suite('PytestInstallationHelper', () => { test('isEnvExtensionAvailable should return result from useEnvExtension', () => { useEnvExtensionStub.returns(true); - + const result = helper.isEnvExtensionAvailable(); - + expect(result).to.be.true; expect(useEnvExtensionStub.calledOnce).to.be.true; }); test('promptToInstallPytest should return false if env extension not available', async () => { useEnvExtensionStub.returns(false); - + appShell .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve('Install pytest')) @@ -80,24 +80,26 @@ suite('PytestInstallationHelper', () => { test('promptToInstallPytest should attempt installation when env extension is available', async () => { useEnvExtensionStub.returns(true); - + const mockEnvironment = { envId: { id: 'test-env', managerId: 'test-manager' } }; const mockEnvExtApi = { - managePackages: sinon.stub().resolves() + managePackages: sinon.stub().resolves(), }; - + getEnvExtApiStub.resolves(mockEnvExtApi); getEnvironmentStub.resolves(mockEnvironment); - + appShell - .setup((a) => a.showInformationMessage( - TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )) + .setup((a) => + a.showInformationMessage( + TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) .returns(() => Promise.resolve('Install pytest')) .verifiable(TypeMoq.Times.once()); - + appShell .setup((a) => a.showInformationMessage(TypeMoq.It.is((msg: string) => msg.includes('successfully')))) .returns(() => Promise.resolve(undefined)) @@ -109,4 +111,4 @@ suite('PytestInstallationHelper', () => { expect(mockEnvExtApi.managePackages.calledOnceWithExactly(mockEnvironment, { install: ['pytest'] })).to.be.true; appShell.verifyAll(); }); -}); \ No newline at end of file +}); diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts index 6d78676a1ff5..cf41136db697 100644 --- a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -9,38 +9,43 @@ suite('buildErrorNodeOptions - pytest not installed detection', () => { const workspaceUri = Uri.file('/test/workspace'); test('Should detect pytest ModuleNotFoundError and provide specific message', () => { - const errorMessage = 'Traceback (most recent call last):\n File "", line 1, in \n import pytest\nModuleNotFoundError: No module named \'pytest\''; - + const errorMessage = + 'Traceback (most recent call last):\n File "", line 1, in \n import pytest\nModuleNotFoundError: No module named \'pytest\''; + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); - + expect(result.label).to.equal('pytest Not Installed [workspace]'); - expect(result.error).to.equal('pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'); + expect(result.error).to.equal( + 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.', + ); }); test('Should detect pytest ImportError and provide specific message', () => { const errorMessage = 'ImportError: No module named pytest'; - + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); - + expect(result.label).to.equal('pytest Not Installed [workspace]'); - expect(result.error).to.equal('pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'); + expect(result.error).to.equal( + 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.', + ); }); test('Should use generic error for non-pytest-related errors', () => { const errorMessage = 'Some other error occurred'; - + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); - + expect(result.label).to.equal('pytest Discovery Error [workspace]'); expect(result.error).to.equal('Some other error occurred'); }); test('Should use generic error for unittest errors', () => { - const errorMessage = 'ModuleNotFoundError: No module named \'pytest\''; - + const errorMessage = "ModuleNotFoundError: No module named 'pytest'"; + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); - + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); - expect(result.error).to.equal('ModuleNotFoundError: No module named \'pytest\''); + expect(result.error).to.equal("ModuleNotFoundError: No module named 'pytest'"); }); -}); \ No newline at end of file +}); From 663b1ce73506af7979503b051291ba0b54fcce6d Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 8 Jul 2025 12:03:01 -0700 Subject: [PATCH 5/9] formatting p2 --- src/client/testing/testController/common/utils.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index af4d9cad2ac8..cc64c733f18c 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -180,21 +180,24 @@ export async function startDiscoveryNamedPipe( * @returns True if the error indicates pytest is not installed */ function isPytestNotInstalledError(message: string): boolean { - return message.includes('ModuleNotFoundError') && message.includes('pytest') || - message.includes('No module named') && message.includes('pytest') || - message.includes('ImportError') && message.includes('pytest'); + return ( + (message.includes('ModuleNotFoundError') && message.includes('pytest')) || + (message.includes('No module named') && message.includes('pytest')) || + (message.includes('ImportError') && message.includes('pytest')) + ); } export function buildErrorNodeOptions(uri: Uri, message: string, testType: string): ErrorTestItemOptions { let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; let errorMessage = message; - + // Provide more specific error message if pytest is not installed if (testType === 'pytest' && isPytestNotInstalledError(message)) { labelText = 'pytest Not Installed'; - errorMessage = 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'; + errorMessage = + 'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.'; } - + return { id: `DiscoveryError:${uri.fsPath}`, label: `${labelText} [${path.basename(uri.fsPath)}]`, From ac4cc4a9f6b5d886e3819a0f047145794b60d41c Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:09:54 -0700 Subject: [PATCH 6/9] fix test --- .../configuration/pytestInstallationHelper.unit.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts index 18f974dddbef..3ca7668e857a 100644 --- a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -100,11 +100,6 @@ suite('PytestInstallationHelper', () => { .returns(() => Promise.resolve('Install pytest')) .verifiable(TypeMoq.Times.once()); - appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.is((msg: string) => msg.includes('successfully')))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - const result = await helper.promptToInstallPytest(workspaceUri); expect(result).to.be.true; From 272c601e80737645cf7658c1ad3074ab9efbe8f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:33:52 +0000 Subject: [PATCH 7/9] Convert pytest installation prompt to modal dialog Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../testing/configuration/pytestInstallationHelper.ts | 7 ++++++- .../configuration/pytestInstallationHelper.unit.test.ts | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts index 3a66a55f028e..8b536829bc2a 100644 --- a/src/client/testing/configuration/pytestInstallationHelper.ts +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -24,7 +24,12 @@ export class PytestInstallationHelper { const installOption = l10n.t('Install pytest'); const ignoreOption = l10n.t('Ignore'); - const selection = await this.appShell.showInformationMessage(message, installOption, ignoreOption); + const selection = await this.appShell.showInformationMessage( + message, + { modal: true }, + installOption, + ignoreOption, + ); if (selection === installOption) { return this.installPytest(workspaceUri); diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts index 3ca7668e857a..46162a16a502 100644 --- a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -33,7 +33,7 @@ suite('PytestInstallationHelper', () => { test('promptToInstallPytest should return false if user selects ignore', async () => { appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve('Ignore')) .verifiable(TypeMoq.Times.once()); @@ -45,7 +45,7 @@ suite('PytestInstallationHelper', () => { test('promptToInstallPytest should return false if user cancels', async () => { appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -68,7 +68,7 @@ suite('PytestInstallationHelper', () => { useEnvExtensionStub.returns(false); appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve('Install pytest')) .verifiable(TypeMoq.Times.once()); @@ -95,6 +95,7 @@ suite('PytestInstallationHelper', () => { TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), TypeMoq.It.isAny(), TypeMoq.It.isAny(), + TypeMoq.It.isAny(), ), ) .returns(() => Promise.resolve('Install pytest')) From 536808a9c88ff3ccd995697e8e8d64b5d0fd7d65 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:45:59 -0700 Subject: [PATCH 8/9] update to modem --- .../testing/configuration/pytestInstallationHelper.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts index 8b536829bc2a..bd5fbcd5bb37 100644 --- a/src/client/testing/configuration/pytestInstallationHelper.ts +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -22,14 +22,8 @@ export class PytestInstallationHelper { async promptToInstallPytest(workspaceUri: Uri): Promise { const message = l10n.t('pytest selected but not installed. Would you like to install pytest?'); const installOption = l10n.t('Install pytest'); - const ignoreOption = l10n.t('Ignore'); - const selection = await this.appShell.showInformationMessage( - message, - { modal: true }, - installOption, - ignoreOption, - ); + const selection = await this.appShell.showInformationMessage(message, { modal: true }, installOption); if (selection === installOption) { return this.installPytest(workspaceUri); From f752cb17e82ce49054f71ce16dfa58d99f98d208 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:11:47 +0000 Subject: [PATCH 9/9] Fix formatting issues in pytest installation helper tests Co-authored-by: anthonykim1 <62267334+anthonykim1@users.noreply.github.com> --- .../pytestInstallationHelper.unit.test.ts | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts index 46162a16a502..d7a1313df591 100644 --- a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -33,7 +33,14 @@ suite('PytestInstallationHelper', () => { test('promptToInstallPytest should return false if user selects ignore', async () => { appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) .returns(() => Promise.resolve('Ignore')) .verifiable(TypeMoq.Times.once()); @@ -45,7 +52,14 @@ suite('PytestInstallationHelper', () => { test('promptToInstallPytest should return false if user cancels', async () => { appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -68,7 +82,14 @@ suite('PytestInstallationHelper', () => { useEnvExtensionStub.returns(false); appShell - .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) .returns(() => Promise.resolve('Install pytest')) .verifiable(TypeMoq.Times.once());