1
1
/* eslint-disable @typescript-eslint/no-explicit-any */
2
2
// Copyright (c) Microsoft Corporation. All rights reserved.
3
3
// Licensed under the MIT License.
4
- import { TestController , TestRun , Uri } from 'vscode' ;
4
+ import { CancellationTokenSource , DebugSession , TestController , TestRun , Uri , debug } from 'vscode' ;
5
5
import * as typeMoq from 'typemoq' ;
6
6
import * as path from 'path' ;
7
7
import * as assert from 'assert' ;
8
8
import * as fs from 'fs' ;
9
+ import * as sinon from 'sinon' ;
10
+ import { Observable } from 'rxjs' ;
9
11
import * as os from 'os' ;
10
12
import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter' ;
11
13
import { ITestController , ITestResultResolver } from '../../../client/testing/testController/common/types' ;
12
- import { IPythonExecutionFactory } from '../../../client/common/process/types' ;
14
+ import { IPythonExecutionFactory , IPythonExecutionService , Output } from '../../../client/common/process/types' ;
13
15
import { IConfigurationService , ITestOutputChannel } from '../../../client/common/types' ;
14
16
import { IServiceContainer } from '../../../client/ioc/types' ;
15
17
import { EXTENSION_ROOT_DIR_FOR_TESTS , initialize } from '../../initialize' ;
@@ -21,6 +23,9 @@ import { PythonResultResolver } from '../../../client/testing/testController/com
21
23
import { TestProvider } from '../../../client/testing/types' ;
22
24
import { PYTEST_PROVIDER , UNITTEST_PROVIDER } from '../../../client/testing/common/constants' ;
23
25
import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types' ;
26
+ import { ITestDebugLauncher } from '../../../client/testing/common/types' ;
27
+ import { MockChildProcess } from '../../mocks/mockChildProcess' ;
28
+ import { createDeferred } from '../../../client/common/utils/async' ;
24
29
25
30
suite ( 'End to End Tests: test adapters' , ( ) => {
26
31
let resultResolver : ITestResultResolver ;
@@ -150,6 +155,9 @@ suite('End to End Tests: test adapters', () => {
150
155
traceLog ( 'Symlink was not found to remove after tests, exiting successfully, nestedSymlink.' ) ;
151
156
}
152
157
} ) ;
158
+ teardown ( async ( ) => {
159
+ sinon . restore ( ) ;
160
+ } ) ;
153
161
test ( 'unittest discovery adapter small workspace' , async ( ) => {
154
162
// result resolver and saved data for assertions
155
163
let actualData : {
@@ -1073,4 +1081,196 @@ suite('End to End Tests: test adapters', () => {
1073
1081
assert . strictEqual ( failureOccurred , false , failureMsg ) ;
1074
1082
} ) ;
1075
1083
} ) ;
1084
+ test ( 'Pytest debug cancelation' , async ( ) => {
1085
+ const debugLauncher = serviceContainer . get < ITestDebugLauncher > ( ITestDebugLauncher ) ;
1086
+ const stopDebuggingStub = sinon . stub ( debug , 'stopDebugging' ) ;
1087
+ let calledStopDebugging = false ;
1088
+ stopDebuggingStub . callsFake ( ( ) => {
1089
+ calledStopDebugging = true ;
1090
+ return Promise . resolve ( ) ;
1091
+ } ) ;
1092
+
1093
+ // // mock exec service and exec factory, not very necessary for this test
1094
+ const execServiceStub = typeMoq . Mock . ofType < IPythonExecutionService > ( ) ;
1095
+ const execFactoryStub = typeMoq . Mock . ofType < IPythonExecutionFactory > ( ) ;
1096
+ const cancellationTokenSource = new CancellationTokenSource ( ) ;
1097
+ let mockProc : MockChildProcess ;
1098
+ execServiceStub
1099
+ . setup ( ( x ) => x . execObservable ( typeMoq . It . isAny ( ) , typeMoq . It . isAny ( ) ) )
1100
+ . returns ( ( ) => ( {
1101
+ proc : mockProc ,
1102
+ out : typeMoq . Mock . ofType < Observable < Output < string > > > ( ) . object ,
1103
+ dispose : ( ) => {
1104
+ /* no-body */
1105
+ } ,
1106
+ } ) ) ;
1107
+ execFactoryStub
1108
+ . setup ( ( x ) => x . createActivatedEnvironment ( typeMoq . It . isAny ( ) ) )
1109
+ . returns ( ( ) => Promise . resolve ( execServiceStub . object ) ) ;
1110
+ execFactoryStub . setup ( ( p ) => ( ( p as unknown ) as any ) . then ) . returns ( ( ) => undefined ) ;
1111
+ execServiceStub . setup ( ( p ) => ( ( p as unknown ) as any ) . then ) . returns ( ( ) => undefined ) ;
1112
+
1113
+ resultResolver = new PythonResultResolver ( testController , pytestProvider , workspaceUri ) ;
1114
+
1115
+ const testId = `${ rootPathErrorWorkspace } /test_seg_fault.py::TestSegmentationFault::test_segfault` ;
1116
+ const testIds : string [ ] = [ testId ] ;
1117
+
1118
+ // set workspace to test workspace folder
1119
+ workspaceUri = Uri . parse ( rootPathErrorWorkspace ) ;
1120
+ configService . getSettings ( workspaceUri ) . testing . pytestArgs = [ ] ;
1121
+
1122
+ const debugSessionStub = typeMoq . Mock . ofType < DebugSession > ( ) ;
1123
+ sinon . stub ( debug , 'onDidStartDebugSession' ) . callsFake ( ( cb ) => {
1124
+ // run the callback right away to add the cancelation token listener
1125
+ cb ( debugSessionStub . object ) ;
1126
+ return {
1127
+ dispose : ( ) => {
1128
+ /* no-body */
1129
+ } ,
1130
+ } ;
1131
+ } ) ;
1132
+ const awaitStopDebugging = createDeferred ( ) ;
1133
+
1134
+ sinon . stub ( debug , 'onDidTerminateDebugSession' ) . callsFake ( ( cb ) => {
1135
+ // wait for the stop debugging to be called before resolving the promise
1136
+ // the terminate debug session does cleanup
1137
+ awaitStopDebugging . promise . then ( ( ) => {
1138
+ cb ( debugSessionStub . object ) ;
1139
+ } ) ;
1140
+ return {
1141
+ dispose : ( ) => {
1142
+ // void
1143
+ } ,
1144
+ } ;
1145
+ } ) ;
1146
+ // handle cancelation token from debugger
1147
+ sinon . stub ( debug , 'startDebugging' ) . callsFake ( ( folder , nameOrConfiguration , _parentSession ) => {
1148
+ // check to make sure start debugging is called correctly
1149
+ if ( typeof nameOrConfiguration !== 'string' ) {
1150
+ assert . strictEqual ( nameOrConfiguration . type , 'debugpy' , 'Expected debugpy' ) ;
1151
+ } else {
1152
+ assert . fail ( 'Expected nameOrConfiguration to be an object' ) ;
1153
+ }
1154
+ assert . ok ( folder , 'Expected folder to be defined' ) ;
1155
+ assert . strictEqual ( folder . name , 'test' , 'Expected folder name to be test' ) ;
1156
+ // cancel the token and trigger the stop debugging callback
1157
+ awaitStopDebugging . resolve ( ) ;
1158
+ cancellationTokenSource . cancel ( ) ;
1159
+ return Promise . resolve ( true ) ;
1160
+ } ) ;
1161
+
1162
+ // run pytest execution
1163
+ const executionAdapter = new PytestTestExecutionAdapter (
1164
+ configService ,
1165
+ testOutputChannel . object ,
1166
+ resultResolver ,
1167
+ envVarsService ,
1168
+ ) ;
1169
+
1170
+ const testRun = typeMoq . Mock . ofType < TestRun > ( ) ;
1171
+ testRun . setup ( ( t ) => t . token ) . returns ( ( ) => cancellationTokenSource . token ) ;
1172
+
1173
+ await executionAdapter
1174
+ . runTests ( workspaceUri , testIds , true , testRun . object , pythonExecFactory , debugLauncher )
1175
+ . finally ( ( ) => {
1176
+ // verify that the stop debugging was called
1177
+ assert . ok ( calledStopDebugging , 'Expected stopDebugging to be called' ) ;
1178
+ } ) ;
1179
+ } ) ;
1180
+ test ( 'UNITTEST debug cancelation' , async ( ) => {
1181
+ const debugLauncher = serviceContainer . get < ITestDebugLauncher > ( ITestDebugLauncher ) ;
1182
+ const stopDebuggingStub = sinon . stub ( debug , 'stopDebugging' ) ;
1183
+ let calledStopDebugging = false ;
1184
+ stopDebuggingStub . callsFake ( ( ) => {
1185
+ calledStopDebugging = true ;
1186
+ return Promise . resolve ( ) ;
1187
+ } ) ;
1188
+
1189
+ // // mock exec service and exec factory, not very necessary for this test
1190
+ const execServiceStub = typeMoq . Mock . ofType < IPythonExecutionService > ( ) ;
1191
+ const execFactoryStub = typeMoq . Mock . ofType < IPythonExecutionFactory > ( ) ;
1192
+ const cancellationTokenSource = new CancellationTokenSource ( ) ;
1193
+ let mockProc : MockChildProcess ;
1194
+ execServiceStub
1195
+ . setup ( ( x ) => x . execObservable ( typeMoq . It . isAny ( ) , typeMoq . It . isAny ( ) ) )
1196
+ . returns ( ( ) => ( {
1197
+ proc : mockProc ,
1198
+ out : typeMoq . Mock . ofType < Observable < Output < string > > > ( ) . object ,
1199
+ dispose : ( ) => {
1200
+ /* no-body */
1201
+ } ,
1202
+ } ) ) ;
1203
+ execFactoryStub
1204
+ . setup ( ( x ) => x . createActivatedEnvironment ( typeMoq . It . isAny ( ) ) )
1205
+ . returns ( ( ) => Promise . resolve ( execServiceStub . object ) ) ;
1206
+ execFactoryStub . setup ( ( p ) => ( ( p as unknown ) as any ) . then ) . returns ( ( ) => undefined ) ;
1207
+ execServiceStub . setup ( ( p ) => ( ( p as unknown ) as any ) . then ) . returns ( ( ) => undefined ) ;
1208
+
1209
+ resultResolver = new PythonResultResolver ( testController , pytestProvider , workspaceUri ) ;
1210
+
1211
+ const testId = `${ rootPathErrorWorkspace } /test_seg_fault.py::TestSegmentationFault::test_segfault` ;
1212
+ const testIds : string [ ] = [ testId ] ;
1213
+
1214
+ // set workspace to test workspace folder
1215
+ workspaceUri = Uri . parse ( rootPathErrorWorkspace ) ;
1216
+ configService . getSettings ( workspaceUri ) . testing . pytestArgs = [ ] ;
1217
+
1218
+ const debugSessionStub = typeMoq . Mock . ofType < DebugSession > ( ) ;
1219
+ sinon . stub ( debug , 'onDidStartDebugSession' ) . callsFake ( ( cb ) => {
1220
+ // run the callback right away to add the cancelation token listener
1221
+ cb ( debugSessionStub . object ) ;
1222
+ return {
1223
+ dispose : ( ) => {
1224
+ /* no-body */
1225
+ } ,
1226
+ } ;
1227
+ } ) ;
1228
+ const awaitStopDebugging = createDeferred ( ) ;
1229
+
1230
+ sinon . stub ( debug , 'onDidTerminateDebugSession' ) . callsFake ( ( cb ) => {
1231
+ // wait for the stop debugging to be called before resolving the promise
1232
+ // the terminate debug session does cleanup
1233
+ awaitStopDebugging . promise . then ( ( ) => {
1234
+ cb ( debugSessionStub . object ) ;
1235
+ } ) ;
1236
+ return {
1237
+ dispose : ( ) => {
1238
+ // void
1239
+ } ,
1240
+ } ;
1241
+ } ) ;
1242
+ // handle cancelation token from debugger
1243
+ sinon . stub ( debug , 'startDebugging' ) . callsFake ( ( folder , nameOrConfiguration , _parentSession ) => {
1244
+ // check to make sure start debugging is called correctly
1245
+ if ( typeof nameOrConfiguration !== 'string' ) {
1246
+ assert . strictEqual ( nameOrConfiguration . type , 'debugpy' , 'Expected debugpy' ) ;
1247
+ } else {
1248
+ assert . fail ( 'Expected nameOrConfiguration to be an object' ) ;
1249
+ }
1250
+ assert . ok ( folder , 'Expected folder to be defined' ) ;
1251
+ assert . strictEqual ( folder . name , 'test' , 'Expected folder name to be test' ) ;
1252
+ // cancel the token and trigger the stop debugging callback
1253
+ awaitStopDebugging . resolve ( ) ;
1254
+ cancellationTokenSource . cancel ( ) ;
1255
+ return Promise . resolve ( true ) ;
1256
+ } ) ;
1257
+
1258
+ // run pytest execution
1259
+ const executionAdapter = new UnittestTestExecutionAdapter (
1260
+ configService ,
1261
+ testOutputChannel . object ,
1262
+ resultResolver ,
1263
+ envVarsService ,
1264
+ ) ;
1265
+
1266
+ const testRun = typeMoq . Mock . ofType < TestRun > ( ) ;
1267
+ testRun . setup ( ( t ) => t . token ) . returns ( ( ) => cancellationTokenSource . token ) ;
1268
+
1269
+ await executionAdapter
1270
+ . runTests ( workspaceUri , testIds , true , testRun . object , pythonExecFactory , debugLauncher )
1271
+ . finally ( ( ) => {
1272
+ // verify that the stop debugging was called
1273
+ assert . ok ( calledStopDebugging , 'Expected stopDebugging to be called' ) ;
1274
+ } ) ;
1275
+ } ) ;
1076
1276
} ) ;
0 commit comments