Skip to content

Commit f2b0b7f

Browse files
authored
support multi-level nested classes pytest (#22681)
fixes #22520
1 parent 81c17e5 commit f2b0b7f

File tree

4 files changed

+154
-11
lines changed

4 files changed

+154
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
5+
class TestFirstClass:
6+
class TestSecondClass:
7+
def test_second(self): # test_marker--test_second
8+
assert 1 == 2
9+
10+
def test_first(self): # test_marker--test_first
11+
assert 1 == 2
12+
13+
class TestSecondClass2:
14+
def test_second2(self): # test_marker--test_second2
15+
assert 1 == 1
16+
17+
18+
def test_independent(): # test_marker--test_independent
19+
assert 1 == 1

Diff for: pythonFiles/tests/pytestadapter/expected_discovery_test_output.py

+108
Original file line numberDiff line numberDiff line change
@@ -886,3 +886,111 @@
886886
],
887887
"id_": os.fspath(tests_path),
888888
}
889+
TEST_MULTI_CLASS_NEST_PATH = TEST_DATA_PATH / "test_multi_class_nest.py"
890+
891+
nested_classes_expected_test_output = {
892+
"name": ".data",
893+
"path": TEST_DATA_PATH_STR,
894+
"type_": "folder",
895+
"children": [
896+
{
897+
"name": "test_multi_class_nest.py",
898+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
899+
"type_": "file",
900+
"id_": str(TEST_MULTI_CLASS_NEST_PATH),
901+
"children": [
902+
{
903+
"name": "TestFirstClass",
904+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
905+
"type_": "class",
906+
"id_": "test_multi_class_nest.py::TestFirstClass",
907+
"children": [
908+
{
909+
"name": "TestSecondClass",
910+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
911+
"type_": "class",
912+
"id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass",
913+
"children": [
914+
{
915+
"name": "test_second",
916+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
917+
"lineno": find_test_line_number(
918+
"test_second",
919+
str(TEST_MULTI_CLASS_NEST_PATH),
920+
),
921+
"type_": "test",
922+
"id_": get_absolute_test_id(
923+
"test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second",
924+
TEST_MULTI_CLASS_NEST_PATH,
925+
),
926+
"runID": get_absolute_test_id(
927+
"test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second",
928+
TEST_MULTI_CLASS_NEST_PATH,
929+
),
930+
}
931+
],
932+
},
933+
{
934+
"name": "test_first",
935+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
936+
"lineno": find_test_line_number(
937+
"test_first", str(TEST_MULTI_CLASS_NEST_PATH)
938+
),
939+
"type_": "test",
940+
"id_": get_absolute_test_id(
941+
"test_multi_class_nest.py::TestFirstClass::test_first",
942+
TEST_MULTI_CLASS_NEST_PATH,
943+
),
944+
"runID": get_absolute_test_id(
945+
"test_multi_class_nest.py::TestFirstClass::test_first",
946+
TEST_MULTI_CLASS_NEST_PATH,
947+
),
948+
},
949+
{
950+
"name": "TestSecondClass2",
951+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
952+
"type_": "class",
953+
"id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass2",
954+
"children": [
955+
{
956+
"name": "test_second2",
957+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
958+
"lineno": find_test_line_number(
959+
"test_second2",
960+
str(TEST_MULTI_CLASS_NEST_PATH),
961+
),
962+
"type_": "test",
963+
"id_": get_absolute_test_id(
964+
"test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2",
965+
TEST_MULTI_CLASS_NEST_PATH,
966+
),
967+
"runID": get_absolute_test_id(
968+
"test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2",
969+
TEST_MULTI_CLASS_NEST_PATH,
970+
),
971+
}
972+
],
973+
},
974+
],
975+
},
976+
{
977+
"name": "test_independent",
978+
"path": str(TEST_MULTI_CLASS_NEST_PATH),
979+
"lineno": find_test_line_number(
980+
"test_independent", str(TEST_MULTI_CLASS_NEST_PATH)
981+
),
982+
"type_": "test",
983+
"id_": get_absolute_test_id(
984+
"test_multi_class_nest.py::test_independent",
985+
TEST_MULTI_CLASS_NEST_PATH,
986+
),
987+
"runID": get_absolute_test_id(
988+
"test_multi_class_nest.py::test_independent",
989+
TEST_MULTI_CLASS_NEST_PATH,
990+
),
991+
},
992+
],
993+
}
994+
],
995+
"id_": str(TEST_DATA_PATH),
996+
}

Diff for: pythonFiles/tests/pytestadapter/test_discovery.py

+4
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ def test_parameterized_error_collect():
123123
@pytest.mark.parametrize(
124124
"file, expected_const",
125125
[
126+
(
127+
"test_multi_class_nest.py",
128+
expected_discovery_test_output.nested_classes_expected_test_output,
129+
),
126130
(
127131
"unittest_skiptest_file_level.py",
128132
expected_discovery_test_output.unittest_skip_file_level_expected_output,

Diff for: pythonFiles/vscode_pytest/__init__.py

+23-11
Original file line numberDiff line numberDiff line change
@@ -391,25 +391,37 @@ def build_test_tree(session: pytest.Session) -> TestNode:
391391
for test_case in session.items:
392392
test_node = create_test_node(test_case)
393393
if isinstance(test_case.parent, pytest.Class):
394-
try:
395-
test_class_node = class_nodes_dict[test_case.parent.nodeid]
396-
except KeyError:
397-
test_class_node = create_class_node(test_case.parent)
398-
class_nodes_dict[test_case.parent.nodeid] = test_class_node
399-
test_class_node["children"].append(test_node)
400-
if test_case.parent.parent:
401-
parent_module = test_case.parent.parent
394+
case_iter = test_case.parent
395+
node_child_iter = test_node
396+
test_class_node: Union[TestNode, None] = None
397+
while isinstance(case_iter, pytest.Class):
398+
# While the given node is a class, create a class and nest the previous node as a child.
399+
try:
400+
test_class_node = class_nodes_dict[case_iter.nodeid]
401+
except KeyError:
402+
test_class_node = create_class_node(case_iter)
403+
class_nodes_dict[case_iter.nodeid] = test_class_node
404+
test_class_node["children"].append(node_child_iter)
405+
# Iterate up.
406+
node_child_iter = test_class_node
407+
case_iter = case_iter.parent
408+
# Now the parent node is not a class node, it is a file node.
409+
if case_iter:
410+
parent_module = case_iter
402411
else:
403-
ERRORS.append(f"Test class {test_case.parent} has no parent")
412+
ERRORS.append(f"Test class {case_iter} has no parent")
404413
break
405-
# Create a file node that has the class as a child.
414+
# Create a file node that has the last class as a child.
406415
try:
407416
test_file_node: TestNode = file_nodes_dict[parent_module]
408417
except KeyError:
409418
test_file_node = create_file_node(parent_module)
410419
file_nodes_dict[parent_module] = test_file_node
411420
# Check if the class is already a child of the file node.
412-
if test_class_node not in test_file_node["children"]:
421+
if (
422+
test_class_node is not None
423+
and test_class_node not in test_file_node["children"]
424+
):
413425
test_file_node["children"].append(test_class_node)
414426
elif hasattr(test_case, "callspec"): # This means it is a parameterized test.
415427
function_name: str = ""

0 commit comments

Comments
 (0)