From 7992da251db4c6eedd382c85019057525c1a79bb Mon Sep 17 00:00:00 2001 From: agu2347 Date: Wed, 29 Jul 2026 15:15:50 +0000 Subject: [PATCH 1/2] 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. Verified directly against the exact reported scenario: calling make_reltoroot() with a relative path arg and an absolute root previously raised the exact "not relative to an rsync root" error; with the fix, it resolves correctly. Also verified: an absolute path arg (the case that already worked) is unaffected; an arg with a "::test_id" suffix is preserved correctly; and a non-existent relative path still passes through unchanged rather than raising. Added regression tests covering all of the above using pytester's tmp_path/monkeypatch.chdir, matching this test file's existing style. Confirmed the relative-path tests fail with the original code (reproducing the exact reported ValueError) and pass with the fix. Ran the full existing test_workermanage.py suite (23 passed: 19 baseline + 4 new; 4 pre-existing, unrelated warning-serialization failures confirmed identical with and without this change via a fixed random seed on a clean checkout). Fixes #971 --- src/xdist/workermanage.py | 11 ++++++- testing/test_workermanage.py | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 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..33ce4eba 100644 --- a/testing/test_workermanage.py +++ b/testing/test_workermanage.py @@ -519,3 +519,65 @@ 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"] From 373a2e18ecd5e96295917169c69fb5ee14ce5e64 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:16:48 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_workermanage.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/testing/test_workermanage.py b/testing/test_workermanage.py index 33ce4eba..a451de47 100644 --- a/testing/test_workermanage.py +++ b/testing/test_workermanage.py @@ -542,9 +542,7 @@ def test_relative_arg_inside_root( test_file.write_text("def test_x(): pass\n") monkeypatch.chdir(root) - result = workermanage.make_reltoroot( - [root], ["tests/test_sample.py"] - ) + 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( @@ -556,9 +554,7 @@ def test_relative_arg_with_test_id_suffix( test_file.write_text("def test_x(): pass\n") monkeypatch.chdir(root) - result = workermanage.make_reltoroot( - [root], ["tests/test_sample.py::test_x"] - ) + 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(