# DOES NOT SUPPORT RUNNING IN PARALLEL: https://github.com/linux-test-project/lcov/issues/37 # TODO: macos xcode coverage: works? What dependencies? get_filename_component(_CODECOVERAGE_MODDIR ${CMAKE_CURRENT_LIST_FILE} PATH) function(codecoverage_enable) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") find_program(GCOV_BIN gcov) if(NOT GCOV_BIN) message(FATAL_ERROR "gcov not found") endif() set(COV_BIN "${GCOV_BIN}") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") find_program(LLVM_COV_BIN llvm-cov) if(NOT LLVM_COV_BIN) message(FATAL_ERROR "llvm-cov not found") endif() configure_file("${_CODECOVERAGE_MODDIR}/llvm-cov-wrapper.in" "${CMAKE_BINARY_DIR}/llvm-cov-wrapper") set(COV_BIN "${CMAKE_BINARY_DIR}/llvm-cov-wrapper") else() message(FATAL_ERROR "Code coverage unsupported for this compiler.") endif() set(COV_BIN "${COV_BIN}" CACHE INTERNAL "Coverage tool") find_program(LCOV_BIN lcov) if(NOT LCOV_BIN) message(FATAL_ERROR "lcov not found") endif() find_program(GENINFO_BIN geninfo) if(NOT GENINFO_BIN) message(FATAL_ERROR "geninfo not found") endif() find_program(GENHTML_BIN genhtml) if(NOT GENHTML_BIN) message(FATAL_ERROR "genhtml not found") endif() find_program(CPPFILT_BIN c++filt) if(NOT CPPFILT_BIN) message(FATAL_ERROR "c++filt not found") endif() message(STATUS "TEST_COVERAGE enabled. Optimizations are disabled, debug data will be added to targets.") # Remove global optimization flags foreach(CONFIG_TYPE ${CMAKE_CONFIGURATION_TYPES} "") foreach(LANGUAGE C CXX) set(FLAGVARNAME "CMAKE_${LANGUAGE}_FLAGS") if(CONFIG_TYPE) string(TOUPPER "${CONFIG_TYPE}" CONFIG_TYPE) set(FLAGVARNAME "${FLAGVARNAME}_${CONFIG_TYPE}") endif() string(REGEX REPLACE "-O[s0-3]" "" _OUT "${${FLAGVARNAME}}") set("${FLAGVARNAME}" "${_OUT}" PARENT_SCOPE) endforeach() endforeach() set(COVERAGE_COMPILE_FLAGS "-O0 -fprofile-arcs -ftest-coverage -g" CACHE STRING "Compile flags for compiling with coverage support") set(COVERAGE_LINK_FLAGS "-O0 -fprofile-arcs -ftest-coverage" CACHE STRING "Link flags for linking with coverage support") set(LCOV_DATA_PATH "${CMAKE_BINARY_DIR}/lcov/data") set(LCOV_DATA_PATH_INIT "${LCOV_DATA_PATH}/init" CACHE PATH "Where to put initial coverage") set(LCOV_DATA_PATH_CAPTURE "${LCOV_DATA_PATH}/capture" CACHE PATH "Where to put final coverage") set(LCOV_HTML_PATH "${CMAKE_BINARY_DIR}/lcov/html" CACHE PATH "Where to put html coverage reports") file(MAKE_DIRECTORY ${LCOV_DATA_PATH_INIT}) file(MAKE_DIRECTORY ${LCOV_DATA_PATH_CAPTURE}) add_custom_target(gcov) add_custom_target(lcov-capture-init) add_custom_target(lcov-capture) define_property(GLOBAL PROPERTY COVERAGE_TARGETS BRIEF_DOCS "List of all coverage targets" FULL_DOCS "All targets that is collected coverage over" ) define_property(GLOBAL PROPERTY LCOV_CAPTURE_FILES BRIEF_DOCS "List of all capture files" FULL_DOCS "All capture files which have coverage data included" ) define_property(GLOBAL PROPERTY LCOV_CAPTURE_INIT_FILES BRIEF_DOCS "List of all init capture files" FULL_DOCS "Empty capture files for the baseline coverage" ) endfunction() function(_coverage_path_dest _RETURNVAR PATH) string(REPLACE "${CMAKE_CURRENT_BINARY_DIR}/" "" PATH "${PATH}") if(IS_ABSOLUTE "${PATH}") file(RELATIVE_PATH PATH "${CMAKE_CURRENT_SOURCE_DIR}" ${PATH}) endif() string(REPLACE ".." "__" PATH "${PATH}") set("${_RETURNVAR}" "${PATH}" PARENT_SCOPE) endfunction() function(_coverage_filter_languages _RETURNVAR) get_property(ENABLED_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) set(FILTERED_FILES) foreach(FILE ${ARGN}) if(FILE MATCHES "^\\$<") #ignore generator expressions continue() endif() get_filename_component(FILE_EXT "${FILE}" EXT) string(TOLOWER "${FILE_EXT}" FILE_EXT) string(SUBSTRING "${FILE_EXT}" 1 -1 FILE_EXT) foreach(LANG ${ENABLED_LANGUAGES}) if(FILE_EXT IN_LIST CMAKE_${LANG}_SOURCE_FILE_EXTENSIONS) list(APPEND FILTERED_FILES "${FILE}") endif() endforeach() endforeach() set("${_RETURNVAR}" "${FILTERED_FILES}" PARENT_SCOPE) endfunction() function(coverage_add_target _TARGET) cmake_parse_arguments(CAT "" "" "EXCEPT" ${ARGN}) get_target_property(TARGET_TYPE "${_TARGET}" TYPE) if(TARGET_TYPE STREQUAL "INTERFACE_LIBRARY") return() endif() get_property(COMPILE_FLAGS TARGET "${_TARGET}" PROPERTY COMPILE_FLAGS) set_property(TARGET "${_TARGET}" APPEND_STRING PROPERTY COMPILE_FLAGS " ${COVERAGE_COMPILE_FLAGS}" ) set_property(TARGET "${_TARGET}" APPEND_STRING PROPERTY LINK_FLAGS " ${COVERAGE_LINK_FLAGS}" ) get_target_property(SOURCES "${_TARGET}" SOURCES) set(COVERAGE_FILES) foreach(SOURCE ${SOURCES}) if("${SOURCE}" IN_LIST CAT_EXCEPT) continue() endif() _coverage_path_dest(RELSOURCE "${SOURCE}") list(APPEND COVERAGE_FILES "CMakeFiles/${_TARGET}.dir/${RELSOURCE}.gcno" "CMakeFiles/${_TARGET}.dir/${RELSOURCE}.gcda" ) endforeach() set_property(GLOBAL APPEND PROPERTY COVERAGE_TARGETS "${_TARGET}" ) set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${COVERAGE_FILES} ) coverage_gcov_target(${_TARGET} EXCEPT "${CAT_EXCEPT}") coverage_lcov_target(${_TARGET} EXCEPT "${CAT_EXCEPT}") endfunction() function(coverage_collect) codecoverage_lcov_capture_initial() codecoverage_lcov_capture() endfunction() function(coverage_gcov_target _TARGET) cmake_parse_arguments(CGT "" "" "EXCEPT" ${ARGN}) set(GCOV_DIR "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_TARGET}.dir") get_target_property(SOURCES "${_TARGET}" SOURCES) _coverage_filter_languages(SOURCES ${SOURCES}) foreach(SOURCE ${SOURCES}) _coverage_path_dest(RELSOURCE "${SOURCE}") if("${SOURCE}" IN_LIST CGT_EXCEPT) continue() endif() get_filename_component(RELSOURCE_PATH "${RELSOURCE}" PATH) add_custom_command(OUTPUT "${GCOV_DIR}/${RELSOURCE}.gcov" COMMAND test -s "${GCOV_DIR}/${RELSOURCE}.gcda" && "${COV_BIN}" "${GCOV_DIR}/${RELSOURCE}.gcno" > /dev/null || true DEPENDS "${_TARGET}" "${GCOV_DIR}/${RELSOURCE}.gcno" "${GCOV_DIR}/${RELSOURCE}.gcda" WORKING_DIRECTORY "${GCOV_DIR}/${RELSOURCE_PATH}" COMMENT "Capturing gcov data for source ${SOURCE} of target ${_TARGET}" ) list(APPEND GCOV_FILES "${GCOV_DIR}/${RELSOURCE}.gcov") endforeach() add_custom_target("${_TARGET}-gcov" DEPENDS ${GCOV_FILES} COMMENT "Capturing gcov data of target ${_TARGET}" ) add_dependencies(gcov "${_TARGET}-gcov") endfunction() function(coverage_lcov_target _TARGET) coverage_lcov_target_initial("${_TARGET}" ${ARGN}) coverage_lcov_capture_target("${_TARGET}" ${ARGN}) endfunction() function(coverage_lcov_target_initial _TARGET) cmake_parse_arguments(CLTI "" "" "EXCEPT" ${ARGN}) set(LCOV_DIR "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_TARGET}.dir") set(INIT_DIR "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_TARGET}-capture-init.dir") get_target_property(SOURCES "${_TARGET}" SOURCES) _coverage_filter_languages(SOURCES ${SOURCES}) set(GENINFO_FILES) foreach(SOURCE ${SOURCES}) if("${SOURCE}" IN_LIST CLTI_EXCEPT) continue() endif() # generate empty coverage files _coverage_path_dest(RELSOURCE "${SOURCE}") get_filename_component(RELSOURCE_DIR "${RELSOURCE}" DIRECTORY) file(MAKE_DIRECTORY "${INIT_DIR}/${RELSOURCE_DIR}") set(GENINFO_FILE "${INIT_DIR}/${RELSOURCE}.info.init") list(APPEND GENINFO_FILES "${GENINFO_FILE}") add_custom_command(OUTPUT "${GENINFO_FILE}" COMMAND "${GENINFO_BIN}" --quiet --base-directory "${PROJECT_SOURCE_DIR}" --initial --gcov-tool "${COV_BIN}" --output-filename "${GENINFO_FILE}" --no-external "${LCOV_DIR}/${RELSOURCE}.gcno" DEPENDS "${_TARGET}" COMMENT "Capturing initial coverage data for ${SOURCE}" ) endforeach() set(OUTFILE "${LCOV_DATA_PATH_INIT}/${_TARGET}.info") coverage_lcov_merge_files( OUTFILE "${OUTFILE}" FILES ${GENINFO_FILES} FLAGS "--initial" REMOVE_PATTERNS ) add_custom_target("${_TARGET}-capture-init" DEPENDS "${OUTFILE}" COMMENT "Capturing initial coverage data for target ${_TARGET}" ) add_dependencies(lcov-capture-init "${_TARGET}-capture-init") set_property(GLOBAL APPEND PROPERTY LCOV_CAPTURE_INIT_FILES "${OUTFILE}") endfunction() function(coverage_lcov_capture_target _TARGET) cmake_parse_arguments(CLC "" "" "EXCEPT" ${ARGN}) set(LCOV_DIR "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_TARGET}.dir") set(CAP_DIR "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_TARGET}-geninfo.dir") set(INIT_DIR "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${_TARGET}-capture-init.dir") get_target_property(SOURCES "${_TARGET}" SOURCES) _coverage_filter_languages(SOURCES ${SOURCES}) set(GCDA_FILES) set(GENINFO_FILES) foreach(SOURCE ${SOURCES}) if("${SOURCE}" IN_LIST CLC_EXCEPT) continue() endif() _coverage_path_dest(RELSOURCE "${SOURCE}") get_filename_component(RELSOURCE_DIR "${RELSOURCE}" DIRECTORY) file(MAKE_DIRECTORY "${CAP_DIR}/${RELSOURCE_DIR}") set(OUTFILE "${CAP_DIR}/${RELSOURCE}.info") list(APPEND GENINFO_FILES "${OUTFILE}") list(APPEND GCDA_FILES "${LCOV_DIR}/${RELSOURCE}.gcda") add_custom_command(OUTPUT "${LCOV_DIR}/${RELSOURCE}.gcda" COMMAND "${CMAKE_COMMAND}" -E touch "${LCOV_DIR}/${RELSOURCE}.gcda" DEPENDS "${_TARGET}" COMMENT "Touching ${LCOV_DIR}/${RELSOURCE}.gcda" ) add_custom_command(OUTPUT "${OUTFILE}" COMMAND test -s "${LCOV_DIR}/${RELSOURCE}.gcda" && "${GENINFO_BIN}" --quiet --base-directory "${PROJECT_SOURCE_DIR}" --gcov-tool "${COV_BIN}" --output-filename "${OUTFILE}" --no-external --rc lcov_branch_coverage=1 "${LCOV_DIR}/${RELSOURCE}.gcda" || cp "${INIT_DIR}/${RELSOURCE}.info.init" "${OUTFILE}" DEPENDS "${_TARGET}" "${_TARGET}-capture-init" "${LCOV_DIR}/${RELSOURCE}.gcda" COMMENT "Capturing coverage data for ${SOURCE}" ) endforeach() # Concatenate all files generated by geninfo to a single file per target. set(OUTFILE "${LCOV_DATA_PATH_CAPTURE}/${_TARGET}.info") coverage_lcov_merge_files(OUTFILE "${OUTFILE}" FILES ${GENINFO_FILES} ) add_custom_target("${_TARGET}-geninfo" DEPENDS "${OUTFILE}" COMMENT "Capturing coverage data for target ${_TARGET}" ) add_custom_target("${_TARGET}-geninfo-clean" COMMAND "${CMAKE_COMMAND}" -E remove "${OUTFILE}" ${GCDA_FILES} COMMENT "Cleaning coverage data of target ${_TARGET}" ) add_dependencies(lcov-capture "${_TARGET}-geninfo") set_property(GLOBAL APPEND PROPERTY LCOV_CAPTURE_FILES "${OUTFILE}") # Add target for generating html output for this target only. file(MAKE_DIRECTORY "${LCOV_HTML_PATH}/${_TARGET}") add_custom_target("${_TARGET}-lcov" COMMAND "${GENHTML_BIN}" --quiet --sort --prefix "${PROJECT_SOURCE_DIR}" --baseline-file "${LCOV_DATA_PATH_INIT}/${_TARGET}.info" --output-directory "${LCOV_HTML_PATH}/${_TARGET}" --highlight --legend --show-details --branch-coverage --function-coverage --title "${PROJECT_NAME}" --target "${_TARGET}" --demangle-cpp "${OUTFILE}" DEPENDS "${_TARGET}-geninfo" "${_TARGET}-capture-init" COMMENT "Generating coverage report for target ${_TARGET}" ) endfunction() function(codecoverage_lcov_capture_initial) set(INITCAPTURE_FILE "${LCOV_DATA_PATH_INIT}/all_targets.info") set(INITCAPTURE_FILE_UPLOAD "${INITCAPTURE_FILE}.upload") get_property(LCOV_CAPTURE_INIT_FILES GLOBAL PROPERTY LCOV_CAPTURE_INIT_FILES) coverage_lcov_merge_files(OUTFILE "${INITCAPTURE_FILE}" FILES ${LCOV_CAPTURE_INIT_FILES}) add_custom_target(lcov-geninfo-init DEPENDS ${INITCAPTURE_FILE} lcov-capture-init COMMENT "Capturing global initial coverage data" ) string(REPLACE "/" "\\/" PROJBINDIR_ESCAPED "${PROJECT_BINARY_DIR}/") string(REPLACE "/" "\\/" PROJSRCDIR_ESCAPED "${PROJECT_SOURCE_DIR}/") add_custom_command(OUTPUT "${INITCAPTURE_FILE_UPLOAD}" COMMAND sed -e 's/${PROJBINDIR_ESCAPED}//g' -e 's/${PROJSRCDIR_ESCAPED}//g' "${INITCAPTURE_FILE}" > "${INITCAPTURE_FILE_UPLOAD}" DEPENDS lcov-geninfo-init "${INITCAPTURE_FILE}" COMMENT "Creating ${INITCAPTURE_FILE_UPLOAD}" ) add_custom_target(lcov-geninfo-init-upload DEPENDS "${INITCAPTURE_FILE_UPLOAD}" COMMENT "Preparing ${INITCAPTURE_FILE_UPLOAD} for upload" ) endfunction() function(codecoverage_lcov_capture) set(TOTALCAPTURE_FILE "${LCOV_DATA_PATH_CAPTURE}/all_targets.info") set(TOTALCAPTURE_FILE_UPLOAD "${TOTALCAPTURE_FILE}.upload") get_property(COVERAGE_TARGETS GLOBAL PROPERTY COVERAGE_TARGETS) get_property(LCOV_CAPTURE_FILES GLOBAL PROPERTY LCOV_CAPTURE_FILES) set(GENINFO_CLEAN_TARGETS) set(GENINFO_TARGETS) foreach(COVERAGE_TARGET ${COVERAGE_TARGETS}) list(APPEND GENINFO_TARGETS "${COVERAGE_TARGET}-geninfo") list(APPEND GENINFO_CLEAN_TARGETS "${COVERAGE_TARGET}-geninfo-clean") endforeach() coverage_lcov_merge_files(OUTFILE "${TOTALCAPTURE_FILE}" FILES ${LCOV_CAPTURE_FILES}) add_custom_target(lcov-geninfo DEPENDS ${GENINFO_TARGETS} "${TOTALCAPTURE_FILE}" COMMENT "Capturing global coverage data" ) string(REPLACE "/" "\\/" PROJBINDIR_ESCAPED "${PROJECT_BINARY_DIR}/") string(REPLACE "/" "\\/" PROJSRCDIR_ESCAPED "${PROJECT_SOURCE_DIR}/") add_custom_command(OUTPUT "${TOTALCAPTURE_FILE_UPLOAD}" COMMAND sed -e 's/^SF:${PROJBINDIR_ESCAPED}/SF:/g' -e 's/^SF:${PROJSRCDIR_ESCAPED}/SF:/g' "${TOTALCAPTURE_FILE}" > "${TOTALCAPTURE_FILE_UPLOAD}" DEPENDS lcov-geninfo "${TOTALCAPTURE_FILE}" COMMENT "Creating ${TOTALCAPTURE_FILE_UPLOAD}" ) add_custom_target(lcov-geninfo-upload DEPENDS "${TOTALCAPTURE_FILE_UPLOAD}" COMMENT "Preparing ${TOTALCAPTURE_FILE_UPLOAD} for upload" ) add_custom_target(lcov-geninfo-clean COMMAND "${CMAKE_COMMAND}" -E remove "${TOTALCAPTURE_FILE}" "${TOTALCAPTURE_FILE_UPLOAD}" COMMENT "Cleaning global coverage data" DEPENDS ${GENINFO_CLEAN_TARGETS} ) file(MAKE_DIRECTORY ${LCOV_HTML_PATH}/all_targets) add_custom_target(lcov COMMAND "${GENHTML_BIN}" --quiet --sort --baseline-file "${LCOV_DATA_PATH_INIT}/all_targets.info" --output-directory "${LCOV_HTML_PATH}/all_targets" --highlight --legend --show-details --branch-coverage --function-coverage --title "${CMAKE_PROJECT_NAME}" --prefix "${PROJECT_SOURCE_DIR}" --demangle-cpp "${TOTALCAPTURE_FILE}" DEPENDS lcov-geninfo-init lcov-geninfo COMMENT "Generating global coverage report" ) endfunction() function(coverage_lcov_merge_files) cmake_parse_arguments(CLMF "" "OUTFILE" "FILES;FLAGS;REMOVE_PATTERNS" ${ARGN}) set(OUTFILE "${CLMF_OUTFILE}") # Generate merged file. string(REPLACE "${CMAKE_BINARY_DIR}/" "" FILE_REL "${OUTFILE}") add_custom_command(OUTPUT "${OUTFILE}.raw" COMMAND cat ${CLMF_FILES} > ${OUTFILE}.raw DEPENDS ${CLMF_FILES} COMMENT "Generating ${FILE_REL}" ) add_custom_command(OUTPUT "${OUTFILE}" COMMAND "${LCOV_BIN}" --quiet -a "${OUTFILE}.raw" --output-file "${OUTFILE}" --rc lcov_branch_coverage=1 --base-directory "${PROJECT_SOURCE_DIR}" ${CLMF_FLAGS} COMMAND "${LCOV_BIN}" --quiet -r "${OUTFILE}" ${CLMF_REMOVE_PATTERNS} --rc lcov_branch_coverage=1 --output-file "${OUTFILE}" ${CLMF_FLAGS} DEPENDS "${OUTFILE}.raw" COMMENT "Post-processing ${FILE_REL}" ) endfunction() function(add_coverage_upload_target_codecov_io) cmake_parse_arguments(CU "" "GITSHA1;GITBRANCH;NAME" "" ${ARGN}) set(CAPTURE_FILES "${LCOV_DATA_PATH_INIT}/all_targets.info.upload" "${LCOV_DATA_PATH_CAPTURE}/all_targets.info.upload") find_program(CURL_BIN curl) if(NOT CURL_BIN) message(FATAL_ERROR "curl not found") endif() find_program(BASH_BIN bash) if(NOT BASH_BIN) message(FATAL_ERROR "bash not found") endif() set(CODECOV_PATH "${PROJECT_BINARY_DIR}/codecov.sh") set(CODECOV_SCRIPT_URL "https://codecov.io/bash" CACHE STRING "codecov.io bash script url") set(CODECOV_FLAGS "" CACHE STRING "codecov.io flags") string(REPLACE ";" "," CODECOV_FLAGS_ARG "${CODECOV_FLAGS}") add_custom_command(OUTPUT "${CODECOV_PATH}" COMMAND "${CURL_BIN}" "${CODECOV_SCRIPT_URL}" --insecure --output "${CODECOV_PATH}" COMMENT "Fetching codecov.io bash script" ) set(EXTRA_ARGS) if(CU_GITSHA1) list(APPEND EXTRA_ARGS -C "${CU_GITSHA1}") endif() if(CU_GITBRANCH) list(APPEND EXTRA_ARGS -B "${CU_GITBRANCH}") endif() if(CU_NAME) list(APPEND EXTRA_ARGS -n "${CU_NAME}") endif() foreach(CAPTURE_FILE ${CAPTURE_FILES}) list(APPEND EXTRA_ARGS -f "${CAPTURE_FILE}") endforeach() if(CODECOV_FLAG) list(APPEND EXTRA_ARGS -F "${CODECOV_FLAGS_ARG}") endif() add_custom_target(coverage_upload COMMAND "${BASH_BIN}" "${CODECOV_PATH}" ${EXTRA_ARGS} -Z -A "--insecure" -U "--insecure" -X gcov -X coveragepy -X search -X fix DEPENDS "${CODECOV_PATH}" lcov-geninfo-upload lcov-geninfo-init-upload WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" COMMENT "Uploading coverage to codecov.io" ) endfunction()