CMake: Add code coverage support for clang

Pretty bare bones but gets the job done, unlike the gcc
tooling, this will work for release builds, the performance cost
of it is on the high side of things, the full test suite tests take over
an hour for me with code coverage support enabled on a release build.
I have not timed a debug build. Given developers can just run their
tests to get coverage data over what they are working on, I feel this
is still useful tooling to have.

This adds the 3 targets for clang and adds a single gcc target

coverage-reset - this removes the collected code coverage data and
report

coverage-report - This merges the collected data and generates the
report (new for gcc)

coverage-show - This merges the collected data and generates the report
and opens it in the browser

This relies on llvm-cov and llvm-profdata being available if not found
code coverage is disabled.

Note: A full test run requires an obscene amount of disk space, a
complete test run takes about 125GB and takes 12 minutes to merge, so
provision the COMPILER_CODE_COVERAGE_DATA_DIR folder accordingly

Example report in PR
This commit is contained in:
Ray Molenkamp
2025-05-12 16:28:41 +02:00
committed by Ray molenkamp
parent dd47ee9e25
commit 2c5b9e182b
5 changed files with 106 additions and 24 deletions

View File

@@ -1033,25 +1033,43 @@ Build and link with code coverage support (only for Debug targets)."
mark_as_advanced(WITH_COMPILER_CODE_COVERAGE) mark_as_advanced(WITH_COMPILER_CODE_COVERAGE)
if(WITH_COMPILER_CODE_COVERAGE) if(WITH_COMPILER_CODE_COVERAGE)
if(NOT CMAKE_COMPILER_IS_GNUCC) if(CMAKE_COMPILER_IS_GNUCC)
message(WARNING "WITH_COMPILER_CODE_COVERAGE only works with GCC currently.") set(_code_coverage_defaults "--coverage")
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
get_filename_component(COMPILER_DIRECTORY ${CMAKE_CXX_COMPILER} DIRECTORY)
find_program(LLVM_COV "llvm-cov" ${COMPILER_DIRECTORY} NO_DEFAULT_PATH)
find_program(LLVM_PROFDATA "llvm-profdata" ${COMPILER_DIRECTORY} NO_DEFAULT_PATH)
if(NOT LLVM_COV OR NOT LLVM_PROFDATA)
message(WARNING "Could not find code coverage tools, disabling code coverage. You may explicitly specify LLVM_COV and LLVM_PROFDATA to work around this warning.")
set(WITH_COMPILER_CODE_COVERAGE OFF)
else()
set(_code_coverage_defaults "-fprofile-instr-generate -fcoverage-mapping")
endif()
else()
message("unsupported compiler ${CMAKE_C_COMPILER_ID} for WITH_COMPILER_CODE_COVERAGE, disabling.")
set(WITH_COMPILER_CODE_COVERAGE OFF) set(WITH_COMPILER_CODE_COVERAGE OFF)
endif() endif()
endif()
if(WITH_COMPILER_CODE_COVERAGE) # The code above could have disabled the feature, so check again.
set(_code_coverage_defaults "--coverage") if(WITH_COMPILER_CODE_COVERAGE)
set(COMPILER_CODE_COVERAGE_CFLAGS set(COMPILER_CODE_COVERAGE_CFLAGS
${_code_coverage_defaults} CACHE STRING ${_code_coverage_defaults} CACHE STRING
"C flags for code coverage" "C flags for code coverage"
) )
mark_as_advanced(COMPILER_CODE_COVERAGE_CFLAGS) mark_as_advanced(COMPILER_CODE_COVERAGE_CFLAGS)
set(COMPILER_CODE_COVERAGE_CXXFLAGS set(COMPILER_CODE_COVERAGE_CXXFLAGS
${_code_coverage_defaults} CACHE STRING ${_code_coverage_defaults} CACHE STRING
"C++ flags for code coverage" "C++ flags for code coverage"
) )
mark_as_advanced(COMPILER_CODE_COVERAGE_CXXFLAGS) mark_as_advanced(COMPILER_CODE_COVERAGE_CXXFLAGS)
unset(_code_coverage_defaults) if(CMAKE_C_COMPILER_ID MATCHES "Clang")
set(_code_coverage_dir_default "${CMAKE_BINARY_DIR}/coverage")
set(COMPILER_CODE_COVERAGE_DATA_DIR ${_code_coverage_dir_default} CACHE STRING "Directory for code coverage data")
mark_as_advanced(COMPILER_CODE_COVERAGE_DATA_DIR)
unset(_code_coverage_dir_default)
endif()
unset(_code_coverage_defaults)
endif()
endif() endif()
if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID MATCHES "Clang") if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID MATCHES "Clang")
@@ -1463,8 +1481,15 @@ set(PLATFORM_LINKFLAGS_RELEASE "")
set(PLATFORM_LINKFLAGS_EXECUTABLE "") set(PLATFORM_LINKFLAGS_EXECUTABLE "")
if(WITH_COMPILER_CODE_COVERAGE) if(WITH_COMPILER_CODE_COVERAGE)
string(APPEND CMAKE_C_FLAGS_DEBUG " ${COMPILER_CODE_COVERAGE_CFLAGS}") if(CMAKE_COMPILER_IS_GNUCC)
string(APPEND CMAKE_CXX_FLAGS_DEBUG " ${COMPILER_CODE_COVERAGE_CXXFLAGS}") # GCC only supports this on debug builds.
string(APPEND CMAKE_C_FLAGS_DEBUG " ${COMPILER_CODE_COVERAGE_CFLAGS}")
string(APPEND CMAKE_CXX_FLAGS_DEBUG " ${COMPILER_CODE_COVERAGE_CXXFLAGS}")
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
# Clang will allow it on all builds.
string(APPEND CMAKE_C_FLAGS " ${COMPILER_CODE_COVERAGE_CFLAGS}")
string(APPEND CMAKE_CXX_FLAGS " ${COMPILER_CODE_COVERAGE_CXXFLAGS}")
endif()
endif() endif()
if(NOT CMAKE_BUILD_TYPE MATCHES "Release") if(NOT CMAKE_BUILD_TYPE MATCHES "Release")
@@ -2509,10 +2534,10 @@ if(WITH_PYTHON)
set(_numpy_include "_core/include") set(_numpy_include "_core/include")
if(PYTHON_VERSION VERSION_LESS "3.13") if(PYTHON_VERSION VERSION_LESS "3.13")
set(_numpy_include "core/include") set(_numpy_include "core/include")
endif() endif()
find_python_package(numpy "${_numpy_include}") find_python_package(numpy "${_numpy_include}")
unset(_numpy_include) unset(_numpy_include)
endif() endif()
endif() endif()
if(WIN32 OR APPLE) if(WIN32 OR APPLE)

