Skip to content

Commit 48a62d2

Browse files
committed
add unittest too
1 parent e2f4691 commit 48a62d2

File tree

5 files changed

+242
-251
lines changed

5 files changed

+242
-251
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import { CancellationTokenSource, Uri } from 'vscode';
4+
import { Deferred } from '../../../common/utils/async';
5+
import { traceError, traceInfo, traceVerbose } from '../../../logging';
6+
import { createDiscoveryErrorPayload, fixLogLinesNoTrailing } from './utils';
7+
import { ITestResultResolver } from './types';
8+
9+
/**
10+
* Test provider type for logging purposes.
11+
*/
12+
export type TestProvider = 'pytest' | 'unittest';
13+
14+
/**
15+
* Creates standard process event handlers for test discovery subprocess.
16+
* Handles stdout/stderr logging and error reporting on process exit.
17+
*
18+
* @param testProvider - The test framework being used ('pytest' or 'unittest')
19+
* @param uri - The workspace URI
20+
* @param cwd - The current working directory
21+
* @param resultResolver - Resolver for test discovery results
22+
* @param deferredTillExecClose - Deferred to resolve when process closes
23+
* @param allowedSuccessCodes - Additional exit codes to treat as success (e.g., pytest exit code 5 for no tests found)
24+
*/
25+
export function createProcessHandlers(
26+
testProvider: TestProvider,
27+
uri: Uri,
28+
cwd: string,
29+
resultResolver: ITestResultResolver | undefined,
30+
deferredTillExecClose: Deferred<void>,
31+
allowedSuccessCodes: number[] = [],
32+
): {
33+
onStdout: (data: any) => void;
34+
onStderr: (data: any) => void;
35+
onExit: (code: number | null, signal: NodeJS.Signals | null) => void;
36+
onClose: (code: number | null, signal: NodeJS.Signals | null) => void;
37+
} {
38+
const isSuccessCode = (code: number | null): boolean => {
39+
return code === 0 || (code !== null && allowedSuccessCodes.includes(code));
40+
};
41+
42+
return {
43+
onStdout: (data: any) => {
44+
const out = fixLogLinesNoTrailing(data.toString());
45+
traceInfo(out);
46+
},
47+
onStderr: (data: any) => {
48+
const out = fixLogLinesNoTrailing(data.toString());
49+
traceError(out);
50+
},
51+
onExit: (code: number | null, signal: NodeJS.Signals | null) => {
52+
// The 'exit' event fires when the process terminates, but streams may still be open.
53+
if (!isSuccessCode(code)) {
54+
const exitCodeNote =
55+
allowedSuccessCodes.length > 0
56+
? ` Note: Exit codes ${allowedSuccessCodes.join(', ')} are also treated as success.`
57+
: '';
58+
traceError(
59+
`${testProvider} discovery subprocess exited with code ${code} and signal ${signal} for workspace ${uri.fsPath}.${exitCodeNote}`,
60+
);
61+
} else if (code === 0) {
62+
traceVerbose(`${testProvider} discovery subprocess exited successfully for workspace ${uri.fsPath}`);
63+
}
64+
},
65+
onClose: (code: number | null, signal: NodeJS.Signals | null) => {
66+
// We resolve the deferred here to ensure all output has been captured.
67+
if (!isSuccessCode(code)) {
68+
traceError(
69+
`${testProvider} discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`,
70+
);
71+
resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd));
72+
} else {
73+
traceVerbose(`${testProvider} discovery subprocess streams closed for workspace ${uri.fsPath}`);
74+
}
75+
deferredTillExecClose?.resolve();
76+
},
77+
};
78+
}
79+
80+
/**
81+
* Handles cleanup when test discovery is cancelled.
82+
* Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe.
83+
*
84+
* @param testProvider - The test framework being used ('pytest' or 'unittest')
85+
* @param proc - The process to kill
86+
* @param processCompletion - Deferred to resolve
87+
* @param pipeCancellation - Cancellation token source to cancel
88+
* @param uri - The workspace URI
89+
*/
90+
export function cleanupOnCancellation(
91+
testProvider: TestProvider,
92+
proc: { kill: () => void } | undefined,
93+
processCompletion: Deferred<void>,
94+
pipeCancellation: CancellationTokenSource,
95+
uri: Uri,
96+
): void {
97+
traceInfo(`Test discovery cancelled, killing ${testProvider} subprocess for workspace ${uri.fsPath}`);
98+
if (proc) {
99+
proc.kill();
100+
}
101+
processCompletion.resolve();
102+
pipeCancellation.cancel();
103+
}

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,8 @@ import { createTestingDeferred, startDiscoveryNamedPipe } from '../common/utils'
1717
import { IEnvironmentVariablesProvider } from '../../../common/variables/types';
1818
import { PythonEnvironment } from '../../../pythonEnvironments/info';
1919
import { useEnvExtension, getEnvironment, runInBackground } from '../../../envExt/api.internal';
20-
import {
21-
cleanupOnCancellation,
22-
buildPytestEnv as configureSubprocessEnv,
23-
createProcessHandlers,
24-
handleSymlinkAndRootDir,
25-
} from './pytestHelpers';
20+
import { buildPytestEnv as configureSubprocessEnv, handleSymlinkAndRootDir } from './pytestHelpers';
21+
import { cleanupOnCancellation, createProcessHandlers } from '../common/discoveryHelpers';
2622

