/ cmake / CodeCoverage.cmake
CodeCoverage.cmake
  1  # Copyright (c) 2012 - 2017, Lars Bilke
  2  # All rights reserved.
  3  #
  4  # Redistribution and use in source and binary forms, with or without modification,
  5  # are permitted provided that the following conditions are met:
  6  #
  7  # 1. Redistributions of source code must retain the above copyright notice, this
  8  #    list of conditions and the following disclaimer.
  9  #
 10  # 2. Redistributions in binary form must reproduce the above copyright notice,
 11  #    this list of conditions and the following disclaimer in the documentation
 12  #    and/or other materials provided with the distribution.
 13  #
 14  # 3. Neither the name of the copyright holder nor the names of its contributors
 15  #    may be used to endorse or promote products derived from this software without
 16  #    specific prior written permission.
 17  #
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 19  # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 20  # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 21  # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 22  # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 24  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 25  # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 26  # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 27  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 28  #
 29  # CHANGES:
 30  #
 31  # 2012-01-31, Lars Bilke
 32  # - Enable Code Coverage
 33  #
 34  # 2013-09-17, Joakim Söderberg
 35  # - Added support for Clang.
 36  # - Some additional usage instructions.
 37  #
 38  # 2016-02-03, Lars Bilke
 39  # - Refactored functions to use named parameters
 40  #
 41  # 2017-06-02, Lars Bilke
 42  # - Merged with modified version from github.com/ufz/ogs
 43  #
 44  # 2019-05-06, Anatolii Kurotych
 45  # - Remove unnecessary --coverage flag
 46  #
 47  # 2019-12-13, FeRD (Frank Dana)
 48  # - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor
 49  #   of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments.
 50  # - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY
 51  # - All setup functions: accept BASE_DIRECTORY, EXCLUDE list
 52  # - Set lcov basedir with -b argument
 53  # - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be
 54  #   overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().)
 55  # - Delete output dir, .info file on 'make clean'
 56  # - Remove Python detection, since version mismatches will break gcovr
 57  # - Minor cleanup (lowercase function names, update examples...)
 58  #
 59  # 2019-12-19, FeRD (Frank Dana)
 60  # - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets
 61  #
 62  # 2020-01-19, Bob Apthorpe
 63  # - Added gfortran support
 64  #
 65  # 2020-02-17, FeRD (Frank Dana)
 66  # - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters
 67  #   in EXCLUDEs, and remove manual escaping from gcovr targets
 68  #
 69  # 2021-01-19, Robin Mueller
 70  # - Add CODE_COVERAGE_VERBOSE option which will allow to print out commands which are run
 71  # - Added the option for users to set the GCOVR_ADDITIONAL_ARGS variable to supply additional
 72  #   flags to the gcovr command
 73  #
 74  # 2020-05-04, Mihchael Davis
 75  #     - Add -fprofile-abs-path to make gcno files contain absolute paths
 76  #     - Fix BASE_DIRECTORY not working when defined
 77  #     - Change BYPRODUCT from folder to index.html to stop ninja from complaining about double defines
 78  #
 79  # 2021-05-10, Martin Stump
 80  #     - Check if the generator is multi-config before warning about non-Debug builds
 81  #
 82  # 2022-02-22, Marko Wehle
 83  #     - Change gcovr output from -o <filename> for --xml <filename> and --html <filename> output respectively.
 84  #       This will allow for Multiple Output Formats at the same time by making use of GCOVR_ADDITIONAL_ARGS, e.g. GCOVR_ADDITIONAL_ARGS "--txt".
 85  #
 86  # 2022-09-28, Sebastian Mueller
 87  #     - fix append_coverage_compiler_flags_to_target to correctly add flags
 88  #     - replace "-fprofile-arcs -ftest-coverage" with "--coverage" (equivalent)
 89  #
 90  # USAGE:
 91  #
 92  # 1. Copy this file into your cmake modules path.
 93  #
 94  # 2. Add the following line to your CMakeLists.txt (best inside an if-condition
 95  #    using a CMake option() to enable it just optionally):
 96  #      include(CodeCoverage)
 97  #
 98  # 3. Append necessary compiler flags for all supported source files:
 99  #      append_coverage_compiler_flags()
