Skip to content
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
4 changes: 2 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1635,8 +1635,8 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> N
nodeid = ""
if nodeid == ".":
nodeid = ""
if os.sep != nodes.SEP:
nodeid = nodeid.replace(os.sep, nodes.SEP)
elif nodeid:
nodeid = nodes.norm_sep(nodeid)
else:
nodeid = None

Expand Down
20 changes: 17 additions & 3 deletions src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@

SEP = "/"


def norm_sep(path: str | os.PathLike[str]) -> str:
"""Normalize path separators to forward slashes for nodeid compatibility.

Replaces backslashes with forward slashes. This handles both Windows native
paths and cross-platform data (e.g., Windows paths in serialized test reports
when running on Linux).

:param path: A path string or PathLike object.
:returns: String with all backslashes replaced by forward slashes.
"""
return os.fspath(path).replace("\\", SEP)


tracebackcutdir = Path(_pytest.__file__).parent


Expand Down Expand Up @@ -589,7 +603,7 @@ def __init__(
pass
else:
name = str(rel)
name = name.replace(os.sep, SEP)
name = norm_sep(name)
self.path = path

if session is None:
Expand All @@ -602,8 +616,8 @@ def __init__(
except ValueError:
nodeid = _check_initialpaths_for_relpath(session._initialpaths, path)

if nodeid and os.sep != SEP:
nodeid = nodeid.replace(os.sep, SEP)
if nodeid:
nodeid = norm_sep(nodeid)

super().__init__(
name=name,
Expand Down
4 changes: 1 addition & 3 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,9 +1034,7 @@ def mkrel(nodeid: str) -> str:
# fspath comes from testid which has a "/"-normalized path.
if fspath:
res = mkrel(nodeid)
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
"\\", nodes.SEP
):
if self.verbosity >= 2 and nodeid.split("::")[0] != nodes.norm_sep(fspath):
res += " <- " + bestrelpath(self.startpath, Path(fspath))
else:
res = "[location]"
Expand Down
31 changes: 31 additions & 0 deletions testing/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ def test():
items[0].warn(Exception("ok")) # type: ignore[arg-type]


class TestNormSep:
"""Tests for the norm_sep helper function."""

def test_forward_slashes_unchanged(self) -> None:
"""Forward slashes pass through unchanged."""
assert nodes.norm_sep("a/b/c") == "a/b/c"

def test_backslashes_converted(self) -> None:
"""Backslashes are converted to forward slashes."""
assert nodes.norm_sep("a\\b\\c") == "a/b/c"

def test_mixed_separators(self) -> None:
"""Mixed separators are all normalized to forward slashes."""
assert nodes.norm_sep("a\\b/c\\d") == "a/b/c/d"

def test_pathlike_input(self, tmp_path: Path) -> None:
"""PathLike objects are converted to string with normalized separators."""
# Create a path and verify it's normalized
result = nodes.norm_sep(tmp_path / "subdir" / "file.py")
assert "\\" not in result
assert "subdir/file.py" in result

def test_empty_string(self) -> None:
"""Empty string returns empty string."""
assert nodes.norm_sep("") == ""

def test_windows_absolute_path(self) -> None:
"""Windows absolute paths have backslashes converted."""
assert nodes.norm_sep("C:\\Users\\test\\project") == "C:/Users/test/project"


def test__check_initialpaths_for_relpath() -> None:
"""Ensure that it handles dirs, and does not always use dirname."""
cwd = Path.cwd()
Expand Down
Loading