From 6280e01730522ac026cf3271f352d950ab8284d2 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Mon, 5 Jun 2023 14:11:01 -0700 Subject: [PATCH 1/3] add function node for parameterized tests pytest --- .../expected_discovery_test_output.py | 78 ++++++++------- pythonFiles/vscode_pytest/__init__.py | 96 ++++++++++++++----- 2 files changed, 117 insertions(+), 57 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 8e96d109ba78..5227c9a0503d 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -1,5 +1,4 @@ import os -import pathlib from .helpers import TEST_DATA_PATH, find_test_line_number @@ -389,9 +388,10 @@ # This is the expected output for the nested_folder tests. # └── parametrize_tests.py -# └── test_adding[3+5-8] -# └── test_adding[2+4-6] -# └── test_adding[6+9-16] +# └── test_adding +# └── test_adding[3+5-8] +# └── test_adding[2+4-6] +# └── test_adding[6+9-16] parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") parametrize_tests_expected_output = { "name": ".data", @@ -405,40 +405,48 @@ "id_": parameterize_tests_path, "children": [ { - "name": "test_adding[3+5-8]", + "name": "test_adding", "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[3+5-8]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[3+5-8]", - "runID": "parametrize_tests.py::test_adding[3+5-8]", - }, - { - "name": "test_adding[2+4-6]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[2+4-6]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[2+4-6]", - "runID": "parametrize_tests.py::test_adding[2+4-6]", - }, - { - "name": "test_adding[6+9-16]", - "path": parameterize_tests_path, - "lineno": find_test_line_number( - "test_adding[6+9-16]", - parameterize_tests_path, - ), - "type_": "test", - "id_": "parametrize_tests.py::test_adding[6+9-16]", - "runID": "parametrize_tests.py::test_adding[6+9-16]", + "type_": "function", + "id_": "parametrize_tests.py::test_adding", + "children": [ + { + "name": "test_adding[3+5-8]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[3+5-8]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[3+5-8]", + "runID": "parametrize_tests.py::test_adding[3+5-8]", + }, + { + "name": "test_adding[2+4-6]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[2+4-6]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[2+4-6]", + "runID": "parametrize_tests.py::test_adding[2+4-6]", + }, + { + "name": "test_adding[6+9-16]", + "path": parameterize_tests_path, + "lineno": find_test_line_number( + "test_adding[6+9-16]", + parameterize_tests_path, + ), + "type_": "test", + "id_": "parametrize_tests.py::test_adding[6+9-16]", + "runID": "parametrize_tests.py::test_adding[6+9-16]", + }, + ], }, ], - } + }, ], "id_": TEST_DATA_PATH_STR, } diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 0f0bcbd1d323..d7984e61f075 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -20,8 +20,8 @@ class TestData(TypedDict): """A general class that all test objects inherit from.""" name: str - path: str - type_: Literal["class", "file", "folder", "test", "error"] + path: pathlib.Path + type_: Literal["class", "function", "file", "folder", "test", "error"] id_: str @@ -196,12 +196,10 @@ def pytest_sessionfinish(session, exitstatus): ) post_response(os.fsdecode(cwd), session_node) except Exception as e: - ERRORS.append( - f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" - ) + f"Error Occurred, description: {e.args[0] if e.args and e.args[0] else ''} traceback: {(traceback.format_exc() if e.__traceback__ else '')}" errorNode: TestNode = { "name": "", - "path": "", + "path": cwd, "type_": "error", "children": [], "id_": "", @@ -232,6 +230,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: session_children_dict: Dict[str, TestNode] = {} file_nodes_dict: Dict[Any, TestNode] = {} class_nodes_dict: Dict[str, TestNode] = {} + function_nodes_dict: Dict[str, TestNode] = {} for test_case in session.items: test_node = create_test_node(test_case) @@ -256,6 +255,32 @@ def build_test_tree(session: pytest.Session) -> TestNode: # Check if the class is already a child of the file node. if 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 = "" + try: + function_name = test_case.originalname # type: ignore + function_test_case = function_nodes_dict[function_name] + except AttributeError: # actual error has occurred + ERRORS.append( + f"unable to find original name for {test_case.name} with parameterization detected." + ) + raise VSCodePytestError( + "Unable to find original name for parameterized test case" + ) + except KeyError: + function_test_case: TestNode = create_parameterized_function_node( + function_name, test_case.path, test_case.nodeid + ) + function_nodes_dict[function_name] = function_test_case + function_test_case["children"].append(test_node) + # Now, add the function node to file node. + try: + parent_test_case = file_nodes_dict[test_case.parent] + except KeyError: + parent_test_case = create_file_node(test_case.parent) + file_nodes_dict[test_case.parent] = parent_test_case + if function_test_case not in parent_test_case["children"]: + parent_test_case["children"].append(function_test_case) else: # This includes test cases that are pytest functions or a doctests. try: parent_test_case = file_nodes_dict[test_case.parent] @@ -264,10 +289,10 @@ def build_test_tree(session: pytest.Session) -> TestNode: file_nodes_dict[test_case.parent] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: Dict[str, TestNode] = {} - for file_module, file_node in file_nodes_dict.items(): + for _, file_node in file_nodes_dict.items(): # Iterate through all the files that exist and construct them into nested folders. root_folder_node: TestNode = build_nested_folders( - file_module, file_node, created_files_folders_dict, session + file_node, created_files_folders_dict, session ) # The final folder we get to is the highest folder in the path # and therefore we add this as a child to the session. @@ -279,7 +304,6 @@ def build_test_tree(session: pytest.Session) -> TestNode: def build_nested_folders( - file_module: Any, file_node: TestNode, created_files_folders_dict: Dict[str, TestNode], session: pytest.Session, @@ -295,7 +319,7 @@ def build_nested_folders( prev_folder_node = file_node # Begin the iterator_path one level above the current file. - iterator_path = file_module.path.parent + iterator_path = file_node["path"].parent while iterator_path != session.path: curr_folder_name = iterator_path.name try: @@ -325,7 +349,7 @@ def create_test_node( ) return { "name": test_case.name, - "path": os.fspath(test_case.path), + "path": test_case.path, "lineno": test_case_loc, "type_": "test", "id_": test_case.nodeid, @@ -341,7 +365,7 @@ def create_session_node(session: pytest.Session) -> TestNode: """ return { "name": session.name, - "path": os.fspath(session.path), + "path": session.path, "type_": "folder", "children": [], "id_": os.fspath(session.path), @@ -356,13 +380,34 @@ def create_class_node(class_module: pytest.Class) -> TestNode: """ return { "name": class_module.name, - "path": os.fspath(class_module.path), + "path": class_module.path, "type_": "class", "children": [], "id_": class_module.nodeid, } +def create_parameterized_function_node( + function_name: str, test_path: pathlib.Path, test_id: str +) -> TestNode: + """Creates a function node to be the parent for the parameterized test nodes. + + Keyword arguments: + function_name -- the name of the function. + test_path -- the path to the test file. + test_id -- the id of the test, which is a parameterized test so it + must be edited to get a unique id for the function node. + """ + function_id: str = test_id.split("::")[0] + "::" + function_name + return { + "name": function_name, + "path": test_path, + "type_": "function", + "children": [], + "id_": function_id, + } + + def create_file_node(file_module: Any) -> TestNode: """Creates a file node from a pytest file module. @@ -371,14 +416,14 @@ def create_file_node(file_module: Any) -> TestNode: """ return { "name": file_module.path.name, - "path": os.fspath(file_module.path), + "path": file_module.path, "type_": "file", "id_": os.fspath(file_module.path), "children": [], } -def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode: +def create_folder_node(folder_name: str, path_iterator: pathlib.Path) -> TestNode: """Creates a folder node from a pytest folder name and its path. Keyword arguments: @@ -386,8 +431,8 @@ def create_folder_node(folderName: str, path_iterator: pathlib.Path) -> TestNode path_iterator -- the path of the folder. """ return { - "name": folderName, - "path": os.fspath(path_iterator), + "name": folder_name, + "path": path_iterator, "type_": "folder", "id_": os.fspath(path_iterator), "children": [], @@ -451,6 +496,13 @@ def execution_post( print(f"[vscode-pytest] data: {request}") +class PathEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, pathlib.Path): + return os.fspath(obj) + return super().default(obj) + + def post_response(cwd: str, session_node: TestNode) -> None: """Sends a post request to the server. @@ -467,13 +519,13 @@ def post_response(cwd: str, session_node: TestNode) -> None: } if ERRORS is not None: payload["error"] = ERRORS - testPort: Union[str, int] = os.getenv("TEST_PORT", 45454) - testuuid: Union[str, None] = os.getenv("TEST_UUID") - addr = "localhost", int(testPort) - data = json.dumps(payload) + test_port: Union[str, int] = os.getenv("TEST_PORT", 45454) + test_uuid: Union[str, None] = os.getenv("TEST_UUID") + addr = "localhost", int(test_port) + data = json.dumps(payload, cls=PathEncoder) request = f"""Content-Length: {len(data)} Content-Type: application/json -Request-uuid: {testuuid} +Request-uuid: {test_uuid} {data}""" try: From 293ec854a7e13a88c1b55d456edcebb3964e8809 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Mon, 5 Jun 2023 14:12:53 -0700 Subject: [PATCH 2/3] add doc string --- pythonFiles/vscode_pytest/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index d7984e61f075..3c4cb638aa7e 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -497,6 +497,8 @@ def execution_post( class PathEncoder(json.JSONEncoder): + """A custom JSON encoder that encodes pathlib.Path objects as strings.""" + def default(self, obj): if isinstance(obj, pathlib.Path): return os.fspath(obj) From 84c9e66efa8a6fd9c01b054d70a639327b7a8d30 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Wed, 7 Jun 2023 17:27:20 -0700 Subject: [PATCH 3/3] fix naming of tests displayed --- .../pytestadapter/expected_discovery_test_output.py | 12 ++++++------ pythonFiles/vscode_pytest/__init__.py | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py index 5227c9a0503d..8b2283029ac7 100644 --- a/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_discovery_test_output.py @@ -389,9 +389,9 @@ # This is the expected output for the nested_folder tests. # └── parametrize_tests.py # └── test_adding -# └── test_adding[3+5-8] -# └── test_adding[2+4-6] -# └── test_adding[6+9-16] +# └── [3+5-8] +# └── [2+4-6] +# └── [6+9-16] parameterize_tests_path = os.fspath(TEST_DATA_PATH / "parametrize_tests.py") parametrize_tests_expected_output = { "name": ".data", @@ -411,7 +411,7 @@ "id_": "parametrize_tests.py::test_adding", "children": [ { - "name": "test_adding[3+5-8]", + "name": "[3+5-8]", "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[3+5-8]", @@ -422,7 +422,7 @@ "runID": "parametrize_tests.py::test_adding[3+5-8]", }, { - "name": "test_adding[2+4-6]", + "name": "[2+4-6]", "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[2+4-6]", @@ -433,7 +433,7 @@ "runID": "parametrize_tests.py::test_adding[2+4-6]", }, { - "name": "test_adding[6+9-16]", + "name": "[6+9-16]", "path": parameterize_tests_path, "lineno": find_test_line_number( "test_adding[6+9-16]", diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 3c4cb638aa7e..4f539e4ea01d 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -257,6 +257,9 @@ def build_test_tree(session: pytest.Session) -> TestNode: test_file_node["children"].append(test_class_node) elif hasattr(test_case, "callspec"): # This means it is a parameterized test. function_name: str = "" + # parameterized test cases cut the repetitive part of the name off. + name_split = test_node["name"].split("[")[1] + test_node["name"] = "[" + name_split try: function_name = test_case.originalname # type: ignore function_test_case = function_nodes_dict[function_name]