[codex] add python test infrastructure#42
Conversation
Refs #20 Co-authored-by: Codex <codex@openai.com>
hartsock
left a comment
There was a problem hiding this comment.
Reviewer: Drake (drake-interactive, Claude Opus 4.7 / 1M context), on behalf of @hartsock.
Solid parts
pyproject.tomlconfig (pytest options, coveragesource/branch/fail_under) matches issue #20 exactly.- The
mock_gitxtendfixture correctly patches two locations (gitxtend._gitxtend.XANDgitxtend.X). That second patch is necessary becausepython/gitxtend/__init__.pydoesfrom ._gitxtend import (...), so the names are bound into thegitxtendnamespace at import time — patching only the source module wouldn't reach call sites that look upgitxtend.is_git_repo. Good catch. pytestmark = pytest.mark.integrationontest_e2e.pyplus--strict-markersis the right shape..githooks/pre-pushadds a Windows code path via.venv/Scripts/maturin.exe.
Issues to address before un-drafting
1. test_mock_gitxtend_fixture_includes_write_side_exports is tautological
MagicMock returns True for hasattr(m, 'anything') regardless — the test would pass even if the fixture loop never ran. Replace with a check that actually inspects what the fixture did:
def test_mock_gitxtend_fixture_includes_write_side_exports(mock_gitxtend):
import gitxtend
import gitxtend._gitxtend as extension
for name in (
"pull", "push", "add", "commit",
"stash_push", "stash_pop", "create_branch",
"reset_hard", "rebase", "stash_rebase",
):
attr = getattr(mock_gitxtend, name)
assert getattr(extension, name) is attr # same mock landed on the source
sentinel = object()
attr.return_value = sentinel
assert getattr(gitxtend, name)() is sentinel # patched through to gitxtend.X2. The integration marker is declared but not used for filtering
Issue #20 says: "test_e2e.py is kept as an integration test (marked --integration) but does not block the coverage gate."
The PR runs pytest python/tests/ (collects everything, integration included), so integration tests contribute to the coverage measurement AND can fail the gate. Either:
- Split into two invocations:
pytest -m "not integration" --cov=gitxtend --cov-fail-under=80(unit, with the gate) followed bypytest -m integration(integration, no gate). - Or amend #20's intent so "everything contributes" is fine.
3. Wasted wheel build in CI
pip install -q ".[dev]" maturin builds + installs gitxtend's wheel via maturin's PEP 517 backend, then maturin develop --release immediately overwrites it. Cheaper:
.venv/bin/pip install -q maturin pytest pytest-cov pytest-mockNot a blocker — CI works as written — but a few minutes per run adds up over a daily cadence.
4. PUBLIC_EXTENSION_NAMES lists write-side names that don't exist yet
pull, push, add, commit, stash_push, etc. are not in python/gitxtend/__init__.py until a future PyO3-binding PR lands. The fixture handles this gracefully (raising=False), but the list will be a maintenance burden until the bindings land — and a typo here could go undetected for a while. Consider importing the list from a single source-of-truth tuple in python/gitxtend/__init__.py once both PRs settle, so a future addition only needs to be made in one place.
Recommended pre-merge checklist
- Rewrite
test_mock_gitxtend_fixture_includes_write_side_exportswith a non-tautological assertion (snippet above). - Decide on integration-vs-unit split for the coverage gate (or update #20 if "everything contributes" is intended).
- Trim the CI install command.
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
What changed
Why
This creates the Python test infrastructure needed by the remaining M2 Python issues and gives CI an 80% Python coverage gate.
Closes #20
Validation