View File

@@ -39,7 +39,9 @@ function(blender_test_set_envvars testname envvar_list)
list(APPEND envvar_list "${_lsan_options}" "${_asan_options}") list(APPEND envvar_list "${_lsan_options}" "${_asan_options}")
endif() endif()
endif() endif()
if(WITH_COMPILER_CODE_COVERAGE AND CMAKE_C_COMPILER_ID MATCHES "Clang")
list(APPEND envvar_list "LLVM_PROFILE_FILE=${COMPILER_CODE_COVERAGE_DATA_DIR}/raw/blender_%p.profraw")
endif()
# Can only be called once per test to define its custom environment variables. # Can only be called once per test to define its custom environment variables.
set_tests_properties(${testname} PROPERTIES ENVIRONMENT "${envvar_list}") set_tests_properties(${testname} PROPERTIES ENVIRONMENT "${envvar_list}")
endfunction() endfunction()

View File

@@ -71,15 +71,70 @@ endif()
add_subdirectory(gtests) add_subdirectory(gtests)
if(WITH_COMPILER_CODE_COVERAGE) if(WITH_COMPILER_CODE_COVERAGE)
set(COVERAGE_SCRIPT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/coverage/coverage.py) if(CMAKE_COMPILER_IS_GNUCC)
set(COVERAGE_SCRIPT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/coverage/coverage.py)
add_custom_target(coverage-report add_custom_target(coverage-report
${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} report --build-directory ${CMAKE_BINARY_DIR} ${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} report --build-directory ${CMAKE_BINARY_DIR} --no-browser
USES_TERMINAL USES_TERMINAL
) )
add_custom_target(coverage-reset add_custom_target(coverage-show
${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} reset --build-directory ${CMAKE_BINARY_DIR} ${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} report --build-directory ${CMAKE_BINARY_DIR}
USES_TERMINAL USES_TERMINAL
) )
add_custom_target(coverage-reset
${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} reset --build-directory ${CMAKE_BINARY_DIR}
USES_TERMINAL
)
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
add_custom_target(coverage-reset
COMMAND ${CMAKE_COMMAND} -E
remove_directory ${COMPILER_CODE_COVERAGE_DATA_DIR}
USES_TERMINAL
)
add_custom_target(coverage-merge
COMMENT "Merging code-coverage data."
BYPRODUCTS "${COMPILER_CODE_COVERAGE_DATA_DIR}/blender.profdata"
COMMAND "${LLVM_PROFDATA}"
merge
-sparse
${COMPILER_CODE_COVERAGE_DATA_DIR}/raw/*.profraw
-o ${COMPILER_CODE_COVERAGE_DATA_DIR}/blender.profdata
)
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 18.1 OR
(CMAKE_C_COMPILER_ID MATCHES "AppleClang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 17.0)
)
# Available clang on 18.1+ only , Apple's compiler doesn't map 1:1 to clang
# versions so on apple we check for version 17+ which maps to clang
# version 19.1.4, given this is somewhat unintuitive there is a handy table on
# https://en.wikipedia.org/wiki/Xcode#Xcode_15.0_-_(since_visionOS_support)_2
set(_cov_extra_args -show-directory-coverage)
endif()
add_custom_target(coverage-report
BYPRODUCTS "${COMPILER_CODE_COVERAGE_DATA_DIR}/report/index.html"
SOURCES "${COMPILER_CODE_COVERAGE_DATA_DIR}/blender.profdata"
COMMENT "Generating code-coverage report."
COMMAND "${LLVM_COV}" show
-format=html
-show-instantiations=true
-show-line-counts-or-regions
${_cov_extra_args}
-instr-profile=${COMPILER_CODE_COVERAGE_DATA_DIR}/blender.profdata
--output-dir=${COMPILER_CODE_COVERAGE_DATA_DIR}/report
$<TARGET_FILE:blender>
USES_TERMINAL
)
if(NOT WIN32)
set(_open_cmd "open")
endif()
add_custom_target(coverage-show
COMMENT "Starting browser for ${COMPILER_CODE_COVERAGE_DATA_DIR}/report/index.html"
COMMAND ${_open_cmd} "${COMPILER_CODE_COVERAGE_DATA_DIR}/report/index.html"
SOURCES "${COMPILER_CODE_COVERAGE_DATA_DIR}/report/index.html"
)
unset(_cov_extra_args)
unset(_open_cmd)
endif()
endif() endif()