Add unified C++/Python coverage via xmsconan 2.15.2#126
Conversation
- Enable [ci].coverage and add [coverage] table (report-only thresholds) - Bump xmsconan pin to >=2.14.0 in regenerated CI workflows - Regenerate CI workflows including new Coverage.yaml - Document coverage workflow in README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Running `xmsconan coverage --version <ver> build.toml` locally (e.g. via
WSL) writes the same artifacts the CI workflow uploads into the
workspace root: cov-{cpp,py}.xml, cov-{cpp,py}-summary.json, and the
coverage-html-{cpp,py}/ directories. Keep them out of git so a
developer running coverage doesn't accidentally commit a snapshot.
The recipe-side .gcno/.gcda were already ignored; this rounds out the
list with the workspace-level outputs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xmsconan 2.14.0 had two issues now fixed upstream:
* The pybind extension was linked against CxxTest, so the coverage
job's pytest collection failed with an undefined symbol for
CxxTest::charToString.
* The C++ coverage report only counted lines exercised by the
pytest tests; the CxxTest binary's coverage was not included.
Pinning the floor to 2.15.1 forces fresh CI environments to pull the
fix instead of any cached 2.14.x. The README pointer is bumped to
match. Note: these YAMLs carry an "auto-generated by xmsconan_ci"
banner — if the workflows are regenerated, the floor will need to be
re-applied (or set in the generator).
The previous commit raised the version floor to 2.15.1, but the Coverage workflow still failed with the same CxxTest undefined symbol because the GitHub runner container (ghcr.io/aquaveo/conan-gcc13-py3.13:latest) ships with xmsconan pre-installed, and `pip install xmsconan>=X` is a no-op when the existing install already satisfies the constraint. Whether a run passes therefore depends on which xmsconan happens to be baked into that mutable :latest image at run time — not on the floor we declare. Add --upgrade so pip actively pulls the newest version that satisfies the floor from the dev index, regardless of what is pre-installed. Also `pip show xmsconan` after install so future log inspections show exactly which xmsconan version actually ran. Applied to all four pip-install steps: Coverage.yaml plus the three XmsCore-CI.yaml legs (Linux build, Linux test, Windows).
xmsconan 2.15.2 splits the coverage build into two independent
passes — a testing-only build drives CxxTest under gcov for
cov-cpp.xml, and a separate pybind-only build drives pytest-cov for
cov-py.xml. The single combined pybind+testing config that produced
the `undefined symbol: CxxTest::charToString` leak is no longer in
the matrix.
Regenerated with `xmsconan_ci --version 0.0.0 build.toml`. Notable
changes from the previous generation:
* Coverage.yaml drops the `container:` block — the workflow now
runs directly on ubuntu-latest with actions/setup-python@v5.
This breaks the silent-no-op coupling where the container's
pre-installed xmsconan satisfied `>=X.Y.Z` and locked the canary
to whatever xmsconan the image happened to carry.
* All four pip-install steps stay on `pip install --upgrade
"xmsconan>=2.15.2"` so they continue to pull the latest matching
release from devpi on every run.
The previous manual `pip show xmsconan` diagnostic lines were
dropped — they aren't part of the canonical template, and with the
container removed there's no longer a hidden-version footgun for
that diagnostic to surface. README pointer bumped to match.
There was a problem hiding this comment.
Code review of PR #126
Scope: changes in this PR only — pre-existing code is out of scope per the user's review policy.
Pure additive build-config/CI plumbing — no production C++ or Python source touched, so coverage/testing policy has nothing to enforce on this diff. The PR body is unusually well-grounded (numbers, per-file table, and upstream issue chain all verified against the latest XmsCore-Coverage run 25978408642). All three CI legs are green at 634155b9. No blockers. One scope-correct observation worth flagging: the new .flake8 is effectively inert under the (pre-existing) CI lint invocation.
Blockers
None.
Recommendations
.flake8(new file) is not consulted by the existing CI lint step —.github/workflows/XmsCore-CI.yamlalready invokesflake8 --isolated …with a hand-rolled argument list (that line is pre-existing, not changed by this PR —git diff master..feature/coverage -- .github/workflows/XmsCore-CI.yamlonly touches the threexmsconan>=2.15.2 --upgradeinstall lines). Because--isolatedignores every config file, the rules the new.flake8introduces beyond the CLI args — theB028, W503additions toignore, thebanned-modules = osgeo.*=…rule, and thetests/files/*/tests/fixtures/*excludes — are not actually enforced on PRs. Scope-respecting recommendation: either drop.flake8from this PR (since nothing exercises it), or accept that it's local-dev-only and document that in a comment at the top of the file. A CI-side change to honour.flake8(removing--isolatedand the inline args) is the cleaner long-term fix but is out of scope for this PR — file it as a follow-up. Not a release blocker.
Verified
- All three install steps in
XmsCore-CI.yamlthat this PR touches pinxmsconan>=2.15.2with--upgradeand consistent quoting (mac line 108, linux line 242, windows line 387). These are the only lines this PR changes in that file. Coverage.yaml(new) is containerless onubuntu-latest, usesactions/setup-python@v5, installsgcovr>=7,<9andxmsconan>=2.15.2 --upgrade, and uploads exactly the two artifact bundles the README documents.build.tomldiff adds[ci].coverage = trueand a[coverage]table with thresholds of0, matching the PR body's "report-only while baseline is being established" framing..gitignorediff covers all six local artifact patterns:cov-{cpp,py}.xml,cov-{cpp,py}-summary.json,coverage-html-{cpp,py}/.README.mddiff adds only a newCoveragesection that accurately describes the new flow (Linux-only, two HTML reports + two XML files, thresholds at 0). No other README sections are touched by this PR.pytest.ini(new) definestestpaths = _package/testsandpython_files = test_*.py *_test.py *_pyt.py, matching what Coverage CI consumes.- PR body numbers cross-check exactly against the Coverage run log: gcovr
TOTAL 3971 3503 88%, pytest25 passed in 0.47s, CxxTestTotal Test time (real) = 1.48 sec. Per-file table matches the gcovr report line-for-line (DynBitset 11/0/0%, XmLog 104/63/60%, Singleton 10/7/70%, functors.h 55/40/72%, Observer 129/105/81%, XmError 145/118/81%, StringUtil 807/720/89%, pt.h 590/540/91%, TimeConversion 155/144/92%, daStreamIo 1147/1108/96%, TestTools.cpp 156/22/14%, TestTools.h 43/17/39%). "Other files at 100%" claim also verified:math.cpp58,math.h26,Progress.cpp86,functors.cpp105,pt.cpp342,XmError.h2 — all 100% in the log. - Two-build flow visible in the run log: first build with
pybind=False testing=True(testing pass → cov-cpp.xml from CxxTest runner), second build with pybind enabled (pytest pass → cov-py.xml). - Branch history is clean — only commits relevant to the coverage feature: 22d4df7 (gitignore), 90199f7 (initial 2.14.0 wire-up), e013bce (2.15.1 bump), e3c8003 (--upgrade fix), 634155b (regenerate at 2.15.2).
git diff master..feature/coverage --statis 147 lines added across 7 files. No accidentally committed binaries, secrets, or local config.- Code conventions: nothing to check — no C++/Python production code in the diff.
Notes
.flake8line 20 setsisolated = trueas a config-file key. That key has no effect inside a config file (it's a CLI flag); harmless but slightly misleading if someone later edits.flake8expecting that knob to mean something. In-scope because.flake8is new in this PR.Coverage.yaml(new in this PR) usesactions/checkout@v2while it uses@v5forsetup-pythonand@v4forupload-artifact. The newly-added file is internally inconsistent on action versions; worth aligning to@v4at minimum within this PR's file.- Per the PR body's own follow-up list, raising
cpp_thresholdto 80 once the gaps land is the obvious next step — andDynBitset.cppat 0% / 11 lines is the cheapest gap to close first. Worth filing as an issue tied to this PR's merge so the follow-up doesn't get lost.
Scope correction note: A prior version of this review flagged (a) the --isolated flag in XmsCore-CI.yaml's lint step and (b) the README Testing section recommending python -m unittest discover. Both of those targets are pre-existing code that this PR does not modify, so per the user's "changes-only" review policy they have been dropped from the recommendations. The .flake8-is-inert observation is retained but reframed to avoid prescribing changes to pre-existing CI lines.
Summary
Adds a two-build instrumented Debug pass that produces both C++ (gcovr) and Python (pytest-cov) coverage reports for every push and pull request to master. Pure additive — no source code changes, only build-config and CI plumbing.
What this PR does
build.toml: sets[ci].coverage = trueand adds a[coverage]table with both thresholds at0(report-only mode while we establish the baseline). The existingpython_namespaced_dir = "core"is already in place, which is the only other thing the coverage flow needs..github/workflows/Coverage.yaml(new, generated byxmsconan ci): Linux-only job running directly onubuntu-latest(containerless under xmsconan 2.15.2, since the old container baked xmsconan in and caused silent floor-pin no-ops). Installsgcovr>=7,<9andxmsconan>=2.15.2, runsxmsconan_coverage, uploadscov-{cpp,py}.xmlandcoverage-html-{cpp,py}/as run artifacts..github/workflows/XmsCore-CI.yaml: regenerated, xmsconan pin bumped2.12.1→2.15.2and all install steps gained--upgradeso the floor is real (not silently overridden by a pre-installed copy)..flake8,pytest.ini: new files emitted byxmsconan genfrom the same template the rest of the family uses..gitignore: ignorescov-*.xml,cov-*-summary.json,coverage-html-*/so a developer running coverage locally doesn't accidentally commit a snapshot.README.md: short "Coverage" section explaining the workflow + artifacts.Baseline numbers from the first green Coverage CI run on the two-build flow
From
XmsCore-Coveragerun 25978408642 againstxmsconan==2.15.2:The C++ jump versus earlier runs of this PR (9.36% on 124/1325 lines) is structural, not a sudden test-writing surge: xmsconan 2.15.2 splits the instrumented build into a CxxTest-driven testing pass and a pytest-driven pybind pass.
cov-cpp.xmlnow reflects coverage from running the CxxTest binaries directly instead of whatever fraction of C++ symbols pytest happened to reach through the pybind ABI. The total line count grew because the testing build compiles the full set of library + testing_sources, whereas the prior pytest-only path only compiled the pybind-linked subset.Per-file C++ breakdown — current gaps
From the same gcovr report. Files marked "production" are real coverage gaps;
testing/TestTools.\*is the test-scaffold library itself, which won't ever be 100% (it has helpers no test currently invokes).xmscore/misc/DynBitset.cppxmscore/misc/XmLog.cppxmscore/misc/Singleton.hxmscore/points/functors.hxmscore/misc/Observer.cppxmscore/misc/XmError.cppxmscore/misc/StringUtil.cppxmscore/points/pt.hxmscore/time/TimeConversion.cppxmscore/dataio/daStreamIo.cppxmscore/testing/TestTools.cppxmscore/testing/TestTools.hOther files (
math.cpp,math.h,Progress.cpp,functors.cpp,pt.cpp,XmError.h) are at 100%.The thresholds are deliberately
0so the gate never fails while the baseline is being established. Once test-writing fills the remaining gaps, raise them in a follow-up.Upstream prerequisites
This works because xmsconan 2.15.2 finally has the coverage flow wired up correctly. The rollout went 2.14.0 → 2.15.2 over several releases, each one uncovering the next downstream gap once the previous one was fixed:
generate_configurationsDebug+pybind gate, filter shape, testing/pybind matrix exclusivity, multi-Python pinning.conan cache path --folder=sourcereference-shape fix.XMS_COVERAGEinto CMake buildenv; rglob for pytest-cov artifacts; loud-fail guard for empty gcovr summaries.2e978fe): passXMS_COVERAGEto CMake as a-Dcache variable since conan's[buildenv]activation isn't reaching the CMake subprocess.0131f6d): anchor gcovr--rootand the doubled filter against the build folder, not the conan source folder — sources are copied into the build folder before compilation, so.gcnopaths live there.pybind=True + testing=True + Debugconfig was structurally fragile — the pybind.socould pick up undefined CxxTest symbols (CxxTest::charToString) atdlopen. Replaced with two independent builds (CxxTest-only testing build forcov-cpp.xml; pybind-only build forcov-py.xml). Also drops the Coverage container so the canary always exercises whatever xmsconan is on devpi, and adds--upgradeto all CI install steps so the in-repo floor is real.Test plan
XmsCore-CIworkflow onfeature/coveragepasses on all three platforms (Mac, Linux, Windows × py3.10/3.13).XmsCore-Coverageworkflow produces real artifacts and exits 0.cov-cpp.xmlis non-empty Cobertura XML withline-rate="0.8821"andlines-valid="3971".cov-py.xmlis non-empty Cobertura XML withline-rate="0.7273"andlines-valid="143".Total Test time (real) = 1.48 secin the Coverage job log), and pytest actually runs (25 passed in 0.47s) — both contribute to their respectivecov-{cpp,py}.xmloutputs.Follow-ups (not in this PR)
misc/DynBitset.cpp(11 lines, 0%) first, then the error-path branches inXmLog.cpp(60%),Observer.cpp(81%), andXmError.cpp(81%).testing/TestTools.{cpp,h}is the test-scaffold and is expected to stay partially covered.[coverage].cpp_thresholdand[coverage].python_thresholdonce the remaining gaps land, so regressions break the build.cpp_thresholdcould reasonably go to80already.🤖 Generated with Claude Code