Skip to content

Commit 178a0b2

Browse files
authored
Fix bug with symlink for pytest execution (#22952)
Fixes #22938
1 parent bae7d40 commit 178a0b2

File tree

3 files changed

+86
-14
lines changed

3 files changed

+86
-14
lines changed

pythonFiles/tests/pytestadapter/expected_execution_test_output.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,3 +684,15 @@
684684
"subtest": None,
685685
},
686686
}
687+
688+
# Constant for the symlink execution test where TEST_DATA_PATH / "root" the target and TEST_DATA_PATH / "symlink_folder" the symlink
689+
test_a_symlink_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py"
690+
symlink_run_expected_execution_output = {
691+
get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path): {
692+
"test": get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path),
693+
"outcome": "success",
694+
"message": None,
695+
"traceback": None,
696+
"subtest": None,
697+
}
698+
}

pythonFiles/tests/pytestadapter/test_execution.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3+
import json
34
import os
45
import shutil
56
from typing import Any, Dict, List
@@ -8,7 +9,13 @@
89

910
from tests.pytestadapter import expected_execution_test_output
1011

11-
from .helpers import TEST_DATA_PATH, runner, runner_with_cwd
12+
from .helpers import (
13+
TEST_DATA_PATH,
14+
create_symlink,
15+
get_absolute_test_id,
16+
runner,
17+
runner_with_cwd,
18+
)
1219

1320

1421
def test_config_file():
@@ -276,3 +283,50 @@ def test_pytest_execution(test_ids, expected_const):
276283
if actual_result_dict[key]["traceback"] is not None:
277284
actual_result_dict[key]["traceback"] = "TRACEBACK"
278285
assert actual_result_dict == expected_const
286+
287+
288+
def test_symlink_run():
289+
"""
290+
Test to test pytest discovery with the command line arg --rootdir specified as a symlink path.
291+
Discovery should succeed and testids should be relative to the symlinked root directory.
292+
"""
293+
with create_symlink(TEST_DATA_PATH, "root", "symlink_folder") as (
294+
source,
295+
destination,
296+
):
297+
assert destination.is_symlink()
298+
test_a_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py"
299+
test_a_id = get_absolute_test_id(
300+
"tests/test_a.py::test_a_function",
301+
test_a_path,
302+
)
303+
304+
# Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node).
305+
actual = runner_with_cwd(
306+
[f"--rootdir={os.fspath(destination)}", test_a_id], source
307+
)
308+
309+
expected_const = (
310+
expected_execution_test_output.symlink_run_expected_execution_output
311+
)
312+
assert actual
313+
actual_list: List[Dict[str, Any]] = actual
314+
if actual_list is not None:
315+
assert actual_list.pop(-1).get("eot")
316+
actual_item = actual_list.pop(0)
317+
try:
318+
# Check if all requirements
319+
assert all(
320+
item in actual_item.keys() for item in ("status", "cwd", "result")
321+
), "Required keys are missing"
322+
assert actual_item.get("status") == "success", "Status is not 'success'"
323+
assert actual_item.get("cwd") == os.fspath(
324+
destination
325+
), f"CWD does not match: {os.fspath(destination)}"
326+
actual_result_dict = dict()
327+
actual_result_dict.update(actual_item["result"])
328+
assert actual_result_dict == expected_const
329+
except AssertionError as e:
330+
# Print the actual_item in JSON format if an assertion fails
331+
print(json.dumps(actual_item, indent=4))
332+
pytest.fail(str(e))

pythonFiles/vscode_pytest/__init__.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ def pytest_report_teststatus(report, config):
225225
config -- configuration object.
226226
"""
227227
cwd = pathlib.Path.cwd()
228+
if SYMLINK_PATH:
229+
cwd = SYMLINK_PATH
228230

229231
if report.when == "call":
230232
traceback = None
@@ -348,10 +350,7 @@ def pytest_sessionfinish(session, exitstatus):
348350
cwd = pathlib.Path.cwd()
349351
if SYMLINK_PATH:
350352
print("Plugin warning[vscode-pytest]: SYMLINK set, adjusting cwd.")
351-
# Get relative between the cwd (resolved path) and the node path.
352-
rel_path = os.path.relpath(cwd, pathlib.Path.cwd())
353-
# Calculate the new node path by making it relative to the symlink path.
354-
cwd = pathlib.Path(os.path.join(SYMLINK_PATH, rel_path))
353+
cwd = pathlib.Path(SYMLINK_PATH)
355354

356355
if IS_DISCOVERY:
357356
if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5):
@@ -681,9 +680,9 @@ def get_node_path(node: Any) -> pathlib.Path:
681680
A function that returns the path of a node given the switch to pathlib.Path.
682681
It also evaluates if the node is a symlink and returns the equivalent path.
683682
"""
684-
path = getattr(node, "path", None) or pathlib.Path(node.fspath)
683+
node_path = getattr(node, "path", None) or pathlib.Path(node.fspath)
685684

686-
if not path:
685+
if not node_path:
687686
raise VSCodePytestError(
688687
f"Unable to find path for node: {node}, node.path: {node.path}, node.fspath: {node.fspath}"
689688
)
@@ -692,17 +691,24 @@ def get_node_path(node: Any) -> pathlib.Path:
692691
if SYMLINK_PATH and not isinstance(node, pytest.Session):
693692
# Get relative between the cwd (resolved path) and the node path.
694693
try:
695-
rel_path = path.relative_to(pathlib.Path.cwd())
696-
697-
# Calculate the new node path by making it relative to the symlink path.
698-
sym_path = pathlib.Path(os.path.join(SYMLINK_PATH, rel_path))
699-
return sym_path
694+
# check to see if the node path contains the symlink root already
695+
common_path = os.path.commonpath([SYMLINK_PATH, node_path])
696+
if common_path == os.fsdecode(SYMLINK_PATH):
697+
# node path is already relative to the SYMLINK_PATH root therefore return
698+
return node_path
699+
else:
700+
# if the node path is not a symlink, then we need to calculate the equivalent symlink path
701+
# get the relative path between the cwd and the node path (as the node path is not a symlink)
702+
rel_path = node_path.relative_to(pathlib.Path.cwd())
703+
# combine the difference between the cwd and the node path with the symlink path
704+
sym_path = pathlib.Path(os.path.join(SYMLINK_PATH, rel_path))
705+
return sym_path
700706
except Exception as e:
701707
raise VSCodePytestError(
702708
f"Error occurred while calculating symlink equivalent from node path: {e}"
703-
"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {path}, \n cwd: {{pathlib.Path.cwd()}}"
709+
f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {pathlib.Path.cwd()}"
704710
)
705-
return path
711+
return node_path
706712

707713

708714
__socket = None

0 commit comments

Comments
 (0)