diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 0a3fc323..17f8a97f 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -151,12 +151,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run Smoke Tests (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 @@ -244,12 +238,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run E2E Tests (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 @@ -337,12 +325,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run Integration Tests (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 @@ -387,12 +369,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run Integration Tests Multi-Root (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 diff --git a/.github/workflows/push-check.yml b/.github/workflows/push-check.yml index e00f0aaf..a0dab018 100644 --- a/.github/workflows/push-check.yml +++ b/.github/workflows/push-check.yml @@ -152,12 +152,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run Smoke Tests (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 @@ -245,12 +239,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run E2E Tests (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 @@ -338,12 +326,6 @@ jobs: - name: Compile Tests run: npm run compile-tests - - name: Configure Test Settings - run: | - mkdir -p .vscode-test/user-data/User - echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json - shell: bash - - name: Run Integration Tests (Linux) if: runner.os == 'Linux' uses: GabrielBB/xvfb-action@v1 diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 79b6d242..23d18eb9 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,8 +1,22 @@ import { defineConfig } from '@vscode/test-cli'; +import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; -// Explicit user data directory - ensures VS Code reads our settings.json -const userDataDir = path.resolve('.vscode-test/user-data'); +// Keep this path short: macOS caps Unix-domain socket paths at 103 chars and +// VS Code creates `/-main.sock`. An in-workspace location +// (e.g. /Users/runner/work///.vscode-test/user-data) overflows. +const userDataDir = path.join(os.tmpdir(), 'vsct-ud'); + +// Seed user settings.json so the extension actually activates: without +// `python.useEnvironmentsExtension=true` it short-circuits during activation +// and the smoke/e2e/integration tests see no registered managers. +const userDir = path.join(userDataDir, 'User'); +fs.mkdirSync(userDir, { recursive: true }); +fs.writeFileSync( + path.join(userDir, 'settings.json'), + JSON.stringify({ 'python.useEnvironmentsExtension': true }) + '\n', +); export default defineConfig([ { diff --git a/src/managers/common/fastPath.ts b/src/managers/common/fastPath.ts index aa69bd29..81370210 100644 --- a/src/managers/common/fastPath.ts +++ b/src/managers/common/fastPath.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as fs from 'fs'; import { Uri } from 'vscode'; import { GetEnvironmentScope, PythonEnvironment, PythonEnvironmentApi } from '../../api'; import { traceError, traceVerbose, traceWarn } from '../../common/logging'; @@ -48,6 +49,28 @@ export function getProjectFsPathForScope(api: Pick { + try { + await fs.promises.access(persistedPath); + return true; + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === 'ENOENT' || code === 'ENOTDIR') { + return false; + } + // Unknown error (e.g. EACCES) — don't treat as missing; let resolve() decide. + return true; + } +} + /** * Attempts fast-path resolution for manager.get(): if full initialization hasn't completed yet * and there's a persisted environment for the workspace, resolve it directly via nativeFinder @@ -100,6 +123,18 @@ export async function tryFastPathGet(opts: FastPathOptions): Promise): FastPathTestOptions { const setInitialized = sinon.stub(); - const persistedPath = path.resolve('persisted', 'path'); + const persistedPath = __filename; return { opts: { initialized: undefined, @@ -77,7 +77,7 @@ suite('tryFastPathGet', () => { }); test('returns resolved env for global scope when getGlobalPersistedPath returns a path', async () => { - const globalPath = path.resolve('usr', 'bin', 'python3'); + const globalPath = __filename; const resolve = sinon.stub().resolves(createMockEnv(globalPath)); const { opts } = createOpts({ scope: undefined, @@ -116,7 +116,7 @@ suite('tryFastPathGet', () => { }); test('reports stale when global cached path resolves to undefined', async () => { - const globalPath = path.resolve('usr', 'bin', 'python3'); + const globalPath = __filename; const { opts } = createOpts({ scope: undefined, getGlobalPersistedPath: sinon.stub().resolves(globalPath), @@ -132,7 +132,7 @@ suite('tryFastPathGet', () => { }); test('returns undefined for global scope when cached path resolve fails', async () => { - const globalPath = path.resolve('usr', 'bin', 'python3'); + const globalPath = __filename; const { opts } = createOpts({ scope: undefined, getGlobalPersistedPath: sinon.stub().resolves(globalPath), @@ -150,7 +150,7 @@ suite('tryFastPathGet', () => { }); test('global scope fast path starts background init when initialized is undefined', async () => { - const globalPath = path.resolve('usr', 'bin', 'python3'); + const globalPath = __filename; const startBackgroundInit = sinon.stub().resolves(); const { opts, setInitialized } = createOpts({ scope: undefined, @@ -201,6 +201,37 @@ suite('tryFastPathGet', () => { assert.strictEqual(result, undefined); }); + test('skips resolve and falls through when workspace persisted path no longer exists on disk', async () => { + const missingPath = path.resolve('does', 'not', 'exist', 'python-missing'); + const resolve = sinon.stub().resolves(createMockEnv(missingPath)); + const { opts } = createOpts({ + getPersistedPath: sinon.stub().resolves(missingPath), + resolve, + }); + const result = await tryFastPathGet(opts); + + assert.strictEqual(result, undefined, 'Should fall through when cached path is missing'); + assert.ok(resolve.notCalled, 'Should not invoke resolve (and thus PET) when path is missing'); + }); + + test('skips resolve and reports stale when global persisted path no longer exists on disk', async () => { + const missingPath = path.resolve('does', 'not', 'exist', 'python-missing'); + const resolve = sinon.stub().resolves(createMockEnv(missingPath)); + const { opts } = createOpts({ + scope: undefined, + getGlobalPersistedPath: sinon.stub().resolves(missingPath), + resolve, + }); + const result = await tryFastPathGet(opts); + + assert.strictEqual(result, undefined, 'Should fall through when cached global path is missing'); + assert.ok(resolve.notCalled, 'Should not invoke resolve (and thus PET) when path is missing'); + assert.ok(sendTelemetryStub.calledOnce, 'Should send telemetry for stale global cache'); + const [eventName, , props] = sendTelemetryStub.firstCall.args; + assert.strictEqual(eventName, EventNames.GLOBAL_ENV_CACHE); + assert.strictEqual(props.result, 'stale'); + }); + test('calls getProjectFsPath with the scope Uri', async () => { const scope = Uri.file(path.resolve('my', 'project')); const getProjectFsPath = sinon.stub().returns(scope.fsPath); @@ -214,7 +245,7 @@ suite('tryFastPathGet', () => { test('passes project fsPath to getPersistedPath', async () => { const projectPath = path.resolve('project', 'path'); const getProjectFsPath = sinon.stub().returns(projectPath); - const getPersistedPath = sinon.stub().resolves(path.resolve('persisted')); + const getPersistedPath = sinon.stub().resolves(__filename); const { opts } = createOpts({ getProjectFsPath, getPersistedPath, @@ -266,7 +297,7 @@ suite('tryFastPathGet', () => { const getPersistedPath = sinon.stub().callsFake( () => new Promise((resolve) => { - releasePersistedRead = () => resolve(path.resolve('persisted', 'path')); + releasePersistedRead = () => resolve(__filename); }), ); const { opts, setInitialized } = createOpts({ getPersistedPath }); diff --git a/src/test/managers/fastPath.get.unit.test.ts b/src/test/managers/fastPath.get.unit.test.ts index 5ad62c74..62a48a88 100644 --- a/src/test/managers/fastPath.get.unit.test.ts +++ b/src/test/managers/fastPath.get.unit.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import * as fs from 'fs'; import * as path from 'path'; import * as sinon from 'sinon'; import { Uri } from 'vscode'; @@ -284,6 +285,9 @@ suite('Manager get() fast path', () => { setup(() => { sandbox = sinon.createSandbox(); sandbox.stub(windowApis, 'withProgress').callsFake((_opts, cb) => cb(undefined as never, undefined as never)); + // fastPath.ts now does a real fs.access on the persisted path; these tests use + // synthetic paths that don't exist on disk, so pretend every path is present. + sandbox.stub(fs.promises, 'access').resolves(); }); teardown(() => {