diff --git a/src/test/pythonFiles/debugging/test_pytest_processpool.py b/src/test/pythonFiles/debugging/test_pytest_processpool.py new file mode 100644 index 00000000..98c0ae21 --- /dev/null +++ b/src/test/pythonFiles/debugging/test_pytest_processpool.py @@ -0,0 +1,26 @@ +import os +import time +from concurrent.futures import ProcessPoolExecutor, wait as futures_wait + + +def sleep_briefly() -> None: + time.sleep(0.1) + + +def run_process_pool_tasks() -> None: + futures = [] + with ProcessPoolExecutor(max_workers=4) as pool: + for _ in range(8): + futures.append(pool.submit(sleep_briefly)) + completed_futures, pending_futures = futures_wait(futures) + assert not pending_futures + for fut in completed_futures: + fut.result() + + +def test_library_process_pool() -> None: + run_process_pool_tasks() + done_file = os.environ.get("DEBUG_DONE_FILE") + if done_file: + with open(done_file, "w", encoding="utf-8") as fp: + fp.write("done") diff --git a/src/test/unittest/adapter/adapter.test.ts b/src/test/unittest/adapter/adapter.test.ts index e9038505..fbcb849e 100644 --- a/src/test/unittest/adapter/adapter.test.ts +++ b/src/test/unittest/adapter/adapter.test.ts @@ -19,10 +19,14 @@ function resolveWSFile(wsRoot: string, ...filePath: string[]): string { return path.join(wsRoot, ...filePath); } +const SUBPROCESS_DEBUG_TIMEOUT_MS = 120_000; + suite('Debugger Integration', () => { const file = resolveWSFile(WS_ROOT, 'pythonFiles', 'debugging', 'wait_for_file.py'); + const processPoolTestFile = resolveWSFile(WS_ROOT, 'pythonFiles', 'debugging', 'test_pytest_processpool.py'); const doneFile = resolveWSFile(WS_ROOT, 'should-not-exist'); const outFile = resolveWSFile(WS_ROOT, 'output.txt'); + const processPoolDoneFile = resolveWSFile(WS_ROOT, 'pytest-processpool-debug-done.txt'); const resource = vscode.Uri.file(file); const defaultScriptArgs = [doneFile]; let workspaceRoot: vscode.WorkspaceFolder; @@ -107,4 +111,38 @@ suite('Debugger Integration', () => { }); } }); + + suite('pytest multiprocess test debugging', () => { + test('process-pool test reaches code after worker joins in debug-test session', async function () { + // This path starts pytest under the debugger with subprocess attach enabled, + // so allow extra time for child session orchestration and process-pool teardown. + this.timeout(SUBPROCESS_DEBUG_TIMEOUT_MS); + fix.addFSCleanup(processPoolDoneFile); + + const config = { + type: 'python', + name: 'debug pytest processpool', + request: 'launch', + module: 'pytest', + args: ['-s', processPoolTestFile, '-k', 'test_library_process_pool'], + console: 'integratedTerminal', + purpose: ['debug-test'], + subProcess: true, + cwd: WS_ROOT, + env: { + DEBUG_DONE_FILE: processPoolDoneFile, + }, + }; + + const session = fix.resolveDebuggerWithConfig(config, workspaceRoot); + await session.start(); + const result = await session.waitUntilDone(); + + expect(result.exitCode).to.equal(0, 'bad exit code'); + expect(await fs.pathExists(processPoolDoneFile)).to.equal( + true, + 'pytest test did not reach code after process pool join', + ); + }); + }); }); diff --git a/src/test/unittest/hooks/childProcessAttachService.unit.test.ts b/src/test/unittest/hooks/childProcessAttachService.unit.test.ts index 7d345ae4..e69e5384 100644 --- a/src/test/unittest/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/unittest/hooks/childProcessAttachService.unit.test.ts @@ -235,4 +235,33 @@ suite('Debug - Attach to Child Process', () => { // The original data object must not be mutated. expect(data).to.have.property('purpose').deep.equal(['debug-test']); }); + test('Child process attach keeps pytest launch fields while removing debug-test purpose', async () => { + const pytestArgs = ['-s', '/workspace/tests/test_multiproc.py']; + const data: AttachRequestArguments = { + request: 'attach', + type: debuggerTypeName, + name: 'Attach', + port: 1234, + subProcessId: 2, + purpose: ['debug-test'], + module: 'pytest', + args: pytestArgs, + console: 'integratedTerminal', + }; + const session: any = {}; + + getWorkspaceFoldersStub.returns(undefined); + startDebuggingStub.resolves(true); + + await attachService.attach(data, session); + + sinon.assert.calledOnce(startDebuggingStub); + const [, secondArg] = startDebuggingStub.args[0]; + expect(secondArg).to.include({ + module: 'pytest', + console: 'integratedTerminal', + }); + expect(secondArg).to.have.property('args').deep.equal(pytestArgs); + expect(secondArg).to.not.have.property('purpose'); + }); }); diff --git a/src/test/unittest/utils.ts b/src/test/unittest/utils.ts index f7b2c731..3bf82284 100644 --- a/src/test/unittest/utils.ts +++ b/src/test/unittest/utils.ts @@ -277,6 +277,16 @@ class DebuggerSession { } export class DebuggerFixture extends PythonFixture { + public resolveDebuggerWithConfig( + config: vscode.DebugConfiguration, + wsRoot?: vscode.WorkspaceFolder, + ): DebuggerSession { + const id = debuggers.track(config); + const session = new DebuggerSession(id, config, wsRoot); + debuggers.setDAPHandler(id, (src, msg) => session.handleDAPMessage(src, msg)); + return session; + } + public resolveDebugger( configName: string, file: string,