Skip to content

Commit 03f6e24

Browse files
vaclavHalaVáclav HálaeleanorjboydCopilot
authored
Improve performance of pytest discovery for large test suites (#25974)
Fixes #25973 At the moment this PR demonstrates how to fix the problem, however I'll be happy to change the code however you see fit. --------- Co-authored-by: Václav Hála <vaclav.hala@codasip.com> Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent ac39905 commit 03f6e24

1 file changed

Lines changed: 36 additions & 23 deletions

File tree

python_files/vscode_pytest/__init__.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,21 @@ class TestItem(TestData):
6161
class TestNode(TestData):
6262
"""A general class that handles all test data which contains children."""
6363

64-
children: list[TestNode | TestItem | None]
64+
children: Children
6565
lineno: NotRequired[str] # Optional field for class/function nodes
6666

6767

68+
class Children:
69+
def __init__(self, init=None):
70+
self._children = dict(init) if init is not None else {}
71+
72+
def add(self, child: TestNode | TestItem):
73+
self._children[child["id_"]] = child
74+
75+
def values(self):
76+
return list(self._children.values())
77+
78+
6879
class VSCodePytestError(Exception):
6980
"""A custom exception class for pytest errors."""
7081

@@ -439,7 +450,7 @@ def pytest_sessionfinish(session, exitstatus):
439450
"name": "",
440451
"path": test_root_path,
441452
"type_": "error",
442-
"children": [],
453+
"children": Children(),
443454
"id_": "",
444455
}
445456
send_discovery_message(os.fsdecode(test_root_path), error_node)
@@ -459,7 +470,7 @@ def pytest_sessionfinish(session, exitstatus):
459470
"name": "",
460471
"path": test_root_path,
461472
"type_": "error",
462-
"children": [],
473+
"children": Children(),
463474
"id_": "",
464475
}
465476
send_discovery_message(os.fsdecode(test_root_path), error_node)
@@ -664,8 +675,7 @@ def process_parameterized_test(
664675
)
665676
function_nodes_dict[parent_id] = function_test_node
666677

667-
if test_node not in function_test_node["children"]:
668-
function_test_node["children"].append(test_node)
678+
function_test_node["children"].add(test_node)
669679

