diff --git a/CHANGES b/CHANGES index ed81d385..24f46ca3 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,12 @@ $ uv add libvcs --prerelease allow _Notes on the upcoming release will go here._ +### Fixes + +- libvcs's bundled pytest plugin no longer overrides pytest's own + collection-ignore rules; suites that build their docs before running + no longer abort on the generated `_build/` output (#544). + ### Documentation #### Documentation examples now run as doctests (#540) diff --git a/src/libvcs/pytest_plugin.py b/src/libvcs/pytest_plugin.py index a80f9e7e..e4a6fe4d 100644 --- a/src/libvcs/pytest_plugin.py +++ b/src/libvcs/pytest_plugin.py @@ -119,18 +119,30 @@ def __next__(self) -> str: namer = RandomStrSequence() -def pytest_ignore_collect(collection_path: pathlib.Path, config: pytest.Config) -> bool: - """Skip tests if VCS binaries are missing.""" +def pytest_ignore_collect( + collection_path: pathlib.Path, + config: pytest.Config, +) -> bool | None: + """Skip collection of tests that require a missing VCS binary. + + ``pytest_ignore_collect`` is a ``firstresult`` hook: the first + implementation to return a non-``None`` value wins and short-circuits the + rest. This hook must therefore return ``None`` (abstain) for paths it has + no opinion on, so that other implementations -- gp-libs' Sphinx ``_build`` + skip and pytest's own ``__pycache__``/``norecursedirs``/``collect_ignore`` + handling -- still run. Returning ``False`` here would suppress all of them. + """ if (not shutil.which("svn") or not shutil.which("svnadmin")) and any( needle in str(collection_path) for needle in ["svn", "subversion"] ): return True if not shutil.which("git") and "git" in str(collection_path): return True - return bool( - not shutil.which("hg") - and any(needle in str(collection_path) for needle in ["hg", "mercurial"]), - ) + if not shutil.which("hg") and any( + needle in str(collection_path) for needle in ["hg", "mercurial"] + ): + return True + return None @pytest.fixture(scope="session") diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index e024d8c1..6c9373d2 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -11,6 +11,7 @@ import pytest from libvcs._internal.run import run +from libvcs.pytest_plugin import pytest_ignore_collect if t.TYPE_CHECKING: import pathlib @@ -311,3 +312,47 @@ def test_git_repo_fixture_submodule_file_protocol( f"git submodule add failed: {result}\n" "git_repo fixture needs set_home dependency for child processes" ) + + +def test_pytest_ignore_collect_abstains_on_non_vcs_path( + pytestconfig: pytest.Config, + tmp_path: pathlib.Path, +) -> None: + """Non-VCS paths abstain (``None``) rather than voting ``False``. + + ``pytest_ignore_collect`` is a ``firstresult`` hook, so returning ``False`` + would short-circuit the chain and suppress gp-libs' Sphinx ``_build`` skip + and pytest's builtin ignores. A path mentioning no VCS must yield ``None`` + regardless of which VCS binaries are installed. + """ + build_artifact = tmp_path / "docs" / "_build" / "html" / "history.md" + + assert pytest_ignore_collect(build_artifact, config=pytestconfig) is None + + +def test_pytest_ignore_collect_ignores_missing_vcs( + pytestconfig: pytest.Config, + monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, +) -> None: + """Paths for a missing VCS binary are ignored (``True``).""" + monkeypatch.setattr( + shutil, + "which", + lambda cmd, *args, **kwargs: None if cmd == "git" else f"/usr/bin/{cmd}", + ) + git_test = tmp_path / "tests" / "sync" / "test_git.py" + + assert pytest_ignore_collect(git_test, config=pytestconfig) is True + + +def test_pytest_ignore_collect_abstains_when_binaries_present( + pytestconfig: pytest.Config, + monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, +) -> None: + """A VCS path abstains when its binary is present, deferring to others.""" + monkeypatch.setattr(shutil, "which", lambda cmd, *args, **kwargs: f"/usr/bin/{cmd}") + git_test = tmp_path / "tests" / "sync" / "test_git.py" + + assert pytest_ignore_collect(git_test, config=pytestconfig) is None