From e9ef11a76559f04a3cbbdeb134e81d4adce6402b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Rubinstein?= Date: Fri, 26 Jun 2026 15:02:44 +0200 Subject: [PATCH] test: enforce 100% coverage in CI addopts in pyproject runs coverage on every pytest invocation with fail_under=100, so the existing test step in ci.yml and release.yml now gates it. To close the gaps: tests for the verbose command echo, run()'s error path, main()'s CommandError handler, and the `python -m` entrypoint. Dropped the unused include_untracked parameter on worktree_dirty rather than test dead code; the version fallback is marked no-cover as it only runs from an uninstalled source tree. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 10 +++++++++ src/git_merge_onto/__init__.py | 9 +++------ tests/test_merge.py | 37 ++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e08990..8315a55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,3 +30,13 @@ build-backend = "uv_build" [dependency-groups] dev = ["pytest>=8", "pytest-cov>=5"] + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=term-missing" + +[tool.coverage.run] +source = ["git_merge_onto"] + +[tool.coverage.report] +fail_under = 100 +show_missing = true diff --git a/src/git_merge_onto/__init__.py b/src/git_merge_onto/__init__.py index e6bfe00..488bdc7 100644 --- a/src/git_merge_onto/__init__.py +++ b/src/git_merge_onto/__init__.py @@ -27,7 +27,7 @@ try: __version__ = _pkg_version("git-merge-onto") -except PackageNotFoundError: # running from a source tree with no install +except PackageNotFoundError: # pragma: no cover - running from a source tree with no install __version__ = "0+unknown" # Point at a specific git binary (used by the test suite; "git" otherwise). @@ -109,11 +109,8 @@ def git_dir() -> Path: return Path(git("rev-parse", "--absolute-git-dir")) -def worktree_dirty(include_untracked: bool = True) -> bool: - args = ["status", "--porcelain"] - if not include_untracked: - args.append("--untracked-files=no") - return bool(git(*args)) +def worktree_dirty() -> bool: + return bool(git("status", "--porcelain")) def in_progress_merge() -> bool: diff --git a/tests/test_merge.py b/tests/test_merge.py index a49a63f..943a209 100644 --- a/tests/test_merge.py +++ b/tests/test_merge.py @@ -1,7 +1,9 @@ """Behavioral tests against real git repos: the three re-parent regimes, conflict handling, and the precondition guards.""" +import runpy import subprocess +import sys import pytest @@ -326,6 +328,41 @@ def test_resolve_commit_origin_fallback(repo): assert gmo._resolve_commit("nope") is None +def test_verbose_echoes_command(capsys, monkeypatch): + # --quiet silences this; the default echoes each git command to stderr. + monkeypatch.setattr(gmo, "VERBOSE", True) + gmo._log_cmd(["git", "merge-recursive", "base"]) + assert "Executing: git merge-recursive base" in capsys.readouterr().err + + +def test_git_raises_command_error_on_failure(repo): + # A failing git command with check=True surfaces as CommandError. + with pytest.raises(gmo.CommandError): + gmo.git("rev-parse", "--verify", "definitely-not-a-ref") + + +def test_main_reports_command_error(monkeypatch): + def boom(*_args): + raise gmo.CommandError(["git", "merge-recursive"], 3, "kaboom") + + monkeypatch.setattr(gmo, "cmd_merge_onto", boom) + assert gmo.main(["--quiet", "new", "old"]) == 3 + + +def test_dunder_main_entrypoint(repo, monkeypatch): + # `python -m git_merge_onto` runs __main__.py, which calls sys.exit(main()). + commit_file(repo, "f.txt", "x\n", "main") + sh(repo, "switch", "-q", "-c", "a") + commit_file(repo, "a.txt", "A\n", "a") + sh(repo, "switch", "-q", "-c", "b") + commit_file(repo, "b.txt", "B\n", "b") + monkeypatch.setattr(sys, "argv", ["git-merge-onto", "--quiet", "main", "a"]) + + with pytest.raises(SystemExit) as excinfo: + runpy.run_module("git_merge_onto.__main__", run_name="__main__") + assert excinfo.value.code == 0 + + def test_merge_abort_recovers_from_conflict(repo): # The conflict message points the user at `git merge --abort`; it works only because # the markers include ORIG_HEAD and MERGE_MODE, not just MERGE_HEAD/MERGE_MSG.