Skip to content

Commit f96d910

Browse files
Refactor pytest and unittest test discovery (#25599)
cleanup to assist with future changes --------- Co-authored-by: Copilot <[email protected]>
1 parent 4b5dc0d commit f96d910

File tree

5 files changed

+524
-336
lines changed

5 files changed

+524
-336
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import { CancellationToken, CancellationTokenSource, Disposable, Uri } from 'vscode';
4+
import { Deferred } from '../../../common/utils/async';
5+
import { traceError, traceInfo, traceVerbose } from '../../../logging';
6+
import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from './utils';
7+
import { DiscoveredTestPayload, ITestResultResolver } from './types';
8+
9+
/**
10+
* Test provider type for logging purposes.
11+
*/
12+
export type TestProvider = 'pytest' | 'unittest';
13+
14+
/**
15+
* Sets up the discovery named pipe and wires up cancellation.
16+
* @param resultResolver The resolver to handle discovered test data
17+
* @param token Optional cancellation token from the caller
18+
* @param uri Workspace URI for logging
19+
* @returns Object containing the pipe name, cancellation source, and disposable for the external token handler
20+
*/
21+
export async function setupDiscoveryPipe(
22+
resultResolver: ITestResultResolver | undefined,
23+
token: CancellationToken | undefined,
24+
uri: Uri,
25+
): Promise<{ pipeName: string; cancellation: CancellationTokenSource; tokenDisposable: Disposable | undefined }> {
26+
const discoveryPipeCancellation = new CancellationTokenSource();
27+
28+
// Wire up cancellation from external token and store the disposable
29+
const tokenDisposable = token?.onCancellationRequested(() => {
30+
traceInfo(`Test discovery cancelled.`);
31+
discoveryPipeCancellation.cancel();
32+
});
33+
34+
// Start the named pipe with the discovery listener
35+
const discoveryPipeName = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => {
36+
if (!token?.isCancellationRequested) {
37+
resultResolver?.resolveDiscovery(data);
38+
}
39+
}, discoveryPipeCancellation.token);
40+
41+
traceVerbose(`Created discovery pipe: ${discoveryPipeName} for workspace ${uri.fsPath}`);
42+
43+
return {
44+
pipeName: discoveryPipeName,
45+
cancellation: discoveryPipeCancellation,
46+
tokenDisposable,
47+
};
48+
}
49+
50+
/**
51+
* Creates standard process event handlers for test discovery subprocess.
52+
* Handles stdout/stderr logging and error reporting on process exit.
53+
*
54+
* @param testProvider - The test framework being used ('pytest' or 'unittest')
55+
* @param uri - The workspace URI
56+
* @param cwd - The current working directory
57+
* @param resultResolver - Resolver for test discovery results
58+
* @param deferredTillExecClose - Deferred to resolve when process closes
59+
* @param allowedSuccessCodes - Additional exit codes to treat as success (e.g., pytest exit code 5 for no tests found)
60+
*/
61+
export function createProcessHandlers(
62+
testProvider: TestProvider,
63+
uri: Uri,
64+
cwd: string,
65+
resultResolver: ITestResultResolver | undefined,
66+
deferredTillExecClose: Deferred<void>,
67+
allowedSuccessCodes: number[] = [],
68+
): {
69+
onStdout: (data: any) => void;
70+
onStderr: (data: any) => void;
71+
onExit: (code: number | null, signal: NodeJS.Signals | null) => void;
72+
onClose: (code: number | null, signal: NodeJS.Signals | null) => void;
73+
} {
74+
const isSuccessCode = (code: number | null): boolean => {
75+
return code === 0 || (code !== null && allowedSuccessCodes.includes(code));
76+
};
77+
78+
return {
79+
onStdout: (data: any) => {
80+
const out = fixLogLinesNoTrailing(data.toString());
81+
traceInfo(out);
82+
},
83+
onStderr: (data: any) => {
84+
const out = fixLogLinesNoTrailing(data.toString());
85+
traceError(out);
86+
},
87+
onExit: (code: number | null, _signal: NodeJS.Signals | null) => {
88+
// The 'exit' event fires when the process terminates, but streams may still be open.
89+
// Only log verbose success message here; error handling happens in onClose.
90+
if (isSuccessCode(code)) {
91+
traceVerbose(`${testProvider} discovery subprocess exited successfully for workspace ${uri.fsPath}`);
92+
}
93+
},
94+
onClose: (code: number | null, signal: NodeJS.Signals | null) => {
95+
// We resolve the deferred here to ensure all output has been captured.
96+
if (!isSuccessCode(code)) {
97+
traceError(
98+
`${testProvider} discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`,
99+
);
100+
resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd));
101+
} else {
102+
traceVerbose(`${testProvider} discovery subprocess streams closed for workspace ${uri.fsPath}`);
103+
}
104+
deferredTillExecClose?.resolve();
105+
},
106+
};
107+
}
108+
109+
/**
110+
* Handles cleanup when test discovery is cancelled.
111+
* Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe.
112+
*
113+
* @param testProvider - The test framework being used ('pytest' or 'unittest')
114+
* @param proc - The process to kill
115+
* @param processCompletion - Deferred to resolve
116+
* @param pipeCancellation - Cancellation token source to cancel
117+
* @param uri - The workspace URI
118+
*/
119+
export function cleanupOnCancellation(
120+
testProvider: TestProvider,
121+
proc: { kill: () => void } | undefined,
122+
processCompletion: Deferred<void>,
123+
pipeCancellation: CancellationTokenSource,
124+
uri: Uri,
125+
): void {
126+
traceInfo(`Test discovery cancelled, killing ${testProvider} subprocess for workspace ${uri.fsPath}`);
127+
if (proc) {
128+
traceVerbose(`Killing ${testProvider} subprocess for workspace ${uri.fsPath}`);
129+
proc.kill();
130+
} else {
131+
traceVerbose(`No ${testProvider} subprocess to kill for workspace ${uri.fsPath} (proc is undefined)`);
132+
}
133+
traceVerbose(`Resolving process completion deferred for ${testProvider} discovery in workspace ${uri.fsPath}`);
134+
processCompletion.resolve();
135+
traceVerbose(`Cancelling discovery pipe for ${testProvider} discovery in workspace ${uri.fsPath}`);
136+
pipeCancellation.cancel();
137+
}

0 commit comments

Comments
 (0)