diff --git a/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py b/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py new file mode 100644 index 000000000000..209f9d51915b --- /dev/null +++ b/pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class TestFirstClass: + class TestSecondClass: + def test_second(self): # test_marker--test_second + assert 1 == 2 + + def test_first(self): # test_marker--test_first + assert 1 == 2 + + class TestSecondClass2: + def test_second2(self): # test_marker--test_second2 + assert 1 == 1 + + +def test_independent(): # test_marker--test_independent + assert 1 == 1 diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 31686d2b3b5d..2d86710e776b 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -886,3 +886,111 @@ ], "id_": os.fspath(tests_path), } +TEST_MULTI_CLASS_NEST_PATH = TEST_DATA_PATH / "test_multi_class_nest.py" + +nested_classes_expected_test_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "test_multi_class_nest.py", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "file", + "id_": str(TEST_MULTI_CLASS_NEST_PATH), + "children": [ + { + "name": "TestFirstClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": "test_multi_class_nest.py::TestFirstClass", + "children": [ + { + "name": "TestSecondClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass", + "children": [ + { + "name": "test_second", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + { + "name": "test_first", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_first", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + { + "name": "TestSecondClass2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", + "children": [ + { + "name": "test_second2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second2", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + ], + }, + { + "name": "test_independent", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_independent", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + ], + } + ], + "id_": str(TEST_DATA_PATH), +} diff --git a/pythonFiles/tests/pytestadapter/test_discovery.py b/pythonFiles/tests/pytestadapter/test_discovery.py index 674d92ac0545..8cc10e6ae9f1 100644 --- a/pythonFiles/tests/pytestadapter/test_discovery.py +++ b/pythonFiles/tests/pytestadapter/test_discovery.py @@ -123,6 +123,10 @@ def test_parameterized_error_collect(): @pytest.mark.parametrize( "file, expected_const", [ + ( + "test_multi_class_nest.py", + expected_discovery_test_output.nested_classes_expected_test_output, + ), ( "unittest_skiptest_file_level.py", expected_discovery_test_output.unittest_skip_file_level_expected_output, diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 51c5e5e14fda..43f9570aa982 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -391,25 +391,37 @@ def build_test_tree(session: pytest.Session) -> TestNode: for test_case in session.items: test_node = create_test_node(test_case) if isinstance(test_case.parent, pytest.Class): - try: - test_class_node = class_nodes_dict[test_case.parent.nodeid] - except KeyError: - test_class_node = create_class_node(test_case.parent) - class_nodes_dict[test_case.parent.nodeid] = test_class_node - test_class_node["children"].append(test_node) - if test_case.parent.parent: - parent_module = test_case.parent.parent + case_iter = test_case.parent + node_child_iter = test_node + test_class_node: Union[TestNode, None] = None + while isinstance(case_iter, pytest.Class): + # While the given node is a class, create a class and nest the previous node as a child. + try: + test_class_node = class_nodes_dict[case_iter.nodeid] + except KeyError: + test_class_node = create_class_node(case_iter) + class_nodes_dict[case_iter.nodeid] = test_class_node + test_class_node["children"].append(node_child_iter) + # Iterate up. + node_child_iter = test_class_node + case_iter = case_iter.parent + # Now the parent node is not a class node, it is a file node. + if case_iter: + parent_module = case_iter else: - ERRORS.append(f"Test class {test_case.parent} has no parent") + ERRORS.append(f"Test class {case_iter} has no parent") break - # Create a file node that has the class as a child. + # Create a file node that has the last class as a child. try: test_file_node: TestNode = file_nodes_dict[parent_module] except KeyError: test_file_node = create_file_node(parent_module) file_nodes_dict[parent_module] = test_file_node # Check if the class is already a child of the file node. - if test_class_node not in test_file_node["children"]: + if ( + test_class_node is not None + and test_class_node not in test_file_node["children"] + ): test_file_node["children"].append(test_class_node) elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = ""