Skip to content
8 changes: 8 additions & 0 deletions src/managers/builtin/venvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,14 @@ export class VenvManager implements EnvironmentManager {
getProjectFsPath: (s) => getProjectFsPathForScope(this.api, s),
getPersistedPath: (fsPath) => getVenvForWorkspace(fsPath),
resolve: (p) => resolveVenvPythonEnvironmentPath(p, this.nativeFinder, this.api, this, this.baseManager),
clearPersistedPath: async (staleScope) => {
if (staleScope instanceof Uri) {
const projectFsPath = getProjectFsPathForScope(this.api, staleScope);
await setVenvForWorkspace(projectFsPath, undefined);
} else {
await setVenvForGlobal(undefined);
}
},
startBackgroundInit: () => this.initializeWithBase(),
});
if (fastResult) {
Expand Down
8 changes: 8 additions & 0 deletions src/managers/common/fastPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface FastPathOptions {
startBackgroundInit: () => Promise<void> | Thenable<void>;
/** Optional: reads the persisted env path for global scope (when scope is undefined). */
getGlobalPersistedPath?: () => Promise<string | undefined>;
/** Optional: clears a stale persisted path after verification fails. */
clearPersistedPath?: (scope: Uri | undefined) => Promise<void>;
}

/**
Expand Down Expand Up @@ -133,6 +135,7 @@ export async function tryFastPathGet(opts: FastPathOptions): Promise<FastPathRes
traceVerbose(
`[${opts.label}] Fast path: persisted path '${persistedPath}' does not exist, falling through to slow path`,
);
await opts.clearPersistedPath?.(undefined);
return undefined;
}
try {
Expand All @@ -149,6 +152,7 @@ export async function tryFastPathGet(opts: FastPathOptions): Promise<FastPathRes
managerLabel: opts.label,
result: 'stale',
});
await opts.clearPersistedPath?.(undefined);
} catch (err) {
sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, {
managerLabel: opts.label,
Expand All @@ -158,6 +162,7 @@ export async function tryFastPathGet(opts: FastPathOptions): Promise<FastPathRes
`[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`,
err,
);
await opts.clearPersistedPath?.(undefined);
}
} else {
sendTelemetryEvent(EventNames.GLOBAL_ENV_CACHE, cacheStopWatch.elapsedTime, {
Expand All @@ -177,18 +182,21 @@ export async function tryFastPathGet(opts: FastPathOptions): Promise<FastPathRes
traceVerbose(
`[${opts.label}] Fast path: persisted path '${persistedPath}' does not exist, falling through to slow path`,
);
await opts.clearPersistedPath?.(scope);
return undefined;
}
try {
const resolved = await opts.resolve(persistedPath);
if (resolved) {
return { env: resolved };
}
await opts.clearPersistedPath?.(scope);
} catch (err) {
traceWarn(
`[${opts.label}] Fast path resolve failed for '${persistedPath}', falling back to full init:`,
err,
);
await opts.clearPersistedPath?.(scope);
}
} else {
traceVerbose(`[${opts.label}] Fast path: no persisted path, falling through to slow path`);
Expand Down
26 changes: 26 additions & 0 deletions src/test/managers/common/fastPath.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ suite('tryFastPathGet', () => {

test('reports stale when global cached path resolves to undefined', async () => {
const globalPath = __filename;
const clearPersistedPath = sinon.stub().resolves();
const { opts } = createOpts({
scope: undefined,
getGlobalPersistedPath: sinon.stub().resolves(globalPath),
resolve: sinon.stub().resolves(undefined),
clearPersistedPath,
});
const result = await tryFastPathGet(opts);

Expand All @@ -129,14 +131,17 @@ suite('tryFastPathGet', () => {
const [eventName, , props] = sendTelemetryStub.firstCall.args;
assert.strictEqual(eventName, EventNames.GLOBAL_ENV_CACHE);
assert.strictEqual(props.result, 'stale');
sinon.assert.calledOnceWithExactly(clearPersistedPath, undefined);
});

test('returns undefined for global scope when cached path resolve fails', async () => {
const globalPath = __filename;
const clearPersistedPath = sinon.stub().resolves();
const { opts } = createOpts({
scope: undefined,
getGlobalPersistedPath: sinon.stub().resolves(globalPath),
resolve: sinon.stub().rejects(new Error('python was uninstalled')),
clearPersistedPath,
});
const result = await tryFastPathGet(opts);

Expand All @@ -147,6 +152,7 @@ suite('tryFastPathGet', () => {
const [eventName, , props] = sendTelemetryStub.firstCall.args;
assert.strictEqual(eventName, EventNames.GLOBAL_ENV_CACHE);
assert.strictEqual(props.result, 'stale');
sinon.assert.calledOnceWithExactly(clearPersistedPath, undefined);
});

test('global scope fast path starts background init when initialized is undefined', async () => {
Expand Down Expand Up @@ -192,13 +198,33 @@ suite('tryFastPathGet', () => {
assert.strictEqual(result, undefined);
});

test('clears workspace persisted path when resolve returns undefined', async () => {
const clearPersistedPath = sinon.stub().resolves();
const scope = Uri.file(path.resolve('test', 'workspace'));
const { opts } = createOpts({
scope,
resolve: sinon.stub().resolves(undefined),
clearPersistedPath,
});
const result = await tryFastPathGet(opts);

assert.strictEqual(result, undefined);
sinon.assert.calledOnceWithExactly(clearPersistedPath, scope);
});


test('returns undefined when resolve throws', async () => {
const clearPersistedPath = sinon.stub().resolves();
const scope = Uri.file(path.resolve('test', 'workspace'));
const { opts } = createOpts({
scope,
resolve: sinon.stub().rejects(new Error('resolve failed')),
clearPersistedPath,
});
const result = await tryFastPathGet(opts);

assert.strictEqual(result, undefined);
sinon.assert.calledOnceWithExactly(clearPersistedPath, scope);
});

test('skips resolve and falls through when workspace persisted path no longer exists on disk', async () => {
Expand Down
Loading