From 1fc37f50d893e7dffbba281b544db40dbf017912 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Tue, 7 Jan 2025 14:45:31 -0800 Subject: [PATCH 1/4] switch to use file path as key in building test tree --- python_files/vscode_pytest/__init__.py | 37 ++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 8a54a7249d71..05c45a2ed677 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -500,7 +500,7 @@ def build_test_tree(session: pytest.Session) -> TestNode: """ session_node = create_session_node(session) session_children_dict: dict[str, TestNode] = {} - file_nodes_dict: dict[Any, TestNode] = {} + file_nodes_dict: dict[str, TestNode] = {} class_nodes_dict: dict[str, TestNode] = {} function_nodes_dict: dict[str, TestNode] = {} @@ -549,11 +549,13 @@ def build_test_tree(session: pytest.Session) -> TestNode: function_test_node["children"].append(test_node) # Check if the parent node of the function is file, if so create/add to this file node. if isinstance(test_case.parent, pytest.File): + # calculate the parent path of the test case + parent_path = get_node_path(test_case.parent) try: - parent_test_case = file_nodes_dict[test_case.parent] + parent_test_case = file_nodes_dict[os.fspath(parent_path)] except KeyError: - parent_test_case = create_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = parent_test_case if function_test_node not in parent_test_case["children"]: parent_test_case["children"].append(function_test_node) # If the parent is not a file, it is a class, add the function node as the test node to handle subsequent nesting. @@ -585,22 +587,24 @@ def build_test_tree(session: pytest.Session) -> TestNode: else: ERRORS.append(f"Test class {case_iter} has no parent") break + parent_path = get_node_path(parent_module) # Create a file node that has the last class as a child. try: - test_file_node: TestNode = file_nodes_dict[parent_module] + test_file_node: TestNode = file_nodes_dict[os.fspath(parent_path)] except KeyError: - test_file_node = create_file_node(parent_module) - file_nodes_dict[parent_module] = test_file_node + test_file_node = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = test_file_node # Check if the class is already a child of the file node. 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 not hasattr(test_case, "callspec"): # This includes test cases that are pytest functions or a doctests. + parent_path = get_node_path(test_case.parent) try: - parent_test_case = file_nodes_dict[test_case.parent] + parent_test_case = file_nodes_dict[os.fspath(parent_path)] except KeyError: - parent_test_case = create_file_node(test_case.parent) - file_nodes_dict[test_case.parent] = parent_test_case + parent_test_case = create_file_node(parent_path) + file_nodes_dict[os.fspath(parent_path)] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: dict[str, TestNode] = {} for file_node in file_nodes_dict.values(): @@ -758,18 +762,17 @@ def create_parameterized_function_node( } -def create_file_node(file_module: Any) -> TestNode: - """Creates a file node from a pytest file module. +def create_file_node(calculated_node_path: pathlib.Path) -> TestNode: + """Creates a file node from a path which has already been calculated using the get_node_path function. Keyword arguments: - file_module -- the pytest file module. + calculated_node_path -- the pytest file path. """ - node_path = get_node_path(file_module) return { - "name": node_path.name, - "path": node_path, + "name": calculated_node_path.name, + "path": calculated_node_path, "type_": "file", - "id_": os.fspath(node_path), + "id_": os.fspath(calculated_node_path), "children": [], } From 980c4f6b45e4dc809ec8fb3307b7e6cc9e9fddf8 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Tue, 7 Jan 2025 16:19:36 -0800 Subject: [PATCH 2/4] support pytest-ruff plugin --- .../.data/folder_with_script/script_random.py | 7 ++ .../.data/folder_with_script/test_simple.py | 7 ++ .../expected_discovery_test_output.py | 91 +++++++++++++++++++ .../tests/pytestadapter/test_discovery.py | 27 ++++++ python_files/vscode_pytest/__init__.py | 39 ++++++-- .../testController/common/resultResolver.ts | 16 ++-- .../testing/testController/common/utils.ts | 8 +- 7 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 python_files/tests/pytestadapter/.data/folder_with_script/script_random.py create mode 100644 python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py new file mode 100644 index 000000000000..d8c32027a9e6 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# This file has no test, it's just a random script. + +if __name__ == "__main__": + print("Hello World!") diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py new file mode 100644 index 000000000000..9f9bfb014f3d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py index aa74a424ea2a..d7e82acc6890 100644 --- a/python_files/tests/pytestadapter/expected_discovery_test_output.py +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -1577,3 +1577,94 @@ ], "id_": TEST_DATA_PATH_STR, } +# This is the expected output for the folder_with_script folder when run with ruff +# └── .data +# └── folder_with_script +# └── script_random.py +# └── ruff +# └── test_simple.py +# └── ruff +# └── test_function +ruff_test_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "folder_with_script", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "children": [ + { + "name": "script_random.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "script_random.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + } + ], + }, + { + "name": "test_simple.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + { + "name": "test_function", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": find_test_line_number( + "test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py index 276753149410..a0ba9c289864 100644 --- a/python_files/tests/pytestadapter/test_discovery.py +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -329,3 +329,30 @@ def test_config_sub_folder(): if actual_item.get("tests") is not None: tests: Any = actual_item.get("tests") assert tests.get("name") == "config_sub_folder" + + +def test_ruff_plugin(): + """Here the session node will be a subfolder of the workspace root and the test are in another subfolder. + + This tests checks to see if test node path are under the session node and if so the + session node is correctly updated to the common path. + """ + file_path = helpers.TEST_DATA_PATH / "folder_with_script" + actual = helpers.runner( + [os.fspath(file_path), "--collect-only", "--ruff"], + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert ( + actual_item.get("status") == "success" + ), f"Status is not 'success', error is: {actual_item.get('error')}" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.ruff_test_expected_output, + ["id_", "lineno", "name", "runID"], + ), f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.ruff_test_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 05c45a2ed677..348f373c62bc 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -10,14 +10,7 @@ import pathlib import sys import traceback -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Generator, - Literal, - TypedDict, -) +from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict, cast import pytest @@ -598,8 +591,19 @@ def build_test_tree(session: pytest.Session) -> TestNode: 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 not hasattr(test_case, "callspec"): - # This includes test cases that are pytest functions or a doctests. parent_path = get_node_path(test_case.parent) + # # This includes test cases that are pytest functions or a doctests. got here with ruff test + # is_ruff = False + # # 'script_a.py::ruff' + # for mark in test_case.own_markers: + # if mark.name == "ruff": + # # This is a ruff test, we do not want to include this in the tree. + # print("[vscode-pytest]: Skipping ruff test: ", test_case.nodeid) + # is_ruff = True + # if is_ruff: + # # cast RuffFile type to pytest.File + # print("is_ruff true") + # # parent_case = pytest.Module.from_parent(test_case.parent) try: parent_test_case = file_nodes_dict[os.fspath(parent_path)] except KeyError: @@ -607,9 +611,24 @@ def build_test_tree(session: pytest.Session) -> TestNode: file_nodes_dict[os.fspath(parent_path)] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: dict[str, TestNode] = {} - for file_node in file_nodes_dict.values(): + for name, file_node in file_nodes_dict.items(): # Iterate through all the files that exist and construct them into nested folders. root_folder_node: TestNode + print(name) + # import pytest_ruff + + # if isinstance(name, pytest_ruff.RuffFile): + # # if ruff file, get the other file node and add the ruff test to it + # other = None + # for key, value in file_nodes_dict.items(): + # if value == file_node and key != name: + # other = value + # break + # if other is None: + # raise ValueError(f"Could not find matching file node for {name}") + # other.children.append(file_node.children[0]) + # break + try: root_folder_node: TestNode = build_nested_folders( file_node, created_files_folders_dict, session_node diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts index 2ce6039adba0..80e57edbabd2 100644 --- a/src/client/testing/testController/common/resultResolver.ts +++ b/src/client/testing/testController/common/resultResolver.ts @@ -189,8 +189,10 @@ export class PythonResultResolver implements ITestResultResolver { // search through freshly built array of testItem to find the failed test and update UI. testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); + if (indiItem.uri) { + if (indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + } runInstance.errored(indiItem, message); } } @@ -210,8 +212,10 @@ export class PythonResultResolver implements ITestResultResolver { // search through freshly built array of testItem to find the failed test and update UI. testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { - message.location = new Location(indiItem.uri, indiItem.range); + if (indiItem.uri) { + if (indiItem.range) { + message.location = new Location(indiItem.uri, indiItem.range); + } runInstance.failed(indiItem, message); } } @@ -222,7 +226,7 @@ export class PythonResultResolver implements ITestResultResolver { if (grabTestItem !== undefined) { testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { + if (indiItem.uri) { runInstance.passed(grabTestItem); } } @@ -234,7 +238,7 @@ export class PythonResultResolver implements ITestResultResolver { if (grabTestItem !== undefined) { testCases.forEach((indiItem) => { if (indiItem.id === grabVSid) { - if (indiItem.uri && indiItem.range) { + if (indiItem.uri) { runInstance.skipped(grabTestItem); } } diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts index 1d2be64f537d..7c5344a5b9e9 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -209,10 +209,10 @@ export function populateTestTree( const testItem = testController.createTestItem(child.id_, child.name, Uri.file(child.path)); testItem.tags = [RunTestTag, DebugTestTag]; - const range = new Range( - new Position(Number(child.lineno) - 1, 0), - new Position(Number(child.lineno), 0), - ); + let range: Range | undefined; + if (child.lineno) { + range = new Range(new Position(Number(child.lineno) - 1, 0), new Position(Number(child.lineno), 0)); + } testItem.canResolveChildren = false; testItem.range = range; testItem.tags = [RunTestTag, DebugTestTag]; From 9e02ee5de1e560970978e201b240f0c247331ddf Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Tue, 7 Jan 2025 16:23:19 -0800 Subject: [PATCH 3/4] fix incorrect additions --- python_files/vscode_pytest/__init__.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py index 59882add22ac..0ba5fd62221a 100644 --- a/python_files/vscode_pytest/__init__.py +++ b/python_files/vscode_pytest/__init__.py @@ -10,7 +10,7 @@ import pathlib import sys import traceback -from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict, cast +from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, TypedDict import pytest @@ -595,24 +595,9 @@ def build_test_tree(session: pytest.Session) -> TestNode: file_nodes_dict[os.fspath(parent_path)] = parent_test_case parent_test_case["children"].append(test_node) created_files_folders_dict: dict[str, TestNode] = {} - for name, file_node in file_nodes_dict.items(): + for file_node in file_nodes_dict.values(): # Iterate through all the files that exist and construct them into nested folders. root_folder_node: TestNode - print(name) - # import pytest_ruff - - # if isinstance(name, pytest_ruff.RuffFile): - # # if ruff file, get the other file node and add the ruff test to it - # other = None - # for key, value in file_nodes_dict.items(): - # if value == file_node and key != name: - # other = value - # break - # if other is None: - # raise ValueError(f"Could not find matching file node for {name}") - # other.children.append(file_node.children[0]) - # break - try: root_folder_node: TestNode = build_nested_folders( file_node, created_files_folders_dict, session_node From 7ff6c0f44c4eae48e3173402dc135ac11cea8921 Mon Sep 17 00:00:00 2001 From: eleanorjboyd Date: Wed, 8 Jan 2025 11:27:04 -0800 Subject: [PATCH 4/4] update test-requirements --- build/test-requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/test-requirements.txt b/build/test-requirements.txt index af19987bc8cb..8b0ea1636157 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -36,3 +36,6 @@ pytest-json # for pytest-describe related tests pytest-describe + +# for pytest-ruff related tests +pytest-ruff