diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ac76f2b3607..ade9d53d4fad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -85,7 +85,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -139,7 +139,7 @@ jobs: path: ${{env.special-working-directory-relative}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -155,7 +155,7 @@ jobs: python-version: ${{matrix.python}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -340,7 +340,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -404,7 +404,7 @@ jobs: # uses: actions/checkout@v2.3.4 # - name: Install Node - # uses: actions/setup-node@v2.1.5 + # uses: actions/setup-node@v2.2.0 # with: # node-version: ${{env.NODE_VERSION}} diff --git a/.github/workflows/nightly-coverage.yml b/.github/workflows/nightly-coverage.yml index 8389bcb45ff8..4c2418989db4 100644 --- a/.github/workflows/nightly-coverage.yml +++ b/.github/workflows/nightly-coverage.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 00dc3840b4be..a9da09a9af15 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -118,7 +118,7 @@ jobs: path: ${{env.special-working-directory-relative}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -134,7 +134,7 @@ jobs: python-version: ${{matrix.python}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -323,7 +323,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -389,7 +389,7 @@ jobs: # uses: actions/checkout@v2.3.4 # - name: Install Node - # uses: actions/setup-node@v2.1.5 + # uses: actions/setup-node@v2.2.0 # with: # node-version: ${{env.NODE_VERSION}} @@ -452,7 +452,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} @@ -468,7 +468,7 @@ jobs: python-version: ${{env.PYTHON_VERSION}} - name: Install Node - uses: actions/setup-node@v2.1.5 + uses: actions/setup-node@v2.2.0 with: node-version: ${{env.NODE_VERSION}} diff --git a/news/1 Enhancements/16461.md b/news/1 Enhancements/16461.md new file mode 100644 index 000000000000..58f5ca182794 --- /dev/null +++ b/news/1 Enhancements/16461.md @@ -0,0 +1 @@ +Support starting a TensorBoard session with a remote URL hosting log files. \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 96ee3df8c1e9..0940a7309557 100644 --- a/package.nls.json +++ b/package.nls.json @@ -251,5 +251,7 @@ "TensorBoard.installProfilerPluginPrompt": "We recommend installing the PyTorch Profiler TensorBoard plugin. Would you like to install the package?", "TensorBoard.upgradePrompt": "Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?", "TensorBoard.launchNativeTensorBoardSessionCodeAction": "Launch TensorBoard session", - "TensorBoard.launchNativeTensorBoardSessionCodeLens": "▶ Launch TensorBoard Session" + "TensorBoard.launchNativeTensorBoardSessionCodeLens": "▶ Launch TensorBoard Session", + "TensorBoard.enterRemoteUrl": "Enter remote URL", + "TensorBoard.enterRemoteUrlDetail": "Enter a URL pointing to a remote directory containing your TensorBoard log files" } diff --git a/requirements.in b/requirements.in index c6b52697690c..b00540bbb513 100644 --- a/requirements.in +++ b/requirements.in @@ -6,4 +6,4 @@ # IntelliSense via Jedi jedi<0.18 # For Python 2.7 support # Sort Imports -isort==5.8.0; python_version >= '3.6' +isort==5.9.2; python_version >= '3.6' diff --git a/requirements.txt b/requirements.txt index c4cdd1a4f6a0..0a83b881cf1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --generate-hashes requirements.in # -isort==5.8.0 ; python_version >= "3.6" \ - --hash=sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6 \ - --hash=sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d +isort==5.9.2 ; python_version >= "3.6" \ + --hash=sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813 \ + --hash=sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e # via -r requirements.in jedi==0.17.2 \ --hash=sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20 \ diff --git a/src/client/common/experiments/helpers.ts b/src/client/common/experiments/helpers.ts index a85a468f494f..127559f4fb12 100644 --- a/src/client/common/experiments/helpers.ts +++ b/src/client/common/experiments/helpers.ts @@ -13,3 +13,10 @@ export async function inDiscoveryExperiment(experimentService: IExperimentServic ]); return results.includes(true); } + +export function inDiscoveryExperimentSync(experimentService: IExperimentService): boolean { + return ( + experimentService.inExperimentSync(DiscoveryVariants.discoverWithFileWatching) || + experimentService.inExperimentSync(DiscoveryVariants.discoveryWithoutFileWatching) + ); +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e7a8e664d32e..82bfacd3fee6 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -139,6 +139,11 @@ export namespace Jupyter { } export namespace TensorBoard { + export const enterRemoteUrl = localize('TensorBoard.enterRemoteUrl', 'Enter remote URL'); + export const enterRemoteUrlDetail = localize( + 'TensorBoard.enterRemoteUrlDetail', + 'Enter a URL pointing to a remote directory containing your TensorBoard log files', + ); export const useCurrentWorkingDirectoryDetail = localize( 'TensorBoard.useCurrentWorkingDirectoryDetail', 'TensorBoard will search for tfevent files in all subdirectories of the current working directory', diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index 5bc13628e3b5..1340c0c4daee 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -6,15 +6,18 @@ import { inject, injectable, named } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import { EnvironmentSorting } from '../../common/experiments/groups'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion'; import { PythonEnvironment } from '../../pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IInterpreterHelper } from '../contracts'; +import { EnvTypeHeuristic, getEnvTypeHeuristic } from '../configuration/environmentTypeComparer'; +import { InterpreterComparisonType, IInterpreterComparer } from '../configuration/types'; +import { IInterpreterHelper, IInterpreterService } from '../contracts'; import { AutoSelectionRule, IInterpreterAutoSelectionRule, @@ -46,6 +49,11 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IInterpreterComparer) + @named(InterpreterComparisonType.EnvType) + private readonly envTypeComparer: IInterpreterComparer, @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.systemWide) systemInterpreter: IInterpreterAutoSelectionRule, @@ -104,19 +112,30 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio winRegInterpreter.setNextRule(systemInterpreter); } + /** + * If there's a cached auto-selected interpreter -> return it. + * If not, check if we are in the env sorting experiment, and use the appropriate auto-selection logic. + */ @captureTelemetry(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { rule: AutoSelectionRule.all }, true) public async autoSelectInterpreter(resource: Resource): Promise { const key = this.getWorkspacePathKey(resource); + if (!this.autoSelectedWorkspacePromises.has(key)) { const deferred = createDeferred(); this.autoSelectedWorkspacePromises.set(key, deferred); + await this.initializeStore(resource); await this.clearWorkspaceStoreIfInvalid(resource); - await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); - this.didAutoSelectedInterpreterEmitter.fire(); - Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors(); + + if (await this.experimentService.inExperiment(EnvironmentSorting.experiment)) { + await this.autoselectInterpreterWithLocators(resource); + } else { + await this.autoselectInterpreterWithRules(resource); + } + deferred.resolve(); } + return this.autoSelectedWorkspacePromises.get(key)!.promise; } @@ -221,4 +240,44 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } return undefined; } + + private async autoselectInterpreterWithRules(resource: Resource): Promise { + await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); + + this.didAutoSelectedInterpreterEmitter.fire(); + + Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors(); + } + + /** + * Auto-selection logic: + * 1. If there are cached interpreters (not the first session in this workspace) + * -> sort using the same logic as in the interpreter quickpick and return the first one; + * 2. If not, we already fire all the locators, so wait for their response, sort the interpreters and return the first one. + * + * `getInterpreters` will check the cache first and return early if there are any cached interpreters, + * and if not it will wait for locators to return. + * As such, we can sort interpreters based on what it returns. + */ + private async autoselectInterpreterWithLocators(resource: Resource): Promise { + const interpreters = await this.interpreterService.getInterpreters(resource); + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + + // When auto-selecting an intepreter for a workspace, we either want to return a local one + // or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment + // because we would have to add a way to match environments to a workspace. + const filteredInterpreters = interpreters.filter( + (i) => getEnvTypeHeuristic(i, workspaceUri?.folderUri.fsPath || '') !== EnvTypeHeuristic.Global, + ); + + filteredInterpreters.sort(this.envTypeComparer.compare.bind(this.envTypeComparer)); + + if (workspaceUri) { + this.setWorkspaceInterpreter(workspaceUri.folderUri, filteredInterpreters[0]); + } else { + this.setGlobalInterpreter(filteredInterpreters[0]); + } + + this.didAutoSelectedInterpreterEmitter.fire(); + } } diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index bf73d96d00bd..ac0eaae0cd9a 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -15,7 +15,7 @@ import { IInterpreterComparer } from './types'; * - Global environments (pipenv, conda); * - Globally-installed interpreters (/usr/bin/python3, Windows Store). */ -enum EnvTypeHeuristic { +export enum EnvTypeHeuristic { Local = 1, Global = 2, GlobalInterpreters = 3, @@ -165,7 +165,7 @@ function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment, work /** * Return a heuristic value depending on the environment type. */ -function getEnvTypeHeuristic(environment: PythonEnvironment, workspacePath: string): EnvTypeHeuristic { +export function getEnvTypeHeuristic(environment: PythonEnvironment, workspacePath: string): EnvTypeHeuristic { const { envType } = environment; if (workspacePath.length > 0 && environment.envPath && isParentPath(environment.envPath, workspacePath)) { diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index ea5da0d2aa44..492d0a4dda7e 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -33,7 +33,7 @@ import { } from './contracts'; import { IVirtualEnvironmentManager } from './virtualEnvs/types'; import { getInterpreterHash } from '../pythonEnvironments/discovery/locators/services/hashProvider'; -import { inDiscoveryExperiment } from '../common/experiments/helpers'; +import { inDiscoveryExperiment, inDiscoveryExperimentSync } from '../common/experiments/helpers'; import { StopWatch } from '../common/utils/stopWatch'; import { PythonVersion } from '../pythonEnvironments/info/pythonVersion'; @@ -139,7 +139,7 @@ export class InterpreterService implements Disposable, IInterpreterService { public async getInterpreters(resource?: Uri, options?: GetInterpreterOptions): Promise { let environments: PythonEnvironment[] = []; const stopWatch = new StopWatch(); - if (await inDiscoveryExperiment(this.experimentService)) { + if (inDiscoveryExperimentSync(this.experimentService)) { environments = await this.pyenvs.getInterpreters(resource, options); } else { const locator = this.serviceContainer.get( diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 6f66343be2f2..814b739ebc1c 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -290,7 +290,6 @@ export class TensorBoardSession { return tensorboardInstallStatus === ProductInstallStatus.Installed; } - // eslint-disable-next-line class-methods-use-this private async showFilePicker(): Promise { const selection = await this.applicationShell.showOpenDialog({ canSelectFiles: false, @@ -307,6 +306,8 @@ export class TensorBoardSession { // eslint-disable-next-line class-methods-use-this private getQuickPickItems(logDir: string | undefined) { + const items = []; + if (logDir) { const useCwd = { label: TensorBoard.useCurrentWorkingDirectory(), @@ -316,13 +317,21 @@ export class TensorBoardSession { label: TensorBoard.selectAnotherFolder(), detail: TensorBoard.selectAnotherFolderDetail(), }; - return [useCwd, selectAnotherFolder]; + items.push(useCwd, selectAnotherFolder); + } else { + const selectAFolder = { + label: TensorBoard.selectAFolder(), + detail: TensorBoard.selectAFolderDetail(), + }; + items.push(selectAFolder); } - const selectAFolder = { - label: TensorBoard.selectAFolder(), - detail: TensorBoard.selectAFolderDetail(), - }; - return [selectAFolder]; + + items.push({ + label: TensorBoard.enterRemoteUrl(), + detail: TensorBoard.enterRemoteUrlDetail(), + }); + + return items; } // Display a quickpick asking the user to acknowledge our autopopulated log directory or @@ -341,6 +350,7 @@ export class TensorBoardSession { const useCurrentWorkingDirectory = TensorBoard.useCurrentWorkingDirectory(); const selectAFolder = TensorBoard.selectAFolder(); const selectAnotherFolder = TensorBoard.selectAnotherFolder(); + const enterRemoteUrl = TensorBoard.enterRemoteUrl(); const items: QuickPickItem[] = this.getQuickPickItems(logDir); const item = await this.applicationShell.showQuickPick(items, { canPickMany: false, @@ -353,6 +363,10 @@ export class TensorBoardSession { case selectAFolder: case selectAnotherFolder: return this.showFilePicker(); + case enterRemoteUrl: + return this.applicationShell.showInputBox({ + prompt: TensorBoard.enterRemoteUrlDetail(), + }); default: return undefined; } diff --git a/src/test/configuration/environmentTypeComparer.unit.test.ts b/src/test/configuration/environmentTypeComparer.unit.test.ts index 66699006dd60..ef4fc630e5f6 100644 --- a/src/test/configuration/environmentTypeComparer.unit.test.ts +++ b/src/test/configuration/environmentTypeComparer.unit.test.ts @@ -4,7 +4,11 @@ import * as assert from 'assert'; import * as path from 'path'; import * as sinon from 'sinon'; -import { EnvironmentTypeComparer } from '../../client/interpreter/configuration/environmentTypeComparer'; +import { + EnvironmentTypeComparer, + EnvTypeHeuristic, + getEnvTypeHeuristic, +} from '../../client/interpreter/configuration/environmentTypeComparer'; import { IInterpreterHelper } from '../../client/interpreter/contracts'; import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; @@ -225,3 +229,64 @@ suite('Environment sorting', () => { }); }); }); + +suite('getEnvTypeHeuristic tests', () => { + const workspacePath = path.join('path', 'to', 'workspace'); + + const localGlobalEnvTypes = [ + EnvironmentType.Venv, + EnvironmentType.Conda, + EnvironmentType.VirtualEnv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Pipenv, + EnvironmentType.Poetry, + ]; + + localGlobalEnvTypes.forEach((envType) => { + test('If the path to an environment starts with the workspace path it should be marked as local', () => { + const environment = { + envType, + envPath: path.join(workspacePath, 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.Local); + }); + + test('If the path to an environment does not start with the workspace path it should be marked as global', () => { + const environment = { + envType, + envPath: path.join('path', 'to', 'my-environment'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.Global); + }); + }); + + const globalInterpretersEnvTypes = [ + EnvironmentType.System, + EnvironmentType.WindowsStore, + EnvironmentType.Global, + EnvironmentType.Unknown, + EnvironmentType.Pyenv, + ]; + + globalInterpretersEnvTypes.forEach((envType) => { + test(`If the environment type is ${envType} and the environment path does not start with the workspace path it should be marked as a global interpreter`, () => { + const environment = { + envType, + envPath: path.join('path', 'to', 'a', 'global', 'interpreter'), + version: { major: 3, minor: 10, patch: 2 }, + } as PythonEnvironment; + + const envTypeHeuristic = getEnvTypeHeuristic(environment, workspacePath); + + assert.strictEqual(envTypeHeuristic, EnvTypeHeuristic.GlobalInterpreters); + }); + }); +}); diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index 4c05d91afd5e..e56db655139b 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -4,11 +4,13 @@ 'use strict'; import { expect } from 'chai'; +import * as path from 'path'; import { SemVer } from 'semver'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; +import { EnvironmentSorting } from '../../../client/common/experiments/groups'; import { ExperimentService } from '../../../client/common/experiments/service'; import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; import { FileSystem } from '../../../client/common/platform/fileSystem'; @@ -27,9 +29,11 @@ import { IInterpreterAutoSelectionRule, IInterpreterAutoSelectionProxyService, } from '../../../client/interpreter/autoSelection/types'; -import { IInterpreterHelper } from '../../../client/interpreter/contracts'; +import { EnvironmentTypeComparer } from '../../../client/interpreter/configuration/environmentTypeComparer'; +import { IInterpreterHelper, IInterpreterService, WorkspacePythonPath } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; -import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -50,6 +54,7 @@ suite('Interpreters - Auto Selection', () => { let helper: IInterpreterHelper; let proxy: IInterpreterAutoSelectionProxyService; let experiments: IExperimentService; + let interpreterService: IInterpreterService; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { public initializeStore(resource: Resource): Promise { return super.initializeStore(resource); @@ -77,12 +82,19 @@ suite('Interpreters - Auto Selection', () => { helper = mock(InterpreterHelper); proxy = mock(InterpreterAutoSelectionProxyService); experiments = mock(ExperimentService); + interpreterService = mock(InterpreterService); + + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + when(experiments.inExperimentSync(anything())).thenReturn(false); autoSelectionService = new InterpreterAutoSelectionServiceTest( instance(workspaceService), instance(stateFactory), instance(fs), + instance(experiments), + instance(interpreterService), + interpreterComparer, instance(systemInterpreter), instance(currentPathInterpreter), instance(winRegInterpreter), @@ -105,44 +117,143 @@ suite('Interpreters - Auto Selection', () => { verify(winRegInterpreter.setNextRule(instance(systemInterpreter))).once(); verify(systemInterpreter.setNextRule(anything())).never(); }); - test('Run rules in background', async () => { - let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { - eventFired = true; - }); - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); - expect(eventFired).to.deep.equal(true, 'event not fired'); + suite('When using rule-based auto-selection', () => { + setup(() => { + when(experiments.inExperiment(EnvironmentSorting.experiment)).thenResolve(false); + }); - const allRules = [ - userDefinedInterpreter, - winRegInterpreter, - currentPathInterpreter, - systemInterpreter, - workspaceInterpreter, - cachedPaths, - ]; - for (const service of allRules) { - verify(service.autoSelectInterpreter(undefined)).once(); - if (service !== userDefinedInterpreter) { - verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); + test('Run rules in background', async () => { + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + + const allRules = [ + userDefinedInterpreter, + winRegInterpreter, + currentPathInterpreter, + systemInterpreter, + workspaceInterpreter, + cachedPaths, + ]; + for (const service of allRules) { + verify(service.autoSelectInterpreter(undefined)).once(); + if (service !== userDefinedInterpreter) { + verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); + } } - } - verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + }); + + test('Run userDefineInterpreter as the first rule', async () => { + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); + }); }); - test('Run userDefineInterpreter as the first rule', async () => { - let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { - eventFired = true; + + suite('When using locator-based auto-selection', () => { + let workspacePath: string; + let resource: Uri; + let eventFired: boolean; + + setup(() => { + workspacePath = path.join('path', 'to', 'workspace'); + resource = Uri.parse('resource'); + eventFired = false; + + const folderUri = { fsPath: workspacePath }; + + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ + folderUri, + } as WorkspacePythonPath); + when( + stateFactory.createWorkspacePersistentState(anyString(), undefined), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), '')).thenReturn('workspaceIdentifier'); + when(experiments.inExperiment(EnvironmentSorting.experiment)).thenResolve(true); + + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); }); - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); + test('If there is a local environment select it', async () => { + const localEnv = { + envType: EnvironmentType.Venv, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenResolve([ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment, + localEnv, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(localEnv)).once(); + }); - expect(eventFired).to.deep.equal(true, 'event not fired'); - verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); + test('If there are no local environments, return a globally-installed interpreter', async () => { + const systemEnv = { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenResolve([ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + systemEnv, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(systemEnv)).once(); + }); }); + test('Initialize the store', async () => { let initialize = false; let eventFired = false; @@ -200,6 +311,12 @@ suite('Interpreters - Auto Selection', () => { undefined, ), ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(true); diff --git a/src/test/tensorBoard/tensorBoardSession.test.ts b/src/test/tensorBoard/tensorBoardSession.test.ts index 2ed76ac69419..23c10a91aef9 100644 --- a/src/test/tensorBoard/tensorBoardSession.test.ts +++ b/src/test/tensorBoard/tensorBoardSession.test.ts @@ -141,7 +141,6 @@ suite('TensorBoard session creation', async () => { assert.ok(daemon?.killed, 'TensorBoard session process not killed after webview closed'); }); test('When user selects file picker, display file picker', async () => { - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); // Stub user selections sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAnotherFolder() }); const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog'); @@ -155,6 +154,22 @@ suite('TensorBoard session creation', async () => { assert.ok(filePickerStub.called, 'User requests to select another folder and file picker was not shown'); }); + test('When user selects remote URL, display input box', async () => { + sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.enterRemoteUrl() }); + const inputBoxStub = sandbox.stub(applicationShell, 'showInputBox'); + + // Create session + await commandManager.executeCommand( + 'python.launchTensorBoard', + TensorBoardEntrypoint.palette, + TensorBoardEntrypointTrigger.palette, + ); + + assert.ok( + inputBoxStub.called, + 'User requested to enter remote URL and input box to enter URL was not shown', + ); + }); }); suite('Installation prompt message', async () => { async function createSessionAndVerifyMessage(message: string) {