From dcfcdc2c1fbe95ba08c6c2ceef5da250fe050fae Mon Sep 17 00:00:00 2001 From: Eleanor Boyd Date: Wed, 8 Jan 2025 14:50:09 -0800 Subject: [PATCH] support pytest-ruff plugin for testing (#24698) half fixes https://github.com/microsoft/vscode-python/issues/23933 --- build/test-requirements.txt | 3 + .../.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 | 9 +- .../testController/common/resultResolver.ts | 16 ++-- .../testing/testController/common/utils.ts | 8 +- 8 files changed, 150 insertions(+), 18 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/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 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 78526557ef1b..0ba5fd62221a 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 import pytest 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 b6848d0245dc..68e10a2213d6 100644 --- a/src/client/testing/testController/common/utils.ts +++ b/src/client/testing/testController/common/utils.ts @@ -195,10 +195,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];