Skip to content

Commit a6b69d1

Browse files
committed
support pytest-subtests plugin
1 parent 74a5cad commit a6b69d1

File tree

8 files changed

+98
-8
lines changed

8 files changed

+98
-8
lines changed

build/test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pytest-json
3636

3737
# for pytest-describe related tests
3838
pytest-describe
39+
pytest-subtests
3940

4041
# for pytest-ruff related tests
4142
pytest-ruff
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
def test_a(subtests):
5+
with subtests.test(msg="test_a"):
6+
assert 1 == 1
7+
with subtests.test(msg="Second subtest"):
8+
assert 2 == 1

python_files/tests/pytestadapter/expected_execution_test_output.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::"
77
SUCCESS = "success"
88
FAILURE = "failure"
9+
SUBTEST_FAILURE = "subtest-failure"
910

1011
# This is the expected output for the unittest_folder execute tests
1112
# └── unittest_folder
@@ -735,6 +736,41 @@
735736
},
736737
}
737738

739+
# This is the expected output for the test_pytest_subtests_plugin.py file.
740+
# └── test_pytest_subtests_plugin.py
741+
# └── test_a
742+
# └── test_a [test_a]: subtest-success
743+
# └── test_a [Second subtest]: subtest-failure
744+
test_pytest_subtests_plugin_path = TEST_DATA_PATH / "test_pytest_subtests_plugin.py"
745+
pytest_subtests_plugin_expected_execution_output = {
746+
get_absolute_test_id(
747+
"test_pytest_subtests_plugin.py::test_a**{test_a [test_a]}**",
748+
test_pytest_subtests_plugin_path,
749+
): {
750+
"test": get_absolute_test_id(
751+
"test_pytest_subtests_plugin.py::test_a**{test_a [test_a]}**",
752+
test_pytest_subtests_plugin_path,
753+
),
754+
"outcome": "subtest-success",
755+
"message": None,
756+
"traceback": None,
757+
"subtest": None,
758+
},
759+
get_absolute_test_id(
760+
"test_pytest_subtests_plugin.py::test_a**{test_a [Second subtest]}**",
761+
test_pytest_subtests_plugin_path,
762+
): {
763+
"test": get_absolute_test_id(
764+
"test_pytest_subtests_plugin.py::test_a**{test_a [Second subtest]}**",
765+
test_pytest_subtests_plugin_path,
766+
),
767+
"outcome": SUBTEST_FAILURE,
768+
"message": "ERROR MESSAGE",
769+
"traceback": None,
770+
"subtest": None,
771+
},
772+
}
773+
738774
skip_test_fixture_path = TEST_DATA_PATH / "skip_test_fixture.py"
739775
skip_test_fixture_execution_expected_output = {
740776
get_absolute_test_id("skip_test_fixture.py::test_docker_client", skip_test_fixture_path): {

python_files/tests/pytestadapter/test_execution.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,34 @@ def test_pytest_execution(test_ids, expected_const):
232232
assert actual_result_dict == expected_const
233233

234234

235+
def test_pytest_subtest_plugin():
236+
test_subtest_plugin_path = TEST_DATA_PATH / "test_pytest_subtests_plugin.py"
237+
238+
test_a_id = get_absolute_test_id(
239+
"test_subtest_plugin_path::test_a",
240+
test_subtest_plugin_path,
241+
)
242+
args = [test_a_id]
243+
expected_const = expected_execution_test_output.pytest_subtests_plugin_expected_execution_output
244+
actual = runner(args)
245+
assert actual
246+
actual_list: List[Dict[str, Dict[str, Any]]] = actual
247+
assert len(actual_list) == len(expected_const)
248+
actual_result_dict = {}
249+
if actual_list is not None:
250+
for actual_item in actual_list:
251+
assert all(item in actual_item for item in ("status", "cwd", "result"))
252+
assert actual_item.get("status") == "success"
253+
assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH)
254+
actual_result_dict.update(actual_item["result"])
255+
for key in actual_result_dict:
256+
if actual_result_dict[key]["outcome"] == "subtest-failure":
257+
actual_result_dict[key]["message"] = "ERROR MESSAGE"
258+
if actual_result_dict[key]["traceback"] is not None:
259+
actual_result_dict[key]["traceback"] = "TRACEBACK"
260+
assert actual_result_dict == expected_const
261+
262+
235263
def test_symlink_run():
236264
"""Test to test pytest discovery with the command line arg --rootdir specified as a symlink path.
237265

python_files/vscode_pytest/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,19 @@ def pytest_report_teststatus(report, config): # noqa: ARG001
271271
node_path = map_id_to_path[report.nodeid]
272272
except KeyError:
273273
node_path = cwd
274-
# Calculate the absolute test id and use this as the ID moving forward.
274+
275275
absolute_node_id = get_absolute_test_id(report.nodeid, node_path)
276+
parent_test_name = report.nodeid.split("::")[-1]
277+
if report.head_line and report.head_line != parent_test_name:
278+
# add parent node to collected_tests_so_far to not double report
279+
collected_tests_so_far.append(absolute_node_id)
280+
# If the report has a head_line, then it is a pytest-subtest
281+
# and we need to adjust the nodeid to reflect the subtest.
282+
if report_value == "failure":
283+
report_value = "subtest-failure"
284+
elif report_value == "success":
285+
report_value = "subtest-success"
286+
absolute_node_id = absolute_node_id + "**{" + report.head_line + "}**"
276287
if absolute_node_id not in collected_tests_so_far:
277288
collected_tests_so_far.append(absolute_node_id)
278289
item_result = create_test_outcome(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export class PythonResultResolver implements ITestResultResolver {
246246
}
247247
} else if (testItem.outcome === 'subtest-failure') {
248248
// split on [] or () based on how the subtest is setup.
249-
const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp);
249+
const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp, this.testProvider);
250250
const parentTestItem = this.runIdToTestItem.get(parentTestCaseId);
251251
const data = testItem;
252252
// find the subtest's parent test item
@@ -288,7 +288,7 @@ export class PythonResultResolver implements ITestResultResolver {
288288
}
289289
} else if (testItem.outcome === 'subtest-success') {
290290
// split on [] or () based on how the subtest is setup.
291-
const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp);
291+
const [parentTestCaseId, subtestId] = splitTestNameWithRegex(keyTemp, this.testProvider);
292292
const parentTestItem = this.runIdToTestItem.get(parentTestCaseId);
293293

294294
// find the subtest's parent test item

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { Deferred, createDeferred } from '../../../common/utils/async';
1919
import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes';
2020
import { EXTENSION_ROOT_DIR } from '../../../constants';
21+
import { TestProvider } from '../../types';
2122

2223
export function fixLogLinesNoTrailing(content: string): string {
2324
const lines = content.split(/\r?\n/g);
@@ -272,10 +273,15 @@ export function createDiscoveryErrorPayload(
272273
* @param testName The full test name string.
273274
* @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists.
274275
*/
275-
export function splitTestNameWithRegex(testName: string): [string, string] {
276+
export function splitTestNameWithRegex(testName: string, testProvider: TestProvider): [string, string] {
276277
// If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets).
277278
// Otherwise, return the entire testName for the parent and entire testName for the subtest.
278-
const regex = /^(.*?) ([\[(].*[\])])$/;
279+
let regex: RegExp;
280+
if (testProvider === 'pytest') {
281+
regex = /^(.*?)\*\*{(.*?)}\*\*$/;
282+
} else {
283+
regex = /^(.*?) ([\[(].*[\])])$/;
284+
}
279285
const match = testName.match(regex);
280286
if (match) {
281287
return [match[1].trim(), match[2] || match[3] || testName];

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// // Copyright (c) Microsoft Corporation. All rights reserved.
2-
// // Licensed under the MIT License.
1+
// // // Copyright (c) Microsoft Corporation. All rights reserved.
2+
// // // Licensed under the MIT License.
33

44
// import * as assert from 'assert';
55
// import {
@@ -106,7 +106,7 @@
106106
// assert.deepStrictEqual(rpcContent.remainingRawData, '');
107107
// });
108108

109-
// suite('Test Controller Utils: Other', () => {
109+
// suite('Test Controller Utils: Unittest', () => {
110110
// interface TestCase {
111111
// name: string;
112112
// input: string;

0 commit comments

Comments
 (0)