From 2c5b9e182b34e151f125258ddd39c720b70702e1 Mon Sep 17 00:00:00 2001 From: Ray Molenkamp Date: Mon, 12 May 2025 16:28:41 +0200 Subject: [PATCH] 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 --- CMakeLists.txt | 49 ++++++++++++++++------ build_files/cmake/testing.cmake | 4 +- lib/macos_arm64 | 2 +- lib/windows_x64 | 2 +- tests/CMakeLists.txt | 73 +++++++++++++++++++++++++++++---- 5 files changed, 106 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index db79e34fdfa..0265c99483f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1033,25 +1033,43 @@ Build and link with code coverage support (only for Debug targets)." mark_as_advanced(WITH_COMPILER_CODE_COVERAGE) if(WITH_COMPILER_CODE_COVERAGE) - if(NOT CMAKE_COMPILER_IS_GNUCC) - message(WARNING "WITH_COMPILER_CODE_COVERAGE only works with GCC currently.") + if(CMAKE_COMPILER_IS_GNUCC) + 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) endif() -endif() -if(WITH_COMPILER_CODE_COVERAGE) - set(_code_coverage_defaults "--coverage") + # The code above could have disabled the feature, so check again. + if(WITH_COMPILER_CODE_COVERAGE) set(COMPILER_CODE_COVERAGE_CFLAGS ${_code_coverage_defaults} CACHE STRING "C flags for code coverage" ) - mark_as_advanced(COMPILER_CODE_COVERAGE_CFLAGS) + mark_as_advanced(COMPILER_CODE_COVERAGE_CFLAGS) set(COMPILER_CODE_COVERAGE_CXXFLAGS ${_code_coverage_defaults} CACHE STRING "C++ flags for code coverage" ) - mark_as_advanced(COMPILER_CODE_COVERAGE_CXXFLAGS) - unset(_code_coverage_defaults) + mark_as_advanced(COMPILER_CODE_COVERAGE_CXXFLAGS) + 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() if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID MATCHES "Clang") @@ -1463,8 +1481,15 @@ set(PLATFORM_LINKFLAGS_RELEASE "") set(PLATFORM_LINKFLAGS_EXECUTABLE "") if(WITH_COMPILER_CODE_COVERAGE) - string(APPEND CMAKE_C_FLAGS_DEBUG " ${COMPILER_CODE_COVERAGE_CFLAGS}") - string(APPEND CMAKE_CXX_FLAGS_DEBUG " ${COMPILER_CODE_COVERAGE_CXXFLAGS}") + if(CMAKE_COMPILER_IS_GNUCC) + # 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() if(NOT CMAKE_BUILD_TYPE MATCHES "Release") @@ -2509,10 +2534,10 @@ if(WITH_PYTHON) set(_numpy_include "_core/include") if(PYTHON_VERSION VERSION_LESS "3.13") set(_numpy_include "core/include") - endif() + endif() find_python_package(numpy "${_numpy_include}") unset(_numpy_include) - endif() + endif() endif() if(WIN32 OR APPLE) diff --git a/build_files/cmake/testing.cmake b/build_files/cmake/testing.cmake index bb139f3e6e0..7b04199663d 100644 --- a/build_files/cmake/testing.cmake +++ b/build_files/cmake/testing.cmake @@ -39,7 +39,9 @@ function(blender_test_set_envvars testname envvar_list) list(APPEND envvar_list "${_lsan_options}" "${_asan_options}") 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. set_tests_properties(${testname} PROPERTIES ENVIRONMENT "${envvar_list}") endfunction() diff --git a/lib/macos_arm64 b/lib/macos_arm64 index 61f8c3337c9..e07a7e9ab96 160000 --- a/lib/macos_arm64 +++ b/lib/macos_arm64 @@ -1 +1 @@ -Subproject commit 61f8c3337c951eef88c254f5f65f808b4ff4ce58 +Subproject commit e07a7e9ab9617d5a5ff5564a5e974c05363eea32 diff --git a/lib/windows_x64 b/lib/windows_x64 index 9bad6ade51f..cdef40845cd 160000 --- a/lib/windows_x64 +++ b/lib/windows_x64 @@ -1 +1 @@ -Subproject commit 9bad6ade51fd04ee8713dccff9e57ec5661e2578 +Subproject commit cdef40845cd1760e0361673529703bfc5c843838 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dce54ffdcf5..842e8247cea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -71,15 +71,70 @@ endif() add_subdirectory(gtests) 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 - ${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} report --build-directory ${CMAKE_BINARY_DIR} - USES_TERMINAL - ) + add_custom_target(coverage-report + ${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} report --build-directory ${CMAKE_BINARY_DIR} --no-browser + USES_TERMINAL + ) - add_custom_target(coverage-reset - ${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} reset --build-directory ${CMAKE_BINARY_DIR} - USES_TERMINAL - ) + add_custom_target(coverage-show + ${PYTHON_EXECUTABLE} ${COVERAGE_SCRIPT_PATH} report --build-directory ${CMAKE_BINARY_DIR} + 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 + $ + 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()