From 60ad18cc9e6026678e1fd7209c8c4cddce1a5283 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Tue, 19 Dec 2023 12:24:44 -0500 Subject: [PATCH] support multi-level nested classes pytest --- .../.data/test_multi_class_nest.py | 19 +++ .../expected_discovery_test_output.py | 108 ++++++++++++++++++ .../tests/pytestadapter/test_discovery.py | 4 + pythonFiles/vscode_pytest/__init__.py | 28 +++-- 4 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 pythonFiles/tests/pytestadapter/.data/test_multi_class_nest.py 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..ede03242c9c0 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -391,18 +391,26 @@ 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 + while isinstance(case_iter, pytest.Class): + # create class node while the parent is a class node + 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: