forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Added workspace virtualenv filesystem watcher #14780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
05e911a
Added workspace virtual env watcher
9f9dd6f
Fix tests
ef7a05d
Remove option
c9139ed
Mege the two tests together
1d83fb0
Skip
641c186
Try this out
377963b
Let's try this fix
0112d34
Clean up
ea2bfe8
Fix lint errors
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,6 @@ import { | |
| IDisposableLocator, IPythonEnvsIterator, PythonLocatorQuery | ||
| } from './base/locator'; | ||
| import { CachingLocator } from './base/locators/composite/cachingLocator'; | ||
| import { WorkspaceVirtualEnvironmentLocator } from './base/locators/lowLevel/workspaceVirtualEnvLocator'; | ||
| import { PythonEnvsChangedEvent } from './base/watcher'; | ||
| import { getGlobalPersistentStore, initializeExternalDependencies as initializeLegacyExternalDependencies } from './common/externalDependencies'; | ||
| import { ExtensionLocators, WorkspaceLocators } from './discovery/locators'; | ||
|
|
@@ -101,7 +100,6 @@ async function initLocators(): Promise<ExtensionLocators> { | |
|
|
||
| const workspaceLocators = new WorkspaceLocators([ | ||
| // Add an ILocator factory func here for each kind of workspace-rooted locator. | ||
| (root: vscode.Uri) => [new WorkspaceVirtualEnvironmentLocator(root.fsPath)], | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Connecting this locator to the external surface changes quite a few things, will do that in a separate PR. |
||
| ]); | ||
|
|
||
| // Any non-workspace locator activation goes here. | ||
|
|
||
187 changes: 187 additions & 0 deletions
187
...t/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| // eslint-disable-next-line max-classes-per-file | ||
| import { assert } from 'chai'; | ||
| import * as fs from 'fs-extra'; | ||
| import * as path from 'path'; | ||
| import { traceWarning } from '../../../../../client/common/logger'; | ||
| import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; | ||
| import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; | ||
| import { getOSType, OSType } from '../../../../../client/common/utils/platform'; | ||
| import { IDisposableLocator } from '../../../../../client/pythonEnvironments/base/locator'; | ||
| import { createWorkspaceVirtualEnvLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator'; | ||
| import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; | ||
| import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; | ||
| import { getInterpreterPathFromDir } from '../../../../../client/pythonEnvironments/common/commonUtils'; | ||
| import { arePathsSame } from '../../../../../client/pythonEnvironments/common/externalDependencies'; | ||
| import { deleteFiles, PYTHON_PATH } from '../../../../common'; | ||
| import { TEST_TIMEOUT } from '../../../../constants'; | ||
| import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; | ||
| import { run } from '../../../discovery/locators/envTestUtils'; | ||
|
|
||
| class WorkspaceVenvs { | ||
| constructor(private readonly root: string, private readonly prefix = '.virtual') { } | ||
|
|
||
| public async create(name: string): Promise<string> { | ||
| const envName = this.resolve(name); | ||
| const argv = [PYTHON_PATH.fileToCommandArgument(), '-m', 'virtualenv', envName]; | ||
| try { | ||
| await run(argv, { cwd: this.root }); | ||
| } catch (err) { | ||
| throw new Error(`Failed to create Env ${path.basename(envName)} Error: ${err}`); | ||
| } | ||
| const dirToLookInto = path.join(this.root, envName); | ||
| const filename = await getInterpreterPathFromDir(dirToLookInto); | ||
| if (!filename) { | ||
| throw new Error(`No environment to update exists in ${dirToLookInto}`); | ||
| } | ||
| return filename; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a dummy environment by creating a fake executable. | ||
| * @param name environment suffix name to create | ||
| */ | ||
| public async createDummyEnv(name: string): Promise<string> { | ||
| const envName = this.resolve(name); | ||
| const filepath = path.join(this.root, envName, getOSType() === OSType.Windows ? 'python.exe' : 'python'); | ||
| try { | ||
| await fs.createFile(filepath); | ||
| } catch (err) { | ||
| throw new Error(`Failed to create python executable ${filepath}, Error: ${err}`); | ||
| } | ||
| return filepath; | ||
| } | ||
|
|
||
| // eslint-disable-next-line class-methods-use-this | ||
| public async update(filename: string): Promise<void> { | ||
| try { | ||
| await fs.writeFile(filename, 'Environment has been updated'); | ||
| } catch (err) { | ||
| throw new Error(`Failed to update Workspace virtualenv executable ${filename}, Error: ${err}`); | ||
| } | ||
| } | ||
|
|
||
| // eslint-disable-next-line class-methods-use-this | ||
| public async delete(filename: string): Promise<void> { | ||
| try { | ||
| await fs.remove(filename); | ||
| } catch (err) { | ||
| traceWarning(`Failed to clean up ${filename}`); | ||
| } | ||
| } | ||
|
|
||
| public async cleanUp() { | ||
| const globPattern = path.join(this.root, `${this.prefix}*`); | ||
| await deleteFiles(globPattern); | ||
| } | ||
|
|
||
| private resolve(name: string): string { | ||
| // Ensure env is random to avoid conflicts in tests (corrupting test data) | ||
| const now = new Date().getTime().toString().substr(-8); | ||
| return `${this.prefix}${name}${now}`; | ||
| } | ||
| } | ||
|
|
||
| suite('WorkspaceVirtualEnvironment Locator', async () => { | ||
| const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); | ||
| const workspaceVenvs = new WorkspaceVenvs(testWorkspaceFolder); | ||
| let locator: IDisposableLocator; | ||
|
|
||
| async function waitForChangeToBeDetected(deferred: Deferred<void>) { | ||
| const timeout = setTimeout( | ||
| () => { | ||
| clearTimeout(timeout); | ||
| deferred.reject(new Error('Environment not detected')); | ||
| }, | ||
| TEST_TIMEOUT, | ||
| ); | ||
| await deferred.promise; | ||
| } | ||
|
|
||
| async function isLocated(executable: string): Promise<boolean> { | ||
| const items = await getEnvs(locator.iterEnvs()); | ||
| return items.some((item) => arePathsSame(item.executable.filename, executable)); | ||
| } | ||
|
|
||
| suiteSetup(async () => workspaceVenvs.cleanUp()); | ||
|
|
||
| async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { | ||
| locator = await createWorkspaceVirtualEnvLocator(testWorkspaceFolder); | ||
| // Wait for watchers to get ready | ||
| await sleep(1000); | ||
| locator.onChanged(onChanged); | ||
| } | ||
|
|
||
| teardown(async () => { | ||
| await workspaceVenvs.cleanUp(); | ||
| locator.dispose(); | ||
| }); | ||
|
|
||
| test('Detect a new environment', async () => { | ||
| let actualEvent: PythonEnvsChangedEvent; | ||
| const deferred = createDeferred<void>(); | ||
| await setupLocator(async (e) => { | ||
| actualEvent = e; | ||
| deferred.resolve(); | ||
| }); | ||
|
|
||
| const executable = await workspaceVenvs.create('one'); | ||
| await waitForChangeToBeDetected(deferred); | ||
| const isFound = await isLocated(executable); | ||
|
|
||
| assert.ok(isFound); | ||
| // Detecting kind of virtual env depends on the file structure around the executable, so we need to wait before | ||
| // attempting to verify it. Omitting that check as we can never deterministically say when it's ready to check. | ||
| assert.deepEqual(actualEvent!.type, FileChangeType.Created, 'Wrong event emitted'); | ||
| }); | ||
|
|
||
| test('Detect when an environment has been deleted', async () => { | ||
| let actualEvent: PythonEnvsChangedEvent; | ||
| const deferred = createDeferred<void>(); | ||
| const executable = await workspaceVenvs.create('one'); | ||
| // Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. | ||
| await sleep(100); | ||
| await setupLocator(async (e) => { | ||
| actualEvent = e; | ||
| deferred.resolve(); | ||
| }); | ||
|
|
||
| // VSCode API has a limitation where it fails to fire event when environment folder is deleted directly: | ||
| // https://github.com/microsoft/vscode/issues/110923 | ||
| // Using chokidar directly in tests work, but it has permission issues on Windows that you cannot delete a | ||
| // folder if it has a subfolder that is being watched inside: https://github.com/paulmillr/chokidar/issues/422 | ||
| // Hence we test directly deleting the executable, and not the whole folder using `workspaceVenvs.cleanUp()`. | ||
| await workspaceVenvs.delete(executable); | ||
| await waitForChangeToBeDetected(deferred); | ||
| const isFound = await isLocated(executable); | ||
|
|
||
| assert.notOk(isFound); | ||
| assert.deepEqual(actualEvent!.type, FileChangeType.Deleted, 'Wrong event emitted'); | ||
| }); | ||
|
|
||
| test('Detect when an environment has been updated', async () => { | ||
| let actualEvent: PythonEnvsChangedEvent; | ||
| const deferred = createDeferred<void>(); | ||
| // Create a dummy environment so we can update its executable later. We can't choose a real environment here. | ||
| // Executables inside real environments can be symlinks, so writing on them can result in the real executable | ||
| // being updated instead of the symlink. | ||
| const executable = await workspaceVenvs.createDummyEnv('one'); | ||
| // Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. | ||
| await sleep(100); | ||
| await setupLocator(async (e) => { | ||
| actualEvent = e; | ||
| deferred.resolve(); | ||
| }); | ||
|
|
||
| await workspaceVenvs.update(executable); | ||
| await waitForChangeToBeDetected(deferred); | ||
| const isFound = await isLocated(executable); | ||
|
|
||
| assert.ok(isFound); | ||
| // Detecting kind of virtual env depends on the file structure around the executable, so we need to wait before | ||
| // attempting to verify it. Omitting that check as we can never deterministically say when it's ready to check. | ||
| assert.deepEqual(actualEvent!.type, FileChangeType.Changed, 'Wrong event emitted'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.