diff --git a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py index 44f3d3d0abce..db4e493c3daa 100644 --- a/pythonFiles/tests/pytestadapter/expected_execution_test_output.py +++ b/pythonFiles/tests/pytestadapter/expected_execution_test_output.py @@ -684,3 +684,15 @@ "subtest": None, }, } + +# Constant for the symlink execution test where TEST_DATA_PATH / "root" the target and TEST_DATA_PATH / "symlink_folder" the symlink +test_a_symlink_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" +symlink_run_expected_execution_output = { + get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path): { + "test": get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/pythonFiles/tests/pytestadapter/test_execution.py b/pythonFiles/tests/pytestadapter/test_execution.py index dd32b61fa262..1defe2f52b82 100644 --- a/pythonFiles/tests/pytestadapter/test_execution.py +++ b/pythonFiles/tests/pytestadapter/test_execution.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import os import shutil from typing import Any, Dict, List @@ -8,7 +9,13 @@ from tests.pytestadapter import expected_execution_test_output -from .helpers import TEST_DATA_PATH, runner, runner_with_cwd +from .helpers import ( + TEST_DATA_PATH, + create_symlink, + get_absolute_test_id, + runner, + runner_with_cwd, +) def test_config_file(): @@ -276,3 +283,50 @@ def test_pytest_execution(test_ids, expected_const): if actual_result_dict[key]["traceback"] is not None: actual_result_dict[key]["traceback"] = "TRACEBACK" assert actual_result_dict == expected_const + + +def test_symlink_run(): + """ + Test to test pytest discovery with the command line arg --rootdir specified as a symlink path. + Discovery should succeed and testids should be relative to the symlinked root directory. + """ + with create_symlink(TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + test_a_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" + test_a_id = get_absolute_test_id( + "tests/test_a.py::test_a_function", + test_a_path, + ) + + # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). + actual = runner_with_cwd( + [f"--rootdir={os.fspath(destination)}", test_a_id], source + ) + + expected_const = ( + expected_execution_test_output.symlink_run_expected_execution_output + ) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + assert actual_list.pop(-1).get("eot") + actual_item = actual_list.pop(0) + try: + # Check if all requirements + assert all( + item in actual_item.keys() for item in ("status", "cwd", "result") + ), "Required keys are missing" + assert actual_item.get("status") == "success", "Status is not 'success'" + assert actual_item.get("cwd") == os.fspath( + destination + ), f"CWD does not match: {os.fspath(destination)}" + actual_result_dict = dict() + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) diff --git a/pythonFiles/vscode_pytest/__init__.py b/pythonFiles/vscode_pytest/__init__.py index 256f2bfdb099..bdeefde469f4 100644 --- a/pythonFiles/vscode_pytest/__init__.py +++ b/pythonFiles/vscode_pytest/__init__.py @@ -225,6 +225,8 @@ def pytest_report_teststatus(report, config): config -- configuration object. """ cwd = pathlib.Path.cwd() + if SYMLINK_PATH: + cwd = SYMLINK_PATH if report.when == "call": traceback = None @@ -348,10 +350,7 @@ def pytest_sessionfinish(session, exitstatus): cwd = pathlib.Path.cwd() if SYMLINK_PATH: print("Plugin warning[vscode-pytest]: SYMLINK set, adjusting cwd.") - # Get relative between the cwd (resolved path) and the node path. - rel_path = os.path.relpath(cwd, pathlib.Path.cwd()) - # Calculate the new node path by making it relative to the symlink path. - cwd = pathlib.Path(os.path.join(SYMLINK_PATH, rel_path)) + cwd = pathlib.Path(SYMLINK_PATH) if IS_DISCOVERY: if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): @@ -681,9 +680,9 @@ def get_node_path(node: Any) -> pathlib.Path: A function that returns the path of a node given the switch to pathlib.Path. It also evaluates if the node is a symlink and returns the equivalent path. """ - path = getattr(node, "path", None) or pathlib.Path(node.fspath) + node_path = getattr(node, "path", None) or pathlib.Path(node.fspath) - if not path: + if not node_path: raise VSCodePytestError( f"Unable to find path for node: {node}, node.path: {node.path}, node.fspath: {node.fspath}" ) @@ -692,17 +691,24 @@ def get_node_path(node: Any) -> pathlib.Path: if SYMLINK_PATH and not isinstance(node, pytest.Session): # Get relative between the cwd (resolved path) and the node path. try: - rel_path = path.relative_to(pathlib.Path.cwd()) - - # Calculate the new node path by making it relative to the symlink path. - sym_path = pathlib.Path(os.path.join(SYMLINK_PATH, rel_path)) - return sym_path + # check to see if the node path contains the symlink root already + common_path = os.path.commonpath([SYMLINK_PATH, node_path]) + if common_path == os.fsdecode(SYMLINK_PATH): + # node path is already relative to the SYMLINK_PATH root therefore return + return node_path + else: + # if the node path is not a symlink, then we need to calculate the equivalent symlink path + # get the relative path between the cwd and the node path (as the node path is not a symlink) + rel_path = node_path.relative_to(pathlib.Path.cwd()) + # combine the difference between the cwd and the node path with the symlink path + sym_path = pathlib.Path(os.path.join(SYMLINK_PATH, rel_path)) + return sym_path except Exception as e: raise VSCodePytestError( f"Error occurred while calculating symlink equivalent from node path: {e}" - "\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {path}, \n cwd: {{pathlib.Path.cwd()}}" + f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {pathlib.Path.cwd()}" ) - return path + return node_path __socket = None