diff --git a/src/client/common/utils/version.ts b/src/client/common/utils/version.ts index 4ef9c3b3d92c..b3d9ed3d2f46 100644 --- a/src/client/common/utils/version.ts +++ b/src/client/common/utils/version.ts @@ -70,11 +70,6 @@ export const EMPTY_VERSION: RawBasicVersionInfo = { major: -1, minor: -1, micro: -1, - unnormalized: { - major: undefined, - minor: undefined, - micro: undefined, - }, }; Object.freeze(EMPTY_VERSION); diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 695e8e706c23..9340792a4f4b 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { cloneDeep } from 'lodash'; +import { cloneDeep, isEqual } from 'lodash'; import * as path from 'path'; import { Uri } from 'vscode'; import { getArchitectureDisplayName } from '../../../common/platform/registry'; @@ -77,6 +77,19 @@ export function buildEnvInfo(init?: { return env; } +export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): boolean { + const env1Clone = cloneDeep(env1); + const env2Clone = cloneDeep(env2); + // Cannot compare searchLocation as they are Uri objects. + delete env1Clone.searchLocation; + delete env2Clone.searchLocation; + env1Clone.source = env1Clone.source.sort(); + env2Clone.source = env2Clone.source.sort(); + const searchLocation1 = env1.searchLocation?.fsPath ?? ''; + const searchLocation2 = env2.searchLocation?.fsPath ?? ''; + return isEqual(env1Clone, env2Clone) && arePathsSame(searchLocation1, searchLocation2); +} + /** * Return a deep copy of the given env info. * diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index a8820a0f82b8..b40f63fe9d27 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -6,7 +6,7 @@ import { isTestExecution } from '../../../../common/constants'; import { traceInfo } from '../../../../logging'; import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; import { PythonEnvInfo } from '../../info'; -import { areSameEnv, getEnvPath } from '../../info/env'; +import { areEnvsDeepEqual, areSameEnv, getEnvPath } from '../../info/env'; import { BasicPythonEnvCollectionChangedEvent, PythonEnvCollectionChangedEvent, @@ -52,15 +52,14 @@ export interface IEnvsCollectionCache { /** * Removes invalid envs from cache. Note this does not check for outdated info when * validating cache. - * @param latestListOfEnvs Carries list of latest envs for this workspace session if known. + * @param envs Carries list of envs for the latest refresh. + * @param isCompleteList Carries whether the list of envs is complete or not. */ - validateCache(latestListOfEnvs?: PythonEnvInfo[]): Promise; + validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise; } -export type PythonEnvLatestInfo = { hasLatestInfo?: boolean } & PythonEnvInfo; - interface IPersistentStorage { - load(): Promise; + get(): PythonEnvInfo[]; store(envs: PythonEnvInfo[]): Promise; } @@ -69,13 +68,24 @@ interface IPersistentStorage { */ export class PythonEnvInfoCache extends PythonEnvsWatcher implements IEnvsCollectionCache { - private envs: PythonEnvLatestInfo[] = []; + private envs: PythonEnvInfo[] = []; + + /** + * Carries the list of envs which have been validated to have latest info. + */ + private validatedEnvs = new Set(); + + /** + * Carries the list of envs which have been flushed to persistent storage. + * It signifies that the env info is likely up-to-date. + */ + private flushedEnvs = new Set(); constructor(private readonly persistentStorage: IPersistentStorage) { super(); } - public async validateCache(latestListOfEnvs?: PythonEnvInfo[]): Promise { + public async validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise { /** * We do check if an env has updated as we already run discovery in background * which means env cache will have up-to-date envs eventually. This also means @@ -86,7 +96,7 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { const { path } = getEnvPath(cachedEnv.executable.filename, cachedEnv.location); if (await pathExists(path)) { - if (latestListOfEnvs) { + if (envs && isCompleteList) { /** * Only consider a cached env to be valid if it's relevant. That means: * * It is either reported in the latest complete refresh for this session. @@ -95,7 +105,7 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher cachedEnv.id === env.id)) { + if (envs.some((env) => cachedEnv.id === env.id)) { return true; } } else { @@ -113,17 +123,26 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { + const cachedEnv = this.envs.find((e) => e.id === env.id); + if (cachedEnv && !areEnvsDeepEqual(cachedEnv, env)) { + this.updateEnv(cachedEnv, env, true); + } + }); + } } public getAllEnvs(): PythonEnvInfo[] { return this.envs; } - public addEnv(env: PythonEnvLatestInfo, hasLatestInfo?: boolean): void { + public addEnv(env: PythonEnvInfo, hasLatestInfo?: boolean): void { const found = this.envs.find((e) => areSameEnv(e, env)); if (hasLatestInfo) { - env.hasLatestInfo = true; - this.flush(false).ignoreErrors(); + this.validatedEnvs.add(env.id!); + this.flush(env).ignoreErrors(); // If we have latest info, flush it so it can be saved. } if (!found) { this.envs.push(env); @@ -131,7 +150,12 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher areSameEnv(e, oldValue)); if (index !== -1) { if (newValue === undefined) { @@ -146,40 +170,42 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { // `path` can either be path to environment or executable path const env = this.envs.find((e) => arePathsSame(e.location, path)) ?? this.envs.find((e) => areSameEnv(e, path)); - if (env?.hasLatestInfo) { - return env; - } - if (env && (env?.hasLatestInfo || (await validateInfo(env)))) { - return env; + if (env) { + if (this.validatedEnvs.has(env.id!)) { + return env; + } + if (await validateInfo(env)) { + this.validatedEnvs.add(env.id!); + return env; + } } return undefined; } - public async clearAndReloadFromStorage(): Promise { - this.envs = await this.persistentStorage.load(); - this.envs.forEach((e) => { - delete e.hasLatestInfo; - }); + public clearAndReloadFromStorage(): void { + this.envs = this.persistentStorage.get(); + this.markAllEnvsAsFlushed(); } - public async flush(allEnvsHaveLatestInfo = true): Promise { - if (this.envs.length) { - traceInfo('Environments added to cache', JSON.stringify(this.envs)); - if (allEnvsHaveLatestInfo) { - this.envs.forEach((e) => { - e.hasLatestInfo = true; - }); - } - await this.persistentStorage.store(this.envs); + public async flush(env?: PythonEnvInfo): Promise { + if (env) { + // Flush only the given env. + const envs = this.persistentStorage.get(); + const index = envs.findIndex((e) => e.id === env.id); + envs[index] = env; + this.flushedEnvs.add(env.id!); + await this.persistentStorage.store(envs); + return; } + traceInfo('Environments added to cache', JSON.stringify(this.envs)); + this.markAllEnvsAsFlushed(); + await this.persistentStorage.store(this.envs); } - public clearCache(): Promise { + private markAllEnvsAsFlushed(): void { this.envs.forEach((e) => { - this.fire({ old: e, new: undefined }); + this.flushedEnvs.add(e.id!); }); - this.envs = []; - return Promise.resolve(); } } @@ -198,7 +224,7 @@ async function validateInfo(env: PythonEnvInfo) { */ export async function createCollectionCache(storage: IPersistentStorage): Promise { const cache = new PythonEnvInfoCache(storage); - await cache.clearAndReloadFromStorage(); + cache.clearAndReloadFromStorage(); await validateCache(cache); return cache; } diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index ca7c93b1c269..a43c0c207f00 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -177,7 +177,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher { const storage = getGlobalStorage(ext.context, 'PYTHON_ENV_INFO_CACHE', []); const cache = await createCache({ - load: async () => storage.get(), + get: () => storage.get(), store: async (e) => storage.set(e), }); return cache; diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 00c94bb70f7d..9261483b45e1 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -32,9 +32,9 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async executeFile(file: Uri) { await this.setCwdForFileExecution(file); - const x = file.fsPath; - const hello = x.fileToCommandArgumentForPythonExt(); - const { command, args } = await this.getExecuteFileArgs(file, [hello]); + const { command, args } = await this.getExecuteFileArgs(file, [ + file.fsPath.fileToCommandArgumentForPythonExt(), + ]); await this.getTerminalService(file).sendCommand(command, args); } diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 603f423037bc..84beb766851f 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import * as path from 'path'; -import { Event } from 'vscode'; +import { Event, Uri } from 'vscode'; import { createDeferred, flattenIterator, iterable, mapToIterator } from '../../../client/common/utils/async'; import { getArchitecture } from '../../../client/common/utils/platform'; import { getVersionString } from '../../../client/common/utils/version'; @@ -35,6 +35,7 @@ export function createLocatedEnv( kind = PythonEnvKind.Unknown, exec: string | PythonExecutableInfo = 'python', distro: PythonDistroInfo = { org: '' }, + searchLocation?: Uri, ): PythonEnvInfo { const location = locationStr === '' @@ -57,6 +58,7 @@ export function createLocatedEnv( executable, location, version, + searchLocation, }); env.arch = getArchitecture(); env.distro = distro; diff --git a/src/test/pythonEnvironments/base/info/env.unit.test.ts b/src/test/pythonEnvironments/base/info/env.unit.test.ts index 6b82a3292adf..bb67a4465f9e 100644 --- a/src/test/pythonEnvironments/base/info/env.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/env.unit.test.ts @@ -2,15 +2,17 @@ // Licensed under the MIT License. import * as assert from 'assert'; +import { Uri } from 'vscode'; import { Architecture } from '../../../../client/common/utils/platform'; import { parseVersionInfo } from '../../../../client/common/utils/version'; import { PythonEnvInfo, PythonDistroInfo, PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; -import { setEnvDisplayString } from '../../../../client/pythonEnvironments/base/info/env'; +import { areEnvsDeepEqual, setEnvDisplayString } from '../../../../client/pythonEnvironments/base/info/env'; import { createLocatedEnv } from '../common'; -suite('pyenvs info - getEnvDisplayString()', () => { +suite('Environment helpers', () => { const name = 'my-env'; const location = 'x/y/z/spam/'; + const searchLocation = 'x/y/z'; const arch = Architecture.x64; const version = '3.8.1'; const kind = PythonEnvKind.Venv; @@ -28,6 +30,7 @@ suite('pyenvs info - getEnvDisplayString()', () => { distro?: PythonDistroInfo; display?: string; location?: string; + searchLocation?: string; }): PythonEnvInfo { const env = createLocatedEnv( info.location || '', @@ -35,28 +38,37 @@ suite('pyenvs info - getEnvDisplayString()', () => { info.kind || PythonEnvKind.Unknown, 'python', // exec info.distro, + info.searchLocation ? Uri.file(info.searchLocation) : undefined, ); env.name = info.name || ''; env.arch = info.arch || Architecture.Unknown; env.display = info.display; return env; } - const tests: [PythonEnvInfo, string, string][] = [ - [getEnv({}), 'Python', 'Python'], - [getEnv({ version, arch, name, kind, distro }), "Python 3.8.1 ('my-env')", "Python 3.8.1 ('my-env': venv)"], - // without "suffix" info - [getEnv({ version }), 'Python 3.8.1', 'Python 3.8.1'], - [getEnv({ arch }), 'Python 64-bit', 'Python 64-bit'], - [getEnv({ version, arch }), 'Python 3.8.1 64-bit', 'Python 3.8.1 64-bit'], - // with "suffix" info - [getEnv({ name }), "Python ('my-env')", "Python ('my-env')"], - [getEnv({ kind }), 'Python', 'Python (venv)'], - [getEnv({ name, kind }), "Python ('my-env')", "Python ('my-env': venv)"], - // env.location is ignored. - [getEnv({ location }), 'Python', 'Python'], - [getEnv({ name, location }), "Python ('my-env')", "Python ('my-env')"], - ]; - tests.forEach(([env, expectedDisplay, expectedDetailedDisplay]) => { + function testGenerator() { + const tests: [PythonEnvInfo, string, string][] = [ + [getEnv({}), 'Python', 'Python'], + [getEnv({ version, arch, name, kind, distro }), "Python 3.8.1 ('my-env')", "Python 3.8.1 ('my-env': venv)"], + // without "suffix" info + [getEnv({ version }), 'Python 3.8.1', 'Python 3.8.1'], + [getEnv({ arch }), 'Python 64-bit', 'Python 64-bit'], + [getEnv({ version, arch }), 'Python 3.8.1 64-bit', 'Python 3.8.1 64-bit'], + // with "suffix" info + [getEnv({ name }), "Python ('my-env')", "Python ('my-env')"], + [getEnv({ kind }), 'Python', 'Python (venv)'], + [getEnv({ name, kind }), "Python ('my-env')", "Python ('my-env': venv)"], + // env.location is ignored. + [getEnv({ location }), 'Python', 'Python'], + [getEnv({ name, location }), "Python ('my-env')", "Python ('my-env')"], + [ + getEnv({ name, location, searchLocation, version, arch }), + "Python 3.8.1 64-bit ('my-env')", + "Python 3.8.1 64-bit ('my-env')", + ], + ]; + return tests; + } + testGenerator().forEach(([env, expectedDisplay, expectedDetailedDisplay]) => { test(`"${expectedDisplay}"`, () => { setEnvDisplayString(env); @@ -64,4 +76,17 @@ suite('pyenvs info - getEnvDisplayString()', () => { assert.equal(env.detailedDisplayName, expectedDetailedDisplay); }); }); + testGenerator().forEach(([env1, _d1, display1], index1) => { + testGenerator().forEach(([env2, _d2, display2], index2) => { + if (index1 === index2) { + test(`"${display1}" === "${display2}"`, () => { + assert.strictEqual(areEnvsDeepEqual(env1, env2), true); + }); + } else { + test(`"${display1}" !== "${display2}"`, () => { + assert.strictEqual(areEnvsDeepEqual(env1, env2), false); + }); + } + }); + }); }); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts index 90dcb8345732..c0915bfa4aa9 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -9,16 +9,13 @@ import { EventEmitter, Uri } from 'vscode'; import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; import { createDeferred, createDeferredFromPromise, sleep } from '../../../../../client/common/utils/async'; import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; -import { buildEnvInfo } from '../../../../../client/pythonEnvironments/base/info/env'; +import { areSameEnv, buildEnvInfo } from '../../../../../client/pythonEnvironments/base/info/env'; import { ProgressNotificationEvent, ProgressReportStage, PythonEnvUpdatedEvent, } from '../../../../../client/pythonEnvironments/base/locator'; -import { - createCollectionCache, - PythonEnvLatestInfo, -} from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionCache'; +import { createCollectionCache } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionCache'; import { EnvsCollectionService } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionService'; import { PythonEnvCollectionChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; @@ -111,17 +108,14 @@ suite('Python envs locator - Environments Collection', async () => { updatedName, ); // Do not include cached envs which were not yielded by the locator, unless it belongs to some workspace. - return [cachedEnvForWorkspace, env1, env2, env3].map((e: PythonEnvLatestInfo) => { - e.hasLatestInfo = true; - return e; - }); + return [cachedEnvForWorkspace, env1, env2, env3]; } setup(async () => { storage = []; const parentLocator = new SimpleLocator(getLocatorEnvs()); const cache = await createCollectionCache({ - load: async () => getCachedEnvs(), + get: () => getCachedEnvs(), store: async (envs) => { storage = envs; }, @@ -166,7 +160,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => getCachedEnvs(), + get: () => getCachedEnvs(), store: async (e) => { storage = e; }, @@ -200,7 +194,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => cachedEnvs, + get: () => cachedEnvs, store: async (e) => { storage = e; }, @@ -223,6 +217,57 @@ suite('Python envs locator - Environments Collection', async () => { assertEnvsEqual(envs, expected); }); + test("Ensure update events are not fired if an environment isn't actually updated", async () => { + const onUpdated = new EventEmitter(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator); + + let events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + expect(events.length).to.not.equal(0, 'Atleast event should be fired'); + const envs = collectionService.getEnvs(); + + // Trigger a refresh again. + events = []; + await collectionService.triggerRefresh(); + // Filter out the events which are related to envs in the cache, we expect no such events to be fired as no + // envs were updated. + events = events.filter((e) => + envs.some((env) => { + const eventEnv = e.old ?? e.new; + if (!eventEnv) { + return true; + } + return areSameEnv(eventEnv, env); + }), + ); + expect(events.length).to.equal(0, 'Do not fire additional events as envs have not updated'); + }); + test('triggerRefresh() refreshes the collection with any new envs & removes cached envs if not relevant', async () => { const onUpdated = new EventEmitter(); const locatedEnvs = getLocatorEnvs(); @@ -241,7 +286,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => cachedEnvs, + get: () => cachedEnvs, store: async (e) => { storage = e; }, @@ -294,7 +339,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => cachedEnvs, + get: () => cachedEnvs, store: async (e) => { storage = e; }, @@ -354,11 +399,10 @@ suite('Python envs locator - Environments Collection', async () => { test('resolveEnv() uses cache if complete and up to date info is available', async () => { const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); const cachedEnvs = getCachedEnvs(); - const env: PythonEnvLatestInfo = cachedEnvs[0]; + const env = cachedEnvs[0]; env.executable.ctime = 100; env.executable.mtime = 100; sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); - env.hasLatestInfo = true; // Has complete info const parentLocator = new SimpleLocator([], { resolve: async (e: PythonEnvInfo) => { if (env.executable.filename === e.executable.filename) { @@ -368,7 +412,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => cachedEnvs, + get: () => cachedEnvs, store: async () => noop(), }); collectionService = new EnvsCollectionService(cache, parentLocator); @@ -378,7 +422,7 @@ suite('Python envs locator - Environments Collection', async () => { test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { const cachedEnvs = getCachedEnvs(); - const env: PythonEnvLatestInfo = cachedEnvs[0]; + const env = cachedEnvs[0]; const resolvedViaLocator = buildEnvInfo({ executable: env.executable.filename, sysPrefix: 'Resolved via locator', @@ -386,7 +430,6 @@ suite('Python envs locator - Environments Collection', async () => { env.executable.ctime = 101; env.executable.mtime = 90; sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); - env.hasLatestInfo = true; // Has complete info const parentLocator = new SimpleLocator([], { resolve: async (e: PythonEnvInfo) => { if (env.executable.filename === e.executable.filename) { @@ -396,7 +439,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => cachedEnvs, + get: () => cachedEnvs, store: async () => noop(), }); collectionService = new EnvsCollectionService(cache, parentLocator); @@ -407,8 +450,7 @@ suite('Python envs locator - Environments Collection', async () => { test('resolveEnv() uses underlying locator if cache does not have complete info for env', async () => { const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); const cachedEnvs = getCachedEnvs(); - const env: PythonEnvLatestInfo = cachedEnvs[0]; - env.hasLatestInfo = false; // Does not have complete info + const env = cachedEnvs[0]; const parentLocator = new SimpleLocator([], { resolve: async (e: PythonEnvInfo) => { if (env.executable.filename === e.executable.filename) { @@ -418,7 +460,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => cachedEnvs, + get: () => cachedEnvs, store: async () => noop(), }); collectionService = new EnvsCollectionService(cache, parentLocator); @@ -437,15 +479,12 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => [], + get: () => [], store: async () => noop(), }); collectionService = new EnvsCollectionService(cache, parentLocator); - const resolved: PythonEnvLatestInfo | undefined = await collectionService.resolveEnv( - resolvedViaLocator.executable.filename, - ); + const resolved = await collectionService.resolveEnv(resolvedViaLocator.executable.filename); const envs = collectionService.getEnvs(); - expect(resolved?.hasLatestInfo).to.equal(true); assertEnvsEqual(envs, [resolved]); }); @@ -459,7 +498,7 @@ suite('Python envs locator - Environments Collection', async () => { }, }); const cache = await createCollectionCache({ - load: async () => [], + get: () => [], store: async () => noop(), }); collectionService = new EnvsCollectionService(cache, parentLocator);