2723
/**
2824
* Wrapper class for unittest test discovery. This is where we call `runTestCommand`. #this seems incorrectly copied
@@ -70,7 +66,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
7066

7167
// Setup process handlers (shared by both execution paths)
7268
const deferredTillExecClose: Deferred<void> = createTestingDeferred();
73-
const handlers = createProcessHandlers(uri, cwd, this.resultResolver, deferredTillExecClose);
69+
const handlers = createProcessHandlers('pytest', uri, cwd, this.resultResolver, deferredTillExecClose, [5]);
7470

7571
// Execute using environment extension if available
7672
if (useEnvExtension()) {
@@ -93,7 +89,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
9389

9490
// Wire up cancellation and process events
9591
token?.onCancellationRequested(() => {
96-
cleanupOnCancellation(proc, deferredTillExecClose, discoveryPipeCancellation, uri);
92+
cleanupOnCancellation('pytest', proc, deferredTillExecClose, discoveryPipeCancellation, uri);
9793
});
9894
proc.stdout.on('data', handlers.onStdout);
9995
proc.stderr.on('data', handlers.onStderr);
@@ -143,7 +139,7 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
143139

144140
// Wire up cancellation and process events
145141
token?.onCancellationRequested(() => {
146-
cleanupOnCancellation(resultProc, deferredTillExecClose, discoveryPipeCancellation, uri);
142+
cleanupOnCancellation('pytest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri);
147143
});
148144
resultProc.stdout?.on('data', handlers.onStdout);
149145
resultProc.stderr?.on('data', handlers.onStderr);

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

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,8 @@
22
// Licensed under the MIT License.
33
import * as path from 'path';
44
import * as fs from 'fs';
5-
import { CancellationTokenSource, Uri } from 'vscode';
6-
import { Deferred } from '../../../common/utils/async';
7-
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging';
8-
import {
9-
addValueIfKeyNotExist,
10-
createDiscoveryErrorPayload,
11-
fixLogLinesNoTrailing,
12-
hasSymlinkParent,
13-
} from '../common/utils';
14-
import { ITestResultResolver } from '../common/types';
5+
import { traceInfo, traceWarn } from '../../../logging';
6+
import { addValueIfKeyNotExist, hasSymlinkParent } from '../common/utils';
157

168
/**
179
* Checks if the current working directory contains a symlink and ensures --rootdir is set in pytest args.
@@ -64,71 +56,3 @@ export async function buildPytestEnv(
6456
);
6557
return mutableEnv;
6658
}
67-
68-
/**
69-
* Creates standard process event handlers for pytest discovery subprocess.
70-
* Handles stdout/stderr logging and error reporting on process exit.
71-
*/
72-
export function createProcessHandlers(
73-
uri: Uri,
74-
cwd: string,
75-
resultResolver: ITestResultResolver | undefined,
76-
deferredTillExecClose: Deferred<void>,
77-
): {
78-
onStdout: (data: any) => void;
79-
onStderr: (data: any) => void;
80-
onExit: (code: number | null, signal: NodeJS.Signals | null) => void;
81-
onClose: (code: number | null, signal: NodeJS.Signals | null) => void;
82-
} {
83-
return {
84-
onStdout: (data: any) => {
85-
const out = fixLogLinesNoTrailing(data.toString());
86-
traceInfo(out);
87-
},
88-
onStderr: (data: any) => {
89-
const out = fixLogLinesNoTrailing(data.toString());
90-
traceError(out);
91-
},
92-
onExit: (code: number | null, signal: NodeJS.Signals | null) => {
93-
// The 'exit' event fires when the process terminates, but streams may still be open.
94-
if (code !== 0 && code !== 5) {
95-
traceError(
96-
`Pytest discovery subprocess exited with code ${code} and signal ${signal} for workspace ${uri.fsPath}. Note: Exit code 5 (no tests found) is expected for empty test suites.`,
97-
);
98-
} else if (code === 0) {
99-
traceVerbose(`Pytest discovery subprocess exited successfully for workspace ${uri.fsPath}`);
100-
}
101-
},
102-
onClose: (code: number | null, signal: NodeJS.Signals | null) => {
103-
// We resolve the deferred here to ensure all output has been captured.
104-
// pytest exits with code of 5 when 0 tests are found- this is not a failure for discovery.
105-
if (code !== 0 && code !== 5) {
106-
traceError(
107-
`Pytest discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`,
108-
);
109-
resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd));
110-
} else {
111-
traceVerbose(`Pytest discovery subprocess streams closed for workspace ${uri.fsPath}`);
112-
}
113-
deferredTillExecClose?.resolve();
114-
},
115-
};
116-
}
117-
118-
/**
119-
* Handles cleanup when test discovery is cancelled.
120-
* Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe.
121-
*/
122-
export function cleanupOnCancellation(
123-
proc: { kill: () => void } | undefined,
124-
processCompletion: Deferred<void>,
125-
pipeCancellation: CancellationTokenSource,
126-
uri: Uri,
127-
): void {
128-
traceInfo(`Test discovery cancelled, killing pytest subprocess for workspace ${uri.fsPath}`);
129-
if (proc) {
130-
proc.kill();
131-
}
132-
processCompletion.resolve();
133-
pipeCancellation.cancel();
134-
}

0 commit comments

Comments
 (0)