Background
xmsconan already has GitLab coverage support gated by [ci] coverage = true in build.toml, but it has several gaps. We just built a richer Phase 1 coverage workflow in xmsgrid that addresses them, and the natural next step is to lift it into xmsconan so every library benefits.
Reference implementation: xmsgrid PR #196 — see dev/coverage.sh, .github/workflows/Coverage.yaml, and the coverage block in build.toml's extra_cmake_text.
Revised after review feedback. The original plan ported the xmsgrid two-build shape (one Debug+testing build, one Release+pybind build) into xmsconan_coverage and deferred cross-credit to a Phase 2. wdolinar's feedback was correct that a single instrumented testing=True, pybind=True build is the cleaner long-term shape — it halves cold runtime, feeds both ctest and pytest from one gcda set, and gets Python-tests-cover-C++ credit for free. This issue now plans the one-build shape from day one (which collapses Phase 2 into Phase 1). Three xmsvtk-specific asks (xvfb, image selection, no sharding) are also folded in.
What xmsconan does today
[ci] coverage = true adds a Coverage: stage to .gitlab-ci.yml.
- Stage uses an in-repo
conan_profiles/linux_testing_debug_coverage.txt and a CMake coverage preset (both expected to be hand-committed in each consuming repo — xmsconan does not generate them).
- Runs
./build/<lib>-coverage/Debug/runner directly, then one gcovr call producing coverage.xml (cobertura) + coverage.html (single-page).
- GitLab pages stage publishes the HTML.
Gaps to close
- C++ only. No Python coverage is collected anywhere.
- No threshold gating. The job emits a coverage badge but never fails on regression.
- Single-page HTML. Uses
--html rather than --html-details, so reviewers can't drill into per-file misses.
- GitHub workflow not generated.
ci_coverage only affects the GitLab template.
- Per-repo profile/preset files. Consumers must hand-create
conan_profiles/linux_testing_debug_coverage.txt and a CMake preset, neither of which xmsconan supplies. Adoption friction.
Proposed plan
Replace the existing GitLab implementation in the same release; introduce a generic command that both CI templates and developers call.
1. Conanfile / CMake refactor: one build, both test paths
To make the rest of the design clean, the generated conanfile and CMakeLists need to allow testing=True and pybind=True in the same configuration. Today they're mutually exclusive.
Changes to xmsconan templates:
CMakeLists.txt.jinja — drop if (IS_PYTHON_BUILD AND BUILD_TESTING) message(FATAL_ERROR …). Both the test runner target and the pybind module target should configure and build together.
xms_conan2_file.py — remove the configure_options() clause that deletes pybind when build_type != Release. Allow pybind=True with any build type. Restructure build() from if testing: … elif pybind: … into two independent if blocks so both test paths run when both options are true.
xms_conan2_file.py (cont.) — extend run_python_tests to install pytest-cov and pass --cov=<python_namespaced_dir> / --cov-report=xml:cov-py.xml / --cov-report=html:build/coverage-html-py / --cov-report=json:build/cov-py-summary.json when XMS_COVERAGE is set in the environment. Non-coverage runs are unaffected. Honors XMS_COVERAGE_PIP_INDEX for the pip extra-index URL so consumers behind alternative devpi configurations can override.
- Existing non-coverage build matrix entries continue to set only one of the options at a time, so this is purely additive — no behavior change for current consumers' regular CI.
Consequence: a single instrumented build with testing=True, pybind=True, build_type=Debug produces both the runner and the pybind module, and ctest + pytest both write into the same gcda set. Python tests automatically credit the underlying C++ in the C++ coverage report — no separate "Phase 2 cross-credit" work needed.
2. New console entry point: xmsconan_coverage
Ship a Python implementation alongside xmsconan_gen and xmsconan_wheel_repair. CI templates and local devs invoke the same command:
xmsconan_coverage --build-toml build.toml
Behavior (single instrumented build, both test paths):
- Run
xmsconan_gen to render build artifacts.
- If
[ci] xvfb = true, re-exec under xvfb-run -a -s "-screen 0 1280x1024x24" as the very first action so both the C++ runner and pybind GL contexts have a display. Same command works locally in the Linux container and in CI.
- Run one instrumented build:
XMS_COVERAGE=1 python build.py \
--filter='{"build_type":"Debug","testing":true,"pybind":true}'
This produces both the test runner and the pybind module from a single compile. ctest runs (via the conanfile's existing run_cxx_tests) and pytest runs (via the conanfile's existing run_python_tests) write gcda into the same Conan build folder. Sharding options (split_tests / test_shards) are explicitly ignored: coverage is a single measurement per commit, and gcda merging across shards adds complexity for a job that's not on the critical path.
- Walk the Conan cache to locate the build folder and matching source folder containing the library subtree.
gcovr --root <source> --html-details build/coverage-html-cpp/index.html --xml cov-cpp.xml --json-summary build/cov-cpp-summary.json <build_folder>.
- Python coverage is collected by the conanfile's own
run_python_tests (see section 1.a below). The pytest invocation inside that method picks up pytest-cov and --cov=<library_namespace> / --cov-report=… flags when XMS_COVERAGE is set, writing cov-py.xml / build/cov-py-summary.json / build/coverage-html-py/ into the build folder. xmsconan_coverage copies the JSON summary up to the workspace root so the threshold-check step can read it.
- Round actuals to one decimal, compare to per-layer thresholds, exit non-zero if either fails.
- Append a markdown summary table to
$GITHUB_STEP_SUMMARY when present (threshold + actual + status per layer).
3. CMake instrumentation moves into the template
Add an env-var-gated coverage block to CMakeLists.txt.jinja, rendered when ci.coverage = true:
if (DEFINED ENV{XMS_COVERAGE} AND NOT "$ENV{XMS_COVERAGE}" STREQUAL "")
if (MSVC)
message(FATAL_ERROR "XMS_COVERAGE is GCC/Clang only")
endif ()
add_compile_options(--coverage -O0 -g)
add_link_options(--coverage)
endif ()
Use a generic env var name (XMS_COVERAGE) rather than <LIB>_COVERAGE since the same env reads for any library. Frees up extra_cmake_text for repos that previously had to land it themselves.
4. build.toml schema additions
[ci]
coverage = true # (existing flag, behavior generalized)
xvfb = true # (existing flag, now also honored by xmsconan_coverage)
[coverage]
cpp_threshold = 70 # optional, default 0 (no gate)
python_threshold = 70 # optional, default 0
filters = ["<library>/"] # default: library_name + "/"
excludes = [ # sensible defaults baked in if omitted
".*\\.t\\.h$",
".*/<library>/python/.*",
".*/_package/tests/.*",
]
Most repos opt in with one line (coverage = true).
5. GitHub workflow generation
When ci.coverage = true and ci_type = "github", render a Coverage.yaml next to the existing <Library>-CI.yaml. Job:
- Linux container, image selected the same way as
<Library>-CI.yaml: conan-gcc13-x11-gdal-py3.13 when [ci] xvfb = true, otherwise conan-gcc13-py3.13. Honors docker_image override.
- Installs xmsconan + gcovr; runs
xmsconan_conan_setup.
- Calls
xmsconan_coverage.
- Uploads
coverage-html (HTML reports) and coverage-xml (Cobertura XMLs) as artifacts with if: always().
- Threshold env vars read from build.toml or set explicitly in the workflow.
6. GitLab template replacement
Replace the existing Coverage: stage body with a single call to xmsconan_coverage. Image selection follows the same xvfb-aware rule. Keep artifacts in the same shape (cobertura at cov-cpp.xml/cov-py.xml, HTML directories) so the existing pages: stage (and any external dashboards keying off the cobertura) continues to work. Note the change in release notes since any library currently relying on the hand-rolled CMake preset will be affected.
Migration for consumers
After the xmsconan release lands, an existing repo migrates by:
- Bumping xmsconan dep version in
build.toml.
- Adding
[ci] coverage = true and [coverage] cpp_threshold = N / python_threshold = N.
- Deleting any local
dev/coverage.sh, hand-written Coverage.yaml, coverage block in extra_cmake_text, or *_coverage.txt conan profiles and coverage* CMake presets that pre-dated this work.
- Re-running
xmsconan_gen.
For xmsgrid: ~200 lines of repo-local plumbing collapse to ~6 lines of build.toml. For xmsvtk: drop two *_coverage.txt profiles, drop the Coverage: stage from .gitlab-ci.yml (regen handles it), drop the coverage* block from CMakePresets.json, add [coverage] cpp_threshold = 0, python_threshold = 0, regen. Ratchet thresholds once a green baseline lands.
Risks / open questions
- Wheel optimization level. Today the user-facing pybind wheel comes from a Release build. Allowing
pybind=True in non-Release builds is additive — the Release wheel build path stays intact for normal CI. But worth a quick check that no downstream tooling assumes pybind is always Release.
- Existing GitLab consumers. Anyone using the current
coverage = true will see their hand-rolled CMake preset / conan profile path become unused. Worth a clear note in release notes.
Background
xmsconan already has GitLab coverage support gated by
[ci] coverage = trueinbuild.toml, but it has several gaps. We just built a richer Phase 1 coverage workflow in xmsgrid that addresses them, and the natural next step is to lift it into xmsconan so every library benefits.Reference implementation: xmsgrid PR #196 — see
dev/coverage.sh,.github/workflows/Coverage.yaml, and the coverage block inbuild.toml'sextra_cmake_text.What xmsconan does today
[ci] coverage = trueadds aCoverage:stage to.gitlab-ci.yml.conan_profiles/linux_testing_debug_coverage.txtand a CMakecoveragepreset (both expected to be hand-committed in each consuming repo — xmsconan does not generate them)../build/<lib>-coverage/Debug/runnerdirectly, then onegcovrcall producingcoverage.xml(cobertura) +coverage.html(single-page).Gaps to close
--htmlrather than--html-details, so reviewers can't drill into per-file misses.ci_coverageonly affects the GitLab template.conan_profiles/linux_testing_debug_coverage.txtand a CMake preset, neither of which xmsconan supplies. Adoption friction.Proposed plan
Replace the existing GitLab implementation in the same release; introduce a generic command that both CI templates and developers call.
1. Conanfile / CMake refactor: one build, both test paths
To make the rest of the design clean, the generated conanfile and CMakeLists need to allow
testing=Trueandpybind=Truein the same configuration. Today they're mutually exclusive.Changes to xmsconan templates:
CMakeLists.txt.jinja— dropif (IS_PYTHON_BUILD AND BUILD_TESTING) message(FATAL_ERROR …). Both the test runner target and the pybind module target should configure and build together.xms_conan2_file.py— remove theconfigure_options()clause that deletespybindwhenbuild_type != Release. Allowpybind=Truewith any build type. Restructurebuild()fromif testing: … elif pybind: …into two independentifblocks so both test paths run when both options are true.xms_conan2_file.py(cont.) — extendrun_python_teststo installpytest-covand pass--cov=<python_namespaced_dir>/--cov-report=xml:cov-py.xml/--cov-report=html:build/coverage-html-py/--cov-report=json:build/cov-py-summary.jsonwhenXMS_COVERAGEis set in the environment. Non-coverage runs are unaffected. HonorsXMS_COVERAGE_PIP_INDEXfor the pip extra-index URL so consumers behind alternative devpi configurations can override.Consequence: a single instrumented build with
testing=True, pybind=True, build_type=Debugproduces both the runner and the pybind module, and ctest + pytest both write into the same gcda set. Python tests automatically credit the underlying C++ in the C++ coverage report — no separate "Phase 2 cross-credit" work needed.2. New console entry point:
xmsconan_coverageShip a Python implementation alongside
xmsconan_genandxmsconan_wheel_repair. CI templates and local devs invoke the same command:Behavior (single instrumented build, both test paths):
xmsconan_gento render build artifacts.[ci] xvfb = true, re-exec underxvfb-run -a -s "-screen 0 1280x1024x24"as the very first action so both the C++ runner and pybind GL contexts have a display. Same command works locally in the Linux container and in CI.run_cxx_tests) and pytest runs (via the conanfile's existingrun_python_tests) write gcda into the same Conan build folder. Sharding options (split_tests/test_shards) are explicitly ignored: coverage is a single measurement per commit, and gcda merging across shards adds complexity for a job that's not on the critical path.gcovr --root <source> --html-details build/coverage-html-cpp/index.html --xml cov-cpp.xml --json-summary build/cov-cpp-summary.json <build_folder>.run_python_tests(see section 1.a below). The pytest invocation inside that method picks uppytest-covand--cov=<library_namespace>/--cov-report=…flags whenXMS_COVERAGEis set, writingcov-py.xml/build/cov-py-summary.json/build/coverage-html-py/into the build folder.xmsconan_coveragecopies the JSON summary up to the workspace root so the threshold-check step can read it.$GITHUB_STEP_SUMMARYwhen present (threshold + actual + status per layer).3. CMake instrumentation moves into the template
Add an env-var-gated coverage block to
CMakeLists.txt.jinja, rendered whenci.coverage = true:Use a generic env var name (
XMS_COVERAGE) rather than<LIB>_COVERAGEsince the same env reads for any library. Frees upextra_cmake_textfor repos that previously had to land it themselves.4.
build.tomlschema additionsMost repos opt in with one line (
coverage = true).5. GitHub workflow generation
When
ci.coverage = trueandci_type = "github", render aCoverage.yamlnext to the existing<Library>-CI.yaml. Job:<Library>-CI.yaml:conan-gcc13-x11-gdal-py3.13when[ci] xvfb = true, otherwiseconan-gcc13-py3.13. Honorsdocker_imageoverride.xmsconan_conan_setup.xmsconan_coverage.coverage-html(HTML reports) andcoverage-xml(Cobertura XMLs) as artifacts withif: always().6. GitLab template replacement
Replace the existing
Coverage:stage body with a single call toxmsconan_coverage. Image selection follows the same xvfb-aware rule. Keep artifacts in the same shape (cobertura atcov-cpp.xml/cov-py.xml, HTML directories) so the existingpages:stage (and any external dashboards keying off the cobertura) continues to work. Note the change in release notes since any library currently relying on the hand-rolled CMake preset will be affected.Migration for consumers
After the xmsconan release lands, an existing repo migrates by:
build.toml.[ci] coverage = trueand[coverage] cpp_threshold = N / python_threshold = N.dev/coverage.sh, hand-writtenCoverage.yaml, coverage block inextra_cmake_text, or*_coverage.txtconan profiles andcoverage*CMake presets that pre-dated this work.xmsconan_gen.For xmsgrid: ~200 lines of repo-local plumbing collapse to ~6 lines of
build.toml. For xmsvtk: drop two*_coverage.txtprofiles, drop theCoverage:stage from.gitlab-ci.yml(regen handles it), drop thecoverage*block fromCMakePresets.json, add[coverage] cpp_threshold = 0, python_threshold = 0, regen. Ratchet thresholds once a green baseline lands.Risks / open questions
pybind=Truein non-Release builds is additive — the Release wheel build path stays intact for normal CI. But worth a quick check that no downstream tooling assumes pybind is always Release.coverage = truewill see their hand-rolled CMake preset / conan profile path become unused. Worth a clear note in release notes.