From 73aae30a18252dbc6c991fc921797be4c46e85a3 Mon Sep 17 00:00:00 2001 From: agu2347 <94227848+agu2347@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:00:08 +0530 Subject: [PATCH] Resolve relative test-selection args before matching rsync roots make_reltoroot() (used to translate a test-selection arg like "tests/test_sample.py" into a path relative to an rsync root before sending it to a remote worker) constructed fspath = Path(parts[0]) directly from the given arg. py.path.local, used here prior to the project's migration to pathlib, transparently resolved a relative path against the current working directory as part of its own construction; plain pathlib.Path does not do this. As a result, a relative path given on the command line (e.g. running `pytest -d --tx socket=... tests/test_sample.py` from the project root) never compared as a match or subpath of any of the (absolute) rsync roots via relative_to(), even when it does in fact point inside one of them once resolved against the cwd -- raising "arg ... not relative to an rsync root" and breaking test selection entirely for any relative path, which worked correctly before the pathlib migration. Resolve fspath explicitly (Path.resolve()) before the existence check and root-matching loop, restoring the old py.path.local behavior. Fixes #971 [pre-commit.ci auto fixes squashed into this commit for a clean rebase] --- src/xdist/workermanage.py | 11 ++++++- testing/test_workermanage.py | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index c54b18fb..47f732e4 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -261,7 +261,16 @@ def make_reltoroot(roots: Sequence[Path], args: list[str]) -> list[str]: result = [] for arg in args: parts = arg.split(splitcode) - fspath = Path(parts[0]) + # py.path.local (used here prior to migrating to pathlib) + # transparently resolved a relative path against the current + # working directory. Plain pathlib.Path does not do this, so + # a relative path given on the command line (e.g. + # "tests/test_sample.py") would never compare equal to, or as + # a subpath of, any of the (absolute) rsync roots below via + # relative_to() -- even when it does in fact point inside one + # of them once resolved against the cwd. Resolve it explicitly + # to restore the old behavior. See GH #971. + fspath = Path(parts[0]).resolve() try: exists = fspath.exists() except OSError: diff --git a/testing/test_workermanage.py b/testing/test_workermanage.py index 4b393150..a451de47 100644 --- a/testing/test_workermanage.py +++ b/testing/test_workermanage.py @@ -519,3 +519,61 @@ def test_warning_serialization_tweaked_module() -> None: # __module__ cannot be found! with pytest.raises(ModuleNotFoundError): unserialize_warning_message(data) + + +class TestMakeReltoroot: + """Regression tests for GH#971. + + A relative path given as a test-selection arg on the command line + (e.g. ``pytest tests/test_sample.py``, as opposed to an absolute + path) must still be correctly recognized as being inside one of + the rsync roots. ``py.path.local`` (used here prior to migrating + to ``pathlib``) transparently resolved a relative path against the + current working directory; plain ``pathlib.Path`` does not do + this on its own. + """ + + def test_relative_arg_inside_root( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + root = tmp_path / "project" + (root / "tests").mkdir(parents=True) + test_file = root / "tests" / "test_sample.py" + test_file.write_text("def test_x(): pass\n") + + monkeypatch.chdir(root) + result = workermanage.make_reltoroot([root], ["tests/test_sample.py"]) + assert result == [f"{root.name}/tests/test_sample.py"] + + def test_relative_arg_with_test_id_suffix( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + root = tmp_path / "project" + (root / "tests").mkdir(parents=True) + test_file = root / "tests" / "test_sample.py" + test_file.write_text("def test_x(): pass\n") + + monkeypatch.chdir(root) + result = workermanage.make_reltoroot([root], ["tests/test_sample.py::test_x"]) + assert result == [f"{root.name}/tests/test_sample.py::test_x"] + + def test_absolute_arg_inside_root_still_works( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + root = tmp_path / "project" + (root / "tests").mkdir(parents=True) + test_file = root / "tests" / "test_sample.py" + test_file.write_text("def test_x(): pass\n") + + monkeypatch.chdir(tmp_path) # cwd unrelated to the arg itself + result = workermanage.make_reltoroot([root], [str(test_file)]) + assert result == [f"{root.name}/tests/test_sample.py"] + + def test_nonexistent_relative_arg_passes_through_unchanged( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + root = tmp_path / "project" + root.mkdir() + monkeypatch.chdir(root) + result = workermanage.make_reltoroot([root], ["does/not/exist.py"]) + assert result == ["does/not/exist.py"]