1
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
2
// Licensed under the MIT License.
3
3
import * as path from 'path' ;
4
- import { Uri } from 'vscode' ;
4
+ import { CancellationToken , CancellationTokenSource , Uri } from 'vscode' ;
5
5
import * as fs from 'fs' ;
6
+ import { ChildProcess } from 'child_process' ;
6
7
import {
7
8
ExecutionFactoryCreateWithEnvironmentOptions ,
8
9
IPythonExecutionFactory ,
9
10
SpawnOptions ,
10
11
} from '../../../common/process/types' ;
11
12
import { IConfigurationService , ITestOutputChannel } from '../../../common/types' ;
12
- import { Deferred } from '../../../common/utils/async' ;
13
+ import { createDeferred , Deferred } from '../../../common/utils/async' ;
13
14
import { EXTENSION_ROOT_DIR } from '../../../constants' ;
14
15
import { traceError , traceInfo , traceVerbose , traceWarn } from '../../../logging' ;
15
16
import { DiscoveredTestPayload , ITestDiscoveryAdapter , ITestResultResolver } from '../common/types' ;
@@ -40,24 +41,39 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
40
41
async discoverTests (
41
42
uri : Uri ,
42
43
executionFactory ?: IPythonExecutionFactory ,
44
+ token ?: CancellationToken ,
43
45
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 ( ) ;
47
54
} ) ;
48
55
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
+ } ) ;
50
66
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 ;
54
68
}
55
69
56
70
async runPytestDiscovery (
57
71
uri : Uri ,
58
72
discoveryPipeName : string ,
73
+ cSource : CancellationTokenSource ,
59
74
executionFactory ?: IPythonExecutionFactory ,
60
75
interpreter ?: PythonEnvironment ,
76
+ token ?: CancellationToken ,
61
77
) : Promise < void > {
62
78
const relativePathToPytest = 'python_files' ;
63
79
const fullPluginPath = path . join ( EXTENSION_ROOT_DIR , relativePathToPytest ) ;
@@ -111,6 +127,12 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
111
127
args : execArgs ,
112
128
env : ( mutableEnv as unknown ) as { [ key : string ] : string } ,
113
129
} ) ;
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
+ } ) ;
114
136
proc . stdout . on ( 'data' , ( data ) => {
115
137
const out = fixLogLinesNoTrailing ( data . toString ( ) ) ;
116
138
traceInfo ( out ) ;
@@ -143,6 +165,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
143
165
throwOnStdErr : true ,
144
166
outputChannel : this . outputChannel ,
145
167
env : mutableEnv ,
168
+ token,
146
169
} ;
147
170
148
171
// Create the Python environment in which to execute the command.
@@ -154,7 +177,21 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
154
177
const execService = await executionFactory ?. createActivatedEnvironment ( creationOptions ) ;
155
178
156
179
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
+ } ) ;
157
193
const result = execService ?. execObservable ( execArgs , spawnOptions ) ;
194
+ resultProc = result ?. proc ;
158
195
159
196
// Take all output from the subprocess and add it to the test output channel. This will be the pytest output.
160
197
// Displays output to user and ensure the subprocess doesn't run into buffer overflow.
0 commit comments