670680
# Check if the parent node of the function is file, if so create/add to this file node.
671681
if isinstance(test_case.parent, pytest.File):
@@ -676,8 +686,7 @@ def process_parameterized_test(
676686
if parent_test_case is None:
677687
parent_test_case = create_file_node(parent_path)
678688
file_nodes_dict[parent_path_key] = parent_test_case
679-
if function_test_node not in parent_test_case["children"]:
680-
parent_test_case["children"].append(function_test_node)
689+
parent_test_case["children"].add(function_test_node)
681690

682691
# Return the function node as the test node to handle subsequent nesting
683692
return function_test_node
@@ -725,8 +734,7 @@ def build_test_tree(session: pytest.Session) -> TestNode:
725734
test_class_node = create_class_node(case_iter)
726735
class_nodes_dict[case_iter.nodeid] = test_class_node
727736
# Check if the class already has the child node. This will occur if the test is parameterized.
728-
if node_child_iter not in test_class_node["children"]:
729-
test_class_node["children"].append(node_child_iter)
737+
test_class_node["children"].add(node_child_iter)
730738
# Iterate up.
731739
node_child_iter = test_class_node
732740
case_iter = case_iter.parent
@@ -744,8 +752,8 @@ def build_test_tree(session: pytest.Session) -> TestNode:
744752
test_file_node = create_file_node(parent_path)
745753
file_nodes_dict[parent_path_key] = test_file_node
746754
# Check if the class is already a child of the file node.
747-
if test_class_node is not None and test_class_node not in test_file_node["children"]:
748-
test_file_node["children"].append(test_class_node)
755+
if test_class_node is not None:
756+
test_file_node["children"].add(test_class_node)
749757
elif not hasattr(test_case, "callspec"):
750758
# This includes test cases that are pytest functions or a doctests.
751759
if test_case.parent is None:
@@ -762,12 +770,13 @@ def build_test_tree(session: pytest.Session) -> TestNode:
762770
if parent_test_case is None:
763771
parent_test_case = create_file_node(parent_path)
764772
file_nodes_dict[parent_path_key] = parent_test_case
765-
parent_test_case["children"].append(test_node)
773+
parent_test_case["children"].add(test_node)
766774
# Process all files and construct them into nested folders
767775
session_children_dict = construct_nested_folders(
768776
file_nodes_dict, session_node, session_children_dict
769777
)
770-
session_node["children"] = list(session_children_dict.values())
778+
session_node["children"] = Children(session_children_dict)
779+
771780
return session_node
772781

773782

@@ -807,8 +816,7 @@ def build_nested_folders(
807816
if curr_folder_node is None:
808817
curr_folder_node = create_folder_node(curr_folder_name, iterator_path)
809818
created_files_folders_dict[iterator_path_key] = curr_folder_node
810-
if prev_folder_node not in curr_folder_node["children"]:
811-
curr_folder_node["children"].append(prev_folder_node)
819+
curr_folder_node["children"].add(prev_folder_node)
812820
iterator_path = iterator_path.parent
813821
prev_folder_node = curr_folder_node
814822
# Handles error where infinite loop occurs.
@@ -857,7 +865,7 @@ def create_session_node(session: pytest.Session) -> TestNode:
857865
"name": node_path.name,
858866
"path": node_path,
859867
"type_": "folder",
860-
"children": [],
868+
"children": Children(),
861869
"id_": os.fspath(node_path),
862870
}
863871

@@ -884,7 +892,7 @@ def create_class_node(class_module: pytest.Class | DescribeBlock) -> TestNode:
884892
"name": class_module.name,
885893
"path": get_node_path(class_module),
886894
"type_": "class",
887-
"children": [],
895+
"children": Children(),
888896
"id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)),
889897
"lineno": class_line,
890898
}
@@ -905,7 +913,7 @@ def create_parameterized_function_node(
905913
"name": function_name,
906914
"path": test_path,
907915
"type_": "function",
908-
"children": [],
916+
"children": Children(),
909917
"id_": function_id,
910918
}
911919

@@ -921,7 +929,7 @@ def create_file_node(calculated_node_path: pathlib.Path) -> TestNode:
921929
"path": calculated_node_path,
922930
"type_": "file",
923931
"id_": os.fspath(calculated_node_path),
924-
"children": [],
932+
"children": Children(),
925933
}
926934

927935

@@ -937,7 +945,7 @@ def create_folder_node(folder_name: str, path_iterator: pathlib.Path) -> TestNod
937945
"path": path_iterator,
938946
"type_": "folder",
939947
"id_": os.fspath(path_iterator),
940-
"children": [],
948+
"children": Children(),
941949
}
942950

943951

@@ -1092,15 +1100,20 @@ def send_discovery_message(cwd: str, session_node: TestNode) -> None:
10921100
}
10931101
if ERRORS is not None:
10941102
payload["error"] = ERRORS
1095-
send_message(payload, cls_encoder=PathEncoder)
1103+
send_message(payload, cls_encoder=CustomEncoder)
1104+
10961105

1106+
class CustomEncoder(json.JSONEncoder):
1107+
"""JSON encoder for pytest discovery payloads.
10971108
1098-
class PathEncoder(json.JSONEncoder):
1099-
"""A custom JSON encoder that encodes pathlib.Path objects as strings."""
1109+
Encodes `pathlib.Path` as strings and `Children` containers as JSON arrays.
1110+
"""
11001111

11011112
def default(self, o):
11021113
if isinstance(o, pathlib.Path):
11031114
return os.fspath(o)
1115+
if isinstance(o, Children):
1116+
return o.values()
11041117
return super().default(o)
11051118

11061119

0 commit comments

Comments
 (0)