From 3f373b06b05083d9395250379a2978b5f6085002 Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Fri, 9 Jan 2026 13:13:26 +0100 Subject: build: Add build target to generate coverage report This adds a 'make coverage' option to conveniently summarize test coverage. If lcov is installed, it will also automatically generate the HTML summary. Signed-off-by: Dimitri Staessens Signed-off-by: Sander Vrijders --- cmake/utils/GenCoverage.cmake | 94 ++++++++++++ cmake/utils/ParseCoverage.cmake | 120 +++++++++++++++ cmake/utils/PrintCoverage.cmake | 333 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 547 insertions(+) create mode 100644 cmake/utils/GenCoverage.cmake create mode 100644 cmake/utils/ParseCoverage.cmake create mode 100644 cmake/utils/PrintCoverage.cmake (limited to 'cmake/utils') diff --git a/cmake/utils/GenCoverage.cmake b/cmake/utils/GenCoverage.cmake new file mode 100644 index 00000000..65f5d8ba --- /dev/null +++ b/cmake/utils/GenCoverage.cmake @@ -0,0 +1,94 @@ +# Creates the coverage target for test coverage analysis +# Requires HAVE_GCOV to be set (from dependencies/gcov.cmake) +# Uses HAVE_LCOV for optional HTML generation (from dependencies/lcov.cmake) + +# Filter patterns for lcov --remove +set(LCOV_FILTERS '*_test.c' '*.h') + +# Ignore inconsistent coverage: legitimate gaps in error paths and +# edge cases that are difficult to exercise in unit tests. +function(get_html_coverage_commands OUTPUT_VAR) + if(HAVE_LCOV) + set(COMMANDS + COMMAND ${LCOV_PATH} + --capture --directory . + --output-file coverage.info + > /dev/null 2>&1 + COMMAND ${LCOV_PATH} + --remove coverage.info ${LCOV_FILTERS} + --output-file coverage_filtered.info + --ignore-errors inconsistent + > /dev/null 2>&1 + COMMAND ${GENHTML_PATH} + coverage_filtered.info + --output-directory coverage_html + > /dev/null 2>&1 + COMMAND ${CMAKE_COMMAND} -E echo "" + COMMAND ${CMAKE_COMMAND} -E echo "HTML report: ${CMAKE_BINARY_DIR}/coverage_html/index.html" + COMMAND ${CMAKE_COMMAND} -E echo "" + ) + set(${OUTPUT_VAR} "${COMMANDS}" PARENT_SCOPE) + else() + set(${OUTPUT_VAR} "" PARENT_SCOPE) + endif() +endfunction() + +function(create_informational_target) + # MESSAGE lines are passed as ARGV, last arg is COMMENT + list(LENGTH ARGV NUM_ARGS) + math(EXPR COMMENT_IDX "${NUM_ARGS} - 1") + list(GET ARGV ${COMMENT_IDX} COMMENT_TEXT) + + # Build command list + set(COMMANDS + COMMAND ${CMAKE_COMMAND} -E echo "" + ) + foreach(i RANGE 0 ${COMMENT_IDX}) + if(NOT i EQUAL ${COMMENT_IDX}) + list(GET ARGV ${i} LINE) + list(APPEND COMMANDS COMMAND ${CMAKE_COMMAND} -E echo "${LINE}") + endif() + endforeach() + list(APPEND COMMANDS COMMAND ${CMAKE_COMMAND} -E echo "") + + add_custom_target(coverage + ${COMMANDS} + COMMENT "${COMMENT_TEXT}" + ) +endfunction() + +macro(create_coverage_target) + if(HAVE_GCOV AND ENABLE_COVERAGE) + get_html_coverage_commands(HTML_COVERAGE_COMMANDS) + + add_custom_target(coverage + COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalStart > /dev/null 2>&1 + COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalConfigure > /dev/null 2>&1 + COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalBuild > /dev/null 2>&1 + COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalTest > /dev/null 2>&1 + COMMAND ${CMAKE_CTEST_COMMAND} -D ExperimentalCoverage > /dev/null 2>&1 + COMMAND ${CMAKE_COMMAND} + -DPROJECT_SOURCE_DIR=${CMAKE_SOURCE_DIR} + -DPROJECT_BINARY_DIR=${CMAKE_BINARY_DIR} + -P ${CMAKE_SOURCE_DIR}/cmake/utils/PrintCoverage.cmake + ${HTML_COVERAGE_COMMANDS} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + DEPENDS build_tests + COMMENT "Running tests with coverage analysis using CTest" + ) + elseif(HAVE_GCOV) + create_informational_target( + "Coverage is currently disabled" + "To enable coverage analysis, reconfigure with" + " cmake -DDISABLE_COVERAGE=OFF .." + "Coverage disabled" + ) + message(STATUS "Coverage disabled. Use 'make coverage' for instructions to enable.") + else() + create_informational_target( + "Coverage analysis is not available" + "Install gcov to enable coverage support" + "Coverage not available" + ) + endif() +endmacro() diff --git a/cmake/utils/ParseCoverage.cmake b/cmake/utils/ParseCoverage.cmake new file mode 100644 index 00000000..0bbed0de --- /dev/null +++ b/cmake/utils/ParseCoverage.cmake @@ -0,0 +1,120 @@ +# Parse CTest Coverage.xml file and extract structured coverage data. +# +# Sets in PARENT_SCOPE: +# LOC_TESTED, LOC_UNTESTED - Overall metrics +# FILE_COVERAGE_LIST - "path|name|tested|untested|total|percent" entries +# COVERED_FILES - Absolute paths of covered files + +# Regex building blocks +set(R_NUMBER "[0-9]+") +set(R_NON_TAG "[^<]*") +set(R_NON_BRACKET "[^>]*") +set(R_NON_QUOTE "[^\"]+") + +# XML element patterns +set(R_LOC_TESTED_ELEM "(${R_NUMBER})") +set(R_LOC_TESTED_SKIP "${R_NUMBER}") +set(R_LOC_UNTESTED_ELEM "(${R_NUMBER})") +set(R_LOC_UNTESTED_CAP "(${R_NUMBER})") + +# Regex patterns for XML parsing +set(REGEX_LOC_TESTED "${R_NON_TAG}${R_LOC_TESTED_ELEM}") +set(REGEX_LOC_UNTESTED "${R_NON_TAG}${R_LOC_TESTED_SKIP}${R_NON_TAG}${R_LOC_UNTESTED_ELEM}") +set(REGEX_FILE_ENTRY "${R_NON_TAG}${R_LOC_TESTED_ELEM}${R_NON_TAG}${R_LOC_UNTESTED_CAP}") + +# Regex patterns for filtering +set(REGEX_TEST_FILE "test") +set(REGEX_TEST_DIR "/tests/") +set(REGEX_PROTOBUF_C "\\.pb-c\\.c$") +set(REGEX_PROTOBUF_ALT "\\.pb\\.c$") +set(REGEX_DOTSLASH_SRC "^\\.\\/src\\/") + +function(should_skip_file FILE_PATH OUTPUT_VAR) + if(FILE_PATH MATCHES "${REGEX_TEST_FILE}" OR FILE_PATH MATCHES "${REGEX_TEST_DIR}" OR + FILE_PATH MATCHES "${REGEX_PROTOBUF_C}" OR FILE_PATH MATCHES "${REGEX_PROTOBUF_ALT}") + set(${OUTPUT_VAR} TRUE PARENT_SCOPE) + else() + set(${OUTPUT_VAR} FALSE PARENT_SCOPE) + endif() +endfunction() + +function(normalize_coverage_path FILE_PATH PROJECT_SOURCE_DIR OUTPUT_VAR) + if(NOT IS_ABSOLUTE "${FILE_PATH}") + string(REGEX REPLACE "${REGEX_DOTSLASH_SRC}" "src/" FILE_PATH "${FILE_PATH}") + get_filename_component(FILE_PATH "${PROJECT_SOURCE_DIR}/${FILE_PATH}" ABSOLUTE) + endif() + set(${OUTPUT_VAR} "${FILE_PATH}" PARENT_SCOPE) +endfunction() + +function(extract_xml_attribute XML_STRING ATTRIBUTE OUTPUT_VAR) + string(REGEX MATCH "${ATTRIBUTE}=\"([^\"]+)\"" _ "${XML_STRING}") + set(${OUTPUT_VAR} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + +function(extract_xml_element XML_STRING ELEMENT OUTPUT_VAR) + string(REGEX MATCH "<${ELEMENT}>([^<]+)" _ "${XML_STRING}") + set(${OUTPUT_VAR} "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + +function(build_coverage_entry PATH NAME TESTED UNTESTED OUTPUT_VAR) + math(EXPR TOTAL "${TESTED} + ${UNTESTED}") + if(NOT TOTAL GREATER 0) + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + math(EXPR PERCENT "(${TESTED} * 100) / ${TOTAL}") + set(${OUTPUT_VAR} "${PATH}|${NAME}|${TESTED}|${UNTESTED}|${TOTAL}|${PERCENT}" PARENT_SCOPE) +endfunction() + +function(parse_coverage_xml COVERAGE_FILE PROJECT_SOURCE_DIR) + if(NOT EXISTS "${COVERAGE_FILE}") + return() + endif() + + file(READ "${COVERAGE_FILE}" COVERAGE_XML) + + string(REGEX MATCH "${REGEX_LOC_TESTED}" _ "${COVERAGE_XML}") + set(TESTED "${CMAKE_MATCH_1}") + + string(REGEX MATCH "${REGEX_LOC_UNTESTED}" _ "${COVERAGE_XML}") + set(UNTESTED "${CMAKE_MATCH_1}") + + if(NOT TESTED OR NOT UNTESTED) + return() + endif() + + string(REGEX MATCHALL "${REGEX_FILE_ENTRY}" FILE_MATCHES "${COVERAGE_XML}") + + set(COVERED_LIST "") + set(COVERAGE_DATA "") + + foreach(MATCH ${FILE_MATCHES}) + extract_xml_attribute("${MATCH}" "FullPath" PATH) + extract_xml_attribute("${MATCH}" "Name" NAME) + + should_skip_file("${PATH}" SKIP) + if(SKIP) + continue() + endif() + + normalize_coverage_path("${PATH}" "${PROJECT_SOURCE_DIR}" ABS_PATH) + list(APPEND COVERED_LIST "${ABS_PATH}") + + extract_xml_element("${MATCH}" "LOCTested" TESTED_LINES) + extract_xml_element("${MATCH}" "LOCUnTested" UNTESTED_LINES) + + if(NOT TESTED_LINES OR NOT UNTESTED_LINES) + continue() + endif() + + build_coverage_entry("${PATH}" "${NAME}" "${TESTED_LINES}" "${UNTESTED_LINES}" ENTRY) + if(ENTRY) + list(APPEND COVERAGE_DATA "${ENTRY}") + endif() + endforeach() + + set(LOC_TESTED "${TESTED}" PARENT_SCOPE) + set(LOC_UNTESTED "${UNTESTED}" PARENT_SCOPE) + set(FILE_COVERAGE_LIST "${COVERAGE_DATA}" PARENT_SCOPE) + set(COVERED_FILES "${COVERED_LIST}" PARENT_SCOPE) +endfunction() diff --git a/cmake/utils/PrintCoverage.cmake b/cmake/utils/PrintCoverage.cmake new file mode 100644 index 00000000..4b75fa09 --- /dev/null +++ b/cmake/utils/PrintCoverage.cmake @@ -0,0 +1,333 @@ +# Script to parse and display coverage results from CTest +# +# This script is invoked by the 'make coverage' target and parses the CTest +# Coverage.xml file to generate a formatted coverage report grouped by component. +# +# This script is run with cmake -P, so CMAKE_SOURCE_DIR won't be set correctly. +# Use PROJECT_SOURCE_DIR and PROJECT_BINARY_DIR passed as -D arguments. + +if(NOT DEFINED PROJECT_SOURCE_DIR) + message(FATAL_ERROR "PROJECT_SOURCE_DIR must be defined") +endif() +if(NOT DEFINED PROJECT_BINARY_DIR) + message(FATAL_ERROR "PROJECT_BINARY_DIR must be defined") +endif() + +# Include coverage parsing functions +include(${CMAKE_CURRENT_LIST_DIR}/ParseCoverage.cmake) + +# Create padding strings (CMake 2.8 compatible) +function(make_padding LENGTH OUTPUT_VAR) + set(RESULT "") + if(LENGTH GREATER 0) + foreach(i RANGE 1 ${LENGTH}) + set(RESULT "${RESULT} ") + endforeach() + endif() + set(${OUTPUT_VAR} "${RESULT}" PARENT_SCOPE) +endfunction() + +# Format a number with padding for right alignment +function(format_number VALUE WIDTH OUTPUT_VAR) + string(LENGTH "${VALUE}" VALUE_LEN) + math(EXPR PAD_LEN "${WIDTH} - ${VALUE_LEN}") + make_padding(${PAD_LEN} PADDING) + set(${OUTPUT_VAR} "${PADDING}${VALUE}" PARENT_SCOPE) +endfunction() + +# Format a complete coverage row with consistent alignment +function(format_coverage_row LABEL TESTED TESTED_FC UNTESTED UNTESTED_FC TOTAL PERCENT OUTPUT_VAR) + string(LENGTH "${LABEL}" LABEL_LEN) + math(EXPR LABEL_PAD "28 - ${LABEL_LEN}") + make_padding(${LABEL_PAD} LP) + + format_number(${TESTED} 6 TS) + format_number(${TESTED_FC} 3 TFC) + format_number(${UNTESTED} 8 US) + format_number(${UNTESTED_FC} 3 UFC) + format_number(${TOTAL} 5 TT) + format_number(${PERCENT} 3 PC) + set(${OUTPUT_VAR} " ${LABEL}${LP}${TS}[${TFC}] ${US}[${UFC}] ${TT} ${PC}%" PARENT_SCOPE) +endfunction() + +# Format the header row to align with data columns +function(format_coverage_header OUTPUT_VAR) + set(HEADER " Component Tested Untested Total %") + set(${OUTPUT_VAR} "${HEADER}" PARENT_SCOPE) +endfunction() + +# Calculate metrics from entry list (pipe-delimited: path|name|tested|untested|total|percent) +function(calculate_metrics ENTRIES OUT_TESTED OUT_UNTESTED OUT_TESTED_FC OUT_UNTESTED_FC) + set(TESTED 0) + set(UNTESTED 0) + set(TESTED_FC 0) + set(UNTESTED_FC 0) + + foreach(ENTRY ${ENTRIES}) + string(REPLACE "|" ";" PARTS "${ENTRY}") + list(GET PARTS 2 ENTRY_TESTED) + list(GET PARTS 3 ENTRY_UNTESTED) + + math(EXPR TESTED "${TESTED} + ${ENTRY_TESTED}") + math(EXPR UNTESTED "${UNTESTED} + ${ENTRY_UNTESTED}") + + if(ENTRY_TESTED EQUAL 0) + math(EXPR UNTESTED_FC "${UNTESTED_FC} + 1") + else() + math(EXPR TESTED_FC "${TESTED_FC} + 1") + endif() + endforeach() + + set(${OUT_TESTED} "${TESTED}" PARENT_SCOPE) + set(${OUT_UNTESTED} "${UNTESTED}" PARENT_SCOPE) + set(${OUT_TESTED_FC} "${TESTED_FC}" PARENT_SCOPE) + set(${OUT_UNTESTED_FC} "${UNTESTED_FC}" PARENT_SCOPE) +endfunction() + +# Discover components and sub-components from source tree +function(discover_components PROJECT_SOURCE_DIR OUT_COMPONENTS OUT_COMP_SUBCOMPS) + file(GLOB COMPONENT_DIRS "${PROJECT_SOURCE_DIR}/src/*") + set(COMPONENTS "") + set(SKIP_DIRS "include;doc;tests") + + foreach(DIR ${COMPONENT_DIRS}) + if(IS_DIRECTORY ${DIR}) + get_filename_component(COMP_NAME ${DIR} NAME) + list(FIND SKIP_DIRS ${COMP_NAME} SKIP_IDX) + if(SKIP_IDX EQUAL -1) + list(APPEND COMPONENTS ${COMP_NAME}) + + file(GLOB SUBCOMP_DIRS "${DIR}/*") + set(COMP_${COMP_NAME}_SUBCOMPS "") + foreach(SUBDIR ${SUBCOMP_DIRS}) + if(IS_DIRECTORY ${SUBDIR}) + get_filename_component(SUBCOMP_NAME ${SUBDIR} NAME) + list(FIND SKIP_DIRS ${SUBCOMP_NAME} SKIP_IDX2) + if(SKIP_IDX2 EQUAL -1) + list(APPEND COMP_${COMP_NAME}_SUBCOMPS ${SUBCOMP_NAME}) + set(COMP_${COMP_NAME}_${SUBCOMP_NAME} "") + endif() + endif() + endforeach() + endif() + endif() + endforeach() + + set(${OUT_COMPONENTS} "${COMPONENTS}" PARENT_SCOPE) + foreach(COMP ${COMPONENTS}) + set(COMP_${COMP}_SUBCOMPS "${COMP_${COMP}_SUBCOMPS}" PARENT_SCOPE) + endforeach() +endfunction() + +# Print a coverage line +function(print_coverage_line LABEL TESTED TESTED_FC UNTESTED UNTESTED_FC TOTAL PERCENT) + format_coverage_row("${LABEL}" ${TESTED} ${TESTED_FC} ${UNTESTED} ${UNTESTED_FC} ${TOTAL} ${PERCENT} ROW) + execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${ROW}") +endfunction() + +# Locate the coverage file +if(NOT EXISTS "${CMAKE_BINARY_DIR}/Testing/TAG") + message(WARNING "Testing/TAG file not found. Coverage may not have run successfully.") + return() +endif() + +file(STRINGS "${CMAKE_BINARY_DIR}/Testing/TAG" TAG_CONTENTS LIMIT_COUNT 1) +string(STRIP "${TAG_CONTENTS}" TEST_DIR) +set(COVERAGE_FILE "${CMAKE_BINARY_DIR}/Testing/${TEST_DIR}/Coverage.xml") + +if(NOT EXISTS "${COVERAGE_FILE}") + message(WARNING "Coverage file not found: ${COVERAGE_FILE}") + execute_process(COMMAND ${CMAKE_COMMAND} -E echo "Note: Coverage results should be in Testing/${TEST_DIR}/") + return() +endif() + +# Parse the coverage XML file +parse_coverage_xml("${COVERAGE_FILE}" "${PROJECT_SOURCE_DIR}") + +if(NOT LOC_TESTED OR NOT LOC_UNTESTED) + message(WARNING "Could not parse coverage metrics") + return() +endif() + +math(EXPR TOTAL_LOC "${LOC_TESTED} + ${LOC_UNTESTED}") +if(NOT TOTAL_LOC GREATER 0) + message(WARNING "No coverage data found") + return() +endif() + +math(EXPR COVERAGE_PERCENT "(${LOC_TESTED} * 100) / ${TOTAL_LOC}") + +# Discover and group files by component +if(NOT FILE_COVERAGE_LIST) + execute_process(COMMAND ${CMAKE_COMMAND} -E echo "No coverage data for source files (excluding tests)") + return() +endif() + +# Discover components from source tree (only src/ directory) +discover_components("${PROJECT_SOURCE_DIR}" COMPONENTS COMP_SUBCOMPS) + +# Find all source files and identify completely untested ones +file(GLOB_RECURSE ALL_SOURCE_FILES "${PROJECT_SOURCE_DIR}/src/*.c") +set(UNCOVERED_FILES "") +foreach(SRC_FILE ${ALL_SOURCE_FILES}) + if(SRC_FILE MATCHES "test" OR SRC_FILE MATCHES "/tests/") + continue() + endif() + + list(FIND COVERED_FILES "${SRC_FILE}" FILE_IDX) + if(FILE_IDX EQUAL -1) + file(STRINGS "${SRC_FILE}" FILE_LINES) + list(LENGTH FILE_LINES LINE_COUNT) + if(LINE_COUNT GREATER 0) + get_filename_component(FILE_NAME "${SRC_FILE}" NAME) + list(APPEND UNCOVERED_FILES "${SRC_FILE}|${FILE_NAME}|0|${LINE_COUNT}|${LINE_COUNT}|0") + endif() + endif() +endforeach() + +# Combine covered and uncovered files +set(ALL_FILES ${FILE_COVERAGE_LIST}) +if(UNCOVERED_FILES) + list(APPEND ALL_FILES ${UNCOVERED_FILES}) +endif() + +# Group files into components and sub-components +foreach(ENTRY ${ALL_FILES}) + string(REPLACE "|" ";" ENTRY_PARTS "${ENTRY}") + list(GET ENTRY_PARTS 0 F_PATH) + + # Normalize path and extract relative path from src/ + # Remove leading ./ if present, then ensure path starts with src/ + string(REGEX REPLACE "^\\./" "" REL_PATH "${F_PATH}") + string(REGEX REPLACE "^.*/src/" "src/" REL_PATH "${REL_PATH}") + + # Tokenize on /: ["src", "component", "subcomponent", "file.c"] + string(REPLACE "/" ";" PATH_TOKENS "${REL_PATH}") + list(LENGTH PATH_TOKENS TOKEN_COUNT) + + if(TOKEN_COUNT EQUAL 3) + # src/component/file.c - add to component main + list(GET PATH_TOKENS 1 FILE_COMP) + list(FIND COMPONENTS ${FILE_COMP} COMP_IDX) + if(COMP_IDX GREATER -1) + list(APPEND COMP_${FILE_COMP} "${ENTRY}") + list(APPEND COMP_${FILE_COMP}_main "${ENTRY}") + endif() + elseif(TOKEN_COUNT GREATER 3) + # src/component/subdir/.../file.c - check if subdir is a known subcomponent + list(GET PATH_TOKENS 1 FILE_COMP) + list(GET PATH_TOKENS 2 FILE_SUBCOMP) + + list(FIND COMPONENTS ${FILE_COMP} COMP_IDX) + if(COMP_IDX GREATER -1) + list(APPEND COMP_${FILE_COMP} "${ENTRY}") + + # Check if subdir is a recognized subcomponent + list(FIND COMP_${FILE_COMP}_SUBCOMPS ${FILE_SUBCOMP} SUBCOMP_IDX) + if(SUBCOMP_IDX GREATER -1) + list(APPEND COMP_${FILE_COMP}_${FILE_SUBCOMP} "${ENTRY}") + else() + # Subdir not recognized, treat as main + list(APPEND COMP_${FILE_COMP}_main "${ENTRY}") + endif() + endif() + endif() +endforeach() + +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "") +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================") +format_coverage_header(HEADER) +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "${HEADER}") +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================") + +# Process and display each component +foreach(COMP_NAME ${COMPONENTS}) + set(COMP_LIST ${COMP_${COMP_NAME}}) + + if(NOT COMP_LIST) + continue() + endif() + + calculate_metrics("${COMP_LIST}" COMP_TESTED COMP_UNTESTED COMP_TESTED_FILE_COUNT COMP_UNTESTED_FILE_COUNT) + math(EXPR COMP_TOTAL "${COMP_TESTED} + ${COMP_UNTESTED}") + if(COMP_TOTAL GREATER 0) + math(EXPR COMP_PERCENT "(${COMP_TESTED} * 100) / ${COMP_TOTAL}") + else() + set(COMP_PERCENT 0) + endif() + + print_coverage_line("${COMP_NAME}" ${COMP_TESTED} ${COMP_TESTED_FILE_COUNT} ${COMP_UNTESTED} ${COMP_UNTESTED_FILE_COUNT} ${COMP_TOTAL} ${COMP_PERCENT}) + + # Display "main" sub-component first if it has files + set(MAIN_LIST ${COMP_${COMP_NAME}_main}) + if(MAIN_LIST) + calculate_metrics("${MAIN_LIST}" MAIN_TESTED MAIN_UNTESTED MAIN_TESTED_FILE_COUNT MAIN_UNTESTED_FILE_COUNT) + math(EXPR MAIN_TOTAL "${MAIN_TESTED} + ${MAIN_UNTESTED}") + if(MAIN_TOTAL GREATER 0) + math(EXPR MAIN_PERCENT "(${MAIN_TESTED} * 100) / ${MAIN_TOTAL}") + else() + set(MAIN_PERCENT 0) + endif() + + print_coverage_line(" ." ${MAIN_TESTED} ${MAIN_TESTED_FILE_COUNT} ${MAIN_UNTESTED} ${MAIN_UNTESTED_FILE_COUNT} ${MAIN_TOTAL} ${MAIN_PERCENT}) + endif() + + # Display sub-components + foreach(SUBCOMP ${COMP_${COMP_NAME}_SUBCOMPS}) + set(SUBCOMP_LIST ${COMP_${COMP_NAME}_${SUBCOMP}}) + if(NOT SUBCOMP_LIST) + continue() + endif() + + calculate_metrics("${SUBCOMP_LIST}" SUBCOMP_TESTED SUBCOMP_UNTESTED SUBCOMP_TESTED_FILE_COUNT SUBCOMP_UNTESTED_FILE_COUNT) + math(EXPR SUBCOMP_TOTAL "${SUBCOMP_TESTED} + ${SUBCOMP_UNTESTED}") + if(SUBCOMP_TOTAL GREATER 0) + math(EXPR SUBCOMP_PERCENT "(${SUBCOMP_TESTED} * 100) / ${SUBCOMP_TOTAL}") + else() + set(SUBCOMP_PERCENT 0) + endif() + + print_coverage_line(" ${SUBCOMP}" ${SUBCOMP_TESTED} ${SUBCOMP_TESTED_FILE_COUNT} ${SUBCOMP_UNTESTED} ${SUBCOMP_UNTESTED_FILE_COUNT} ${SUBCOMP_TOTAL} ${SUBCOMP_PERCENT}) + endforeach() + + execute_process(COMMAND ${CMAKE_COMMAND} -E echo "") +endforeach() + +# Calculate overall coverage +set(TOTAL_TESTED ${LOC_TESTED}) +set(TOTAL_UNTESTED ${LOC_UNTESTED}) + +# Count file coverage for totals +set(TOTAL_TESTED_FILE_COUNT 0) +set(TOTAL_UNTESTED_FILE_COUNT 0) + +foreach(ENTRY ${FILE_COVERAGE_LIST}) + string(REPLACE "|" ";" PARTS "${ENTRY}") + list(GET PARTS 2 ENTRY_TESTED) + if(ENTRY_TESTED EQUAL 0) + math(EXPR TOTAL_UNTESTED_FILE_COUNT "${TOTAL_UNTESTED_FILE_COUNT} + 1") + else() + math(EXPR TOTAL_TESTED_FILE_COUNT "${TOTAL_TESTED_FILE_COUNT} + 1") + endif() +endforeach() + +# Add untested file counts +foreach(ENTRY ${UNCOVERED_FILES}) + string(REPLACE "|" ";" PARTS "${ENTRY}") + list(GET PARTS 3 ENTRY_UNTESTED) + math(EXPR TOTAL_UNTESTED "${TOTAL_UNTESTED} + ${ENTRY_UNTESTED}") + math(EXPR TOTAL_UNTESTED_FILE_COUNT "${TOTAL_UNTESTED_FILE_COUNT} + 1") +endforeach() + +math(EXPR TOTAL_ALL_LOC "${TOTAL_TESTED} + ${TOTAL_UNTESTED}") +if(TOTAL_ALL_LOC GREATER 0) + math(EXPR OVERALL_COVERAGE_PERCENT "(${TOTAL_TESTED} * 100) / ${TOTAL_ALL_LOC}") +else() + set(OVERALL_COVERAGE_PERCENT 0) +endif() + +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================") +print_coverage_line("Total" ${TOTAL_TESTED} ${TOTAL_TESTED_FILE_COUNT} ${TOTAL_UNTESTED} ${TOTAL_UNTESTED_FILE_COUNT} ${TOTAL_ALL_LOC} ${OVERALL_COVERAGE_PERCENT}) +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "=============================================================================") +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "") +execute_process(COMMAND ${CMAKE_COMMAND} -E echo "Detailed XML report: ${COVERAGE_FILE}") -- cgit v1.2.3