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"]