Repository navigation
Conversation
Adds opt-in clang -fprofile-instr-generate -fcoverage-mapping instrumentation to the macOS and Linux pytest builds and wires the resulting coverage to Codecov alongside the existing Python coverage. Determinism under pytest-xdist: LLVM_PROFILE_FILE is set to grass-%p-%9m.profraw so every subprocess and every instrumented binary writes its own file, eliminating the collision that made re-runs non-deterministic for untouched files. Merge uses llvm-profdata -sparse which is bit-identical for a given input set (verified locally). Fixes a latent ordering bug where utils/coverage_mapper.py ran before coverage combine and could not read a .coverage that did not yet exist in parallel mode. The mapper now runs on the combined data. Adds utils/coverage_disk_usage.sh to log raw coverage disk usage before the combine step so a run that threatens the runner storage budget is visible in the log, and utils/llvm_coverage_export.sh to merge .profraw into .profdata and export lcov for Codecov, filtering system, conda, and build-tree paths. macOS Codecov uploads use distinct flags macos-pytest-python and macos-llvm-c; Linux adds ubuntu-llvm-c to exercise the same path during local iteration. macOS coverage is combined after the gunittest run so profraw from both pytest and gunittest are included. Large use of AI assistance (Claude Code) helped draft the workflow diffs and the utility scripts.
The instrumented build runs each tool with --interface-description to emit its XML, which drops default.profraw next to the binary. Those files are build artifacts (no tests ran, coverage from the build tree would contaminate the test-run merge), so they belong ignored, and the CI coverage-prep step deletes any stray default.profraw before the test steps start writing controlled per-PID files under llvm-cov-raw/. Adds *.profraw, *.profdata, coverage-llvm.lcov, llvm-cov-raw/, and coverage_html_report/ to .gitignore.
Adds -fprofile-update=atomic to the coverage CFLAGS on both macOS and Linux pytest builds. Without it, LLVM's coverage counter increments are non-atomic, and GRASS tools with #pragma omp parallel / for regions (e.g. r.univar's process_raster) lose counter updates across threads. Two identical runs against a locally-built, instrumented grass produced 244 differing lcov lines, all inside r.univar_main.c's outlined OpenMP region. This is the collision reported previously as coverage of untouched files changing between runs. Also tightens utils/llvm_coverage_export.sh so its -object list only picks up real ELF / Mach-O binaries. The previous perm -u+x sweep would pull in executable Python scripts (e.g. installed grass scripts), and llvm-cov aborts on the first one it cannot recognize. A full local rebuild + rerun is queued to verify the diff closes; if anything remains after -fprofile-update=atomic it will land as a follow-up commit on this branch.
Clang's SourceBasedCodeCoverage doc explicitly states that llvm optimizations (inlining, CFG simplification) have no impact on coverage report quality: the mapping from source regions to profile counters is immutable and is generated before the optimizer runs, and the feature does not rely on debug info line tables. Force -O0 -g was gcov guidance, not the right choice here, and it inflated the instrumented build size and the runtime cost this PR promised to keep small. Also fix the Linux path: setting CFLAGS explicitly at configure time suppresses autoconf's default "-g -O2", so include -O2 in the coverage CFLAGS (macOS already sets -O2 explicitly before the coverage block).
The pytest workflow's Ubuntu build failed at configure with "Unable to locate OpenMP includes" once GRASS_LLVM_COVERAGE=1 flipped the compiler to clang. Ubuntu's libomp-dev keeps omp.h at /usr/lib/llvm-18/lib/clang/<ver>/include/omp.h rather than /usr/include, and configure's plain omp.h probe doesn't add clang's own resource-dir to its include search. Fix by passing --with-openmp-includes derived from `$CC -print-resource-dir` when the coverage build is active. Also add libomp-dev to apt.txt so the runtime lib is linkable. Make the Python coverage combine step tolerate "no data" so that when an earlier step fails and pytest is skipped, the combine step reports "nothing to export" instead of failing on top of the real error.
The pytest workflow's Ubuntu build now clears the OpenMP-include probe but fails to link every OpenMP-using tool (r.mapcalc, r.univar, r.slope.aspect, r.sun, ...) once GRASS_LLVM_COVERAGE=1 flipped the compiler to clang. Root cause: libomp-dev on Ubuntu 24.04 installs the unversioned libomp.so at /usr/lib/llvm-18/lib, which is not in the linker's default search path. configure's link probe for omp_get_num_threads in -lomp therefore fails and it falls back to -lgomp - but clang's OpenMP-instrumented code emits __kmpc_* calls that gomp doesn't provide. Pass --with-openmp-libs=<clang's llvm libdir> so configure finds -lomp and every OMP tool links cleanly. The libdir is derived from clang -print-resource-dir so the fix is not tied to any specific LLVM version number.
Adding clang, llvm, and libomp-dev to the shared apt.txt slowed down every workflow that reads that list (ubuntu.yml, gcc.yml, codeql, cmake, docs...) for a toolchain none of them need. Move them into a new "Install LLVM coverage toolchain" step in pytest.yml, gated on env.GRASS_LLVM_COVERAGE == '1', so only the coverage matrix pays the apt cost.
Instrumenting the build costs ~1 min extra compile time and adds a merge/export step. Running it in every matrix cell duplicated all of that for no additional information. Gate GRASS_LLVM_COVERAGE, the llvm/libomp toolchain install, the merge/export step, and the ubuntu-llvm-c Codecov upload on matrix.python-version == '3.13' so the 3.11 cell now takes the fast gcc/no-instrumentation path. Python coverage still runs on both cells and continues to upload under its own per-version flag.
utils/llvm_coverage_export.sh globbed all .profraw files onto llvm-profdata's argv. The per-process %p-%9m naming produces one file per (binary, pid) pair, and a full test run generates hundreds of thousands of them (454754 on the macOS job that failed), which overflows the OS argument-list limit on both the Linux pytest and macOS jobs. Pass the file list via llvm-profdata's -f option (a newline-separated list file) instead of argv. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P53N6mt8mDNedgUDBcYqfW
Resolves a conflict in .github/workflows/pytest.yml between this branch's new "Install LLVM coverage toolchain" step and main's routine version-pin bump of rui314/setup-mold: keeps both.
The macOS job's "Uninstalling Homebrew" step removes gpg early on, same as it always has. The pre-existing "Upload test results to Codecov" step already sets skip_validation: true to work around this, but the two new coverage-upload steps this PR added did not, so codecov-action's own dependency check failed before it could upload anything. Match the existing step's skip_validation: true.
echoix
added a commit
that referenced
this pull request
Oct 1, 2026
Instrument the Ubuntu build with clang under GRASS_LLVM_COVERAGE, enabled only on the Python 3.13 matrix cell so the 3.11 cell keeps the default gcc build. LLVM_PROFILE_FILE encodes pid and a hash per binary so xdist workers and C-tool subprocesses never collide on one file, and -fprofile-update=atomic makes counter increments atomic in OpenMP-parallel code (r.univar). Both were needed to make merged coverage deterministic between runs. utils/llvm_coverage_export.sh merges .profraw into .profdata and exports lcov for Codecov. It passes the file list to llvm-profdata via -f instead of argv, since a full test run's per-process .profraw count overflows the shell argument-list limit. utils/ coverage_disk_usage.sh logs .profraw count/size and disk usage before the merge for observability. Also fixes coverage_mapper.py running before coverage combine, which left it unable to read a .coverage file that parallel mode had not created yet. This is the Linux/pytest slice of #674, which also instruments macOS; macOS is out of scope here. AI assistance (Claude Code) helped draft the workflow and script changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
-fprofile-instr-generate -fcoverage-mapping -fprofile-update=atomicinstrumentation to the macOS build (macos_install.sh) and to the Linux pytest build (build_ubuntu-24.04.sh), gated byGRASS_LLVM_COVERAGE=1. macOS keeps its native clang; Linux switches to clang when opted in.LLVM_PROFILE_FILE=$PWD/llvm-cov-raw/grass-%p-%9m.profrawviaGITHUB_ENVbefore the test steps, so every xdist worker and every C-tool subprocess writes its own.profrawfile. This eliminates the write-collision that made re-runs non-deterministic on files the tests never touched.utils/llvm_coverage_export.shto merge.profraw→.profdata(llvm-profdata merge -sparse, deterministic for a given input set) and export lcov for Codecov, filtering system, conda, and build-tree paths. The-objectlist is filtered by ELF/Mach-O magic bytes so executable Python scripts don't breakllvm-cov.utils/coverage_disk_usage.shto log.profrawcount/size anddf -hbefore combine, so a run approaching the runner storage budget is visible in the log.utils/coverage_mapper.pyran beforecoverage combineand could not read a.coveragethat did not yet exist in parallel mode. The mapper now runs after combine.coverage,pytest-cov,llvm-toolsto the macOS conda env;clang,llvmto the Linux apt list.macos-pytest-python,macos-llvm-c) and one on Linux (ubuntu-llvm-c). The macOS combine+upload runs after the gunittest step so.profrawfrom both pytest and gunittest are included.*.profraw,*.profdata,coverage-llvm.lcov,llvm-cov-raw/,coverage_html_report/. The coverage-prep step alsofind . -name default.profraw -deletes stray files the instrumented build itself emits (Makefiles run each tool with--interface-descriptionto dump XML).Determinism investigation
The previous attempt saw coverage of untouched files change between runs. I reproduced it locally against an instrumented
HEADbuild, then narrowed and fixed:default.profrawproduced 20 vs 12 counts on identical inputs. Adding%p-%9mtoLLVM_PROFILE_FILEmade two runs bit-identical (32/32).grass --tmp-project --execsessions each callingg.*,r.mapcalc,r.info,r.univar. Two runs produced 244 differing lcov lines — every one insider.univar_main.cprocess_raster.omp_outlined_debug__, i.e.#pragma omp parallel for. That's LLVM's non-atomic coverage counters racing between threads.-fprofile-update=atomicin the coverage CFLAGS. Verified locally in isolation (a small OpenMP loop went from 284296/304669 counts across runs → byte-identical) and against GRASS: two runs of the same 6-way parallel workload are now bit-identical on line coverage (DA:). The only residual diff is workload-timing noise inparser.c(±1 line hit between runs of the same command set) and inherent floating-point reduction order inr.univar's lock-protected min/max reduction, both of which reproduce identically without any coverage flags in play.Test plan
GRASS_LLVM_COVERAGE=1and producescoverage-llvm.lcovnon-emptymacos-llvm-candmacos-pytest-pythonflags with non-zero coveragecoverage-llvm.lcovnon-emptyubuntu-llvm-calongside the existingpytest-python-<ver>.profrawcount/size within runner budget (<1 GB expected)coverage combineruns beforecoverage_mapper.pyand no ordering warning appearsLarge use of AI assistance (Claude Code) helped draft the workflow diffs, the utility scripts, and the local determinism reproduction. Human: EC.
Generated by Claude Code