22// Licensed under the MIT License.
33
44import * as assert from 'assert' ;
5+ import * as path from 'path' ;
56import * as sinon from 'sinon' ;
67import { Disposable , EventEmitter , LogOutputChannel , Uri } from 'vscode' ;
7- import { PythonEnvironmentApi } from '../../../api' ;
8+ import { PythonEnvironment , PythonEnvironmentApi } from '../../../api' ;
89import * as workspaceApis from '../../../common/workspace.apis' ;
910import * as extensionApi from '../../../extensionApi' ;
1011import { registerSystemPythonFeatures } from '../../../managers/builtin/main' ;
@@ -61,7 +62,9 @@ suite('registerSystemPythonFeatures - activation watcher', () => {
6162
6263 test ( 'activation watcher is created with change events enabled and triggers refresh on change' , async ( ) => {
6364 const mockWatcher = createMockWatcher ( ) ;
64- createFileSystemWatcherStub . returns ( mockWatcher ) ;
65+ const deletionWatcher = createMockWatcher ( ) ;
66+ createFileSystemWatcherStub . withArgs ( '{**/activate}' ) . returns ( mockWatcher ) ;
67+ createFileSystemWatcherStub . withArgs ( '**/*' ) . returns ( deletionWatcher ) ;
6568
6669 const disposables : Disposable [ ] = [ ] ;
6770 const nativeFinder = { } as NativePythonFinder ;
@@ -76,7 +79,7 @@ suite('registerSystemPythonFeatures - activation watcher', () => {
7679 await registerSystemPythonFeatures ( nativeFinder , disposables , log , envManager ) ;
7780
7881 // Assertion A: Watcher is created with [false, false, false]
79- assert . strictEqual ( createFileSystemWatcherStub . callCount , 1 ) ;
82+ assert . strictEqual ( createFileSystemWatcherStub . callCount , 2 ) ;
8083 assert . deepStrictEqual ( createFileSystemWatcherStub . firstCall . args , [ '{**/activate}' , false , false , false ] ) ;
8184
8285 // Assertion B & C: CHANGE event triggers debounced watcherRefresh
@@ -112,4 +115,83 @@ suite('registerSystemPythonFeatures - activation watcher', () => {
112115 mockWatcher . _deleteEmitter . dispose ( ) ;
113116 disposables . forEach ( ( d ) => d . dispose ( ) ) ;
114117 } ) ;
118+
119+ test ( 'deleting a folder that holds a known venv triggers refresh' , async ( ) => {
120+ const deletionWatcher = createMockWatcher ( ) ;
121+ createFileSystemWatcherStub . withArgs ( '{**/activate}' ) . returns ( createMockWatcher ( ) ) ;
122+ createFileSystemWatcherStub . withArgs ( '**/*' ) . returns ( deletionWatcher ) ;
123+ const venvFolder = Uri . file ( path . resolve ( 'workspace' , '.venv' ) ) ;
124+ sandbox
125+ . stub ( VenvManager . prototype , 'hasEnvironmentAt' )
126+ . callsFake ( ( fsPath : string ) => fsPath === venvFolder . fsPath ) ;
127+
128+ const disposables : Disposable [ ] = [ ] ;
129+ await registerSystemPythonFeatures (
130+ { } as NativePythonFinder ,
131+ disposables ,
132+ { } as LogOutputChannel ,
133+ { } as SysPythonManager ,
134+ ) ;
135+
136+ // Only deletions are watched: the folder is reported, not the files in it.
137+ assert . ok ( createFileSystemWatcherStub . calledWithExactly ( '**/*' , true , true , false ) ) ;
138+
139+ deletionWatcher . _deleteEmitter . fire ( Uri . file ( path . resolve ( 'workspace' , 'notes.txt' ) ) ) ;
140+ await clock . tickAsync ( 600 ) ;
141+ assert . strictEqual ( watcherRefreshStub . callCount , 0 ) ;
142+
143+ deletionWatcher . _deleteEmitter . fire ( venvFolder ) ;
144+ await clock . tickAsync ( 600 ) ;
145+ assert . strictEqual ( watcherRefreshStub . callCount , 1 ) ;
146+
147+ disposables . forEach ( ( d ) => d . dispose ( ) ) ;
148+ } ) ;
149+ } ) ;
150+
151+ suite ( 'VenvManager.hasEnvironmentAt' , ( ) => {
152+ const project = path . resolve ( 'venv-delete-project' ) ;
153+ const venvPrefix = path . join ( project , '.venv' ) ;
154+
155+ function createManager ( ) : VenvManager {
156+ const manager = new VenvManager (
157+ { } as NativePythonFinder ,
158+ { } as PythonEnvironmentApi ,
159+ { } as SysPythonManager ,
160+ { } as LogOutputChannel ,
161+ ) ;
162+ ( manager as unknown as { collection : PythonEnvironment [ ] } ) . collection = [
163+ { sysPrefix : venvPrefix } as PythonEnvironment ,
164+ ] ;
165+ return manager ;
166+ }
167+
168+ test ( 'is true for the venv folder and its parent folders' , ( ) => {
169+ const manager = createManager ( ) ;
170+
171+ assert . strictEqual ( manager . hasEnvironmentAt ( venvPrefix ) , true ) ;
172+ assert . strictEqual ( manager . hasEnvironmentAt ( project ) , true ) ;
173+ } ) ;
174+
175+ test ( 'is false for unrelated paths and paths inside the venv' , ( ) => {
176+ const manager = createManager ( ) ;
177+
178+ assert . strictEqual ( manager . hasEnvironmentAt ( path . join ( project , 'src' ) ) , false ) ;
179+ assert . strictEqual ( manager . hasEnvironmentAt ( path . join ( venvPrefix , 'Lib' , 'site-packages' , 'pkg' ) ) , false ) ;
180+ assert . strictEqual ( manager . hasEnvironmentAt ( path . resolve ( 'venv-delete-project-other' ) ) , false ) ;
181+ assert . strictEqual ( manager . hasEnvironmentAt ( `${ venvPrefix } 2` ) , false ) ;
182+ } ) ;
183+
184+ test ( 'follows changes to the known environments' , ( ) => {
185+ const manager = createManager ( ) ;
186+ const otherPrefix = path . resolve ( 'venv-delete-other' , '.venv' ) ;
187+ assert . strictEqual ( manager . hasEnvironmentAt ( otherPrefix ) , false ) ;
188+
189+ const state = manager as unknown as { collection : PythonEnvironment [ ] } ;
190+ state . collection . push ( { sysPrefix : otherPrefix } as PythonEnvironment ) ;
191+ assert . strictEqual ( manager . hasEnvironmentAt ( otherPrefix ) , true ) ;
192+
193+ state . collection = [ ] ;
194+ assert . strictEqual ( manager . hasEnvironmentAt ( venvPrefix ) , false ) ;
195+ assert . strictEqual ( manager . hasEnvironmentAt ( otherPrefix ) , false ) ;
196+ } ) ;
115197} ) ;
0 commit comments