diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-01-09 13:13:26 +0100 |
|---|---|---|
| committer | Sander Vrijders <sander@ouroboros.rocks> | 2026-01-12 08:12:45 +0100 |
| commit | 3f373b06b05083d9395250379a2978b5f6085002 (patch) | |
| tree | c6edaa135feac5336ec0e6601772ad6f513c8a2f /cmake/utils/ParseCoverage.cmake | |
| parent | 5b11550644b0ce7a79b967b6aabb1a59b86d5ca2 (diff) | |
| download | ouroboros-3f373b06b05083d9395250379a2978b5f6085002.tar.gz ouroboros-3f373b06b05083d9395250379a2978b5f6085002.zip | |
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 <dimitri@ouroboros.rocks>
Signed-off-by: Sander Vrijders <sander@ouroboros.rocks>
Diffstat (limited to 'cmake/utils/ParseCoverage.cmake')
| -rw-r--r-- | cmake/utils/ParseCoverage.cmake | 120 |
1 files changed, 120 insertions, 0 deletions
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 "<LOCTested>(${R_NUMBER})</LOCTested>") +set(R_LOC_TESTED_SKIP "<LOCTested>${R_NUMBER}</LOCTested>") +set(R_LOC_UNTESTED_ELEM "<LOCUntested>(${R_NUMBER})</LOCUntested>") +set(R_LOC_UNTESTED_CAP "<LOCUnTested>(${R_NUMBER})</LOCUnTested>") + +# Regex patterns for XML parsing +set(REGEX_LOC_TESTED "</File>${R_NON_TAG}${R_LOC_TESTED_ELEM}") +set(REGEX_LOC_UNTESTED "</File>${R_NON_TAG}${R_LOC_TESTED_SKIP}${R_NON_TAG}${R_LOC_UNTESTED_ELEM}") +set(REGEX_FILE_ENTRY "<File${R_NON_BRACKET}Name=\"(${R_NON_QUOTE})\"${R_NON_BRACKET}>${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}>([^<]+)</${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() |
