-
Notifications
You must be signed in to change notification settings - Fork 267
Add ctest Coverage report #674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,39 @@ set(CMAKE_CXX_STANDARD 17) | |
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||
| set(CMAKE_EXPORT_COMPILE_COMMANDS ON) | ||
|
|
||
| option(DFLASH27B_COVERAGE | ||
| "Instrument C and C++ host code for LLVM source-based coverage" OFF) | ||
| if(DFLASH27B_COVERAGE) | ||
| if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang" OR | ||
| NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") | ||
| message(FATAL_ERROR | ||
| "DFLASH27B_COVERAGE requires Clang for both C and C++ " | ||
| "(configure with -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++)") | ||
| endif() | ||
|
|
||
| file(GLOB _dflash_llvm_tool_dirs | ||
| "/usr/lib/llvm-*/bin" | ||
| "/usr/local/opt/llvm*/bin") | ||
| find_program(DFLASH27B_LLVM_COV_EXECUTABLE | ||
| NAMES llvm-cov | ||
| HINTS ${_dflash_llvm_tool_dirs} | ||
| REQUIRED) | ||
| find_program(DFLASH27B_LLVM_PROFDATA_EXECUTABLE | ||
| NAMES llvm-profdata | ||
| HINTS ${_dflash_llvm_tool_dirs} | ||
| REQUIRED) | ||
| unset(_dflash_llvm_tool_dirs) | ||
|
|
||
| # CUDA/HIP device compilation is intentionally excluded: LLVM's | ||
| # source-based coverage records host-side C and C++ execution only. | ||
| add_compile_options( | ||
| $<$<COMPILE_LANGUAGE:C,CXX>:-fprofile-instr-generate> | ||
| $<$<COMPILE_LANGUAGE:C,CXX>:-fcoverage-mapping>) | ||
| add_link_options( | ||
| $<$<LINK_LANGUAGE:C>:-fprofile-instr-generate> | ||
| $<$<LINK_LANGUAGE:CXX>:-fprofile-instr-generate>) | ||
| endif() | ||
|
|
||
| # MSVC: treat source files as UTF-8 (the codebase contains UTF-8 string | ||
| # literals such as tags). Without this, MSVC defaults to the system | ||
| # code page and emits C4819 warnings or garbles multi-byte literals. | ||
|
|
@@ -1881,6 +1914,91 @@ if(DFLASH27B_TESTS) | |
| COMMENT "No unit-test binaries are enabled in this configuration" | ||
| ) | ||
| endif() | ||
|
|
||
| if(DFLASH27B_COVERAGE) | ||
| set(_coverage_report_dir "${CMAKE_CURRENT_BINARY_DIR}/coverage") | ||
| set(_coverage_profile_dir "${_coverage_report_dir}/raw") | ||
| set(_coverage_object_list "${CMAKE_CURRENT_BINARY_DIR}/coverage_objects.txt") | ||
| set(_coverage_objects) | ||
| set(_coverage_deps) | ||
| set(_coverage_ctest_exclude) | ||
| foreach(_coverage_target IN LISTS _check_deps) | ||
| if(TARGET ${_coverage_target}) | ||
| get_target_property(_coverage_sources ${_coverage_target} SOURCES) | ||
| set(_coverage_has_device_source FALSE) | ||
| foreach(_coverage_source IN LISTS _coverage_sources) | ||
| get_source_file_property(_coverage_source_language | ||
| "${_coverage_source}" LANGUAGE) | ||
| if(_coverage_source MATCHES "\\.cu$" OR | ||
| _coverage_source_language STREQUAL "CUDA" OR | ||
| _coverage_source_language STREQUAL "HIP") | ||
| set(_coverage_has_device_source TRUE) | ||
| break() | ||
| endif() | ||
| endforeach() | ||
| unset(_coverage_source) | ||
| unset(_coverage_source_language) | ||
|
|
||
| if(_coverage_has_device_source) | ||
| # Host-side LLVM coverage cannot instrument CUDA sources. | ||
| # Skip their standalone CTest entry and do not build them. | ||
| list(APPEND _coverage_ctest_exclude "^${_coverage_target}$") | ||
| else() | ||
| list(APPEND _coverage_deps ${_coverage_target}) | ||
| list(APPEND _coverage_objects "$<TARGET_FILE:${_coverage_target}>") | ||
| endif() | ||
| unset(_coverage_has_device_source) | ||
| unset(_coverage_sources) | ||
| endif() | ||
| endforeach() | ||
|
|
||
| if(NOT _coverage_objects) | ||
| message(FATAL_ERROR | ||
| "DFLASH27B_COVERAGE requires at least one registered test target") | ||
| endif() | ||
|
|
||
| file(GENERATE | ||
| OUTPUT "${_coverage_object_list}" | ||
| CONTENT "$<JOIN:${_coverage_objects},\n>\n") | ||
|
|
||
| if(_coverage_ctest_exclude) | ||
| list(JOIN _coverage_ctest_exclude "|" _coverage_ctest_exclude_regex) | ||
| else() | ||
| set(_coverage_ctest_exclude_regex "") | ||
| endif() | ||
|
|
||
| # This target intentionally depends on the test binaries rather than | ||
| # `check`: CTest must run with LLVM_PROFILE_FILE set to capture data. | ||
| add_custom_target(coverage | ||
| COMMAND ${CMAKE_COMMAND} -E rm -rf "${_coverage_report_dir}" | ||
| COMMAND ${CMAKE_COMMAND} -E make_directory "${_coverage_profile_dir}" | ||
| COMMAND ${CMAKE_COMMAND} | ||
| "-DCTEST_EXECUTABLE=${CMAKE_CTEST_COMMAND}" | ||
| "-DBUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}" | ||
| "-DCTEST_EXCLUDE_REGEX=${_coverage_ctest_exclude_regex}" | ||
| "-DLLVM_COV=${DFLASH27B_LLVM_COV_EXECUTABLE}" | ||
| "-DLLVM_PROFDATA=${DFLASH27B_LLVM_PROFDATA_EXECUTABLE}" | ||
| "-DPROFILE_DIR=${_coverage_profile_dir}" | ||
| "-DPROFILE_DATA=${_coverage_report_dir}/coverage.profdata" | ||
| "-DOBJECT_LIST_FILE=${_coverage_object_list}" | ||
| "-DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/src" | ||
| "-DHTML_DIR=${_coverage_report_dir}/html" | ||
| -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/RunCoverageTests.cmake" | ||
| DEPENDS ${_coverage_deps} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: On a fresh build, Prompt for AI agents |
||
| USES_TERMINAL | ||
| VERBATIM | ||
| COMMENT "Running CTest and generating LLVM coverage reports") | ||
|
|
||
| unset(_coverage_report_dir) | ||
| unset(_coverage_profile_dir) | ||
| unset(_coverage_object_list) | ||
| unset(_coverage_objects) | ||
| unset(_coverage_deps) | ||
| unset(_coverage_ctest_exclude) | ||
| unset(_coverage_ctest_exclude_regex) | ||
| unset(_coverage_target) | ||
| endif() | ||
|
|
||
| unset(_check_deps) | ||
| unset(_raw_unit_test_targets) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| foreach(_coverage_required_var IN ITEMS | ||
| LLVM_COV | ||
| LLVM_PROFDATA | ||
| PROFILE_DIR | ||
| PROFILE_DATA | ||
| OBJECT_LIST_FILE | ||
| SOURCE_DIR | ||
| HTML_DIR) | ||
| if(NOT DEFINED ${_coverage_required_var} OR | ||
| "${${_coverage_required_var}}" STREQUAL "") | ||
| message(FATAL_ERROR "${_coverage_required_var} is required") | ||
| endif() | ||
| endforeach() | ||
| unset(_coverage_required_var) | ||
|
|
||
| file(GLOB _coverage_raw_profiles "${PROFILE_DIR}/*.profraw") | ||
| if(NOT _coverage_raw_profiles) | ||
| message(FATAL_ERROR | ||
| "CTest produced no LLVM profiles in ${PROFILE_DIR}") | ||
| endif() | ||
|
|
||
| execute_process( | ||
| COMMAND "${LLVM_PROFDATA}" merge -sparse ${_coverage_raw_profiles} | ||
| -o "${PROFILE_DATA}" | ||
| RESULT_VARIABLE _coverage_merge_result | ||
| ) | ||
| if(NOT _coverage_merge_result EQUAL 0) | ||
| message(FATAL_ERROR "llvm-profdata failed with exit code ${_coverage_merge_result}") | ||
| endif() | ||
|
|
||
| file(STRINGS "${OBJECT_LIST_FILE}" _coverage_objects) | ||
| if(NOT _coverage_objects) | ||
| message(FATAL_ERROR "No coverage objects were generated in ${OBJECT_LIST_FILE}") | ||
| endif() | ||
|
|
||
| list(GET _coverage_objects 0 _coverage_main_object) | ||
| list(REMOVE_AT _coverage_objects 0) | ||
| set(_coverage_object_args) | ||
| foreach(_coverage_object IN LISTS _coverage_objects) | ||
| list(APPEND _coverage_object_args -object "${_coverage_object}") | ||
| endforeach() | ||
| unset(_coverage_object) | ||
|
|
||
| execute_process( | ||
| COMMAND "${LLVM_COV}" report "${_coverage_main_object}" | ||
| ${_coverage_object_args} | ||
| "-instr-profile=${PROFILE_DATA}" | ||
| "${SOURCE_DIR}" | ||
| RESULT_VARIABLE _coverage_report_result | ||
| ) | ||
| if(NOT _coverage_report_result EQUAL 0) | ||
| message(FATAL_ERROR "llvm-cov report failed with exit code ${_coverage_report_result}") | ||
| endif() | ||
|
|
||
| execute_process( | ||
| COMMAND "${LLVM_COV}" show "${_coverage_main_object}" | ||
| ${_coverage_object_args} | ||
| "-instr-profile=${PROFILE_DATA}" | ||
| -format=html | ||
| "-output-dir=${HTML_DIR}" | ||
| "${SOURCE_DIR}" | ||
| RESULT_VARIABLE _coverage_html_result | ||
| ) | ||
| if(NOT _coverage_html_result EQUAL 0) | ||
| message(FATAL_ERROR "llvm-cov show failed with exit code ${_coverage_html_result}") | ||
| endif() | ||
|
|
||
| message(STATUS "Coverage summary: ${PROFILE_DATA}") | ||
| message(STATUS "Coverage HTML report: ${HTML_DIR}/index.html") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| foreach(_coverage_required_var IN ITEMS | ||
| CTEST_EXECUTABLE | ||
| BUILD_DIR | ||
| PROFILE_DIR | ||
| CTEST_EXCLUDE_REGEX) | ||
| if(NOT DEFINED ${_coverage_required_var}) | ||
| message(FATAL_ERROR "${_coverage_required_var} is required") | ||
| endif() | ||
| endforeach() | ||
| unset(_coverage_required_var) | ||
|
|
||
| set(ENV{LLVM_PROFILE_FILE} "${PROFILE_DIR}/%m_%p.profraw") | ||
| set(_coverage_ctest_args --test-dir "${BUILD_DIR}" --output-on-failure) | ||
| if(NOT "${CTEST_EXCLUDE_REGEX}" STREQUAL "") | ||
| list(APPEND _coverage_ctest_args -E "${CTEST_EXCLUDE_REGEX}") | ||
| endif() | ||
| execute_process( | ||
| COMMAND "${CTEST_EXECUTABLE}" ${_coverage_ctest_args} | ||
| RESULT_VARIABLE _coverage_ctest_result) | ||
| unset(_coverage_ctest_args) | ||
|
|
||
| include("${CMAKE_CURRENT_LIST_DIR}/GenerateCoverageReport.cmake") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: When ctest fails in a way that produces no profiles, GenerateCoverageReport.cmake aborts first with "CTest produced no LLVM profiles", so the real ctest exit code and this script's "CTest failed" message are never surfaced. Check Prompt for AI agents |
||
|
|
||
| if(NOT _coverage_ctest_result EQUAL 0) | ||
| message(FATAL_ERROR "CTest failed with exit code ${_coverage_ctest_result}") | ||
| endif() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # Host C/C++ Coverage | ||
|
|
||
| The `coverage` target produces an LLVM source-based coverage report for host | ||
| C and C++ code. CUDA and HIP source files are intentionally excluded. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Clang and matching `llvm-cov` and `llvm-profdata` executables | ||
| - The usual backend build prerequisites (CUDA or HIP) | ||
|
|
||
| On Debian or Ubuntu, install the LLVM toolchain package matching the Clang | ||
| version used for the build. | ||
|
|
||
| ## Generate the report | ||
|
|
||
| Configure a separate Debug build so coverage instrumentation does not affect | ||
| the normal build: | ||
|
|
||
| ```bash | ||
| cd server | ||
| cmake -S . -B build-coverage \ | ||
| -DCMAKE_BUILD_TYPE=Debug \ | ||
| -DCMAKE_C_COMPILER=clang \ | ||
| -DCMAKE_CXX_COMPILER=clang++ \ | ||
| -DDFLASH27B_COVERAGE=ON \ | ||
| -DDFLASH27B_TESTS=ON | ||
| cmake --build build-coverage --target coverage -j | ||
| ``` | ||
|
|
||
| For CUDA builds where `nvcc` is not on `PATH`, set `CUDACXX` before | ||
| configuring: | ||
|
|
||
| ```bash | ||
| CUDACXX=/usr/local/cuda/bin/nvcc cmake -S . -B build-coverage ... | ||
| ``` | ||
|
|
||
| The target runs CTest with `LLVM_PROFILE_FILE` configured, merges the emitted | ||
| profiles, prints a text summary, and writes the HTML report to: | ||
|
|
||
| ```text | ||
| server/build-coverage/coverage/html/index.html | ||
| ``` | ||
|
|
||
| The merged profile is `server/build-coverage/coverage/coverage.profdata`. | ||
|
|
||
| ## Test failures | ||
|
|
||
| The HTML report is generated even when CTest fails, then the `coverage` target | ||
| returns CTest's failure status. Review the test failures before treating the | ||
| report as a passing coverage run. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: When a device-backed test has a renamed or discovered CTest name, this regex does not exclude it. CTest then runs an executable that
coveragedeliberately did not build; exclude the actual names, including${target}.*discovered tests.Prompt for AI agents