From 684e05109d8ab4214baa365b9e0b9dc67c6a74b6 Mon Sep 17 00:00:00 2001 From: berrux_dev Date: Thu, 15 Oct 2026 08:45:31 +0200 Subject: [PATCH] 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///**. - 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/ --- qt6-after/qt6-migrated-legatus/.gitignore | 1 + .../Common/LtsAppDescriptor/CMakeLists.txt | 23 ---- .../Lts/Common/httq/CMakeLists.txt | 8 -- qt6-after/qt6-migrated-legatus/README.md | 46 +++++++- .../jenkins-ci-rme/Jenkinsfile | 15 ++- .../jenkins-ci-rme/release-16-10-2026.sh | 14 +++ .../qt6-migrated-legatus/new_docker_build.sh | 107 +++++++++++++++++- 7 files changed, 178 insertions(+), 36 deletions(-) diff --git a/qt6-after/qt6-migrated-legatus/.gitignore b/qt6-after/qt6-migrated-legatus/.gitignore index 3300f96a5..642197bb6 100644 --- a/qt6-after/qt6-migrated-legatus/.gitignore +++ b/qt6-after/qt6-migrated-legatus/.gitignore @@ -14,3 +14,4 @@ build/ build2/ .envrc build4 +coverage-out/ diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsAppDescriptor/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsAppDescriptor/CMakeLists.txt index 8f67cf885..cd3a38b3c 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsAppDescriptor/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsAppDescriptor/CMakeLists.txt @@ -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 diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/httq/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/httq/CMakeLists.txt index faaf27b77..d2ba5ca45 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Common/httq/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/httq/CMakeLists.txt @@ -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() diff --git a/qt6-after/qt6-migrated-legatus/README.md b/qt6-after/qt6-migrated-legatus/README.md index 7712258c8..bf58b47a0 100644 --- a/qt6-after/qt6-migrated-legatus/README.md +++ b/qt6-after/qt6-migrated-legatus/README.md @@ -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///`; 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 diff --git a/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/Jenkinsfile b/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/Jenkinsfile index 56c67e366..d44929030 100644 --- a/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/Jenkinsfile +++ b/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/Jenkinsfile @@ -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', diff --git a/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/release-16-10-2026.sh b/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/release-16-10-2026.sh index 8f5d48266..044d9ebd2 100755 --- a/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/release-16-10-2026.sh +++ b/qt6-after/qt6-migrated-legatus/jenkins-ci-rme/release-16-10-2026.sh @@ -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 ===" diff --git a/qt6-after/qt6-migrated-legatus/new_docker_build.sh b/qt6-after/qt6-migrated-legatus/new_docker_build.sh index 9d87e69d6..246f48f5c 100755 --- a/qt6-after/qt6-migrated-legatus/new_docker_build.sh +++ b/qt6-after/qt6-migrated-legatus/new_docker_build.sh @@ -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= BUILD_TAG= + RUN_COVERAGE=ON|OFF + COVERAGE_OUTPUT_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__FLAGS_RELEASE replaces + # the default "-O3 -DNDEBUG" entirely, so -O0/--coverage must be carried + # there (plain CMAKE__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