diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts index f8fdeebbcf20..0e303e70138e 100644 --- a/src/client/pythonEnvironments/creation/common/commonUtils.ts +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -6,8 +6,10 @@ import { executeCommand } from '../../../common/vscodeApis/commandApis'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; export async function showErrorMessageWithLogs(message: string): Promise { - const result = await showErrorMessage(message, Common.openOutputPanel); + const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter); if (result === Common.openOutputPanel) { await executeCommand(Commands.ViewOutput); + } else if (result === Common.selectPythonInterpreter) { + await executeCommand(Commands.Set_Interpreter); } } diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts index 3845a9ce8dad..ac77e51f22f4 100644 --- a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -17,7 +17,7 @@ import { createDeferred } from '../../../common/utils/async'; import { getEnvironmentVariable, getOSType, OSType } from '../../../common/utils/platform'; import { createCondaScript } from '../../../common/process/internal/scripts'; import { Common, CreateEnv } from '../../../common/utils/localize'; -import { getConda, pickPythonVersion } from './condaUtils'; +import { getCondaBaseEnv, pickPythonVersion } from './condaUtils'; import { showErrorMessageWithLogs } from '../common/commonUtils'; import { withProgress } from '../../../common/vscodeApis/windowApis'; import { EventName } from '../../../telemetry/constants'; @@ -99,7 +99,7 @@ async function createCondaEnv( pathEnv = `${libPath}${path.delimiter}${pathEnv}`; } traceLog('Running Conda Env creation script: ', [command, ...args]); - const { out, dispose } = execObservable(command, args, { + const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, token, cwd: workspace.uri.fsPath, @@ -125,7 +125,10 @@ async function createCondaEnv( }, () => { dispose(); - if (!deferred.rejected) { + if (proc?.exitCode !== 0) { + traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); + deferred.reject(progressAndTelemetry.getLastError()); + } else { deferred.resolve(condaEnvPath); } }, @@ -133,29 +136,22 @@ async function createCondaEnv( return deferred.promise; } -function getExecutableCommand(condaPath: string): string { +function getExecutableCommand(condaBaseEnvPath: string): string { if (getOSType() === OSType.Windows) { // Both Miniconda3 and Anaconda3 have the following structure: // Miniconda3 (or Anaconda3) - // |- condabin - // | |- conda.bat <--- this actually points to python.exe below, - // | after adding few paths to PATH. - // |- Scripts - // | |- conda.exe <--- this is the path we get as condaPath, - // | which is really a stub for `python.exe -m conda`. // |- python.exe <--- this is the python that we want. - return path.join(path.dirname(path.dirname(condaPath)), 'python.exe'); + return path.join(condaBaseEnvPath, 'python.exe'); } // On non-windows machines: // miniconda (or miniforge or anaconda3) // |- bin - // |- conda <--- this is the path we get as condaPath. // |- python <--- this is the python that we want. - return path.join(path.dirname(condaPath), 'python'); + return path.join(condaBaseEnvPath, 'bin', 'python'); } async function createEnvironment(options?: CreateEnvironmentOptions): Promise { - const conda = await getConda(); + const conda = await getCondaBaseEnv(); if (!conda) { return undefined; } diff --git a/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts index 49707c8ae31e..304e90aec84f 100644 --- a/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts @@ -24,6 +24,8 @@ export class CondaProgressAndTelemetry { private condaInstalledPackagesReported = false; + private lastError: string | undefined = undefined; + constructor(private readonly progress: CreateEnvironmentProgress) {} public process(output: string): void { @@ -51,6 +53,7 @@ export class CondaProgressAndTelemetry { environmentType: 'conda', reason: 'other', }); + this.lastError = CREATE_CONDA_FAILED_MARKER; } else if (!this.condaInstallingPackagesReported && output.includes(CONDA_INSTALLING_YML)) { this.condaInstallingPackagesReported = true; this.progress.report({ @@ -66,6 +69,7 @@ export class CondaProgressAndTelemetry { environmentType: 'conda', using: 'environment.yml', }); + this.lastError = CREATE_FAILED_INSTALL_YML; } else if (!this.condaInstalledPackagesReported && output.includes(CREATE_CONDA_INSTALLED_YML)) { this.condaInstalledPackagesReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { @@ -74,4 +78,8 @@ export class CondaProgressAndTelemetry { }); } } + + public getLastError(): string | undefined { + return this.lastError; + } } diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts index 256bdb6b01fb..0e8714bc6034 100644 --- a/src/client/pythonEnvironments/creation/provider/condaUtils.ts +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -6,9 +6,10 @@ import { Common } from '../../../browser/localize'; import { CreateEnv } from '../../../common/utils/localize'; import { executeCommand } from '../../../common/vscodeApis/commandApis'; import { showErrorMessage, showQuickPick } from '../../../common/vscodeApis/windowApis'; +import { traceLog } from '../../../logging'; import { Conda } from '../../common/environmentManagers/conda'; -export async function getConda(): Promise { +export async function getCondaBaseEnv(): Promise { const conda = await Conda.getConda(); if (!conda) { @@ -18,7 +19,20 @@ export async function getConda(): Promise { } return undefined; } - return conda.command; + + const envs = (await conda.getEnvList()).filter((e) => e.name === 'base'); + if (envs.length === 1) { + return envs[0].prefix; + } + if (envs.length > 1) { + traceLog( + 'Multiple conda base envs detected: ', + envs.map((e) => e.prefix), + ); + return undefined; + } + + return undefined; } export async function pickPythonVersion(token?: CancellationToken): Promise { diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts index 8d9a677dc3f2..cf0ac950410c 100644 --- a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -78,7 +78,7 @@ async function createVenv( const deferred = createDeferred(); traceLog('Running Env creation script: ', [command, ...args]); - const { out, dispose } = execObservable(command, args, { + const { proc, out, dispose } = execObservable(command, args, { mergeStdOutErr: true, token, cwd: workspace.uri.fsPath, @@ -101,7 +101,10 @@ async function createVenv( }, () => { dispose(); - if (!deferred.rejected) { + if (proc?.exitCode !== 0) { + traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); + deferred.reject(progressAndTelemetry.getLastError()); + } else { deferred.resolve(venvPath); } }, diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts index 423fb16b3110..96749fc3d564 100644 --- a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -33,6 +33,8 @@ export class VenvProgressAndTelemetry { private venvInstalledPackagesReported = false; + private lastError: string | undefined = undefined; + constructor(private readonly progress: CreateEnvironmentProgress) {} public process(output: string): void { @@ -60,18 +62,21 @@ export class VenvProgressAndTelemetry { environmentType: 'venv', reason: 'noVenv', }); + this.lastError = VENV_NOT_INSTALLED_MARKER; } else if (!this.venvOrPipMissingReported && output.includes(PIP_NOT_INSTALLED_MARKER)) { this.venvOrPipMissingReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { environmentType: 'venv', reason: 'noPip', }); + this.lastError = PIP_NOT_INSTALLED_MARKER; } else if (!this.venvFailedReported && output.includes(CREATE_VENV_FAILED_MARKER)) { this.venvFailedReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { environmentType: 'venv', reason: 'other', }); + this.lastError = CREATE_VENV_FAILED_MARKER; } else if (!this.venvInstallingPackagesReported && output.includes(INSTALLING_REQUIREMENTS)) { this.venvInstallingPackagesReported = true; this.progress.report({ @@ -96,18 +101,21 @@ export class VenvProgressAndTelemetry { environmentType: 'venv', using: 'pipUpgrade', }); + this.lastError = PIP_UPGRADE_FAILED_MARKER; } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_REQUIREMENTS_FAILED_MARKER)) { this.venvInstallingPackagesFailedReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { environmentType: 'venv', using: 'requirements.txt', }); + this.lastError = INSTALL_REQUIREMENTS_FAILED_MARKER; } else if (!this.venvInstallingPackagesFailedReported && output.includes(INSTALL_PYPROJECT_FAILED_MARKER)) { this.venvInstallingPackagesFailedReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { environmentType: 'venv', using: 'pyproject.toml', }); + this.lastError = INSTALL_PYPROJECT_FAILED_MARKER; } else if (!this.venvInstalledPackagesReported && output.includes(INSTALLED_REQUIREMENTS_MARKER)) { this.venvInstalledPackagesReported = true; sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { @@ -122,4 +130,8 @@ export class VenvProgressAndTelemetry { }); } } + + public getLastError(): string | undefined { + return this.lastError; + } } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index e5f5334f08c2..08e1b7b72aaa 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -139,7 +139,7 @@ export function sendTelemetryEvent

{ let condaProvider: CreateEnvironmentProvider; let progressMock: typemoq.IMock; - let getCondaStub: sinon.SinonStub; + let getCondaBaseEnvStub: sinon.SinonStub; let pickPythonVersionStub: sinon.SinonStub; let pickWorkspaceFolderStub: sinon.SinonStub; let execObservableStub: sinon.SinonStub; @@ -38,7 +38,7 @@ suite('Conda Creation provider tests', () => { setup(() => { pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); - getCondaStub = sinon.stub(condaUtils, 'getConda'); + getCondaBaseEnvStub = sinon.stub(condaUtils, 'getCondaBaseEnv'); pickPythonVersionStub = sinon.stub(condaUtils, 'pickPythonVersion'); execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); withProgressStub = sinon.stub(windowApis, 'withProgress'); @@ -55,20 +55,20 @@ suite('Conda Creation provider tests', () => { }); test('No conda installed', async () => { - getCondaStub.resolves(undefined); + getCondaBaseEnvStub.resolves(undefined); assert.isUndefined(await condaProvider.createEnvironment()); }); test('No workspace selected', async () => { - getCondaStub.resolves('/usr/bin/conda'); + getCondaBaseEnvStub.resolves('/usr/bin/conda'); pickWorkspaceFolderStub.resolves(undefined); assert.isUndefined(await condaProvider.createEnvironment()); }); test('No python version picked selected', async () => { - getCondaStub.resolves('/usr/bin/conda'); + getCondaBaseEnvStub.resolves('/usr/bin/conda'); pickWorkspaceFolderStub.resolves({ uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', @@ -80,7 +80,7 @@ suite('Conda Creation provider tests', () => { }); test('Create conda environment', async () => { - getCondaStub.resolves('/usr/bin/conda/conda_bin/conda'); + getCondaBaseEnvStub.resolves('/usr/bin/conda'); const workspace1 = { uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', @@ -95,7 +95,9 @@ suite('Conda Creation provider tests', () => { execObservableStub.callsFake(() => { deferred.resolve(); return { - proc: undefined, + proc: { + exitCode: 0, + }, out: { subscribe: ( next?: (value: Output) => void, @@ -134,7 +136,7 @@ suite('Conda Creation provider tests', () => { }); test('Create conda environment failed', async () => { - getCondaStub.resolves('/usr/bin/conda/conda_bin/conda'); + getCondaBaseEnvStub.resolves('/usr/bin/conda'); pickWorkspaceFolderStub.resolves({ uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), name: 'workspace1', @@ -183,4 +185,60 @@ suite('Conda Creation provider tests', () => { await assert.isRejected(promise); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); + + test('Create conda environment failed (non-zero exit code)', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + await assert.isRejected(promise); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); }); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts index 1fb959f228ea..6b6187ed3e4a 100644 --- a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -93,7 +93,9 @@ suite('venv Creation provider tests', () => { execObservableStub.callsFake(() => { deferred.resolve(); return { - proc: undefined, + proc: { + exitCode: 0, + }, out: { subscribe: ( next?: (value: Output) => void, @@ -153,7 +155,9 @@ suite('venv Creation provider tests', () => { execObservableStub.callsFake(() => { deferred.resolve(); return { - proc: undefined, + proc: { + exitCode: 0, + }, out: { subscribe: ( _next?: (value: Output) => void, @@ -188,4 +192,65 @@ suite('venv Creation provider tests', () => { await assert.isRejected(promise); assert.isTrue(showErrorMessageWithLogsStub.calledOnce); }); + + test('Create venv failed (non-zero exit code)', async () => { + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + await assert.isRejected(promise); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); });