Skip to content

Commit 12bd780

Browse files
Add pytest cancellation subprocess regression coverage (microsoft#26058)
Pytest cancellation must terminate the child process in the non-environment-extension execution path. This change adds regression coverage for that behavior. - **Subprocess tracking** - Retain the spawned process for cancellation handling. - **Regression coverage** - Cancel an active pytest run. - Verify the child process receives `kill()`. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent 30e230b commit 12bd780

2 files changed

Lines changed: 24 additions & 1 deletion

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export class PytestTestExecutionAdapter implements ITestExecutionAdapter {
246246
});
247247

248248
const result = execService?.execObservable(runArgs, spawnOptions);
249+
resultProc = result?.proc;
249250

250251
// Take all output from the subprocess and add it to the test output channel. This will be the pytest output.
251252
// Displays output to user and ensure the subprocess doesn't run into buffer overflow.

src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Copyright (c) Microsoft Corporation. All rights reserved.
33
// Licensed under the MIT License.
44
import * as assert from 'assert';
5-
import { TestRun, Uri, TestRunProfileKind, DebugSessionOptions } from 'vscode';
5+
import { CancellationTokenSource, TestRun, Uri, TestRunProfileKind, DebugSessionOptions } from 'vscode';
66
import * as typeMoq from 'typemoq';
77
import * as sinon from 'sinon';
88
import * as path from 'path';
@@ -182,6 +182,28 @@ suite('pytest test execution adapter', () => {
182182
typeMoq.Times.once(),
183183
);
184184
});
185+
test('cancelling pytest execution kills the subprocess', async () => {
186+
utilsWriteTestIdsFileStub.resolves('testIdPipe-mockName');
187+
utilsStartRunResultNamedPipeStub.callsFake((_callback, deferredTillServerClose, token) => {
188+
token?.onCancellationRequested(() => deferredTillServerClose.resolve());
189+
return Promise.resolve('runResultPipe-mockName');
190+
});
191+
const cancellationToken = new CancellationTokenSource();
192+
const testRun = typeMoq.Mock.ofType<TestRun>();
193+
testRun.setup((t) => t.token).returns(() => cancellationToken.token);
194+
const killStub = sinon.stub(mockProc, 'kill');
195+
const uri = Uri.file(myTestPath);
196+
adapter = new PytestTestExecutionAdapter(configService);
197+
198+
const execution = adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object);
199+
await deferred4.promise;
200+
cancellationToken.cancel();
201+
202+
sinon.assert.calledOnce(killStub);
203+
mockProc.emit('close', 0, null);
204+
await execution;
205+
cancellationToken.dispose();
206+
});
185207
test('pytest execution respects settings.testing.cwd when present', async () => {
186208
const deferred2 = createDeferred();
187209
const deferred3 = createDeferred();

0 commit comments

Comments
 (0)