Skip to content

Unify coverage tooling: ship xmsconan_coverage + GitHub support + Python coverage #57

Description

@gagelarsen

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:

  1. Bumping xmsconan dep version in build.toml.
  2. Adding [ci] coverage = true and [coverage] cpp_threshold = N / python_threshold = N.
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions