Skip to content

Commit 754f8ef

Browse files
authored
remove node deletion for error tolerant discovery (microsoft#22207)
helps with a part of microsoft#21757
1 parent ebaf8fe commit 754f8ef

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

pythonFiles/vscode_pytest/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,12 @@ def pytest_sessionfinish(session, exitstatus):
302302
session -- the pytest session object.
303303
exitstatus -- the status code of the session.
304304
305-
0: All tests passed successfully.
306-
1: One or more tests failed.
307-
2: Pytest was unable to start or run any tests due to issues with test discovery or test collection.
308-
3: Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution.
309-
4: Pytest encountered an internal error or exception during test execution.
310-
5: Pytest was unable to find any tests to run.
305+
Exit code 0: All tests were collected and passed successfully
306+
Exit code 1: Tests were collected and run but some of the tests failed
307+
Exit code 2: Test execution was interrupted by the user
308+
Exit code 3: Internal error happened while executing tests
309+
Exit code 4: pytest command line usage error
310+
Exit code 5: No tests were collected
311311
"""
312312
cwd = pathlib.Path.cwd()
313313
if IS_DISCOVERY:

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,6 @@ export class PythonResultResolver implements ITestResultResolver {
103103
// If the test root for this folder exists: Workspace refresh, update its children.
104104
// Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree.
105105
populateTestTree(this.testController, rawTestData.tests, undefined, this, token);
106-
} else {
107-
// Delete everything from the test controller.
108-
const errorNode = this.testController.items.get(`DiscoveryError:${workspacePath}`);
109-
this.testController.items.replace([]);
110-
// Add back the error node if it exists.
111-
if (errorNode !== undefined) {
112-
this.testController.items.add(errorNode);
113-
}
114106
}
115107

116108
sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
120120
}
121121
});
122122
result?.proc?.on('close', (code, signal) => {
123-
if (code !== 0) {
123+
// pytest exits with code of 5 when 0 tests are found- this is not a failure for discovery.
124+
if (code !== 0 && code !== 5) {
124125
traceError(
125-
`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal}. Creating and sending error discovery payload`,
126+
`Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}. Creating and sending error discovery payload`,
126127
);
127128
// if the child process exited with a non-zero exit code, then we need to send the error payload.
128129
this.testServer.triggerDiscoveryDataReceivedEvent({

src/test/testing/testController/resultResolver.unit.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,69 @@ suite('Result Resolver tests', () => {
195195
cancelationToken, // token
196196
);
197197
});
198+
test('resolveDiscovery should create error and not clear test items to allow for error tolerant discovery', async () => {
199+
// test specific constants used expected values
200+
testProvider = 'pytest';
201+
workspaceUri = Uri.file('/foo/bar');
202+
resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri);
203+
const errorMessage = 'error msg A';
204+
const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`;
205+
206+
// create test result node
207+
const tests: DiscoveredTestNode = {
208+
path: 'path',
209+
name: 'name',
210+
type_: 'folder',
211+
id_: 'id',
212+
children: [],
213+
};
214+
// stub out return values of functions called in resolveDiscovery
215+
const errorPayload: DiscoveredTestPayload = {
216+
cwd: workspaceUri.fsPath,
217+
status: 'error',
218+
error: [errorMessage],
219+
};
220+
const regPayload: DiscoveredTestPayload = {
221+
cwd: workspaceUri.fsPath,
222+
status: 'success',
223+
error: [errorMessage],
224+
tests,
225+
};
226+
const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = {
227+
id: 'id',
228+
label: 'label',
229+
error: 'error',
230+
};
231+
232+
// stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery
233+
const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions);
234+
const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem);
235+
236+
// stub out functionality of populateTestTreeStub which is called in resolveDiscovery
237+
sinon.stub(util, 'populateTestTree').returns();
238+
// add spies to insure these aren't called
239+
const deleteSpy = sinon.spy(testController.items, 'delete');
240+
const replaceSpy = sinon.spy(testController.items, 'replace');
241+
// call resolve discovery
242+
let deferredTillEOT: Deferred<void> = createDeferred<void>();
243+
resultResolver.resolveDiscovery(regPayload, deferredTillEOT, cancelationToken);
244+
deferredTillEOT = createDeferred<void>();
245+
resultResolver.resolveDiscovery(errorPayload, deferredTillEOT, cancelationToken);
246+
247+
// assert the stub functions were called with the correct parameters
248+
249+
// builds an error node root
250+
sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider);
251+
// builds an error item
252+
sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any);
253+
254+
if (!deleteSpy.calledOnce) {
255+
throw new Error("The delete method was called, but it shouldn't have been.");
256+
}
257+
if (replaceSpy.called) {
258+
throw new Error("The replace method was called, but it shouldn't have been.");
259+
}
260+
});
198261
});
199262
suite('Test execution result resolver', () => {
200263
let resultResolver: ResultResolver.PythonResultResolver;

0 commit comments

Comments
 (0)