22// Copyright (c) Microsoft Corporation. All rights reserved.
33// Licensed under the MIT License.
44import * as assert from 'assert' ;
5- import { Uri } from 'vscode' ;
5+ import { Uri , CancellationTokenSource } from 'vscode' ;
66import * as typeMoq from 'typemoq' ;
77import * as path from 'path' ;
88import { Observable } from 'rxjs/Observable' ;
@@ -13,6 +13,7 @@ import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testContr
1313import {
1414 IPythonExecutionFactory ,
1515 IPythonExecutionService ,
16+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1617 SpawnOptions ,
1718 Output ,
1819} from '../../../../client/common/process/types' ;
@@ -31,11 +32,13 @@ suite('pytest test discovery adapter', () => {
3132 let outputChannel : typeMoq . IMock < ITestOutputChannel > ;
3233 let expectedPath : string ;
3334 let uri : Uri ;
35+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3436 let expectedExtraVariables : Record < string , string > ;
3537 let mockProc : MockChildProcess ;
3638 let deferred2 : Deferred < void > ;
3739 let utilsStartDiscoveryNamedPipeStub : sinon . SinonStub ;
3840 let useEnvExtensionStub : sinon . SinonStub ;
41+ let cancellationTokenSource : CancellationTokenSource ;
3942
4043 setup ( ( ) => {
4144 useEnvExtensionStub = sinon . stub ( extapi , 'useEnvExtension' ) ;
@@ -86,9 +89,12 @@ suite('pytest test discovery adapter', () => {
8689 } ,
8790 } ;
8891 } ) ;
92+
93+ cancellationTokenSource = new CancellationTokenSource ( ) ;
8994 } ) ;
9095 teardown ( ( ) => {
9196 sinon . restore ( ) ;
97+ cancellationTokenSource . dispose ( ) ;
9298 } ) ;
9399 test ( 'Discovery should call exec with correct basic args' , async ( ) => {
94100 // set up exec mock
@@ -333,4 +339,77 @@ suite('pytest test discovery adapter', () => {
333339 typeMoq . Times . once ( ) ,
334340 ) ;
335341 } ) ;
342+ test ( 'Test discovery canceled before exec observable call finishes' , async ( ) => {
343+ // set up exec mock
344+ execFactory = typeMoq . Mock . ofType < IPythonExecutionFactory > ( ) ;
345+ execFactory
346+ . setup ( ( x ) => x . createActivatedEnvironment ( typeMoq . It . isAny ( ) ) )
347+ . returns ( ( ) => Promise . resolve ( execService . object ) ) ;
348+
349+ sinon . stub ( fs . promises , 'lstat' ) . callsFake (
350+ async ( ) =>
351+ ( {
352+ isFile : ( ) => true ,
353+ isSymbolicLink : ( ) => false ,
354+ } as fs . Stats ) ,
355+ ) ;
356+ sinon . stub ( fs . promises , 'realpath' ) . callsFake ( async ( pathEntered ) => pathEntered . toString ( ) ) ;
357+
358+ adapter = new PytestTestDiscoveryAdapter ( configService , outputChannel . object ) ;
359+ const discoveryPromise = adapter . discoverTests ( uri , execFactory . object , cancellationTokenSource . token ) ;
360+
361+ // Trigger cancellation before exec observable call finishes
362+ cancellationTokenSource . cancel ( ) ;
363+
364+ await discoveryPromise ;
365+
366+ assert . ok (
367+ true ,
368+ 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.' ,
369+ ) ;
370+ } ) ;
371+
372+ test ( 'Test discovery cancelled while exec observable is running and proc is closed' , async ( ) => {
373+ //
374+ const execService2 = typeMoq . Mock . ofType < IPythonExecutionService > ( ) ;
375+ execService2 . setup ( ( p ) => ( ( p as unknown ) as any ) . then ) . returns ( ( ) => undefined ) ;
376+ execService2
377+ . setup ( ( x ) => x . execObservable ( typeMoq . It . isAny ( ) , typeMoq . It . isAny ( ) ) )
378+ . returns ( ( ) => {
379+ // Trigger cancellation while exec observable is running
380+ cancellationTokenSource . cancel ( ) ;
381+ return {
382+ proc : mockProc as any ,
383+ out : new Observable < Output < string > > ( ) ,
384+ dispose : ( ) => {
385+ /* no-body */
386+ } ,
387+ } ;
388+ } ) ;
389+ // set up exec mock
390+ deferred = createDeferred ( ) ;
391+ execFactory = typeMoq . Mock . ofType < IPythonExecutionFactory > ( ) ;
392+ execFactory
393+ . setup ( ( x ) => x . createActivatedEnvironment ( typeMoq . It . isAny ( ) ) )
394+ . returns ( ( ) => {
395+ deferred . resolve ( ) ;
396+ return Promise . resolve ( execService2 . object ) ;
397+ } ) ;
398+
399+ sinon . stub ( fs . promises , 'lstat' ) . callsFake (
400+ async ( ) =>
401+ ( {
402+ isFile : ( ) => true ,
403+ isSymbolicLink : ( ) => false ,
404+ } as fs . Stats ) ,
405+ ) ;
406+ sinon . stub ( fs . promises , 'realpath' ) . callsFake ( async ( pathEntered ) => pathEntered . toString ( ) ) ;
407+
408+ adapter = new PytestTestDiscoveryAdapter ( configService , outputChannel . object ) ;
409+ const discoveryPromise = adapter . discoverTests ( uri , execFactory . object , cancellationTokenSource . token ) ;
410+
411+ // add in await and trigger
412+ await discoveryPromise ;
413+ assert . ok ( true , 'Test resolves correctly when triggering a cancellation token in exec observable.' ) ;
414+ } ) ;
336415} ) ;
0 commit comments