diff --git a/.github/workflows/sonarcloud.yaml b/.github/workflows/sonarcloud.yaml index f147354d..54073efd 100644 --- a/.github/workflows/sonarcloud.yaml +++ b/.github/workflows/sonarcloud.yaml @@ -19,6 +19,7 @@ jobs: sonarcloud: name: SonarCloud runs-on: ubuntu-24.04 + container: silkeh/clang:18 env: BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed steps: @@ -26,11 +27,12 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Install gcovr - run: pip install gcovr==8.2 - - - name: Install sonar-scanner and build-wrapper - uses: SonarSource/sonarcloud-github-c-cpp@v3 + - name: Install Prerequisites + run: | + apt-get update + apt-get install -y \ + ccache \ + unzip - name: Install CMake uses: lukka/get-cmake@v3.30.5 @@ -53,20 +55,22 @@ jobs: -GNinja \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTS=ON \ -DCI_BUILD=ON \ -DENABLE_COVERAGE=ON \ -DENABLE_CLANG_TIDY=OFF - - name: Build the code in debug mode + - name: Build the code timeout-minutes: 30 - run: cmake --build build/ --config Debug + run: cmake --build build/ --config Release - name: Run tests to generate coverage statistics timeout-minutes: 10 working-directory: build - run: ninja run_all_tests -j1 -k 0 + run: ninja coverage -k 0 - name: Test Summary if: ${{ !cancelled() }} @@ -74,42 +78,28 @@ jobs: with: paths: "build/reports/tests/*.junit.xml" - - name: Collect coverage into one XML report - if: ${{ !cancelled() }} - run: | - gcovr \ - --root . \ - --object-directory build \ - --force-color \ - --no-markers \ - --decisions \ - --calls \ - --exclude-noncode-lines \ - --gcov-ignore-parse-errors negative_hits.warn \ - --sonarqube "coverage.xml" - - name: Upload coverage report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: coverage-sonar - path: coverage.xml + name: coverage + path: build/reports/coverage.txt retention-days: 1 # This sets the artifact TTL to 1 day (minimum is 1 day) overwrite: true - - name: Run sonar-scanner + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v4 if: ${{ !cancelled() }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - sonar-scanner \ - --define sonar.projectKey=Fastcode_NUClear \ - --define sonar.organization=fastcode \ - --define sonar.sources=src \ - --define sonar.tests=tests \ - --define sonar.cfamily.compile-commands=build/compile_commands.json \ - --define sonar.coverageReportPaths=coverage.xml + with: + args: > + --define sonar.projectKey=Fastcode_NUClear + --define sonar.organization=fastcode + --define sonar.sources=src + --define sonar.tests=tests + --define sonar.cfamily.compile-commands=build/compile_commands.json + --define sonar.cfamily.llvm-cov.reportPath=build/reports/coverage.txt - name: Upload Traces if: ${{ !cancelled() }} diff --git a/cmake/TestRunner.cmake b/cmake/TestRunner.cmake index 0d307082..b590467f 100644 --- a/cmake/TestRunner.cmake +++ b/cmake/TestRunner.cmake @@ -36,25 +36,68 @@ get_all_catch_test_targets(all_targets ${PROJECT_SOURCE_DIR}) # Create a custom command for each test target to run it # Make sure that coverage data is written with paths relative to the source directory -unset(reports) +unset(report_outputs) +unset(coverage_reports) foreach(target ${all_targets}) - set(sonarqube_report_file "${PROJECT_BINARY_DIR}/reports/tests/${target}.sonarqube.xml") + unset(command_prefix) + if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # Extra output files for the profile data + set(raw_coverage "${PROJECT_BINARY_DIR}/reports/tests/${target}.profraw") + set(indexed_coverage "${PROJECT_BINARY_DIR}/reports/tests/${target}.profdata") + set(coverage_report "${PROJECT_BINARY_DIR}/reports/tests/${target}.txt") + set(command_prefix "cmake" "-E" "env" "LLVM_PROFILE_FILE=${raw_coverage}") + list(APPEND coverage_reports ${coverage_report}) + endif() + set(junit_report_file "${PROJECT_BINARY_DIR}/reports/tests/${target}.junit.xml") - list(APPEND reports ${sonarqube_report_file} ${junit_report_file}) + list(APPEND report_outputs ${junit_report_file}) add_custom_command( - OUTPUT ${sonarqube_report_file} ${junit_report_file} - COMMAND $ --reporter console --reporter SonarQube::out=${sonarqube_report_file} --reporter - JUnit::out=${junit_report_file} + OUTPUT ${junit_report_file} ${raw_coverage} + COMMAND ${command_prefix} $ --reporter console --reporter JUnit::out=${junit_report_file} WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${target} USES_TERMINAL COMMENT "Running test ${target}" ) + + if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_custom_command( + OUTPUT ${indexed_coverage} + COMMAND llvm-profdata merge -sparse ${raw_coverage} -o ${indexed_coverage} + DEPENDS ${raw_coverage} + ) + + add_custom_command( + OUTPUT ${coverage_report} + COMMAND llvm-cov show --show-branches=count -Xdemangler c++filt -instr-profile=${indexed_coverage} + $ > ${coverage_report} + DEPENDS ${indexed_coverage} + ) + endif() + endforeach() # Create a custom target that depends on all test targets add_custom_target( run_all_tests - DEPENDS ${reports} + DEPENDS ${report_outputs} ${all_targets} COMMENT "Running all Catch2 tests" ) + +# Concatenate all the coverage reports together +if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + + add_custom_command( + OUTPUT ${PROJECT_BINARY_DIR}/reports/coverage.txt + COMMAND cat ${coverage_reports} > ${PROJECT_BINARY_DIR}/reports/coverage.txt + DEPENDS ${coverage_reports} + ) + + add_custom_target( + coverage + DEPENDS ${PROJECT_BINARY_DIR}/reports/coverage.txt + COMMENT "Running all Catch2 tests with coverage" + ) + +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 65f18091..7ed0fb8a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,11 +39,18 @@ target_compile_features(nuclear PUBLIC cxx_std_14) option(ENABLE_COVERAGE "Compile with coverage support enabled.") if(ENABLE_COVERAGE) - if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") - message(WARNING "Coverage is enabled but not build in debug mode. Coverage results may be misleading.") + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + message(WARNING "Coverage is enabled but not build in debug mode. Coverage results may be misleading.") + endif() + target_compile_options(nuclear PUBLIC -fprofile-arcs -ftest-coverage -fprofile-abs-path -fprofile-update=atomic) + target_link_options(nuclear PUBLIC -fprofile-arcs) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(nuclear PUBLIC -fprofile-instr-generate -fcoverage-mapping) + target_link_options(nuclear PUBLIC -fprofile-instr-generate) + else() + message(FATAL_ERROR "Coverage is not supported for the current compiler.") endif() - target_compile_options(nuclear PUBLIC -fprofile-arcs -ftest-coverage -fprofile-abs-path -fprofile-update=atomic) - target_link_options(nuclear PUBLIC -fprofile-arcs) endif(ENABLE_COVERAGE) # Enable warnings, and all warnings are errors