diff --git a/package.json b/package.json index dd7cba3cf..8c1a7c346 100644 --- a/package.json +++ b/package.json @@ -190,6 +190,13 @@ "category": "Python", "icon": "$(add)" }, + { + "command": "python-envs.setupInlineScriptEnvironment", + "title": "%python-envs.setupInlineScriptEnvironment.title%", + "category": "Python", + "icon": "$(python)", + "when": "config.python.useEnvironmentsExtension != false" + }, { "command": "python-envs.set", "title": "%python-envs.set.title%", @@ -446,6 +453,10 @@ "command": "python-envs.createAny", "when": "false" }, + { + "command": "python-envs.setupInlineScriptEnvironment", + "when": "config.python.useEnvironmentsExtension != false" + }, { "command": "python-envs.revealProjectInExplorer", "when": "false" diff --git a/package.nls.json b/package.nls.json index 483ecfd29..2e9e9da20 100644 --- a/package.nls.json +++ b/package.nls.json @@ -26,6 +26,7 @@ "python-envs.copyProjectPathCopied.title": "Copied!", "python-envs.create.title": "Create Environment", "python-envs.createAny.title": "Create Environment", + "python-envs.setupInlineScriptEnvironment.title": "Set Up Environment for Inline Script", "python-envs.set.title": "Set Project Environment", "python-envs.setEnv.title": "Set As Project Environment", "python-envs.setEnvSelected.title": "Set!", diff --git a/src/extension.ts b/src/extension.ts index 1d2d98aee..cdca1e7db 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,6 +62,7 @@ import { setEnvironmentCommand, setEnvManagerCommand, setPackageManagerCommand, + setupInlineScriptEnvironmentCommand, } from './features/envCommands'; import { PythonEnvironmentManagers } from './features/envManagers'; import { EnvVarManager, PythonEnvVariableManager } from './features/execution/envVariableManager'; @@ -304,6 +305,9 @@ export async function activate(context: ExtensionContext): Promise { + return setupInlineScriptEnvironmentCommand(item, envManagers); + }), commands.registerCommand('python-envs.remove', async (item) => { await removeEnvironmentCommand(item, envManagers); }), diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 1de8a13a6..80d133419 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -19,6 +19,7 @@ import { PythonProjectCreatorOptions, } from '../api'; import { traceError, traceInfo, traceVerbose } from '../common/logging'; +import { INLINE_SCRIPT_MANAGER_ID } from '../common/constants'; import { EnvironmentManagers, InternalEnvironmentManager, @@ -31,6 +32,7 @@ import { removePythonProjectSetting, setEnvironmentManager, setPackageManager } import { valid as pep440Valid } from '@renovatebot/pep440'; import { executeCommand } from '../common/command.api'; import { clipboardWriteText } from '../common/env.apis'; +import { readInlineScriptMetadataFromFile } from '../common/inlineScript/metadata'; import { Pickers } from '../common/localize'; import { pickEnvironment } from '../common/pickers/environments'; import { @@ -42,6 +44,7 @@ import { } from '../common/pickers/managers'; import { pickProject, pickProjectMany } from '../common/pickers/projects'; import { isWindows } from '../common/utils/platformUtils'; +import { normalizePath } from '../common/utils/pathUtils'; import { handlePythonPath } from '../common/utils/pythonPath'; import { activeTextEditor, @@ -52,7 +55,9 @@ import { showQuickPick, withProgress, } from '../common/window.apis'; +import { getOpenTextDocuments } from '../common/workspace.apis'; import { runAsTask } from './execution/runAsTask'; +import { waitForEnvManagerId } from './common/managerReady'; import { runInTerminal } from './terminal/runInTerminal'; import { TerminalManager } from './terminal/terminalManager'; import { EnvManagerView } from './views/envManagersView'; @@ -66,6 +71,7 @@ import { ProjectPackage, PythonEnvTreeItem, } from './views/treeViewItems'; +import { isInlineScriptsFeatureEnabled } from '../helpers'; /** * Opens a file dialog to browse for a Python interpreter and resolves it using available managers. @@ -285,6 +291,92 @@ export async function createAnyEnvironmentCommand( } } +function isLocalPythonFile(uri: Uri): boolean { + return uri.scheme === 'file' && path.extname(uri.fsPath).toLowerCase() === '.py'; +} + +function hasSameIdentity(left: Uri, right: Uri): boolean { + if (left.scheme !== right.scheme) { + return false; + } + + if (left.scheme === 'file') { + return normalizePath(left.fsPath) === normalizePath(right.fsPath); + } + + return left.toString() === right.toString(); +} + +export async function setupInlineScriptEnvironmentCommand( + context: unknown, + em: EnvironmentManagers, +): Promise { + if (context !== undefined && !(context instanceof Uri)) { + showErrorMessage(l10n.t('Inline script environment setup requires a local .py file.')); + return undefined; + } + + const document = + context instanceof Uri + ? getOpenTextDocuments().find((openDocument) => hasSameIdentity(openDocument.uri, context)) + : activeTextEditor()?.document; + const uri = context instanceof Uri ? context : document?.uri; + if (!uri) { + showErrorMessage( + l10n.t('Open or select a saved local .py file with valid PEP 723 inline script metadata to continue.'), + ); + return undefined; + } + + if (!isLocalPythonFile(uri)) { + showErrorMessage(l10n.t('Inline script environment setup requires a local .py file.')); + return undefined; + } + + const inlineScriptsFeatureEnabled = isInlineScriptsFeatureEnabled(); + if (!inlineScriptsFeatureEnabled) { + showErrorMessage( + l10n.t( + 'Inline script environment setup is disabled. Add "python-envs.inlineScripts.enabled": true to your settings and reload the window to use this command.', + ), + ); + return undefined; + } + + if (document?.isDirty) { + showErrorMessage(l10n.t('Save the file before setting up an inline script environment.')); + return undefined; + } + + if ((await readInlineScriptMetadataFromFile(uri)) === undefined) { + showErrorMessage( + l10n.t( + 'Save a local .py file with valid PEP 723 inline script metadata before setting up an environment.', + ), + ); + return undefined; + } + + await waitForEnvManagerId([INLINE_SCRIPT_MANAGER_ID]); + const inlineManager = em.getEnvironmentManager(INLINE_SCRIPT_MANAGER_ID); + if (!inlineManager) { + showErrorMessage( + l10n.t( + 'Inline script environment setup is unavailable because the preview manager is not registered. Reload the window and try again.', + ), + ); + return undefined; + } + + const environment = await inlineManager.create(uri, undefined); + if (!environment) { + return undefined; + } + + await em.setEnvironment(uri, environment, false); + return environment; +} + export async function removeEnvironmentCommand(context: unknown, managers: EnvironmentManagers): Promise { if (context instanceof PythonEnvTreeItem) { const view = context as PythonEnvTreeItem; diff --git a/src/test/features/envCommands.unit.test.ts b/src/test/features/envCommands.unit.test.ts index 079ca9d73..48f49f92d 100644 --- a/src/test/features/envCommands.unit.test.ts +++ b/src/test/features/envCommands.unit.test.ts @@ -1,16 +1,41 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import * as typeMoq from 'typemoq'; -import { Uri } from 'vscode'; +import { Disposable, EventEmitter, Uri } from 'vscode'; import { PythonEnvironment, PythonProject } from '../../api'; import * as commandApi from '../../common/command.api'; +import { INLINE_SCRIPT_MANAGER_ID } from '../../common/constants'; +import * as inlineScriptMetadata from '../../common/inlineScript/metadata'; +import * as extensionApis from '../../common/extension.apis'; import * as managerApi from '../../common/pickers/managers'; import * as projectApi from '../../common/pickers/projects'; -import { createAnyEnvironmentCommand, removePythonProject, revealEnvInManagerView } from '../../features/envCommands'; +import * as logging from '../../common/logging'; +import * as telemetrySender from '../../common/telemetry/sender'; +import * as platformUtils from '../../common/utils/platformUtils'; +import * as windowApis from '../../common/window.apis'; +import * as workspaceApis from '../../common/workspace.apis'; +import * as helpers from '../../helpers'; +import { + _resetManagerReadyForTesting, + createManagerReady, + MANAGER_READY_TIMEOUT_MS, +} from '../../features/common/managerReady'; +import * as managerReady from '../../features/common/managerReady'; +import { + createAnyEnvironmentCommand, + removePythonProject, + revealEnvInManagerView, + setupInlineScriptEnvironmentCommand, +} from '../../features/envCommands'; import * as settingHelpers from '../../features/settings/settingHelpers'; import { EnvManagerView } from '../../features/views/envManagersView'; import { ProjectEnvironment, ProjectItem } from '../../features/views/treeViewItems'; -import { EnvironmentManagers, InternalEnvironmentManager, PythonProjectManager } from '../../internal.api'; +import { + DidChangeEnvironmentManagerEventArgs, + EnvironmentManagers, + InternalEnvironmentManager, + PythonProjectManager, +} from '../../internal.api'; import { setupNonThenable } from '../mocks/helper'; suite('Create Any Environment Command Tests', () => { @@ -261,3 +286,379 @@ suite('Reveal Env In Manager View Command Tests', () => { managerView.verify((m) => m.reveal(environment), typeMoq.Times.once()); }); }); + +suite('Setup Inline Script Environment Command Tests', () => { + let createStub: sinon.SinonStub; + let getEnvironmentManagerStub: sinon.SinonStub; + let setEnvironmentStub: sinon.SinonStub; + let activeTextEditorStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + let getOpenTextDocumentsStub: sinon.SinonStub; + let readInlineScriptMetadataStub: sinon.SinonStub; + let isInlineScriptsFeatureEnabledStub: sinon.SinonStub; + let waitForEnvManagerIdStub: sinon.SinonStub; + + function createEnvironment(): PythonEnvironment { + return { + envId: { id: 'inline-env', managerId: INLINE_SCRIPT_MANAGER_ID }, + name: 'inline-env', + displayName: 'Inline Environment', + displayPath: '/path/to/inline-env', + version: '3.12.0', + environmentPath: Uri.file('/path/to/inline-env'), + execInfo: { run: { executable: '/path/to/inline-env/python' } }, + sysPrefix: '/path/to/inline-env', + }; + } + + function createDocument(uri: Uri, isDirty = false) { + return { uri, isDirty } as { uri: Uri; isDirty: boolean }; + } + + function createManagers(): EnvironmentManagers { + return { + getEnvironmentManager: getEnvironmentManagerStub, + setEnvironment: setEnvironmentStub, + } as unknown as EnvironmentManagers; + } + + function registerInlineManager(): void { + getEnvironmentManagerStub.withArgs(INLINE_SCRIPT_MANAGER_ID).returns({ + create: createStub, + } as unknown as InternalEnvironmentManager); + } + + setup(() => { + createStub = sinon.stub(); + getEnvironmentManagerStub = sinon.stub(); + setEnvironmentStub = sinon.stub().resolves(); + activeTextEditorStub = sinon.stub(windowApis, 'activeTextEditor').returns(undefined); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage').resolves(undefined); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments').returns([]); + readInlineScriptMetadataStub = sinon + .stub(inlineScriptMetadata, 'readInlineScriptMetadataFromFile') + .resolves({ range: { start: 0, end: 0 } }); + isInlineScriptsFeatureEnabledStub = sinon.stub(helpers, 'isInlineScriptsFeatureEnabled').returns(true); + waitForEnvManagerIdStub = sinon.stub(managerReady, 'waitForEnvManagerId').resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('uses the supplied uri and sets the created environment without persisting settings', async () => { + const uri = Uri.file('/workspace/script.py'); + const activeUri = Uri.file('/workspace/other.py'); + activeTextEditorStub.returns({ document: createDocument(activeUri) }); + getOpenTextDocumentsStub.returns([createDocument(uri)]); + const environment = createEnvironment(); + registerInlineManager(); + createStub.resolves(environment); + + const result = await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + assert.strictEqual(result, environment); + sinon.assert.calledOnceWithExactly(readInlineScriptMetadataStub, uri); + sinon.assert.calledOnceWithExactly(waitForEnvManagerIdStub, [INLINE_SCRIPT_MANAGER_ID]); + sinon.assert.calledOnceWithExactly(createStub, uri, undefined); + sinon.assert.calledOnceWithExactly(setEnvironmentStub, uri, environment, false); + sinon.assert.notCalled(showErrorMessageStub); + }); + + test('falls back to the active editor when no uri is supplied', async () => { + const uri = Uri.file('/workspace/active.py'); + activeTextEditorStub.returns({ document: createDocument(uri) }); + const environment = createEnvironment(); + registerInlineManager(); + createStub.resolves(environment); + + const result = await setupInlineScriptEnvironmentCommand(undefined, createManagers()); + + assert.strictEqual(result, environment); + sinon.assert.calledOnceWithExactly(readInlineScriptMetadataStub, uri); + sinon.assert.calledOnceWithExactly(waitForEnvManagerIdStub, [INLINE_SCRIPT_MANAGER_ID]); + sinon.assert.calledOnceWithExactly(createStub, uri, undefined); + sinon.assert.calledOnceWithExactly(setEnvironmentStub, uri, environment, false); + }); + + test('shows an error when no active editor is available', async () => { + await setupInlineScriptEnvironmentCommand(undefined, createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /Open or select a saved local \.py file/); + sinon.assert.notCalled(readInlineScriptMetadataStub); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + }); + + test('rejects non-file uris', async () => { + await setupInlineScriptEnvironmentCommand(Uri.parse('untitled:script.py'), createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /requires a local \.py file/); + sinon.assert.notCalled(readInlineScriptMetadataStub); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + }); + + test('rejects non-python files', async () => { + await setupInlineScriptEnvironmentCommand(Uri.file('/workspace/script.txt'), createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /requires a local \.py file/); + sinon.assert.notCalled(readInlineScriptMetadataStub); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + }); + + test('requires dirty documents to be saved first', async () => { + const uri = Uri.file('/workspace/script.py'); + getOpenTextDocumentsStub.returns([createDocument(uri, true)]); + + await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /Save the file before setting up/); + sinon.assert.notCalled(readInlineScriptMetadataStub); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + }); + + test('matches equivalent Windows file uris when checking for a dirty open document', async () => { + const uri = Uri.file('/workspace/package/script.py'); + const equivalentOpenUri = { + scheme: 'file', + fsPath: '\\WORKSPACE\\package\\script.py', + toString: () => 'file:///WORKSPACE%5Cpackage%5Cscript.py', + } as unknown as Uri; + sinon.stub(platformUtils, 'isWindows').returns(true); + getOpenTextDocumentsStub.returns([createDocument(equivalentOpenUri, true)]); + + await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /Save the file before setting up/); + sinon.assert.notCalled(readInlineScriptMetadataStub); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + }); + + test('requires valid saved PEP 723 metadata', async () => { + const uri = Uri.file('/workspace/script.py'); + readInlineScriptMetadataStub.resolves(undefined); + + await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /valid PEP 723 inline script metadata/); + sinon.assert.calledOnceWithExactly(readInlineScriptMetadataStub, uri); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + }); + + test('shows the feature-disabled error before dirty, metadata, or readiness checks', async () => { + const uri = Uri.file('/workspace/script.py'); + getOpenTextDocumentsStub.returns([createDocument(uri, true)]); + isInlineScriptsFeatureEnabledStub.returns(false); + + await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /python-envs\.inlineScripts\.enabled/); + sinon.assert.notCalled(readInlineScriptMetadataStub); + sinon.assert.notCalled(waitForEnvManagerIdStub); + sinon.assert.notCalled(createStub); + sinon.assert.notCalled(setEnvironmentStub); + }); + + test('leaves the existing association unchanged when setup is cancelled', async () => { + const uri = Uri.file('/workspace/script.py'); + registerInlineManager(); + createStub.resolves(undefined); + + const result = await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + assert.strictEqual(result, undefined); + sinon.assert.calledOnceWithExactly(waitForEnvManagerIdStub, [INLINE_SCRIPT_MANAGER_ID]); + sinon.assert.calledOnceWithExactly(createStub, uri, undefined); + sinon.assert.notCalled(setEnvironmentStub); + sinon.assert.notCalled(showErrorMessageStub); + }); + + test('propagates create errors', async () => { + const uri = Uri.file('/workspace/script.py'); + registerInlineManager(); + createStub.rejects(new Error('create failed')); + + await assert.rejects(setupInlineScriptEnvironmentCommand(uri, createManagers()), /create failed/); + + sinon.assert.calledOnceWithExactly(waitForEnvManagerIdStub, [INLINE_SCRIPT_MANAGER_ID]); + sinon.assert.calledOnceWithExactly(createStub, uri, undefined); + sinon.assert.notCalled(setEnvironmentStub); + }); + + test('propagates set errors', async () => { + const uri = Uri.file('/workspace/script.py'); + const environment = createEnvironment(); + registerInlineManager(); + createStub.resolves(environment); + setEnvironmentStub.rejects(new Error('set failed')); + + await assert.rejects(setupInlineScriptEnvironmentCommand(uri, createManagers()), /set failed/); + + sinon.assert.calledOnceWithExactly(waitForEnvManagerIdStub, [INLINE_SCRIPT_MANAGER_ID]); + sinon.assert.calledOnceWithExactly(createStub, uri, undefined); + sinon.assert.calledOnceWithExactly(setEnvironmentStub, uri, environment, false); + }); + + test('re-runs setup on repeated invocation so metadata changes can affect the cache key', async () => { + const uri = Uri.file('/workspace/script.py'); + const environment = createEnvironment(); + registerInlineManager(); + createStub.resolves(environment); + + await setupInlineScriptEnvironmentCommand(uri, createManagers()); + await setupInlineScriptEnvironmentCommand(uri, createManagers()); + + sinon.assert.calledTwice(readInlineScriptMetadataStub); + sinon.assert.calledTwice(waitForEnvManagerIdStub); + sinon.assert.calledTwice(createStub); + sinon.assert.calledTwice(setEnvironmentStub); + assert.deepStrictEqual(createStub.firstCall.args, [uri, undefined]); + assert.deepStrictEqual(createStub.secondCall.args, [uri, undefined]); + assert.deepStrictEqual(setEnvironmentStub.firstCall.args, [uri, environment, false]); + assert.deepStrictEqual(setEnvironmentStub.secondCall.args, [uri, environment, false]); + }); +}); + +suite('Setup Inline Script Environment Command Manager Readiness Tests', () => { + let clock: sinon.SinonFakeTimers; + let envManagerEmitter: EventEmitter; + let disposables: Disposable[]; + let createStub: sinon.SinonStub; + let getEnvironmentManagerStub: sinon.SinonStub; + let setEnvironmentStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + let readInlineScriptMetadataStub: sinon.SinonStub; + let managerAvailable: boolean; + + function createEnvironment(): PythonEnvironment { + return { + envId: { id: 'inline-env', managerId: INLINE_SCRIPT_MANAGER_ID }, + name: 'inline-env', + displayName: 'Inline Environment', + displayPath: '/path/to/inline-env', + version: '3.12.0', + environmentPath: Uri.file('/path/to/inline-env'), + execInfo: { run: { executable: '/path/to/inline-env/python' } }, + sysPrefix: '/path/to/inline-env', + }; + } + + function createManagers(): EnvironmentManagers { + return { + getEnvironmentManager: getEnvironmentManagerStub, + setEnvironment: setEnvironmentStub, + onDidChangeEnvironmentManager: envManagerEmitter.event, + onDidChangePackageManager: new EventEmitter().event, + } as unknown as EnvironmentManagers; + } + + setup(() => { + clock = sinon.useFakeTimers(); + disposables = []; + managerAvailable = false; + envManagerEmitter = new EventEmitter(); + createStub = sinon.stub().resolves(createEnvironment()); + getEnvironmentManagerStub = sinon.stub().callsFake((managerId: string) => { + if (managerId === INLINE_SCRIPT_MANAGER_ID && managerAvailable) { + return { create: createStub } as unknown as InternalEnvironmentManager; + } + return undefined; + }); + setEnvironmentStub = sinon.stub().resolves(); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage').resolves(undefined); + sinon.stub(windowApis, 'activeTextEditor').returns(undefined); + sinon.stub(workspaceApis, 'getOpenTextDocuments').returns([]); + readInlineScriptMetadataStub = sinon + .stub(inlineScriptMetadata, 'readInlineScriptMetadataFromFile') + .resolves({ range: { start: 0, end: 0 } }); + sinon.stub(helpers, 'isInlineScriptsFeatureEnabled').returns(true); + sinon.stub(logging, 'traceWarn'); + sinon.stub(logging, 'traceError'); + sinon.stub(logging, 'traceInfo'); + sinon.stub(telemetrySender, 'sendTelemetryEvent'); + sinon.stub(extensionApis, 'getExtension').returns({ + id: 'ms-python.python', + isActive: true, + } as unknown as ReturnType); + + _resetManagerReadyForTesting(); + createManagerReady( + { + onDidChangeEnvironmentManager: envManagerEmitter.event, + onDidChangePackageManager: new EventEmitter().event, + } as unknown as EnvironmentManagers, + { getProjects: () => [] } as unknown as PythonProjectManager, + disposables, + ); + }); + + teardown(() => { + clock.restore(); + disposables.forEach((disposable) => disposable.dispose()); + envManagerEmitter.dispose(); + sinon.restore(); + _resetManagerReadyForTesting(); + }); + + test('waits for inline manager readiness before direct lookup', async () => { + const uri = Uri.file('/workspace/script.py'); + let settled = false; + + const resultPromise = setupInlineScriptEnvironmentCommand(uri, createManagers()).then((result) => { + settled = true; + return result; + }); + + await clock.tickAsync(0); + assert.strictEqual(settled, false); + sinon.assert.notCalled(getEnvironmentManagerStub); + sinon.assert.notCalled(createStub); + + managerAvailable = true; + envManagerEmitter.fire({ + kind: 'registered', + manager: { id: INLINE_SCRIPT_MANAGER_ID } as unknown as InternalEnvironmentManager, + }); + + const result = await resultPromise; + + assert.strictEqual(settled, true); + assert.strictEqual(result?.envId.managerId, INLINE_SCRIPT_MANAGER_ID); + sinon.assert.calledOnceWithExactly(readInlineScriptMetadataStub, uri); + sinon.assert.calledOnceWithExactly(getEnvironmentManagerStub, INLINE_SCRIPT_MANAGER_ID); + sinon.assert.calledOnceWithExactly(createStub, uri, undefined); + sinon.assert.calledOnceWithExactly(setEnvironmentStub, uri, result, false); + sinon.assert.notCalled(showErrorMessageStub); + }); + + test('shows the not-registered error after manager-ready timeout', async () => { + const uri = Uri.file('/workspace/script.py'); + + const resultPromise = setupInlineScriptEnvironmentCommand(uri, createManagers()); + await clock.tickAsync(0); + clock.tick(MANAGER_READY_TIMEOUT_MS); + await clock.tickAsync(0); + + const result = await resultPromise; + + assert.strictEqual(result, undefined); + sinon.assert.calledOnceWithExactly(readInlineScriptMetadataStub, uri); + sinon.assert.calledOnceWithExactly(getEnvironmentManagerStub, INLINE_SCRIPT_MANAGER_ID); + sinon.assert.notCalled(createStub); + sinon.assert.notCalled(setEnvironmentStub); + sinon.assert.calledOnce(showErrorMessageStub); + assert.match(String(showErrorMessageStub.firstCall.args[0]), /preview manager is not registered/); + }); +}); diff --git a/src/test/smoke/registration.smoke.test.ts b/src/test/smoke/registration.smoke.test.ts index bd8d469e0..57c827659 100644 --- a/src/test/smoke/registration.smoke.test.ts +++ b/src/test/smoke/registration.smoke.test.ts @@ -57,6 +57,7 @@ suite('Smoke: Registration Checks', function () { // Environment management 'python-envs.create', 'python-envs.createAny', + 'python-envs.setupInlineScriptEnvironment', 'python-envs.set', 'python-envs.setEnv', 'python-envs.setEnvSelected',