diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d8d19fcac6d..84f90f946be 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -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 diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6690f6ab1f8..bc1dfc90d96 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -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 @@ -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: @@ -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, diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 837a78cc568..bb6f35633b9 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -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]" diff --git a/testing/test_nodes.py b/testing/test_nodes.py index de7875ca427..f66a11ce5c8 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -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()