11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License.
33import * as path from 'path' ;
4- import { Uri } from 'vscode' ;
4+ import { CancellationToken , CancellationTokenSource , Uri } from 'vscode' ;
55import * as fs from 'fs' ;
6+ import { ChildProcess } from 'child_process' ;
67import {
78 ExecutionFactoryCreateWithEnvironmentOptions ,
89 IPythonExecutionFactory ,
910 SpawnOptions ,
1011} from '../../../common/process/types' ;
1112import { IConfigurationService , ITestOutputChannel } from '../../../common/types' ;
12- import { Deferred } from '../../../common/utils/async' ;
13+ import { createDeferred , Deferred } from '../../../common/utils/async' ;
1314import { EXTENSION_ROOT_DIR } from '../../../constants' ;
1415import { traceError , traceInfo , traceVerbose , traceWarn } from '../../../logging' ;
1516import { DiscoveredTestPayload , ITestDiscoveryAdapter , ITestResultResolver } from '../common/types' ;
@@ -40,24 +41,39 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
4041 async discoverTests (
4142 uri : Uri ,
4243 executionFactory ?: IPythonExecutionFactory ,
44+ token ?: CancellationToken ,
4345 interpreter ?: PythonEnvironment ,
44- ) : Promise < DiscoveredTestPayload > {
45- const name = await startDiscoveryNamedPipe ( ( data : DiscoveredTestPayload ) => {
46- this . resultResolver ?. resolveDiscovery ( data ) ;
46+ ) : Promise < void > {
47+ const cSource = new CancellationTokenSource ( ) ;
48+ const deferredReturn = createDeferred < void > ( ) ;
49+
50+ token ?. onCancellationRequested ( ( ) => {
51+ traceInfo ( `Test discovery cancelled.` ) ;
52+ cSource . cancel ( ) ;
53+ deferredReturn . resolve ( ) ;
4754 } ) ;
4855
49- await this . runPytestDiscovery ( uri , name , executionFactory , interpreter ) ;
56+ const name = await startDiscoveryNamedPipe ( ( data : DiscoveredTestPayload ) => {
57+ // if the token is cancelled, we don't want process the data
58+ if ( ! token ?. isCancellationRequested ) {
59+ this . resultResolver ?. resolveDiscovery ( data ) ;
60+ }
61+ } , cSource . token ) ;
62+
63+ this . runPytestDiscovery ( uri , name , cSource , executionFactory , interpreter , token ) . then ( ( ) => {
64+ deferredReturn . resolve ( ) ;
65+ } ) ;
5066
51- // this is only a placeholder to handle function overloading until rewrite is finished
52- const discoveryPayload : DiscoveredTestPayload = { cwd : uri . fsPath , status : 'success' } ;
53- return discoveryPayload ;
67+ return deferredReturn . promise ;
5468 }
5569
5670 async runPytestDiscovery (
5771 uri : Uri ,
5872 discoveryPipeName : string ,
73+ cSource : CancellationTokenSource ,
5974 executionFactory ?: IPythonExecutionFactory ,
6075 interpreter ?: PythonEnvironment ,
76+ token ?: CancellationToken ,
6177 ) : Promise < void > {
6278 const relativePathToPytest = 'python_files' ;
6379 const fullPluginPath = path . join ( EXTENSION_ROOT_DIR , relativePathToPytest ) ;
@@ -111,6 +127,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
111127 args : execArgs ,
112128 env : ( mutableEnv as unknown ) as { [ key : string ] : string } ,
113129 } ) ;
130+ token ?. onCancellationRequested ( ( ) => {
131+ traceInfo ( `Test discovery cancelled, killing pytest subprocess for workspace ${ uri . fsPath } ` ) ;
132+ proc . kill ( ) ;
133+ deferredTillExecClose . resolve ( ) ;
134+ cSource . cancel ( ) ;
135+ } ) ;
114136 proc . stdout . on ( 'data' , ( data ) => {
115137 const out = fixLogLinesNoTrailing ( data . toString ( ) ) ;
116138 traceInfo ( out ) ;
@@ -143,6 +165,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
143165 throwOnStdErr : true ,
144166 outputChannel : this . outputChannel ,
145167 env : mutableEnv ,
168+ token,
146169 } ;
147170
148171 // Create the Python environment in which to execute the command.
@@ -154,7 +177,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
154177 const execService = await executionFactory ?. createActivatedEnvironment ( creationOptions ) ;
155178
156179 const deferredTillExecClose : Deferred < void > = createTestingDeferred ( ) ;
180+
181+ let resultProc : ChildProcess | undefined ;
182+
183+ token ?. onCancellationRequested ( ( ) => {
184+ traceInfo ( `Test discovery cancelled, killing pytest subprocess for workspace ${ uri . fsPath } ` ) ;
185+ // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here.
186+ if ( resultProc ) {
187+ resultProc ?. kill ( ) ;
188+ } else {
189+ deferredTillExecClose . resolve ( ) ;
190+ cSource . cancel ( ) ;
191+ }
192+ } ) ;
157193 const result = execService ?. execObservable ( execArgs , spawnOptions ) ;
194+ resultProc = result ?. proc ;
158195
159196 // Take all output from the subprocess and add it to the test output channel. This will be the pytest output.
160197 // Displays output to user and ensure the subprocess doesn't run into buffer overflow.
0 commit comments