Skip to content

Commit 1fa1c39

Browse files
eleanorjboydanthonykim1
authored andcommitted
Debug cancelation (microsoft#23262)
1 parent a98296e commit 1fa1c39

File tree

5 files changed

+235
-6
lines changed

5 files changed

+235
-6
lines changed

src/client/testing/common/debugLauncher.ts

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { inject, injectable, named } from 'inversify';
22
import * as path from 'path';
3-
import { DebugConfiguration, l10n, Uri, WorkspaceFolder } from 'vscode';
3+
import { debug, DebugConfiguration, Disposable, l10n, Uri, WorkspaceFolder } from 'vscode';
44
import { IApplicationShell, IDebugService } from '../../common/application/types';
55
import { EXTENSION_ROOT_DIR } from '../../common/constants';
66
import * as internalScripts from '../../common/process/internal/scripts';
@@ -9,7 +9,7 @@ import { DebuggerTypeName, PythonDebuggerTypeName } from '../../debugger/constan
99
import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types';
1010
import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types';
1111
import { IServiceContainer } from '../../ioc/types';
12-
import { traceError } from '../../logging';
12+
import { traceError, traceLog } from '../../logging';
1313
import { TestProvider } from '../types';
1414
import { ITestDebugLauncher, LaunchOptions } from './types';
1515
import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader';
@@ -48,7 +48,29 @@ export class DebugLauncher implements ITestDebugLauncher {
4848
);
4949
const debugManager = this.serviceContainer.get<IDebugService>(IDebugService);
5050

51-
debugManager.onDidTerminateDebugSession(() => {
51+
let disposeOfDebugger: Disposable | undefined;
52+
const disposeOfStartDebugging = debugManager.onDidStartDebugSession((session) => {
53+
if (options.token) {
54+
disposeOfDebugger = options?.token.onCancellationRequested(() => {
55+
console.log('Canceling debugger, due to cancelation token called.');
56+
debug.stopDebugging(session);
57+
});
58+
}
59+
});
60+
61+
let disposeTerminateWatcher: Disposable | undefined;
62+
// eslint-disable-next-line prefer-const
63+
disposeTerminateWatcher = debugManager.onDidTerminateDebugSession(() => {
64+
traceLog('Terminating the debugging session and disposing of debugger listeners.');
65+
if (disposeOfDebugger !== undefined) {
66+
disposeOfDebugger.dispose();
67+
}
68+
if (disposeOfStartDebugging !== undefined) {
69+
disposeOfStartDebugging.dispose();
70+
}
71+
if (disposeTerminateWatcher !== undefined) {
72+
disposeTerminateWatcher.dispose();
73+
}
5274
deferred.resolve();
5375
callback?.();
5476
});

src/client/testing/testController/common/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export async function startRunResultNamedPipe(
220220
perConnectionDisposables.push(
221221
// per connection, add a listener for the cancellation token and the data
222222
cancellationToken?.onCancellationRequested(() => {
223-
console.log(`Test Result named pipe ${pipeName} cancelled`);
223+
traceVerbose(`Test Result named pipe ${pipeName} cancelled`);
224224
// if cancel is called on one connection, dispose of all connections
225225
disposeOfServer();
226226
}),

src/client/testing/testController/pytest/pytestExecutionAdapter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
167167
};
168168
traceInfo(`Running DEBUG pytest with arguments: ${testArgs} for workspace ${uri.fsPath} \r\n`);
169169
await debugLauncher!.launchDebugger(launchOptions, () => {
170+
traceInfo("Debugger callback called, resolving 'till EOT' deferred for the workspace.");
170171
serverDispose(); // this will resolve deferredTillServerClose
171172
deferredTillEOT?.resolve();
172173
});

src/test/testing/common/debugLauncher.unit.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ suite('Unit Tests - Debug Launcher', () => {
139139
return undefined as any;
140140
})
141141
.verifiable(TypeMoq.Times.once());
142+
debugService
143+
.setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny()))
144+
.returns(() => {
145+
return undefined as any;
146+
})
147+
.verifiable(TypeMoq.Times.once());
142148
}
143149
function createWorkspaceFolder(folderPath: string): WorkspaceFolder {
144150
return {

src/test/testing/common/testingAdapter.test.ts

+202-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
// Copyright (c) Microsoft Corporation. All rights reserved.
33
// Licensed under the MIT License.
4-
import { TestController, TestRun, Uri } from 'vscode';
4+
import { CancellationTokenSource, DebugSession, TestController, TestRun, Uri, debug } from 'vscode';
55
import * as typeMoq from 'typemoq';
66
import * as path from 'path';
77
import * as assert from 'assert';
88
import * as fs from 'fs';
9+
import * as sinon from 'sinon';
10+
import { Observable } from 'rxjs';
911
import * as os from 'os';
1012
import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter';
1113
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';
1315
import { IConfigurationService, ITestOutputChannel } from '../../../client/common/types';
1416
import { IServiceContainer } from '../../../client/ioc/types';
1517
import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize';
@@ -21,6 +23,9 @@ import { PythonResultResolver } from '../../../client/testing/testController/com
2123
import { TestProvider } from '../../../client/testing/types';
2224
import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants';
2325
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';
2429

2530
suite('End to End Tests: test adapters', () => {
2631
let resultResolver: ITestResultResolver;
@@ -150,6 +155,9 @@ suite('End to End Tests: test adapters', () => {
150155
traceLog('Symlink was not found to remove after tests, exiting successfully, nestedSymlink.');
151156
}
152157
});
158+
teardown(async () => {
159+
sinon.restore();
160+
});
153161
test('unittest discovery adapter small workspace', async () => {
154162
// result resolver and saved data for assertions
155163
let actualData: {
@@ -1073,4 +1081,196 @@ suite('End to End Tests: test adapters', () => {
10731081
assert.strictEqual(failureOccurred, false, failureMsg);
10741082
});
10751083
});
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+
});
10761276
});

0 commit comments

Comments
 (0)