Add lcov code coverage to the release pipeline and Jenkins

- new_docker_build.sh: new --run-coverage mode (mutually exclusive with
  --run-jenkins-builder). Instruments the release-scope build with
  -O0 -g --coverage -fPIC -DNDEBUG (RELEASE flag override keeps -DNDEBUG
  and -fPIC so behavior and PIC match the green baseline), runs ctest,
  then collects the lcov report: capture --external -> extract on the
  four release dirs -> remove test sources -> summary -> genhtml, all
  with --ignore-errors mismatch,inconsistent,unused (GCC 15 + lcov 2.4).
  Report lands in --coverage-output-dir (default ./coverage-out):
  html/index.html, coverage.info, coverage_summary.txt.
- Dockerfile image gains lcov via APT_PACKAGES (genhtml ships in the
  same package).
- Jenkinsfile: RunCoverage parameter (default on) + per-OS Coverage
  stage running the driver on the agent and archiving
  coverage/<project>/<os>/**.
- release-16-10-2026.sh: same coverage step, gated by DRYRUN_COVERAGE
  (default ON, mirrors the Jenkins default).
- README: Step 6 (code coverage) and options/env reference entries.
- .gitignore: coverage-out/
This commit is contained in:
berrux-dev committed 2026-10-15 09:00:33 +02:00
1 parent 676ae05335
commit 684e05109d
7 files changed
+178 -36

No files matched your search

@@ -14,3 +14,4 @@ build/
build2/
.envrc
build4
coverage-out/
@@ -35,8 +35,6 @@ NGL_TARGET_LINK_LIBRARIES(LtsAppDescriptor
NGL_INSTALL_TARGET(LtsAppDescriptor)
# LtsAppDescriptorChecker
SET(LtsAppDescriptorChecker_SRCS
checker/main.cpp
@@ -51,27 +49,6 @@ NGL_TARGET_LINK_LIBRARIES(LtsAppDescriptorChecker
Qt6::Core
)
# LtsAppDescriptorEditor => Hat niemand benutzt, wird keiner benutzen, also wird der bis auf Weiteres auch nicht an die neue Syntax angepasst.
#SET(LtsAppDescriptorEditor_SRCS
# lts_app_descriptor_editor.cpp
# lts_app_descriptor_editor.h
#)
#NGL_WRAP_UI(LtsAppDescriptorEditor_SRCS
# lts_app_descriptor_editor.ui
#)
#NGL_ADD_LIBRARY2(WS NO_TRANSLATION LtsAppDescriptorEditor SHARED ${LtsAppDescriptorEditor_SRCS})
#NGL_TARGET_INCLUDE_DIRECTORIES(LtsAppDescriptorEditor
# PUBLIC
# ${CMAKE_CURRENT_SOURCE_DIR}/../
#)
if(QT6_GUI_AVAILABLE)
#NGL_TARGET_LINK_LIBRARIES(LtsAppDescriptorEditor
# PUBLIC
@@ -1,14 +1,6 @@
# Modified CMakeLists.txt from github.com/wille-io/httq
# Make ${Qt6_WebSockets} optional
# Link the real WebSockets module whenever the websocket code path is
# compiled: QT6_WEBSOCKETS_AVAILABLE (root CMakeLists) and the manual
# Qt6::WebSockets import are set when the real module was found. The old
# QT6_GUI_AVAILABLE condition never held in this environment (Qt6
# discovery takes the minimal path) and the Qt6_WebSockets variable was
# never defined, so AbstractServer.cpp compiled the websocket code
# without the module on the link line (undefined reference to
# QWebSocketServer::handleConnection).
if(QT6_WEBSOCKETS_REAL_FOUND OR Qt6WebSockets_FOUND)
set(Qt6_WebSockets Qt6::WebSockets)
else()
+44 -2
View File
@@ -212,6 +212,45 @@ jenkins-ci-rme/release-16-10-2026.sh
See `jenkins-ci-rme/README.md` for the pipeline details.
## Step 6 — Code coverage (lcov)
```bash
export WORKSPACE=/path/to/pipeline-root # e.g. ~/release/qt6-migration-pipeline
export PROJECT=guez
export OS_VERSION=26.04
./new_docker_build.sh --docker-image legatus-qt6:1.0 \
--run-coverage \
--coverage-output-dir "${WORKSPACE}/coverage/${PROJECT}/${OS_VERSION}/" \
> /path/to/coverage.log 2>&1
```
(export the variables *before* the command — command-prefix variables are
not expanded in the same command's arguments.)
The driver builds the release scope with `-O0 --coverage`, runs the same ctest
suite, then captures the data with lcov: the report covers the release scope
(`TetraIF`, `Z/src`, `Lts/Common`, `Lts/Proxies/TrainingAreaBase`), and test
sources are filtered out of the report, so the percentages reflect the module
code.
Artifacts in the coverage output directory:
| File | Contents |
|---|---|
| `html/index.html` | browsable report (line + branch coverage per file) |
| `coverage.info` | machine-readable lcov tracefile |
| `coverage_summary.txt` | whole-scope totals |
`--run-coverage` is mutually exclusive with `--run-jenkins-builder` (different
instrumentation of the same build tree).
The Jenkins pipeline runs the same step on every build (`RunCoverage`
parameter, on by default) and archives the report as build artifacts under
`coverage/<project>/<os>/`; the local dry run
(`jenkins-ci-rme/release-16-10-2026.sh`) does the same, with
`DRYRUN_COVERAGE=OFF` to skip coverage there.
## Options reference
`./new_docker_build.sh --help` prints the full option list. The main
@@ -225,10 +264,13 @@ ones:
| `--run-jenkins-builder` | off | produce the DEB packages (Step 4) |
| `--package-output-dir DIR` | `./packages-out` | where the DEBs are written |
| `--lts-version / --project / --build-number / --build-tag` | `1.0` / `guez` / `1` / `manual-build` | package metadata |
| `--run-coverage` | off | coverage build + lcov report (Step 6) |
| `--coverage-output-dir DIR` | `./coverage-out` | where the coverage report is written |
Environment overrides: `DOCKER_IMAGE`, `RUN_JENKINS_BUILDER`,
`PACKAGE_OUTPUT_DIR`, `PROJECT`, `LTS_VERSION`, `GUEZ_MODE`,
`BUILD_NUMBER`, `BUILD_TAG`, `MIGRATE_PIPELINE_ROOT`.
`PACKAGE_OUTPUT_DIR`, `RUN_COVERAGE`, `COVERAGE_OUTPUT_DIR`, `PROJECT`,
`LTS_VERSION`, `GUEZ_MODE`, `BUILD_NUMBER`, `BUILD_TAG`,
`MIGRATE_PIPELINE_ROOT`.
## Troubleshooting
+14 -1
View File
@@ -26,7 +26,8 @@ properties([
string(name: 'LtsVersion', defaultValue: '1.0', description: 'LTS version (major.minor) passed to make_rpm_ubuntu26.py --version'),
booleanParam(name: 'ClearWorkspace', defaultValue: false, description: 'Clear entire workspace on all nodes used for build'),
booleanParam(name: 'PublishArtifactRepo', defaultValue: true, description: 'Upload produced DEBs to the internal artifact repository'),
booleanParam(name: 'RebuildImage', defaultValue: false, description: 'Rebuild the prebuilt build image from the in-tree Dockerfile (default: reuse when present)')
booleanParam(name: 'RebuildImage', defaultValue: false, description: 'Rebuild the prebuilt build image from the in-tree Dockerfile (default: reuse when present)'),
booleanParam(name: 'RunCoverage', defaultValue: true, description: 'Build the release scope with -O0 --coverage, run ctest, collect the lcov report and archive it')
])
])
@@ -84,6 +85,18 @@ osVersions.each { osVersion ->
}
}
stage("Coverage ${project} (${osVersion})") {
if (!params.RunCoverage) {
echo "Coverage skipped for ${project} (OS ${osVersion}, RunCoverage=false)"
return
}
// The driver (run on the agent, same pattern as the image-build
// stage) builds the scope with -O0 --coverage, runs ctest and
// collects the lcov report into the workspace mount.
sh "cd ${legatusRoot} && bash ./new_docker_build.sh --docker-image ${imageName} --run-coverage --coverage-output-dir ${WORKSPACE}/coverage/${project}/${osVersion}"
archiveArtifacts artifacts: "coverage/${project}/${osVersion}/**", allowEmptyArchive: true
}
stage("Publish ${project} (${osVersion})") {
if (params.PublishArtifactRepo) {
withCredentials([usernamePassword(credentialsId: 'internal-repo-uploader',
@@ -8,6 +8,8 @@
# image is not present yet
# - builds the release scope, runs ctest and produces the release DEB
# packages with the in-tree rpmbuild script
# - collects the lcov code-coverage report (--run-coverage; set
# DRYRUN_COVERAGE=OFF to skip)
#
# The DEB packages land in ${WORKSPACE}/rpms/${PROJECT}/${OS_VERSION}/
# (Jenkins-style layout; locally WORKSPACE is the pipeline root).
@@ -60,4 +62,16 @@ bash ./new_docker_build.sh \
--build-number "${BUILD_NUMBER}" \
--build-tag "${BUILD_TAG}"
# Code coverage (lcov) - mirrors the Jenkins Coverage stage. Set
# DRYRUN_COVERAGE=OFF for a fast local DEB-only run.
DRYRUN_COVERAGE="${DRYRUN_COVERAGE:-ON}"
if [[ "${DRYRUN_COVERAGE}" == "ON" ]]; then
bash ./new_docker_build.sh \
--docker-image "${DOCKER_IMAGE}" \
--run-coverage \
--coverage-output-dir "${WORKSPACE}/coverage/${PROJECT}/${OS_VERSION}"
else
log_info "Coverage skipped (DRYRUN_COVERAGE=OFF)"
fi
log_info "=== Release Pipeline Complete ==="
@@ -43,6 +43,13 @@ Package production options (RPM/DEB):
--build-number NUM Build number for package (default: 1)
--build-tag TAG Build tag for package (default: manual-build)
Coverage options (lcov):
--run-coverage Build the release scope with -O0 --coverage, run ctest
and collect an lcov coverage report (mutually
exclusive with --run-jenkins-builder)
--coverage-output-dir DIR Directory to mount for coverage output
(default: ./coverage-out)
Environment variable overrides:
BUILD_PREBUILT_IMAGE=ON|OFF
DELETE_PREBUILT_IMAGE=ON|OFF
@@ -54,6 +61,8 @@ Environment variable overrides:
GUEZ_MODE=ON|OFF
BUILD_NUMBER=<num>
BUILD_TAG=<tag>
RUN_COVERAGE=ON|OFF
COVERAGE_OUTPUT_DIR=<dir>
MIGRATE_PIPELINE_ROOT resolution (highest precedence first):
1. exported MIGRATE_PIPELINE_ROOT (e.g. from a Jenkinsfile)
@@ -67,6 +76,9 @@ Examples:
./new_docker_build.sh --docker-image legatus-qt6:1.0 \
--run-jenkins-builder \
--package-output-dir "${WORKSPACE}/rpms/${PROJECT}/${OS_VERSION}/"
./new_docker_build.sh --docker-image legatus-qt6:1.0 \
--run-coverage \
--coverage-output-dir "${WORKSPACE}/coverage/${PROJECT}/${OS_VERSION}/"
EOF
}
@@ -184,6 +196,8 @@ APT_PACKAGES=(
postgresql-server-dev-all
git
python3-pip
# Code coverage (genhtml ships in the same package)
lcov
# Packaging tools for RPM/DEB build
rpm
debhelper
@@ -295,6 +309,9 @@ DELETE_PREBUILT_IMAGE="${DELETE_PREBUILT_IMAGE:-OFF}"
# Jenkins builder options for RPM/DEB package production
RUN_JENKINS_BUILDER="${RUN_JENKINS_BUILDER:-OFF}"
PACKAGE_OUTPUT_DIR="${PACKAGE_OUTPUT_DIR:-./packages-out}"
# Code coverage (lcov)
RUN_COVERAGE="${RUN_COVERAGE:-OFF}"
COVERAGE_OUTPUT_DIR="${COVERAGE_OUTPUT_DIR:-./coverage-out}"
LTS_VERSION="${LTS_VERSION:-1.0}"
PROJECT="${PROJECT:-guez}"
GUEZ_MODE="${GUEZ_MODE:-ON}"
@@ -327,6 +344,14 @@ while [[ $# -gt 0 ]]; do
[[ $# -gt 0 ]] || die "--package-output-dir requires a directory path"
PACKAGE_OUTPUT_DIR="$1"
;;
--run-coverage)
RUN_COVERAGE=ON
;;
--coverage-output-dir)
shift
[[ $# -gt 0 ]] || die "--coverage-output-dir requires a directory path"
COVERAGE_OUTPUT_DIR="$1"
;;
--lts-version)
shift
[[ $# -gt 0 ]] || die "--lts-version requires a version string"
@@ -362,12 +387,17 @@ done
BUILD_PREBUILT_IMAGE="$(normalize_on_off "$BUILD_PREBUILT_IMAGE" "BUILD_PREBUILT_IMAGE")"
DELETE_PREBUILT_IMAGE="$(normalize_on_off "$DELETE_PREBUILT_IMAGE" "DELETE_PREBUILT_IMAGE")"
RUN_JENKINS_BUILDER="$(normalize_on_off "$RUN_JENKINS_BUILDER" "RUN_JENKINS_BUILDER")"
RUN_COVERAGE="$(normalize_on_off "$RUN_COVERAGE" "RUN_COVERAGE")"
if [[ "${RUN_COVERAGE}" == "ON" && "${RUN_JENKINS_BUILDER}" == "ON" ]]; then
die "--run-coverage is mutually exclusive with --run-jenkins-builder"
fi
# Default for the Docker image name
DOCKER_IMAGE="${DOCKER_IMAGE:-legatus-qt6-prebuilt:latest}"
# Export variables for Docker container
export RELEASE_BUILD RUN_CTEST BUILD_PREBUILT_IMAGE DELETE_PREBUILT_IMAGE DOCKER_IMAGE RUN_JENKINS_BUILDER PACKAGE_OUTPUT_DIR LTS_VERSION PROJECT GUEZ_MODE BUILD_NUMBER BUILD_TAG
export RELEASE_BUILD RUN_CTEST BUILD_PREBUILT_IMAGE DELETE_PREBUILT_IMAGE DOCKER_IMAGE RUN_JENKINS_BUILDER PACKAGE_OUTPUT_DIR RUN_COVERAGE COVERAGE_OUTPUT_DIR LTS_VERSION PROJECT GUEZ_MODE BUILD_NUMBER BUILD_TAG
require_non_empty "${DOCKER_IMAGE}" "DOCKER_IMAGE"
@@ -391,6 +421,8 @@ log_info "DELETE_PREBUILT_IMAGE=${DELETE_PREBUILT_IMAGE}"
log_info "DOCKER_IMAGE=${DOCKER_IMAGE}"
log_info "RUN_JENKINS_BUILDER=${RUN_JENKINS_BUILDER}"
log_info "PACKAGE_OUTPUT_DIR=${PACKAGE_OUTPUT_DIR}"
log_info "RUN_COVERAGE=${RUN_COVERAGE}"
log_info "COVERAGE_OUTPUT_DIR=${COVERAGE_OUTPUT_DIR}"
log_info "LTS_VERSION=${LTS_VERSION}"
log_info "PROJECT=${PROJECT}"
log_info "GUEZ_MODE=${GUEZ_MODE}"
@@ -426,6 +458,7 @@ RELEASE_BUILD="${RELEASE_BUILD:-ON}"
RUN_CTEST="${RUN_CTEST:-ON}"
# Packaging variables
RUN_JENKINS_BUILDER="${RUN_JENKINS_BUILDER:-OFF}"
RUN_COVERAGE="${RUN_COVERAGE:-OFF}"
LTS_VERSION="${LTS_VERSION:-1.0}"
PROJECT="${PROJECT:-guez}"
GUEZ_MODE="${GUEZ_MODE:-ON}"
@@ -457,12 +490,29 @@ cd build4
# RELEASE_BUILD=ON selects the release scope (TetraIF, Z/src, Lts/Common +
# TrainingAreaBase); all other BUILD_* flags are left at their CMake defaults.
COV_CMAKE_ARGS=()
if [[ "${RUN_COVERAGE}" == "ON" ]]; then
# Instrument the Release build with gcov. CMAKE_<LANG>_FLAGS_RELEASE replaces
# the default "-O3 -DNDEBUG" entirely, so -O0/--coverage must be carried
# there (plain CMAKE_<LANG>_FLAGS would come first on the command line and
# lose to -O3); -DNDEBUG is kept so behavior matches the standard build.
# -fPIC: plain static libs (e.g. tabTelegrams) are not PIC by default and
# are linked into shared libraries - at -O0 the non-PIC relocations show up
# at link time. Never add USE_IPO/USE_UNITY_BUILD here - LTO breaks gcov.
COV_CMAKE_ARGS=(
"-DCMAKE_C_FLAGS_RELEASE=-O0 -g --coverage -DNDEBUG -fPIC"
"-DCMAKE_CXX_FLAGS_RELEASE=-O0 -g --coverage -DNDEBUG -fPIC"
"-DCMAKE_EXE_LINKER_FLAGS=--coverage"
"-DCMAKE_SHARED_LINKER_FLAGS=--coverage"
)
fi
cmake .. \
-DBUILD_TESTING=ON \
-DRELEASE_BUILD="${RELEASE_BUILD}" \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="${QT_BASE_DIR}/${QT_VERSION}/${QT_ARCH_DIR}:/usr/local"
-DCMAKE_PREFIX_PATH="${QT_BASE_DIR}/${QT_VERSION}/${QT_ARCH_DIR}:/usr/local" \
"${COV_CMAKE_ARGS[@]}"
read -r -a RELEASE_TARGETS_ARRAY <<< "${RELEASE_BUILD_TARGETS}"
@@ -479,6 +529,56 @@ else
log_info "Skipping CTest"
fi
# Code coverage (lcov): capture, filter to the release scope and generate the
# HTML report. Runs only in --run-coverage mode, after ctest (so the .gcda
# data files exist). cwd is build4, so --directory . is the build tree; all
# artifacts are written via absolute paths.
if [[ "${RUN_COVERAGE}" == "ON" ]]; then
log_info "=== Code coverage (lcov) ==="
COVOUT=/workspace/coverage-out
SRCROOT=/workspace/qt-migration-legatus/qt6-after/qt6-migrated-legatus
mkdir -p "${COVOUT}"
# GCC 15 + lcov 2.x: the branch data (libstdc++, Qt headers and some of
# our sources) trips lcov's consistency checks (mismatch / inconsistent),
# and a filter pattern that matches no file is an "unused" error. The
# extract step below keeps only the release scope, so these quirks do not
# affect the report - ignore those error classes on every step.
COV_IGNORE=(--ignore-errors mismatch,inconsistent,unused)
lcov --capture --directory . --external "${COV_IGNORE[@]}" \
--rc branch_coverage=1 \
--output-file "${COVOUT}/raw.info"
lcov --extract "${COVOUT}/raw.info" \
"${SRCROOT}/TetraIF/*" \
"${SRCROOT}/Z/src/*" \
"${SRCROOT}/Lts/Common/*" \
"${SRCROOT}/Lts/Proxies/TrainingAreaBase/*" \
"${COV_IGNORE[@]}" \
--rc branch_coverage=1 \
--output-file "${COVOUT}/scoped.info"
lcov --remove "${COVOUT}/scoped.info" \
"*/unit-test/*" \
"*CommonUnitTestsPlaceholder*" \
"*/UnitTests/*" \
"*/tests/*" \
"*/test_*" \
"*_test.cpp" \
"*_test.h" \
"*_unit_test*" \
"${COV_IGNORE[@]}" \
--rc branch_coverage=1 \
--output-file "${COVOUT}/coverage.info"
lcov --summary "${COVOUT}/coverage.info" "${COV_IGNORE[@]}" --rc branch_coverage=1 \
> "${COVOUT}/coverage_summary.txt"
genhtml "${COVOUT}/coverage.info" \
--output-directory "${COVOUT}/html" \
"${COV_IGNORE[@]}" \
--branch-coverage --legend \
--rc branch_coverage=1
log_info "Coverage report written to ${COVOUT} (host: ${COVERAGE_OUTPUT_DIR})"
cat "${COVOUT}/coverage_summary.txt"
fi
# DEB package production with the in-tree rpmbuild script
if [[ "${RUN_JENKINS_BUILDER}" == "ON" ]]; then
log_info "=== DEB Package Production (in-tree rpmbuild) ==="
@@ -551,6 +651,8 @@ docker run --rm \
-e RELEASE_BUILD \
-e RUN_CTEST \
-e RUN_JENKINS_BUILDER \
-e RUN_COVERAGE \
-e COVERAGE_OUTPUT_DIR \
-e PACKAGE_OUTPUT_DIR \
-e LTS_VERSION \
-e PROJECT \
@@ -566,5 +668,6 @@ docker run --rm \
-v "${MIGRATE_PIPELINE_ROOT}/external:/workspace/external:ro" \
-v "${CONTAINER_SCRIPT_HOST}:/tmp/run-build-inside-container.sh:ro" \
-v "${PACKAGE_OUTPUT_DIR}:/workspace/packages-out" \
-v "${COVERAGE_OUTPUT_DIR}:/workspace/coverage-out" \
"${DOCKER_IMAGE}" \
bash /tmp/run-build-inside-container.sh