Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug with symlink for pytest execution #22952

Merged
merged 3 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pythonFiles/tests/pytestadapter/expected_execution_test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
56 changes: 55 additions & 1 deletion pythonFiles/tests/pytestadapter/test_execution.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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))
32 changes: 19 additions & 13 deletions pythonFiles/vscode_pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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}"
)
Expand All @@ -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
Expand Down
Loading