diff --git a/build/ci/addEnvPath.py b/build/ci/addEnvPath.py new file mode 100644 index 000000000000..abad9ec3b5c9 --- /dev/null +++ b/build/ci/addEnvPath.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +#Adds the virtual environment's executable path to json file + +import json,sys +import os.path +jsonPath = sys.argv[1] +key = sys.argv[2] + +if os.path.isfile(jsonPath): + with open(jsonPath, 'r') as read_file: + data = json.load(read_file) +else: + directory = os.path.dirname(jsonPath) + if not os.path.exists(directory): + os.makedirs(directory) + with open(jsonPath, 'w+') as read_file: + data = {} + data = {} +with open(jsonPath, 'w') as outfile: + if key == 'condaExecPath': + data[key] = sys.argv[3] + else: + data[key] = sys.executable + json.dump(data, outfile, sort_keys=True, indent=4) diff --git a/build/ci/templates/virtual_env_tests.yml b/build/ci/templates/virtual_env_tests.yml new file mode 100644 index 000000000000..908287f04cbc --- /dev/null +++ b/build/ci/templates/virtual_env_tests.yml @@ -0,0 +1,191 @@ +jobs: +- job: ${{ parameters.name }} + dependsOn: 'PR_Validate_Windows_py37' + pool: + name: ${{ parameters.PoolName }} + + variables: + nodeVersion: ${{ parameters.NodeVersion }} + npmVersion: ${{ parameters.NpmVersion }} + pythonVersion: ${{ parameters.PythonVersion }} + platform: ${{ parameters.Platform }} + azureStorageAcctName: ${{ parameters.AzureStorageAccountName }} + azureStorageContainerName: ${{ parameters.AzureStorageContainerName }} + environmentExecutableFolder: ${{ parameters.EnvironmentExecutableFolder }} + PYTHON_VIRTUAL_ENVS_LOCATION: ${{ parameters.PYTHON_VIRTUAL_ENVS_LOCATION }} + TEST_FILES_SUFFIX: ${{ parameters.TEST_FILES_SUFFIX }} + TestSuiteName: ${{ parameters.TestSuiteName }} + + steps: + - bash: echo REQUESTED VARIABLE VALUES + + echo Node Version = $(nodeVersion) + + echo Python Version = $(pythonVersion) + + echo NPM Version = $(npmVersion) + + echo Mocha reportfile = '$(mochaReportFile)' + + echo MOCHA_CI_REPORTFILE = $MOCHA_CI_REPORTFILE + + echo MOCHA_CI_REPORTER_ID = $MOCHA_CI_REPORTER_ID + + echo MOCHA_REPORTER_JUNIT = $MOCHA_REPORTER_JUNIT + + echo COV_UUID = $COV_UUID + + echo Run Hygiene = $(runHygiene) + + displayName: 'Show build vars' + name: 'show_bld_vars' + + + - powershell: | + New-Item -ItemType directory -Path "$(System.ArtifactsDirectory)/bin-artifacts" + + $buildArtifactUri = "https://$(azureStorageAcctName).blob.core.windows.net/$(azureStorageContainerName)/$(Build.BuildNumber)/bin-artifacts.zip" + Write-Verbose "Downloading from $buildArtifactUri" + + $destination = "$(System.ArtifactsDirectory)/bin-artifacts/bin-artifacts.zip" + Write-Verbose "Destination file: $destination" + + Invoke-WebRequest -Uri $buildArtifactUri -OutFile $destination -Verbose + + displayName: 'Download bin-artifacts from cloud-storage' + + + - task: ExtractFiles@1 + displayName: 'Splat bin-artifacts' + inputs: + archiveFilePatterns: '$(System.ArtifactsDirectory)/bin-artifacts/bin-artifacts.zip' + + destinationFolder: '$(Build.SourcesDirectory)' + + cleanDestinationFolder: false + + + - task: NodeTool@0 + displayName: 'Use Node $(nodeVersion)' + inputs: + versionSpec: '$(nodeVersion)' + + + - task: UsePythonVersion@0 + displayName: 'Use Python $(pythonVersion)' + inputs: + versionSpec: '$(pythonVersion)' + + + - task: CmdLine@1 + displayName: 'pip install pipenv' + inputs: + filename: python + + arguments: '-m pip install pipenv' + + + - bash: | + pipenv run python ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) pipenvPath + + displayName: 'Create and save pipenv environment' + + + - task: CmdLine@1 + displayName: 'Create venv environment' + inputs: + filename: python + + arguments: '-m venv .venv' + + + - bash: | + .venv/$(environmentExecutableFolder)/python ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) venvPath + + displayName: 'Save venv environment executable' + + + - task: CmdLine@1 + displayName: 'pip install virtualenv' + inputs: + filename: python + + arguments: '-m pip install virtualenv' + + + - task: CmdLine@1 + displayName: 'Create virtualenv environment' + inputs: + filename: python + + arguments: '-m virtualenv .virtualenv' + + + - bash: | + .virtualenv/$(environmentExecutableFolder)/python ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) virtualEnvPath + + displayName: 'Save virtualenv environment executable' + + - powershell: | + Write-Host $Env:CONDA + Write-Host $Env:PYTHON_VIRTUAL_ENVS_LOCATION + + if( '$(platform)' -eq 'Windows' ){ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath conda + + } else{ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath $(environmentExecutableFolder) | Join-Path -ChildPath python + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath $(environmentExecutableFolder) | Join-Path -ChildPath conda + + } + + & $condaPythonPath ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) condaPath + + & $condaPythonPath ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) condaExecPath $condaExecPath + + Get-Content $Env:PYTHON_VIRTUAL_ENVS_LOCATION + + displayName: 'Save conda environment executable' + + + - task: Npm@1 + displayName: 'update npm' + inputs: + command: custom + + verbose: true + + customCommand: 'install -g npm@$(NpmVersion)' + + + - task: Npm@1 + displayName: 'npm ci' + inputs: + command: custom + + verbose: true + + customCommand: ci + + + - script: | + set -e + /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & + disown -ar + displayName: 'Start xvfb' + condition: and(succeeded(), eq(variables['Platform'], 'Linux')) + + + - task: Npm@1 + displayName: 'run $(TestSuiteName)' + inputs: + command: custom + + verbose: true + + customCommand: 'run $(TestSuiteName)' + env: + DISPLAY: :10 + + diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml index a0113b9c6a28..bc1b0a1dba0a 100644 --- a/build/ci/vscode-python-pr-validation.yaml +++ b/build/ci/vscode-python-pr-validation.yaml @@ -16,6 +16,54 @@ jobs: AzureStorageAccountName: 'vscodepythonci' AzureStorageContainerName: 'vscode-python-ci' + +- template: templates/virtual_env_tests.yml + parameters: + name: 'VirtualEnv_Tests_Windows_py37' + PythonVersion: '3.7' + NodeVersion: '8.11.2' + NpmVersion: 'latest' + AzureStorageAccountName: 'vscodepythonci' + AzureStorageContainerName: 'vscode-python-ci' + Platform: 'Windows' + PoolName: 'Hosted VS2017' + EnvironmentExecutableFolder: 'Scripts' + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + TEST_FILES_SUFFIX: 'testvirtualenvs' + TestSuiteName: 'testSingleWorkspace' + + +- template: templates/virtual_env_tests.yml + parameters: + name: 'VirtualEnv_Tests_Linux_py37' + PythonVersion: '3.7' + NodeVersion: '8.11.2' + NpmVersion: 'latest' + AzureStorageAccountName: 'vscodepythonci' + AzureStorageContainerName: 'vscode-python-ci' + Platform: 'Linux' + PoolName: 'Hosted Ubuntu 1604' + EnvironmentExecutableFolder: 'bin' + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + TEST_FILES_SUFFIX: 'testvirtualenvs' + TestSuiteName: 'testSingleWorkspace' + + +- template: templates/virtual_env_tests.yml + parameters: + name: 'VirtualEnv_Tests_MacOS_py37' + PythonVersion: '3.7' + NodeVersion: '8.11.2' + NpmVersion: 'latest' + AzureStorageAccountName: 'vscodepythonci' + AzureStorageContainerName: 'vscode-python-ci' + Platform: 'macOS' + PoolName: 'Hosted macOS' + EnvironmentExecutableFolder: 'bin' + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + TEST_FILES_SUFFIX: 'testvirtualenvs' + TestSuiteName: 'testSingleWorkspace' + - job: 'System_Test_macOS' dependsOn: 'PR_Validate_Windows_py37' pool: diff --git a/news/3 Code Health/1521.md b/news/3 Code Health/1521.md new file mode 100644 index 000000000000..7a3f68b59fe7 --- /dev/null +++ b/news/3 Code Health/1521.md @@ -0,0 +1 @@ +Created system test to ensure terminal gets activated with anaconda environment \ No newline at end of file diff --git a/news/3 Code Health/1522.md b/news/3 Code Health/1522.md new file mode 100644 index 000000000000..d0226766eef9 --- /dev/null +++ b/news/3 Code Health/1522.md @@ -0,0 +1 @@ +Added system tests to ensure terminal gets activated with virtualenv environment diff --git a/news/3 Code Health/1523.md b/news/3 Code Health/1523.md new file mode 100644 index 000000000000..ac9d6c029eba --- /dev/null +++ b/news/3 Code Health/1523.md @@ -0,0 +1 @@ +Added system test to ensure terminal gets activated with pipenv \ No newline at end of file diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index 41708be66388..f119ed80cf85 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -59,7 +59,6 @@ export class TerminalHelper implements ITerminalHelper { public getTerminalShellPath(): string { const workspace = this.serviceContainer.get(IWorkspaceService); const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); - const platformService = this.serviceContainer.get(IPlatformService); let osSection = ''; if (platformService.isWindows) { diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts index a1c10872a18b..e595cdf60d15 100644 --- a/src/client/interpreter/locators/services/condaService.ts +++ b/src/client/interpreter/locators/services/condaService.ts @@ -1,6 +1,9 @@ import { inject, injectable, named, optional } from 'inversify'; import * as path from 'path'; import { parse, SemVer } from 'semver'; + +import { ConfigurationChangeEvent, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; import { Logger } from '../../../common/logger'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; import { ExecutionResult, IProcessServiceFactory } from '../../../common/process/types'; @@ -23,7 +26,13 @@ const untildify: (value: string) => string = require('untildify'); // This glob pattern will match all of the following: // ~/anaconda/bin/conda, ~/anaconda3/bin/conda, ~/miniconda/bin/conda, ~/miniconda3/bin/conda -export const CondaLocationsGlob = untildify('~/*conda*/bin/conda'); +// /usr/share/anaconda/bin/conda, /usr/share/anaconda3/bin/conda, /usr/share/miniconda/bin/conda, /usr/share/miniconda3/bin/conda + +const condaGlobPathsForLinux = [ + '/usr/share/*conda*/bin/conda', + untildify('~/*conda*/bin/conda')]; + +export const CondaLocationsGlob = `{${condaGlobPathsForLinux.join(',')}}`; // ...and for windows, the known default install locations: const condaGlobPathsForWindows = [ @@ -64,12 +73,14 @@ export class CondaService implements ICondaService { @inject(IInterpreterService) private interpreterService: IInterpreterService, @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService ) { this.disposableRegistry.push(this.interpreterService.onDidChangeInterpreter(this.onInterpreterChanged.bind(this))); this.activationProvider = serviceContainer.get(ITerminalActivationCommandProvider, this.platform.isWindows ? 'commandPromptAndPowerShell' : 'bashCShellFish'); this.shellType = this.platform.isWindows ? TerminalShellType.commandPrompt : TerminalShellType.bash; // Defaults for Child_Process.exec + this.addCondaPathChangedHandler(); } public get condaEnvironmentsFile(): string | undefined { @@ -429,6 +440,18 @@ export class CondaService implements ICondaService { } } + private addCondaPathChangedHandler() { + const disposable = this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)); + this.disposableRegistry.push(disposable); + } + private async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; + if (workspacesUris.findIndex(uri => event.affectsConfiguration('python.condaPath', uri)) === -1) { + return; + } + this.condaFile = undefined; + } + /** * Return the path to the "conda file", if there is one (in known locations). */ diff --git a/src/test/ciConstants.ts b/src/test/ciConstants.ts index e1fc989e5fd8..1be1ccf6a339 100644 --- a/src/test/ciConstants.ts +++ b/src/test/ciConstants.ts @@ -6,7 +6,7 @@ // // Constants that pertain to CI processes/tests only. No dependencies on vscode! // - +export const PYTHON_VIRTUAL_ENVS_LOCATION = process.env.PYTHON_VIRTUAL_ENVS_LOCATION; export const IS_APPVEYOR = process.env.APPVEYOR === 'true'; export const IS_TRAVIS = process.env.TRAVIS === 'true'; export const IS_VSTS = process.env.TF_BUILD !== undefined; diff --git a/src/test/common.ts b/src/test/common.ts index 89119668d510..056c93e5ffa9 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -46,7 +46,7 @@ export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' | 'unitTest.nosetestArgs' | 'unitTest.pyTestArgs' | 'unitTest.unittestArgs' | 'formatting.provider' | 'sortImports.args' | 'unitTest.nosetestsEnabled' | 'unitTest.pyTestEnabled' | 'unitTest.unittestEnabled' | - 'envFile' | 'jediEnabled' | 'linting.ignorePatterns'; + 'envFile' | 'jediEnabled' | 'linting.ignorePatterns' | 'terminal.activateEnvironment'; async function disposePythonSettings() { if (!IS_SMOKE_TEST) { @@ -87,6 +87,11 @@ export async function setPythonPathInWorkspaceRoot(pythonPath: string) { return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, pythonPath); } +export async function restorePythonPathInWorkspaceRoot() { + const vscode = require('vscode') as typeof import('vscode'); + return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, PYTHON_PATH); +} + export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); function getWorkspaceRoot() { diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts new file mode 100644 index 000000000000..0afd5c99b564 --- /dev/null +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../ciConstants'; +import { PYTHON_PATH, restorePythonPathInWorkspaceRoot, setPythonPathInWorkspaceRoot, updateSetting, waitForCondition } from '../../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { sleep } from '../../../core'; +import { initialize, initializeTest } from '../../../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Activation of Environments in Terminal', () => { + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.py'); + const outputFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.log'); + const envsLocation = PYTHON_VIRTUAL_ENVS_LOCATION !== undefined ? + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); + const waitTimeForActivation = 5000; + type EnvPath = { + condaExecPath: string; + condaPath: string; + venvPath: string; + pipenvPath: string; + virtualEnvPath: string; + }; + let envPaths: EnvPath; + const defaultShell = { + Windows: '', + Linux: '', + MacOS: '' + }; + let terminalSettings; + let pythonSettings; + suiteSetup(async () => { + envPaths = await fs.readJson(envsLocation); + terminalSettings = vscode.workspace.getConfiguration('terminal', vscode.workspace.workspaceFolders[0].uri); + pythonSettings = vscode.workspace.getConfiguration('python', vscode.workspace.workspaceFolders[0].uri); + defaultShell.Windows = terminalSettings.inspect('integrated.shell.windows').globalValue; + defaultShell.Linux = terminalSettings.inspect('integrated.shell.linux').globalValue; + await terminalSettings.update('integrated.shell.linux', '/bin/bash', vscode.ConfigurationTarget.Global); + await initialize(); + }); + setup(async () => { + await initializeTest(); + await cleanUp(); + }); + teardown(cleanUp); + suiteTeardown(revertSettings); + async function revertSettings() { + await updateSetting('terminal.activateEnvironment', undefined, vscode.workspace.workspaceFolders[0].uri, vscode.ConfigurationTarget.WorkspaceFolder); + await terminalSettings.update('integrated.shell.windows', defaultShell.Windows, vscode.ConfigurationTarget.Global); + await terminalSettings.update('integrated.shell.linux', defaultShell.Linux, vscode.ConfigurationTarget.Global); + await pythonSettings.update('condaPath', undefined, vscode.ConfigurationTarget.Workspace); + await restorePythonPathInWorkspaceRoot(); + } + async function cleanUp() { + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + } + + async function testActivation(envPath) { + await updateSetting('terminal.activateEnvironment', true, vscode.workspace.workspaceFolders[0].uri, vscode.ConfigurationTarget.WorkspaceFolder); + await setPythonPathInWorkspaceRoot(envPath); + const terminal = vscode.window.createTerminal(); + await sleep(waitTimeForActivation); + terminal.sendText(`python ${file}`, true); + await waitForCondition(() => fs.pathExists(outputFile), 5_000, '\'testExecInTerminal.log\' file not created'); + const content = await fs.readFile(outputFile, 'utf-8'); + expect(content).to.equal(envPath); + } + async function testNonActivation() { + await updateSetting('terminal.activateEnvironment', false, vscode.workspace.workspaceFolders[0].uri, vscode.ConfigurationTarget.WorkspaceFolder); + const terminal = vscode.window.createTerminal(); + terminal.sendText(`python ${file}`, true); + await waitForCondition(() => fs.pathExists(outputFile), 5_000, '\'testExecInTerminal.log\' file not created'); + const content = await fs.readFile(outputFile, 'utf-8'); + expect(content).to.not.equal(PYTHON_PATH); + } + test('Should not activate', async () => { + await testNonActivation(); + }); + test('Should activate with venv', async () => { + await testActivation(envPaths.venvPath); + }); + test('Should activate with pipenv', async () => { + await testActivation(envPaths.pipenvPath); + }); + test('Should activate with virtualenv', async () => { + await testActivation(envPaths.virtualEnvPath); + }); + test('Should activate with conda', async () => { + await terminalSettings.update('integrated.shell.windows', 'C:\\Windows\\System32\\cmd.exe', vscode.ConfigurationTarget.Global); + await pythonSettings.update('condaPath', envPaths.condaExecPath, vscode.ConfigurationTarget.Workspace); + await testActivation(envPaths.condaPath); + }); +}); diff --git a/src/test/interpreters/condaService.unit.test.ts b/src/test/interpreters/condaService.unit.test.ts index 3a999336a961..9f05c2ded3ef 100644 --- a/src/test/interpreters/condaService.unit.test.ts +++ b/src/test/interpreters/condaService.unit.test.ts @@ -7,6 +7,7 @@ import { parse, SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { Disposable, EventEmitter } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; @@ -53,6 +54,7 @@ suite('Interpreters Conda Service', () => { let condaPathSetting: string; let disposableRegistry: Disposable[]; let interpreterService: TypeMoq.IMock; + let workspaceService : TypeMoq.IMock; let mockState: MockState; let terminalProvider: TypeMoq.IMock; setup(async () => { @@ -64,6 +66,7 @@ suite('Interpreters Conda Service', () => { interpreterService = TypeMoq.Mock.ofType(); registryInterpreterLocatorService = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); config = TypeMoq.Mock.ofType(); settings = TypeMoq.Mock.ofType(); procServiceFactory = TypeMoq.Mock.ofType(); @@ -104,6 +107,7 @@ suite('Interpreters Conda Service', () => { interpreterService.object, disposableRegistry, serviceContainer.object, + workspaceService.object, registryInterpreterLocatorService.object); }); @@ -394,7 +398,8 @@ suite('Interpreters Conda Service', () => { logger.object, interpreterService.object, disposableRegistry, - serviceContainer.object); + serviceContainer.object, + workspaceService.object); const result = await condaSrv.getCondaFile(); expect(result).is.equal(expected);