100  #    Or for specific target:
101  #      append_coverage_compiler_flags_to_target(YOUR_TARGET_NAME)
102  #
103  # 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og
104  #
105  # 4. If you need to exclude additional directories from the report, specify them
106  #    using full paths in the COVERAGE_EXCLUDES variable before calling
107  #    setup_target_for_coverage_*().
108  #    Example:
109  #      set(COVERAGE_EXCLUDES
110  #          '${PROJECT_SOURCE_DIR}/src/dir1/*'
111  #          '/path/to/my/src/dir2/*')
112  #    Or, use the EXCLUDE argument to setup_target_for_coverage_*().
113  #    Example:
114  #      setup_target_for_coverage_lcov(
115  #          NAME coverage
116  #          EXECUTABLE testrunner
117  #          EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*")
118  #
119  # 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set
120  #     relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR)
121  #     Example:
122  #       set(COVERAGE_EXCLUDES "dir1/*")
123  #       setup_target_for_coverage_gcovr_html(
124  #           NAME coverage
125  #           EXECUTABLE testrunner
126  #           BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src"
127  #           EXCLUDE "dir2/*")
128  #
129  # 5. Use the functions described below to create a custom make target which
130  #    runs your test executable and produces a code coverage report.
131  #
132  # 6. Build a Debug build:
133  #      cmake -DCMAKE_BUILD_TYPE=Debug ..
134  #      make
135  #      make my_coverage_target
136  #
137  
138  include(CMakeParseArguments)
139  
140  option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE)
141  
142  # Check prereqs
143  find_program( GCOV_PATH gcov )
144  find_program( LCOV_PATH  NAMES lcov lcov.bat lcov.exe lcov.perl)
145  find_program( FASTCOV_PATH NAMES fastcov fastcov.py )
146  find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat )
147  find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
148  find_program( CPPFILT_PATH NAMES c++filt )
149  
150  if(NOT GCOV_PATH)
151      message(FATAL_ERROR "gcov not found! Aborting...")
152  endif() # NOT GCOV_PATH
153  
154  # Check supported compiler (Clang, GNU and Flang)
155  get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES)
156  foreach(LANG ${LANGUAGES})
157    if("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
158      if("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3)
159        message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...")
160      endif()
161    elseif(NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "GNU"
162           AND NOT "${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(LLVM)?[Ff]lang")
163      message(FATAL_ERROR "Compiler is not GNU or Flang! Aborting...")
164    endif()
165  endforeach()
166  
167  set(COVERAGE_COMPILER_FLAGS "-g --coverage"
168      CACHE INTERNAL "")
169  
170  if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)")
171      include(CheckCXXCompilerFlag)
172      check_cxx_compiler_flag(-fprofile-abs-path HAVE_cxx_fprofile_abs_path)
173      if(HAVE_cxx_fprofile_abs_path)
174          set(COVERAGE_CXX_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path")
175      endif()
176  endif()
177  if(CMAKE_C_COMPILER_ID MATCHES "(GNU|Clang)")
178      include(CheckCCompilerFlag)
179      check_c_compiler_flag(-fprofile-abs-path HAVE_c_fprofile_abs_path)
180      if(HAVE_c_fprofile_abs_path)
181          set(COVERAGE_C_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path")
182      endif()
183  endif()
184  
185  set(CMAKE_Fortran_FLAGS_COVERAGE
186      ${COVERAGE_COMPILER_FLAGS}
187      CACHE STRING "Flags used by the Fortran compiler during coverage builds."
188      FORCE )
189  set(CMAKE_CXX_FLAGS_COVERAGE
190      ${COVERAGE_COMPILER_FLAGS}
191      CACHE STRING "Flags used by the C++ compiler during coverage builds."
192      FORCE )
193  set(CMAKE_C_FLAGS_COVERAGE
194      ${COVERAGE_COMPILER_FLAGS}
195      CACHE STRING "Flags used by the C compiler during coverage builds."
196      FORCE )
197  set(CMAKE_EXE_LINKER_FLAGS_COVERAGE
198      ""
199      CACHE STRING "Flags used for linking binaries during coverage builds."
200      FORCE )
201  set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
202      ""
203      CACHE STRING "Flags used by the shared libraries linker during coverage builds."
204      FORCE )
205  mark_as_advanced(
206      CMAKE_Fortran_FLAGS_COVERAGE
207      CMAKE_CXX_FLAGS_COVERAGE
208      CMAKE_C_FLAGS_COVERAGE
209      CMAKE_EXE_LINKER_FLAGS_COVERAGE
210      CMAKE_SHARED_LINKER_FLAGS_COVERAGE )
211  
212  get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
213  if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG))
214      message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
215  endif() # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)
216  
217  if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
218      link_libraries(gcov)
219  endif()
220  
221  # Defines a target for running and collection code coverage information
222  # Builds dependencies, runs the given executable and outputs reports.
223  # NOTE! The executable should always have a ZERO as exit code otherwise
224  # the coverage generation will not complete.
225  #
226  # setup_target_for_coverage_lcov(
227  #     NAME testrunner_coverage                    # New target name
228  #     EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
229  #     DEPENDENCIES testrunner                     # Dependencies to build first
230  #     BASE_DIRECTORY "../"                        # Base directory for report
231  #                                                 #  (defaults to PROJECT_SOURCE_DIR)
232  #     EXCLUDE "src/dir1/*" "src/dir2/*"           # Patterns to exclude (can be relative
233  #                                                 #  to BASE_DIRECTORY, with CMake 3.4+)
234  #     NO_DEMANGLE                                 # Don't demangle C++ symbols
235  #                                                 #  even if c++filt is found
236  # )
237  function(setup_target_for_coverage_lcov)
238  
239      set(options NO_DEMANGLE SONARQUBE)
240      set(oneValueArgs BASE_DIRECTORY NAME)
241      set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS)
242      cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
243  
244      if(NOT LCOV_PATH)
245          message(FATAL_ERROR "lcov not found! Aborting...")
246      endif() # NOT LCOV_PATH
247  
248      if(NOT GENHTML_PATH)
249          message(FATAL_ERROR "genhtml not found! Aborting...")
250      endif() # NOT GENHTML_PATH
251  
252      # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
253      if(DEFINED Coverage_BASE_DIRECTORY)
254          get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
255      else()
256          set(BASEDIR ${PROJECT_SOURCE_DIR})
257      endif()
258  
259      # Collect excludes (CMake 3.4+: Also compute absolute paths)
260      set(LCOV_EXCLUDES "")
261      foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES})
262          if(CMAKE_VERSION VERSION_GREATER 3.4)
263              get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
264          endif()
265          list(APPEND LCOV_EXCLUDES "${EXCLUDE}")
266      endforeach()
267      list(REMOVE_DUPLICATES LCOV_EXCLUDES)
268  
269      # Conditional arguments
270      if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE})
271        set(GENHTML_EXTRA_ARGS "--demangle-cpp")
272      endif()
273  
274      # Setting up commands which will be run to generate coverage data.
275      # Cleanup lcov
276      set(LCOV_CLEAN_CMD
277          ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory .
278          -b ${BASEDIR} --zerocounters
279      )
280      # Create baseline to make sure untouched files show up in the report
281      set(LCOV_BASELINE_CMD
282          ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b
283          ${BASEDIR} -o ${Coverage_NAME}.base
284      )
285      # Run tests
286      set(LCOV_EXEC_TESTS_CMD
287          ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
288      )
289      # Capturing lcov counters and generating report
290      set(LCOV_CAPTURE_CMD
291          ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b
292          ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture
293      )
294      # add baseline counters
295      set(LCOV_BASELINE_COUNT_CMD
296          ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base
297          -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total
298      )
299      # filter collected data to final coverage report
300      set(LCOV_FILTER_CMD
301          ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove
302          ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info
303      )
304      # Generate HTML output
305      set(LCOV_GEN_HTML_CMD
306          ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o
307          ${Coverage_NAME} ${Coverage_NAME}.info
308      )
309      if(${Coverage_SONARQUBE})
310          # Generate SonarQube output
311          set(GCOVR_XML_CMD
312              ${GCOVR_PATH} --sonarqube ${Coverage_NAME}_sonarqube.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS}
313              ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR}
314          )
315          set(GCOVR_XML_CMD_COMMAND
316              COMMAND ${GCOVR_XML_CMD}
317          )
318          set(GCOVR_XML_CMD_BYPRODUCTS ${Coverage_NAME}_sonarqube.xml)
319          set(GCOVR_XML_CMD_COMMENT COMMENT "SonarQube code coverage info report saved in ${Coverage_NAME}_sonarqube.xml.")
320      endif()
321  
322  
323      if(CODE_COVERAGE_VERBOSE)
324          message(STATUS "Executed command report")
325          message(STATUS "Command to clean up lcov: ")
326          string(REPLACE ";" " " LCOV_CLEAN_CMD_SPACED "${LCOV_CLEAN_CMD}")
327          message(STATUS "${LCOV_CLEAN_CMD_SPACED}")
328  
329          message(STATUS "Command to create baseline: ")
330          string(REPLACE ";" " " LCOV_BASELINE_CMD_SPACED "${LCOV_BASELINE_CMD}")
331          message(STATUS "${LCOV_BASELINE_CMD_SPACED}")
332  
333          message(STATUS "Command to run the tests: ")
334          string(REPLACE ";" " " LCOV_EXEC_TESTS_CMD_SPACED "${LCOV_EXEC_TESTS_CMD}")
335          message(STATUS "${LCOV_EXEC_TESTS_CMD_SPACED}")
336  
337          message(STATUS "Command to capture counters and generate report: ")
338          string(REPLACE ";" " " LCOV_CAPTURE_CMD_SPACED "${LCOV_CAPTURE_CMD}")
339          message(STATUS "${LCOV_CAPTURE_CMD_SPACED}")
340  
341          message(STATUS "Command to add baseline counters: ")
342          string(REPLACE ";" " " LCOV_BASELINE_COUNT_CMD_SPACED "${LCOV_BASELINE_COUNT_CMD}")
343          message(STATUS "${LCOV_BASELINE_COUNT_CMD_SPACED}")
344  
345          message(STATUS "Command to filter collected data: ")
346          string(REPLACE ";" " " LCOV_FILTER_CMD_SPACED "${LCOV_FILTER_CMD}")
347          message(STATUS "${LCOV_FILTER_CMD_SPACED}")
348  
349          message(STATUS "Command to generate lcov HTML output: ")
350          string(REPLACE ";" " " LCOV_GEN_HTML_CMD_SPACED "${LCOV_GEN_HTML_CMD}")
351          message(STATUS "${LCOV_GEN_HTML_CMD_SPACED}")
352  
353          if(${Coverage_SONARQUBE})
354              message(STATUS "Command to generate SonarQube XML output: ")
355              string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}")
356              message(STATUS "${GCOVR_XML_CMD_SPACED}")
357          endif()
358      endif()
359  
360      # Setup target
361      add_custom_target(${Coverage_NAME}
362          COMMAND ${LCOV_CLEAN_CMD}
363          COMMAND ${LCOV_BASELINE_CMD}
364          COMMAND ${LCOV_EXEC_TESTS_CMD}
365          COMMAND ${LCOV_CAPTURE_CMD}
366          COMMAND ${LCOV_BASELINE_COUNT_CMD}
367          COMMAND ${LCOV_FILTER_CMD}
368          COMMAND ${LCOV_GEN_HTML_CMD}
369          ${GCOVR_XML_CMD_COMMAND}
370  
371          # Set output files as GENERATED (will be removed on 'make clean')
372          BYPRODUCTS
373              ${Coverage_NAME}.base
374              ${Coverage_NAME}.capture
375              ${Coverage_NAME}.total
376              ${Coverage_NAME}.info
377              ${GCOVR_XML_CMD_BYPRODUCTS}
378              ${Coverage_NAME}/index.html
379          WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
380          DEPENDS ${Coverage_DEPENDENCIES}
381          VERBATIM # Protect arguments to commands
382          COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
383      )
384  
385      # Show where to find the lcov info report
386      add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
387          COMMAND ;
388          COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info."
389          ${GCOVR_XML_CMD_COMMENT}
390      )
391  
392      # Show info where to find the report
393      add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
394          COMMAND ;
395          COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
396      )
397  
398  endfunction() # setup_target_for_coverage_lcov
399  
400  # Defines a target for running and collection code coverage information
401  # Builds dependencies, runs the given executable and outputs reports.
402  # NOTE! The executable should always have a ZERO as exit code otherwise
403  # the coverage generation will not complete.
404  #
405  # setup_target_for_coverage_gcovr_xml(
406  #     NAME ctest_coverage                    # New target name
407  #     EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
408  #     DEPENDENCIES executable_target         # Dependencies to build first
409  #     BASE_DIRECTORY "../"                   # Base directory for report
410  #                                            #  (defaults to PROJECT_SOURCE_DIR)
411  #     EXCLUDE "src/dir1/*" "src/dir2/*"      # Patterns to exclude (can be relative
412  #                                            #  to BASE_DIRECTORY, with CMake 3.4+)
413  # )
414  # The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the
415  # GCVOR command.
416  function(setup_target_for_coverage_gcovr_xml)
417  
418      set(options NONE)
419      set(oneValueArgs BASE_DIRECTORY NAME)
420      set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
421      cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
422  
423      if(NOT GCOVR_PATH)
424          message(FATAL_ERROR "gcovr not found! Aborting...")
425      endif() # NOT GCOVR_PATH
426  
427      # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
428      if(DEFINED Coverage_BASE_DIRECTORY)
429          get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
430      else()
431          set(BASEDIR ${PROJECT_SOURCE_DIR})
432      endif()
433  
434      # Collect excludes (CMake 3.4+: Also compute absolute paths)
435      set(GCOVR_EXCLUDES "")
436      foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
437          if(CMAKE_VERSION VERSION_GREATER 3.4)
438              get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
439          endif()
440          list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
441      endforeach()
442      list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
443  
444      # Combine excludes to several -e arguments
445      set(GCOVR_EXCLUDE_ARGS "")
446      foreach(EXCLUDE ${GCOVR_EXCLUDES})
447          list(APPEND GCOVR_EXCLUDE_ARGS "-e")
448          list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
449      endforeach()
450  
451      # Set up commands which will be run to generate coverage data
452      # Run tests
453      set(GCOVR_XML_EXEC_TESTS_CMD
454          ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
455      )
456      # Running gcovr
457      set(GCOVR_XML_CMD
458          ${GCOVR_PATH} --xml ${Coverage_NAME}.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS}
459          ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR}
460      )
461  
462      if(CODE_COVERAGE_VERBOSE)
463          message(STATUS "Executed command report")
464  
465          message(STATUS "Command to run tests: ")
466          string(REPLACE ";" " " GCOVR_XML_EXEC_TESTS_CMD_SPACED "${GCOVR_XML_EXEC_TESTS_CMD}")
467          message(STATUS "${GCOVR_XML_EXEC_TESTS_CMD_SPACED}")
468  
469          message(STATUS "Command to generate gcovr XML coverage data: ")
470          string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}")
471          message(STATUS "${GCOVR_XML_CMD_SPACED}")
472      endif()
473  
474      add_custom_target(${Coverage_NAME}
475          COMMAND ${GCOVR_XML_EXEC_TESTS_CMD}
476          COMMAND ${GCOVR_XML_CMD}
477  
478          BYPRODUCTS ${Coverage_NAME}.xml
479          WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
480          DEPENDS ${Coverage_DEPENDENCIES}
481          VERBATIM # Protect arguments to commands
482          COMMENT "Running gcovr to produce Cobertura code coverage report."
483      )
484  
485      # Show info where to find the report
486      add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
487          COMMAND ;
488          COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml."
489      )
490  endfunction() # setup_target_for_coverage_gcovr_xml
491  
492  # Defines a target for running and collection code coverage information
493  # Builds dependencies, runs the given executable and outputs reports.
494  # NOTE! The executable should always have a ZERO as exit code otherwise
495  # the coverage generation will not complete.
496  #
497  # setup_target_for_coverage_gcovr_html(
498  #     NAME ctest_coverage                    # New target name
499  #     EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
500  #     DEPENDENCIES executable_target         # Dependencies to build first
501  #     BASE_DIRECTORY "../"                   # Base directory for report
502  #                                            #  (defaults to PROJECT_SOURCE_DIR)
503  #     EXCLUDE "src/dir1/*" "src/dir2/*"      # Patterns to exclude (can be relative
504  #                                            #  to BASE_DIRECTORY, with CMake 3.4+)
505  # )
506  # The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the
507  # GCVOR command.
508  function(setup_target_for_coverage_gcovr_html)
509  
510      set(options NONE)
511      set(oneValueArgs BASE_DIRECTORY NAME)
512      set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
513      cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
514  
515      if(NOT GCOVR_PATH)
516          message(FATAL_ERROR "gcovr not found! Aborting...")
517      endif() # NOT GCOVR_PATH
518  
519      # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
520      if(DEFINED Coverage_BASE_DIRECTORY)
521          get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
522      else()
523          set(BASEDIR ${PROJECT_SOURCE_DIR})
524      endif()
525  
526      # Collect excludes (CMake 3.4+: Also compute absolute paths)
527      set(GCOVR_EXCLUDES "")
528      foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
529          if(CMAKE_VERSION VERSION_GREATER 3.4)
530              get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
531          endif()
532          list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
533      endforeach()
534      list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
535  
536      # Combine excludes to several -e arguments
537      set(GCOVR_EXCLUDE_ARGS "")
538      foreach(EXCLUDE ${GCOVR_EXCLUDES})
539          list(APPEND GCOVR_EXCLUDE_ARGS "-e")
540          list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
541      endforeach()
542  
543      # Set up commands which will be run to generate coverage data
544      # Run tests
545      set(GCOVR_HTML_EXEC_TESTS_CMD
546          ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
547      )
548      # Create folder
549      set(GCOVR_HTML_FOLDER_CMD
550          ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME}
551      )
552      # Running gcovr
553      set(GCOVR_HTML_CMD
554          ${GCOVR_PATH} --html ${Coverage_NAME}/index.html --html-details -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS}
555          ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR}
556      )
557  
558      if(CODE_COVERAGE_VERBOSE)
559          message(STATUS "Executed command report")
560  
561          message(STATUS "Command to run tests: ")
562          string(REPLACE ";" " " GCOVR_HTML_EXEC_TESTS_CMD_SPACED "${GCOVR_HTML_EXEC_TESTS_CMD}")
563          message(STATUS "${GCOVR_HTML_EXEC_TESTS_CMD_SPACED}")
564  
565          message(STATUS "Command to create a folder: ")
566          string(REPLACE ";" " " GCOVR_HTML_FOLDER_CMD_SPACED "${GCOVR_HTML_FOLDER_CMD}")
567          message(STATUS "${GCOVR_HTML_FOLDER_CMD_SPACED}")
568  
569          message(STATUS "Command to generate gcovr HTML coverage data: ")
570          string(REPLACE ";" " " GCOVR_HTML_CMD_SPACED "${GCOVR_HTML_CMD}")
571          message(STATUS "${GCOVR_HTML_CMD_SPACED}")
572      endif()
573  
574      add_custom_target(${Coverage_NAME}
575          COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD}
576          COMMAND ${GCOVR_HTML_FOLDER_CMD}
577          COMMAND ${GCOVR_HTML_CMD}
578  
579          BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html  # report directory
580          WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
581          DEPENDS ${Coverage_DEPENDENCIES}
582          VERBATIM # Protect arguments to commands
583          COMMENT "Running gcovr to produce HTML code coverage report."
584      )
585  
586      # Show info where to find the report
587      add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
588          COMMAND ;
589          COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
590      )
591  
592  endfunction() # setup_target_for_coverage_gcovr_html
593  
594  # Defines a target for running and collection code coverage information
595  # Builds dependencies, runs the given executable and outputs reports.
596  # NOTE! The executable should always have a ZERO as exit code otherwise
597  # the coverage generation will not complete.
598  #
599  # setup_target_for_coverage_fastcov(
600  #     NAME testrunner_coverage                    # New target name
601  #     EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
602  #     DEPENDENCIES testrunner                     # Dependencies to build first
603  #     BASE_DIRECTORY "../"                        # Base directory for report
604  #                                                 #  (defaults to PROJECT_SOURCE_DIR)
605  #     EXCLUDE "src/dir1/" "src/dir2/"             # Patterns to exclude.
606  #     NO_DEMANGLE                                 # Don't demangle C++ symbols
607  #                                                 #  even if c++filt is found
608  #     SKIP_HTML                                   # Don't create html report
609  #     POST_CMD perl -i -pe s!${PROJECT_SOURCE_DIR}/!!g ctest_coverage.json  # E.g. for stripping source dir from file paths
610  # )
611  function(setup_target_for_coverage_fastcov)
612  
613      set(options NO_DEMANGLE SKIP_HTML)
614      set(oneValueArgs BASE_DIRECTORY NAME)
615      set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES FASTCOV_ARGS GENHTML_ARGS POST_CMD)
616      cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
617  
618      if(NOT FASTCOV_PATH)
619          message(FATAL_ERROR "fastcov not found! Aborting...")
620      endif()
621  
622      if(NOT Coverage_SKIP_HTML AND NOT GENHTML_PATH)
623          message(FATAL_ERROR "genhtml not found! Aborting...")
624      endif()
625  
626      # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
627      if(Coverage_BASE_DIRECTORY)
628          get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
629      else()
630          set(BASEDIR ${PROJECT_SOURCE_DIR})
631      endif()
632  
633      # Collect excludes (Patterns, not paths, for fastcov)
634      set(FASTCOV_EXCLUDES "")
635      foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_FASTCOV_EXCLUDES})
636          list(APPEND FASTCOV_EXCLUDES "${EXCLUDE}")
637      endforeach()
638      list(REMOVE_DUPLICATES FASTCOV_EXCLUDES)
639  
640      # Conditional arguments
641      if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE})
642          set(GENHTML_EXTRA_ARGS "--demangle-cpp")
643      endif()
644  
645      # Set up commands which will be run to generate coverage data
646      set(FASTCOV_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS})
647  
648      set(FASTCOV_CAPTURE_CMD ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH}
649          --search-directory ${BASEDIR}
650          --process-gcno
651          --output ${Coverage_NAME}.json
652          --exclude ${FASTCOV_EXCLUDES}
653      )
654  
655      set(FASTCOV_CONVERT_CMD ${FASTCOV_PATH}
656          -C ${Coverage_NAME}.json --lcov --output ${Coverage_NAME}.info
657      )
658  
659      if(Coverage_SKIP_HTML)
660          set(FASTCOV_HTML_CMD ";")
661      else()
662          set(FASTCOV_HTML_CMD ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS}
663              -o ${Coverage_NAME} ${Coverage_NAME}.info
664          )
665      endif()
666  
667      set(FASTCOV_POST_CMD ";")
668      if(Coverage_POST_CMD)
669          set(FASTCOV_POST_CMD ${Coverage_POST_CMD})
670      endif()
671  
672      if(CODE_COVERAGE_VERBOSE)
673          message(STATUS "Code coverage commands for target ${Coverage_NAME} (fastcov):")
674  
675          message("   Running tests:")
676          string(REPLACE ";" " " FASTCOV_EXEC_TESTS_CMD_SPACED "${FASTCOV_EXEC_TESTS_CMD}")
677          message("     ${FASTCOV_EXEC_TESTS_CMD_SPACED}")
678  
679          message("   Capturing fastcov counters and generating report:")
680          string(REPLACE ";" " " FASTCOV_CAPTURE_CMD_SPACED "${FASTCOV_CAPTURE_CMD}")
681          message("     ${FASTCOV_CAPTURE_CMD_SPACED}")
682  
683          message("   Converting fastcov .json to lcov .info:")
684          string(REPLACE ";" " " FASTCOV_CONVERT_CMD_SPACED "${FASTCOV_CONVERT_CMD}")
685          message("     ${FASTCOV_CONVERT_CMD_SPACED}")
686  
687          if(NOT Coverage_SKIP_HTML)
688              message("   Generating HTML report: ")
689              string(REPLACE ";" " " FASTCOV_HTML_CMD_SPACED "${FASTCOV_HTML_CMD}")
690              message("     ${FASTCOV_HTML_CMD_SPACED}")
691          endif()
692          if(Coverage_POST_CMD)
693              message("   Running post command: ")
694              string(REPLACE ";" " " FASTCOV_POST_CMD_SPACED "${FASTCOV_POST_CMD}")
695              message("     ${FASTCOV_POST_CMD_SPACED}")
696          endif()
697      endif()
698  
699      # Setup target
700      add_custom_target(${Coverage_NAME}
701  
702          # Cleanup fastcov
703          COMMAND ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH}
704              --search-directory ${BASEDIR}
705              --zerocounters
706  
707          COMMAND ${FASTCOV_EXEC_TESTS_CMD}
708          COMMAND ${FASTCOV_CAPTURE_CMD}
709          COMMAND ${FASTCOV_CONVERT_CMD}
710          COMMAND ${FASTCOV_HTML_CMD}
711          COMMAND ${FASTCOV_POST_CMD}
712  
713          # Set output files as GENERATED (will be removed on 'make clean')
714          BYPRODUCTS
715               ${Coverage_NAME}.info
716               ${Coverage_NAME}.json
717               ${Coverage_NAME}/index.html  # report directory
718  
719          WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
720          DEPENDS ${Coverage_DEPENDENCIES}
721          VERBATIM # Protect arguments to commands
722          COMMENT "Resetting code coverage counters to zero. Processing code coverage counters and generating report."
723      )
724  
725      set(INFO_MSG "fastcov code coverage info report saved in ${Coverage_NAME}.info and ${Coverage_NAME}.json.")
726      if(NOT Coverage_SKIP_HTML)
727          string(APPEND INFO_MSG " Open ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html in your browser to view the coverage report.")
728      endif()
729      # Show where to find the fastcov info report
730      add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
731          COMMAND ${CMAKE_COMMAND} -E echo ${INFO_MSG}
732      )
733  
734  endfunction() # setup_target_for_coverage_fastcov
735  
736  function(append_coverage_compiler_flags)
737      set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
738      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
739      set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
740      message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
741  endfunction() # append_coverage_compiler_flags
742  
743  # Setup coverage for specific library
744  function(append_coverage_compiler_flags_to_target name)
745      separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}")
746      target_compile_options(${name} PRIVATE ${_flag_list})
747      if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
748          target_link_libraries(${name} PRIVATE gcov)
749      endif()
750  endfunction()