@@ -5,7 +5,7 @@ import { CancellationToken, Progress, ProgressOptions, Uri } from 'vscode';
55import { PythonEnvironmentApi , PythonProject } from '../../../api' ;
66import * as winapi from '../../../common/window.apis' ;
77import * as wapi from '../../../common/workspace.apis' ;
8- import { getProjectInstallable } from '../../../managers/builtin/pipUtils' ;
8+ import { getProjectInstallable , hasProjectDependencies } from '../../../managers/builtin/pipUtils' ;
99
1010suite ( 'Pip Utils - getProjectInstallable' , ( ) => {
1111 let findFilesStub : sinon . SinonStub ;
@@ -216,13 +216,15 @@ suite('Pip Utils - getProjectInstallable', () => {
216216 } ) ;
217217
218218 test ( 'should handle cancellation during file search' , async ( ) => {
219- // Arrange: Create a cancellation token that is already cancelled
219+ // ARRANGE: Simulate a scenario where the user cancels the operation
220+ // Step 1: Create a pre-cancelled token to simulate user clicking "Cancel" button
220221 const cancelledToken : CancellationToken = {
221222 isCancellationRequested : true ,
222223 onCancellationRequested : ( ) => ( { dispose : ( ) => { } } ) ,
223224 } ;
224225
225- // Mock withProgress to immediately call the callback with the cancelled token
226+ // Step 2: Override withProgress stub to pass the cancelled token to the search callback
227+ // This simulates the progress dialog being cancelled and the token being propagated
226228 withProgressStub . callsFake (
227229 async (
228230 _options : ProgressOptions ,
@@ -231,24 +233,149 @@ suite('Pip Utils - getProjectInstallable', () => {
231233 token : CancellationToken ,
232234 ) => Thenable < unknown > ,
233235 ) => {
236+ // Execute the callback with the cancelled token (simulating cancellation during the operation)
234237 return await callback ( { } as Progress < { message ?: string ; increment ?: number } > , cancelledToken ) ;
235238 } ,
236239 ) ;
237240
238- // Mock findFiles to check that token is passed
241+ // Step 3: Mock findFiles to verify the cancelled token is properly passed through
242+ // This ensures cancellation propagates from withProgress -> getProjectInstallable -> findFiles
239243 findFilesStub . callsFake ( ( _pattern : string , _exclude : string , _maxResults : number , token : CancellationToken ) => {
240- // Verify the cancellation token is passed to findFiles
244+ // VERIFY: The same cancellation token should be passed to each findFiles call
241245 assert . strictEqual ( token , cancelledToken , 'Cancellation token should be passed to findFiles' ) ;
242246 return Promise . resolve ( [ ] ) ;
243247 } ) ;
244248
245- // Act : Call getProjectInstallable
249+ // ACT : Call the function under test
246250 const workspacePath = Uri . file ( '/test/path/root' ) . fsPath ;
247251 const projects = [ { name : 'workspace' , uri : Uri . file ( workspacePath ) } ] ;
248252 await getProjectInstallable ( mockApi as PythonEnvironmentApi , projects ) ;
249253
250- // Assert: Verify findFiles was called with the cancellation token
254+ // ASSERT: Verify the cancellation token was passed to all file search operations
255+ // Even though cancelled, the function should attempt all searches (they'll just return empty quickly)
251256 assert . ok ( findFilesStub . called , 'findFiles should be called' ) ;
257+ // getProjectInstallable searches for dependencies using 4 different file patterns
252258 assert . strictEqual ( findFilesStub . callCount , 4 , 'Should call findFiles 4 times for different patterns' ) ;
253259 } ) ;
254260} ) ;
261+
262+ suite ( 'Pip Utils - hasProjectDependencies' , ( ) => {
263+ let findFilesStub : sinon . SinonStub ;
264+
265+ setup ( ( ) => {
266+ findFilesStub = sinon . stub ( wapi , 'findFiles' ) ;
267+ } ) ;
268+
269+ teardown ( ( ) => {
270+ sinon . restore ( ) ;
271+ } ) ;
272+
273+ test ( 'should return true when requirements.txt exists' , async ( ) => {
274+ // Arrange: Mock findFiles to return a requirements file
275+ findFilesStub . callsFake ( ( pattern : string , _exclude : string , maxResults ?: number ) => {
276+ // Verify maxResults=1 is used for performance (quick check)
277+ assert . strictEqual ( maxResults , 1 , 'Should use maxResults=1 for quick check' ) ;
278+
279+ if ( pattern === '*requirements*.txt' ) {
280+ return Promise . resolve ( [ Uri . file ( '/test/path/root/requirements.txt' ) ] ) ;
281+ }
282+ return Promise . resolve ( [ ] ) ;
283+ } ) ;
284+
285+ // Act
286+ const projects = [ { name : 'workspace' , uri : Uri . file ( '/test/path/root' ) } ] ;
287+ const result = await hasProjectDependencies ( projects ) ;
288+
289+ // Assert
290+ assert . strictEqual ( result , true , 'Should return true when requirements files exist' ) ;
291+ } ) ;
292+
293+ test ( 'should return true when pyproject.toml exists' , async ( ) => {
294+ // Arrange: Mock findFiles to return pyproject.toml
295+ findFilesStub . callsFake ( ( pattern : string , _exclude : string , maxResults ?: number ) => {
296+ assert . strictEqual ( maxResults , 1 , 'Should use maxResults=1 for quick check' ) ;
297+
298+ if ( pattern === '**/pyproject.toml' ) {
299+ return Promise . resolve ( [ Uri . file ( '/test/path/root/pyproject.toml' ) ] ) ;
300+ }
301+ return Promise . resolve ( [ ] ) ;
302+ } ) ;
303+
304+ // Act
305+ const projects = [ { name : 'workspace' , uri : Uri . file ( '/test/path/root' ) } ] ;
306+ const result = await hasProjectDependencies ( projects ) ;
307+
308+ // Assert
309+ assert . strictEqual ( result , true , 'Should return true when pyproject.toml exists' ) ;
310+ } ) ;
311+
312+ test ( 'should return false when no dependency files exist' , async ( ) => {
313+ // Arrange: Mock findFiles to return empty arrays
314+ findFilesStub . resolves ( [ ] ) ;
315+
316+ // Act
317+ const projects = [ { name : 'workspace' , uri : Uri . file ( '/test/path/root' ) } ] ;
318+ const result = await hasProjectDependencies ( projects ) ;
319+
320+ // Assert
321+ assert . strictEqual ( result , false , 'Should return false when no dependency files exist' ) ;
322+ // Verify all 4 patterns were checked
323+ assert . strictEqual ( findFilesStub . callCount , 4 , 'Should check all 4 file patterns' ) ;
324+ } ) ;
325+
326+ test ( 'should return false when no projects provided' , async ( ) => {
327+ // Act
328+ const result = await hasProjectDependencies ( undefined ) ;
329+
330+ // Assert
331+ assert . strictEqual ( result , false , 'Should return false when no projects provided' ) ;
332+ assert . ok ( ! findFilesStub . called , 'Should not call findFiles when no projects' ) ;
333+ } ) ;
334+
335+ test ( 'should return false when empty projects array provided' , async ( ) => {
336+ // Act
337+ const result = await hasProjectDependencies ( [ ] ) ;
338+
339+ // Assert
340+ assert . strictEqual ( result , false , 'Should return false when empty projects array' ) ;
341+ assert . ok ( ! findFilesStub . called , 'Should not call findFiles when projects array is empty' ) ;
342+ } ) ;
343+
344+ test ( 'should use maxResults=1 for all patterns for performance' , async ( ) => {
345+ // Arrange: Track all maxResults values
346+ const maxResultsUsed : ( number | undefined ) [ ] = [ ] ;
347+ findFilesStub . callsFake ( ( _pattern : string , _exclude : string , maxResults ?: number ) => {
348+ maxResultsUsed . push ( maxResults ) ;
349+ return Promise . resolve ( [ ] ) ;
350+ } ) ;
351+
352+ // Act
353+ const projects = [ { name : 'workspace' , uri : Uri . file ( '/test/path/root' ) } ] ;
354+ await hasProjectDependencies ( projects ) ;
355+
356+ // Assert: All calls should use maxResults=1 for performance
357+ assert . strictEqual ( maxResultsUsed . length , 4 , 'Should make 4 findFiles calls' ) ;
358+ maxResultsUsed . forEach ( ( value , index ) => {
359+ assert . strictEqual ( value , 1 , `Call ${ index + 1 } should use maxResults=1` ) ;
360+ } ) ;
361+ } ) ;
362+
363+ test ( 'should short-circuit when first pattern finds a file' , async ( ) => {
364+ // Arrange: First pattern returns a result
365+ findFilesStub . callsFake ( ( pattern : string ) => {
366+ if ( pattern === '**/*requirements*.txt' ) {
367+ return Promise . resolve ( [ Uri . file ( '/test/path/root/dev-requirements.txt' ) ] ) ;
368+ }
369+ return Promise . resolve ( [ ] ) ;
370+ } ) ;
371+
372+ // Act
373+ const projects = [ { name : 'workspace' , uri : Uri . file ( '/test/path/root' ) } ] ;
374+ const result = await hasProjectDependencies ( projects ) ;
375+
376+ // Assert: Should still return true even though only first pattern matched
377+ assert . strictEqual ( result , true , 'Should return true when any pattern finds files' ) ;
378+ // Note: All 4 patterns are checked in parallel with Promise.all, so all 4 calls happen
379+ assert . strictEqual ( findFilesStub . callCount , 4 , 'All patterns checked in parallel' ) ;
380+ } ) ;
381+ } ) ;
0 commit comments