diff --git a/CMakeLists.txt b/CMakeLists.txt index 21c7930..06790ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ block (SCOPE_FOR VARIABLES) # Setting internal variables message(TRACE "Setting internal variables") set(IXM_FILES_DIR "${CMAKE_BINARY_DIR}/.ixm" CACHE INTERNAL "") set(IXM_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "IXM Root Directory") + set(IXM_TEMPLATES_DIR "${IXM_ROOT_DIR}/templates" CACHE INTERNAL "IXM Templates Directory") endblock() block (SCOPE_FOR VARIABLES) # Setting global variables diff --git a/docs/packages/coverage/gnu.md b/docs/packages/coverage/gnu.md index 30a9368..6498a1f 100644 --- a/docs/packages/coverage/gnu.md +++ b/docs/packages/coverage/gnu.md @@ -8,5 +8,41 @@ title: GNU More information can be found in `gcov`'s [documentation](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html) -> [!CAUTION] -> Support for gcov is not yet implemented. +> [!IMPORTANT] +> This component is *always* found as long as `gcov` can be found, but the +> `IMPORTED` library target it provides will have *no effect* unless you are +> using `Clang`, `AppleClang`, or `GNU` for the `OBJCXX`, `OBJC`, `CXX`, or `C` +> languages. +> +> This was done to speed up "finding" the component. Support for conditional +> flag detection might be added in the future. Additional compilers might be +> added or supported as well under some circumstances. + +## Targets + +The following `IMPORTED` targets are created when the GNU component is found. + +`Coverage::GNU` {#coverage-gnu} +: Provides gcov compiler and linker options. +: This target is *always* available if the `gcov` tool is found, but only +affects dependents if the language for a given source file is `OBJCXX`, `OBJC`, +`CXX`, or `C` and if the `CMAKE__COMPILER_ID` is `GNU`, `AppleClang`, or +`Clang`. + +`Coverage::GNU::Tool` {#coverage-gnu-tool} +: The `gcov` executable +: *Type*: PROGRAM +: *Required*: YES + +## Properties + +Some properties allow the modification of some code coverage information. + +`COVERAGE_GNU_ABSOLUTE_PATHS` +: Pass `-fprofile-abs-path` to the compiler. +: By default, gcov uses relative paths from when the source code was compiled. + With this flag enabled full filesystem paths are placed into the generated + output. +: *Type*: `boolean` +: *Scopes*: `TARGET` +: Default: `NO` diff --git a/docs/packages/coverage/index.md b/docs/packages/coverage/index.md index ec8e7ff..4287b38 100644 --- a/docs/packages/coverage/index.md +++ b/docs/packages/coverage/index.md @@ -34,5 +34,92 @@ Each component provides its own target, as well as some additional binary targets that might be necessary for tooling. See each component's documentation page for more information. +## Commands + +### `add_coverage_target` + +This creates a target that can be referred to directly with `set_property` and, +when built will collate raw profiling data into a format used by the selected +implementation. + +> [!NOTE] +> This command uses `add_custom_target` internally, which does not permit +> creating alias targets, nor does it permit creating "scoped" targets (e.g., +> `${PROJECT_NAME}::coverage`). As a result *all targets* passed to this +> command must be globally unique. + +#### Required Parameters {#add_coverage_target/required} + +`name` +: name of the target to create + +#### Keyword Parameters {#add_coverage_target/keyword} + +`LLVM` +: Use the LLVM Source-Based code coverage implementation + +`GNU` +: Use the gcov code coverage implementation + +`OUTPUT` +: Name of the final file generated at the end of the target's build step. + +`FORMAT` +: File format to use for the given implementation. +: > [!TIP] + > This can vary wildly between what tool might be used to merge and export + > the coverage information. + +### `target_coverage` + +#### Required Parameters {#target_coverage/required} + +`target` +: Target created with `add_coverage_target` to add dependencies to. + +#### Keyword Parameters {#target_coverage/keyword} + +#### Example + +```cmake +add_coverage_target(coverage LLVM) +add_library(${PROJECT_NAME}) +add_executable(${PROJECT_NAME}-cli) +target_coverage(coverage PRIVATE ${PROJECT_NAME} ${PROJECT_NAME}-cli) +``` + +## Properties + +There are multiple properties provided to users that allow them to tweak either +the LLVM or GNU operations. These can be directly *on* a given build target +from the user (e.g., adding `-fcoverage-mcdc`) or modify the execution of the +coverage target itself. + +`COVERAGE_LLVM_COVERAGE_PREFIX_MAP` +: Remap file source paths from `` to `` in coverage mapping. If there + are multiple options, prefix replacement is applied in reverse order starting + from the last one. +: *Type*: `list` +: *Default*: `$=.` +: *Context*: Coverage Target + +`COVERAGE_LLVM_MCDC` +: Pass [`-fcoverage-mcdc`][mcdc] to the compiler +: *Type*: `boolean` +: *Context*: User Target +: *Default*: `` + +`COVERAGE_LLVM_MERGE_FAILURE_MODE` {#property/coverage-llvm-merge-failure-mode} +: `llvm-profdata`'s failure mode when merging `.profraw` files into a + `.profdata` file. +: *Type*: `enum` +: *Values*: `warn`, `any`, `all` +: *Default*: `any` +: *Context*: Coverage Target + + + [1]: https://gcc.gnu.org/onlinedocs/gcc/Gcov-Intro.html [2]: https://clang.llvm.org/docs/SourceBasedCodeCoverage.html + +[mcdc]: https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#mc-dc-instrumentation diff --git a/docs/packages/coverage/llvm.md b/docs/packages/coverage/llvm.md index e7453b0..c43b478 100644 --- a/docs/packages/coverage/llvm.md +++ b/docs/packages/coverage/llvm.md @@ -116,44 +116,3 @@ endforeach() Some properties need to be mentioned directly on targets that will link against a given target, others are specifically for a *coverage* target. - -`LLVM_MODIFIED_CONDITION_DECISION_COVERAGE` -: Pass [`-fcoverage-mcdc`][] to the compiler -: *Type*: `boolean` -: *Scopes*: `TARGET` -: *Default*: `` - -`LLVM_COVERAGE_PREFIX_MAP` -: Remap file source paths `` to `` in coverage mapping. If there are - multiple options, prefix replacement is applied in reverse order starting - from the last one -: *Type*: `list` -: *Default*: `` - -`LLVM_PROFRAW_SOURCES` {#property/llvm-profraw-sources} -: TODO - -`LLVM_TEST_EXECUTABLES` {#property/llvm-test-executables} -: TODO - -`LLVM_IGNORE_FILENAME_REGEX` {#property/llvm-ignore-filename-regex} -: TODO - -`LLVM_PROFILE_FILENAME` {#property/llvm-profile-filename} -: TODO - - -`LLVM_MERGE_FAILURE_MODE` {#property/llvm-merge-failure-mode} -: `llvm-profdata`'s failure mode when merging `.profraw` files into a - `.profdata` file. -: *Type*: `enum` -: *Values*: `warn`, `any`, `all` -: *Default*: `any` - -`LLVM_SOURCES` {#property/llvm-sources} -: Source files to be read from when exporting coverage information. -: *Type*: `list` -: *Scope*: `TARGET` -: *Default*: `$` - -[`-fcoverage-mcdc`]: https://clang.llvm.org/docs/SourceBasedCodeCoverage.html#mc-dc-instrumentation diff --git a/modules/FindCoverage.cmake b/modules/FindCoverage.cmake index 5117d01..4f64287 100644 --- a/modules/FindCoverage.cmake +++ b/modules/FindCoverage.cmake @@ -1,201 +1,254 @@ -if ("GNU" IN_LIST ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS) - message(WARNING "GNU code coverage is not currently a supported component for Coverage at this time.") -endif() - -if ("LLVM" IN_LIST ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS) - cmake_language(CALL ixm::find::program - NAMES llvm-undname - DESCRIPTION "LLVM name undecorator" - PACKAGE - COMPONENT LLVM - TARGET UndecorateName - OPTIONAL) - cmake_language(CALL ixm::find::program - NAMES llvm-cxxfilt - DESCRIPTION "LLVM symbol undecoration tool" - PACKAGE - COMPONENT LLVM - TARGET C++Filter - OPTIONAL) - cmake_language(CALL ixm::find::program - NAMES llvm-profdata - DESCRIPTION "LLVM profile data tool" - PACKAGE - COMPONENT LLVM - TARGET ProfileData) - cmake_language(CALL ixm::find::program - NAMES llvm-cov - DESCRIPTION "Code coverage information tool" - PACKAGE - COMPONENT LLVM - TARGET Tool) - cmake_language(CALL ixm::check::option::compile CXX -fprofile-instr-generate - OUTPUT_VARIABLE ${CMAKE_FIND_PACKAGE_NAME}_LLVM_COMPILE_OPTIONS - COMPILE_OPTIONS -fcoverage-mapping - LINK_OPTIONS -fprofile-instr-generate - QUIET) - - if (${CMAKE_FIND_PACKAGE_NAME}_LLVM_COMPILE_OPTIONS) - set(${CMAKE_FIND_PACKAGE_NAME}_LLVM_COMPILE_FLAGS "-fprofile-instr-generate") - endif() +cmake_language(CALL ixm::find::program + NAMES gcovr + PACKAGE + COMPONENT GNU + TARGET Merge) + +cmake_language(CALL ixm::find::program + NAMES llvm-undname + DESCRIPTION "LLVM name undecorator" + PACKAGE + COMPONENT LLVM + TARGET UndecorateName + OPTIONAL) +cmake_language(CALL ixm::find::program + NAMES llvm-cxxfilt + DESCRIPTION "LLVM symbol undecoration tool" + PACKAGE + COMPONENT LLVM + TARGET C++Filter + OPTIONAL) +cmake_language(CALL ixm::find::program + NAMES llvm-profdata + DESCRIPTION "LLVM profile data tool" + PACKAGE + COMPONENT LLVM + TARGET Merge) + +cmake_language(CALL ixm::find::program + NAMES gcovr + PACKAGE + COMPONENT Report + TARGET GCovR + OPTIONAL) +cmake_language(CALL ixm::find::program + NAMES llvm-cov + PACKAGE + COMPONENT Report + TARGET LLVM + OPTIONAL) + +block (SCOPE_FOR VARIABLES PROPAGATE Coverage_Report_DEFAULT) + # Users can set this prior to this line as either a cache variable or a + # normal variable and it will "just work" and use their definition instead. + set(Coverage_Report_TARGETS GCovR LLVM LTP + CACHE "STRING" "Order of targets to search for viability") + foreach (reporter IN LISTS Coverage_Report_TARGETS) + if (Coverage_Report_${reporter}_EXECUTABLE) + set(Coverage_Report_DEFAULT Coverage::Report::${reporter}) + endif() + endforeach() set_property(GLOBAL APPEND - PROPERTY - ${CMAKE_FIND_PACKAGE_NAME}::LLVM::variables - ${CMAKE_FIND_PACKAGE_NAME}_LLVM_COMPILE_FLAGS) -endif() + PROPERTY Coverage::Report::variables Coverage_Report_DEFAULT) +endblock() + +block (SCOPE_FOR VARIABLES PROPAGATE Coverage_GNU_FLAG) + set(Coverage_GNU_FLAG "--coverage") + set(target Coverage::GNU) + set(prefix ${target}::{${target}}) + set_property(GLOBAL APPEND PROPERTY ${target}::targets ${target}) + set_property(GLOBAL APPEND PROPERTY ${target}::variables Coverage_GNU_FLAG) + foreach (language IN ITEMS OBJCXX OBJC CXX C) + string(CONCAT abs.path $>, + $ + >) + + set_property(GLOBAL APPEND PROPERTY ${prefix}::options::compile + $<$:--coverage> + $<${abs.path}:-fprofile-abs-path>) + set_property(GLOBAL APPEND PROPERTY ${prefix}::options::link + $<$:--coverage>) + if (DEFINED CMAKE_${language}_COMPILER_ID) + list(APPEND compilers ${CMAKE_${language}_COMPILER_ID}) + endif() + endforeach() + + # We need to do some sanity checking to select the correct target + # and then to create the Generate IMPORTED target. + # There are compiler/linker limitations, so we need to ensure things "just + # work" for these use cases. + list(REMOVE_DUPLICATES compilers) + list(LENGTH compilers count) + set(generate gcov) + if (compilers MATCHES "Clang") + set(generate llvm-cov) + endif() + set(generate gcov) + if (compilers MATCHES "Clang") + # We need to check the version of the compiler at the very least for + # consistency + foreach (language IN ITEMS OBJCXX OBJC CXX C) + if (CMAKE_${language}_COMPILER_VERSION) + string(REPLACE "." ";" version "${CMAKE_${language}_COMPILER_VERSION}") + list(POP_FRONT version major) + endif() + endforeach() + set(generate llvm-cov-${major} llvm-cov) + endif() + if (count EQUAL 1) + cmake_language(CALL ixm::find::program + NAMES ${generate} + PACKAGE + COMPONENT GNU + TARGET Generate) + elseif (compilers MATCHES "Clang") + message(WARNING "To use GNU Code Coverage, all C, C++, ObjC, and ObjC++ compilers *must* match") + # We hook into the machinery to tell `ixm::package::check` that GNU::Generate was never found + set_property(GLOBAL APPEND + PROPERTY + ${target}::targets Coverage::GNU::Generate) + set_property(GLOBAL APPEND + PROPERTY + ${target}::variables Coverage_GNU_Generate_EXECUTABLE) + set(Coverage_GNU_Generate_EXECUTABLE "Coverage_GNU_Generate_EXECUTABLE-NOTFOUND" + CACHE FILEPATH "") + endif() +endblock() + +block (SCOPE_FOR VARIABLES PROPAGATE Coverage_LLVM_COMPILE_FLAG Coverage_LLVM_LINK_FLAG) + set(Coverage_LLVM_COMPILE_FLAG "-fprofile-instr-generate -fcoverage-mapping") + set(Coverage_LLVM_LINK_FLAG "-fprofile-inst-generate") + + set(target Coverage::LLVM) + set(prefix ${target}::{${target}}) + set_property(GLOBAL APPEND PROPERTY ${target}::targets ${target}) + set_property(GLOBAL APPEND PROPERTY ${target}::variables + Coverage_Report_LLVM_EXECUTABLE # llvm-cov is OPTIONAL for Report, but not for LLVM + Coverage_LLVM_COMPILE_FLAG + Coverage_LLVM_LINK_FLAG) + + foreach (language IN ITEMS OBJCXX OBJC CXX C) + ixm_target_property(LLVM_INSTRUMENTED_FILENAME PREFIX COVERAGE) + ixm_target_property(LLVM_COVERAGE_PREFIX_MAP PREFIX COVERAGE) + ixm_target_property(LLVM_COVERAGE_MCDC PREFIX COVERAGE) + + string(CONCAT mcdc $< + $, + $ + >:-fcoverage-mcdc + >) + string(CONCAT prefix.map $< + $:-fcoverage-prefix-map= + $-fcoverage-prefix-map=> + >) + set(instrumented.filename -fprofile-instr-generate=${LLVM_INSTRUMENTED_FILENAME}) + set_property(GLOBAL APPEND PROPERTY ${prefix}::options::compile + $<$:${instrumented.filename}> + $<$:-fcoverage-mapping> + ${prefix.map} + ${mcdc}) + set_property(GLOBAL APPEND PROPERTY ${prefix}::options::link + $<$:${instrumented.filename}>) + endforeach() +endblock() cmake_language(CALL ixm::package::check) cmake_language(CALL ixm::package::properties DESCRIPTION "Code Coverage Support") -if (${CMAKE_FIND_PACKAGE_NAME}_LLVM_FOUND) - if (NOT TARGET ${CMAKE_FIND_PACKAGE_NAME}::LLVM) - block(SCOPE_FOR VARIABLES) - string(CONCAT mcdc $, - $> - > - >) - string(CONCAT coverage-prefix-map $>, - -fcoverage-prefix-map= - >) - string(CONCAT coverage-prefix-maps $< - $>: - -fcoverage-prefix-map=${coverage-prefix-map} - >) - string(CONCAT profile.file $< - $>: - =$> - >) - set(target ${CMAKE_FIND_PACKAGE_NAME}::LLVM) - add_library(${target} INTERFACE IMPORTED) - target_compile_options(${target} - INTERFACE - $<$:-fprofile-instr-generate${profile.file}> - $<$:-fcoverage-mapping> - $<${mcdc}:-fcoverage-mcdc> - ${coverage-prefix-maps}) - target_link_options(${target} - INTERFACE - $<$:-fprofile-instr-generate${profile.file}>) - endblock() +#[============================================================================[ +# @param {option} LLVM - Set the coverage target type to use source-based code +# coverage +# @param {option} GNU - Set the coverage target type to use gcov. +# @param {filepath} [OUTPUT] - Set the filepath to write the target's merged +# profile data. +#]============================================================================] +function (add_coverage_target name) + cmake_parse_arguments(ARG "LLVM;GNU" "FORMAT" "" ${ARGN}) + cmake_language(CALL 🈯::ixm::coverage::type "targets") + + cmake_language(CALL 🈯::ixm::coverage::target ${name}) + + set(type gnu) + if (ARG_LLVM) + set(type llvm) endif() -endif() - -# Setting a default value -set(IXM_LLVM_COVERAGE_PREFIX_MAP $=. - CACHE STRING "Default LLVM Coverage prefix map path") - -define_property(TARGET - PROPERTY - LLVM_MCDC - INHERITED - BRIEF_DOCS - "Enable modified condition/decision code coverage support for LLVM instrumented targets") - -define_property(TARGET - PROPERTY - LLVM_COVERAGE_PREFIX_MAP - INHERITED - BRIEF_DOCS - "remap file source paths to in coverage mapping." - INITIALIZE_FROM_VARIABLE - IXM_LLVM_COVERAGE_PREFIX_MAP) - -define_property(TARGET - PROPERTY - LLVM_PROFRAW_SOURCES - INHERITED - BRIEF_DOCS - ".profraw file locations") - -define_property(TARGET - PROPERTY - LLVM_TEST_EXECUTABLES - INHERITED - BRIEF_DOCS - "Executables that generate LLVM Profile Data") - -define_property(TARGET - PROPERTY - LLVM_IGNORE_FILENAME_REGEX - INHERITED - BRIEF_DOCS - "Skip source code files with file paths that match the given regular expression" - INITIALIZE_FROM_VARIABLE - IXM_LLVM_IGNORE_FILENAME_REGEX) - -define_property(TARGET - PROPERTY - LLVM_PROFILE_FILENAME - BRIEF_DOCS - "Equal to setting LLVM_PROFILE_FILE environment variable when adding a test") + cmake_language(CALL 🈯::ixm::coverage::${type}::merge ${name}) +endfunction() #[============================================================================[ -Creates all the steps necessary to generate an lcov or json coverage report -file -]============================================================================] -function (add_llvm_coverage name) - cmake_parse_arguments(ARG "ALL" "EXPORT;OUTPUT;FORMAT;GITHUB" "TARGETS;IGNORE_FILENAME_REGEX" ${ARGN}) - cmake_language(CALL 🈯::ixm::default ARG_EXPORT "${CMAKE_CURRENT_BINARY_DIR}/$/${name}.lcov.info") - cmake_language(CALL 🈯::ixm::default ARG_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/${name}.profdata") - cmake_language(CALL 🈯::ixm::default ARG_FORMAT lcov) - # Can't really remember why there's a ARG_GITHUB. Probably for - # uploading/writing to a step summary automatically? - cmake_language(CALL 🈯::ixm::default ARG_GITHUB "${name}") - set(ignore.filename.regex $>) - set(test.executables $>) - set(profraw.sources $>) - set(failure.mode $>) - set(sources $>) - # llvm-undname is broken, so we can *only* care about llvm-cxxfilter 😔 - set(demangler-name $) - string(CONCAT demangler $< - $: - $ - >) - add_custom_target(${name} - DEPENDS - ${ARG_EXPORT}) - add_custom_command(OUTPUT "${ARG_OUTPUT}" - COMMAND Coverage::LLVM::ProfileData merge - --sparse - --instr - --failure-mode=$,${failure.mode},any> - --output=${ARG_OUTPUT} - ${profraw.sources} - DEPENDS - ${test.executables} - ${profraw.sources} - $ - COMMENT "Merging raw profiling data" - COMMAND_EXPAND_LISTS - VERBATIM) - # Unfortunate that we need a shell command (and require the `>` pipe - # direction operator), but I've grown accustomed to LLVM tooling never doing - # the right thing when you need it to. - add_custom_command(OUTPUT ${ARG_EXPORT} - DEPENDS ${ARG_OUTPUT} - COMMAND Coverage::LLVM::Tool export - --format=${ARG_FORMAT} - --instr-profile=${ARG_OUTPUT} - $<$:-Xdemangler=${demangler}> - "$<$:--ignore-filename-regex=($)>" - $--object$> - --sources $--sources$> - > "${ARG_EXPORT}" - COMMENT "Exporting coverage information to ${ARG_EXPORT}" - COMMAND_EXPAND_LISTS - USES_TERMINAL - VERBATIM) - foreach (target IN LISTS ARG_TARGETS) - set_property(TARGET ${name} APPEND - PROPERTY - LLVM_SOURCES $) +# @param {string} FORMAT - File format to generate the report in. This depends +# on which reporter is being used. +#]============================================================================] +function (add_coverage_report name) + cmake_parse_arguments(ARG "LLVM;GNU" "OUTPUT;FORMAT" "" ${ARGN}) + cmake_language(CALL 🈯::ixm::coverage::type "reports") + + + if (ARG_LLVM) + cmake_language(CALL 🈯::ixm::default ARG_FORMAT LCOV) + cmake_language(CALL 🈯::ixm::requires::choice FORMAT LCOV JSON) + cmake_language(CALL 🈯::ixm::default ARG_OUTPUT $* $, + ${name}.$ + >) + else() + cmake_language(CALL 🈯::ixm::default ARG_FORMAT LCOV) + cmake_language(CALL 🈯::ixm::requires::choice FORMAT + JSON HTML CSV LCOV Clover Cobertura Coveralls SonarQube JaCoCo) + endif() + + cmake_language(CALL 🈯::ixm::coverage::target ${name}) + + if (ARG_LLVM) + cmake_language(CALL 🈯::ixm::coverage::llvm::merge ${name}) + cmake_language(CALL 🈯::ixm::coverage::llvm::report ${name}) + else() + cmake_language(CALL 🈯::ixm::coverage::gnu::merge ${name}) + endif() +endfunction() + +function (target_coverage name) + cmake_parse_arguments(ARG "" "" "PRIVATE;PUBLIC;INTERFACE" ${ARGN}) + get_property(implementation TARGET ${name} PROPERTY COVERAGE_IMPLEMENTATION) + + foreach (visibility IN ITEMS PRIVATE PUBLIC) + foreach (target IN LISTS ARG_${visibility}) + set(is.executable $,EXECUTABLE>) + string(CONCAT is.mergeable $, + $ + >) + ixm_target_property(LLVM_INSTRUMENTED_FILENAME + PREFIX COVERAGE + TARGET ${target} + CONTEXT) + set(instrumented $<${is.executable}:${LLVM_INSTRUMENTED_FILENAME}>) + set(merged $<${is.mergeable}:${LLVM_INSTRUMENTED_FILENAME}>) + set_property(TARGET ${name} APPEND + PROPERTY + COVERAGE_LLVM_INSTRUMENTED_SOURCES $) + set_property(TARGET ${name} APPEND + PROPERTY + COVERAGE_LLVM_INSTRUMENTED_EXECUTABLES $<${is.executable}:$> ) + set_property(TARGET ${name} APPEND + PROPERTY + COVERAGE_LLVM_MERGE_PROFILES ${merged}) + set_property(TARGET ${name} APPEND + PROPERTY + ADDITIONAL_CLEAN_FILES ${instrumented}) + # We only add the link libraries call for targets that aren't genexpr + string(GENEX_STRIP "${target}" stripped) + if (target STREQUAL stripped) + target_link_libraries(${target} + ${visibility} + Coverage::${implementation}) + endif() + endforeach() endforeach() - set_property(TARGET ${name} APPEND - PROPERTY - LLVM_IGNORE_FILENAME_REGEX ${ARG_IGNORE_FILENAME_REGEX}) endfunction() + +include("${CMAKE_CURRENT_LIST_DIR}/runtime/coverage.cmake") diff --git a/modules/runtime/coverage.cmake b/modules/runtime/coverage.cmake new file mode 100644 index 0000000..2d28ec3 --- /dev/null +++ b/modules/runtime/coverage.cmake @@ -0,0 +1,192 @@ +include_guard(GLOBAL) + +# These are cache variables that let us set defaults and ONLY update if the +# actual file itself has changed. +# While there is additional I/O on "first" or "fresh" runs, the overhead for +# incremental builds and setting `find_package` defaults is more than worth it. + +# It also lets us ensure this file is only included ONCE per run (thankfully) +define_property(TARGET PROPERTY COVERAGE_IMPLEMENTATION) + +define_property(TARGET PROPERTY COVERAGE_REPORT_SCRIPT_PATH) +define_property(TARGET PROPERTY COVERAGE_REPORT_SCRIPT_ECHO) +define_property(TARGET PROPERTY COVERAGE_REPORT_LOCATION) +define_property(TARGET PROPERTY COVERAGE_REPORT_FORMAT) + +define_property(TARGET PROPERTY COVERAGE_LLVM_INSTRUMENTED_EXECUTABLES) +define_property(TARGET PROPERTY COVERAGE_LLVM_INSTRUMENTED_SOURCES) + +define_property(TARGET + PROPERTY COVERAGE_LLVM_INSTRUMENTED_FILENAME + INITIALIZE_FROM_VARIABLE IXM_COVERAGE_LLVM_INSTRUMENTED_FILENAME) + +define_property(TARGET + PROPERTY COVERAGE_LLVM_COVERAGE_PREFIX_MAP + INITIALIZE_FROM_VARIABLE IXM_COVERAGE_LLVM_COVERAGE_PREFIX_MAP) + +define_property(TARGET PROPERTY COVERAGE_LLVM_MCDC) + +define_property(TARGET PROPERTY COVERAGE_LLVM_MERGE_FAILURE_MODE) +define_property(TARGET PROPERTY COVERAGE_LLVM_MERGE_PROFILES) +define_property(TARGET PROPERTY COVERAGE_LLVM_MERGE_OPTIONS) +define_property(TARGET PROPERTY COVERAGE_LLVM_MERGE_SPARSE + INITIALIZE_FROM_VARIABLE IXM_COVERAGE_LLVM_MERGE_SPARSE) + +define_property(TARGET PROPERTY COVERAGE_LLVM_EXPORT_IGNORE_FILENAME_REGEX + INITIALIZE_FROM_VARIABLE IXM_COVERAGE_LLVM_EXPORT_IGNORE_FILENAME_REGEX) + +define_property(TARGET PROPERTY COVERAGE_GNU_ABSOLUTE_PATHS + INITIALIZE_FROM_VARIABLE IXM_COVERAGE_GNU_ABSOLUTE_PATHS) +define_property(TARGET PROPERTY COVERAGE_GNU_CONDITIONS + INITIALIZE_FROM_VARIABLE IXM_COVERAGE_GNU_CONDITIONS) + + +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_EXCLUDE_DATA_DIRS) +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_EXCLUDE_DATA) +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_FILTER_DATA) +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_OPTIONS) +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_FILTER) +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_INCLUDE) +define_property(TARGET PROPERTY COVERAGE_GNU_MERGE_EXCLUDE) + +set(IXM_COVERAGE_LLVM_INSTRUMENTED_FILENAME + $,$,$.profraw> + CACHE STRING "Default path for the LLVM .profraw file") + +set(IXM_COVERAGE_LLVM_COVERAGE_PREFIX_MAP + "$=." + CACHE STRING "Default coverage prefix map.") + +set(IXM_COVERAGE_LLVM_MERGE_SPARSE YES + CACHE BOOL "Add `--sparse` to llvm-profdata merge commands") + +set(IXM_COVERAGE_LLVM_EXPORT_IGNORE_FILENAME_REGEX + $ + CACHE STRING "Default filepaths to ignore") + +set(IXM_COVERAGE_GNU_ABSOLUTE_PATHS YES + CACHE BOOL "Create absolute paths in the .gcno files") +set(IXM_COVERAGE_GNU_CONDITIONS NO + CACHE BOOL "Whether to instrument program conditions (GCC Only)") + +mark_as_advanced( + IXM_COVERAGE_LLVM_INSTRUMENTED_FILENAME + + IXM_COVERAGE_LLVM_COVERAGE_PREFIX_MAP + + IXM_COVERAGE_LLVM_MERGE_SPARSE + + IXM_COVERAGE_LLVM_EXPORT_IGNORE_FILENAME_REGEX + + IXM_COVERAGE_GNU_ABSOLUTE_PATHS + IXM_COVERAGE_GNU_CONDITIONS +) + +function (🈯::ixm::coverage::type kind) + if (ARG_LLVM AND ARG_GNU) + message(FATAL_ERROR "Coverage ${kind} targets may only have one of type: LLVM or GNU") + endif() +endfunction() + +function (🈯::ixm::coverage::target name) + add_custom_target(${name} + DEPENDS + $>) + + set_property(TARGET ${name} + PROPERTY + COVERAGE_IMPLEMENTATION $,LLVM,GNU>) +endfunction() + +function (🈯::ixm::coverage::llvm::merge name) + set(profile.data $,${name}.profdata>) + + ixm_target_property(LLVM_INSTRUMENTED_EXECUTABLES TARGET ${name} PREFIX COVERAGE) + + ixm_target_property(LLVM_MERGE_FAILURE_MODE TARGET ${name} PREFIX COVERAGE) + ixm_target_property(LLVM_MERGE_PROFILES TARGET ${name} PREFIX COVERAGE) + ixm_target_property(LLVM_MERGE_OPTIONS TARGET ${name} PREFIX COVERAGE) + ixm_target_property(LLVM_MERGE_SPARSE TARGET ${name} PREFIX COVERAGE) + + add_custom_command(OUTPUT "${profile.data}" + COMMAND Coverage::LLVM::Merge merge + --instr + $<$:--sparse> + $<$:--failure-mode=${LLVM_MERGE_FAILURE_MODE}> + --output=${profile.data} + ${LLVM_MERGE_OPTIONS} + ${LLVM_MERGE_PROFILES} + DEPENDS + $ + $ + $ + COMMENT "Merging profiling data to ${profile.data}" + COMMAND_EXPAND_LISTS + VERBATIM) + + # Setting this lets us merge multiple .profdata files for amalgamations + set_property(TARGET ${name} + PROPERTY + COVERAGE_LLVM_INSTRUMENTED_FILENAME "${profile.data}") + set_property(TARGET ${name} APPEND + PROPERTY + ADDITIONAL_CLEAN_FILES "${profile.data}") + set_property(TARGET ${name} APPEND + PROPERTY TRANSITIVE_COMPILE_PROPERTIES + COVERAGE_LLVM_INSTRUMENTED_EXECUTABLES + COVERAGE_LLVM_INSTRUMENTED_SOURCES) +endfunction() + +function (🈯::ixm::coverage::gnu::merge name) + +endfunction() + +function (🈯::ixm::coverage::llvm::report name) + ixm_target_property(LLVM_INSTRUMENTED_FILENAME TARGET ${name} PREFIX COVERAGE) + ixm_target_property(LLVM_INSTRUMENTED_EXECUTABLES PREFIX COVERAGE) + ixm_target_property(LLVM_INSTRUMENTED_SOURCES PREFIX COVERAGE) + + ixm_target_property(IMPORTED_LOCATION + OUTPUT_VARIABLE demangler + TARGET $) + + # Do the same thing, but for sources + # NAMESPACE generated with + # python -m uuid + # --uuid uuid5 + # --namespace @url + # --name ixm.one/find/coverage/report/llvm-cov.cmake.in + string(UUID basename + NAMESPACE 0b14e254-813c-50bf-a996-d997a15cb70b + NAME ${name} + TYPE SHA1) + set(template.src "${IXM_TEMPLATES_DIR}/coverage/report/llvm-cov.cmake.in") + set(script.src "${IXM_FILES_DIR}/${basename}.cmake.in") + set(script.dst "${IXM_FILES_DIR}/${basename}-$.cmake") + # Some string work needs to be done *before* we use this script in CONTENT + configure_file("${template.src}" "${script.src}" @ONLY NEWLINE_STYLE LF) + + file(GENERATE + OUTPUT "${script.dst}" + INPUT "${script.src}" + TARGET ${name} + NEWLINE_STYLE LF) + + add_custom_command(OUTPUT "${ARG_OUTPUT}" + COMMAND "${CMAKE_COMMAND}" -P "${REPORT_SCRIPT_PATH}" + DEPENDS + "${LLVM_INSTRUMENTED_FILENAME}" + "${REPORT_SCRIPT_PATH}" + COMMENT "Exporting coverage information to ${ARG_OUTPUT}" + COMMAND_EXPAND_LISTS + VERBATIM) + set_target_properties(${name} + PROPERTIES + COVERAGE_REPORT_SCRIPT_PATH "${script.dst}" + COVERAGE_REPORT_LOCATION "${ARG_OUTPUT}" + COVERAGE_REPORT_FORMAT "${ARG_FORMAT}") +endfunction() + +function (🈯::ixm::coverage::gnu::report name) + +endfunction() diff --git a/runtime/common.cmake b/runtime/common.cmake index e94c0b1..c802c1c 100644 --- a/runtime/common.cmake +++ b/runtime/common.cmake @@ -58,15 +58,48 @@ function (🈯::ixm::experiment name uuid) endif() endfunction() +function (ixm::target::property property) + cmake_parse_arguments(ARG "CONTEXT" "TARGET;OUTPUT_VARIABLE" "PREFIX" ${ARGN}) + cmake_language(CALL 🈯::ixm::default ARG_OUTPUT_VARIABLE ${property}) + string(JOIN _ property ${ARG_PREFIX} ${property}) + + set(eval GENEX_EVAL:) + set(target) + if (ARG_TARGET) + set(target "${ARG_TARGET},") + endif() + if (ARG_TARGET AND ARG_CONTEXT) + set(eval TARGET_GENEX_EVAL:${target}) + endif() + + set(${ARG_OUTPUT_VARIABLE} $<${eval}$> PARENT_SCOPE) +endfunction() + +function (🈯::ixm::requires::choice name) + if (NOT ARG_${name} IN_LIST ARGN) + list(JOIN valid ", " valid) + message(FATAL_ERROR "Argument '${name}' must be one of the following: ${valid}") + endif() +endfunction() + #[[Used to set missing arguments to a well known default]] -macro(🈯::ixm::default var default) - if (NOT ${var}) - set(${var} ${default}) +function (🈯::ixm::default var) + cmake_parse_arguments("" "$*" "" "" ${ARGN}) + if (NOT ${var} AND NOT _$*) + set(${var} ${ARGN}) + elseif (NOT ${var} AND _$*) + string(CONCAT ${var} ${_UNPARSED_ARGUMENTS}) endif() -endmacro() + return(PROPAGATE ${var}) +endfunction() macro(🈯::ixm::requires name) if (NOT ARG_${name}) message(FATAL_ERROR "function '${CMAKE_CURRENT_FUNCTION}' requires a '${name}' argument") endif() endmacro() + +# Friendly wrapper :> +macro(ixm_target_property) + cmake_language(CALL ixm::target::property ${ARGN}) +endmacro() diff --git a/templates/coverage/report/llvm-cov.cmake.in b/templates/coverage/report/llvm-cov.cmake.in new file mode 100644 index 0000000..56aedb6 --- /dev/null +++ b/templates/coverage/report/llvm-cov.cmake.in @@ -0,0 +1,47 @@ +# Each set is here for easier debugging purposes. +# Each 'set' in the original template file has a purpose, even if the syntax +# seems unobvious. +set(ignore-filename-regex $$>,|>$) +set(demangler $<$:$-Xdemangler=$$>) +set(llvm-cov $$$) +set(output $$>$) +set(report $$>$) +set(format $>>) +set(echo $>,STDOUT,NONE>) + +set(objects + $,$>,REPLACE,(.+),$\1$>,PREPEND,--object >, + >) + +# To whoever is reading this nightmare in the template script, I'm sorry. +# It makes the final script output easier to read, I promise. +set(sources + $,EXCLUDE,cmake_pch>,REPLACE,(.+),$\1$>,PREPEND,--sources >, + >) + +if (format STREQUAL "json") + set(format text) +endif() + +execute_process( + COMMAND "${llvm-cov}" + export + "--format=${format}" + "--instr-profile=${output}" + "--ignore-filename-regex=(${ignore-filename-regex})" + ${demangler} + ${objects} + ${sources} + COMMAND_ECHO ${echo} + ENCODING UTF-8 + ERROR_VARIABLE error + RESULT_VARIABLE status + OUTPUT_FILE ${report} + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE) + +if (NOT status EQUAL 0) + message(SEND_ERROR "Failed to export coverage report. ${llvm-cov} returned ${status}:\n" ${error}) + cmake_language(EXIT ${status}) +endif() +message(NOTICE "Exporting coverage information to " ${report})