CTestCoverageCollectGCOV.cmake 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. # Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. # file LICENSE.rst or https://cmake.org/licensing for details.
  3. #[=======================================================================[.rst:
  4. CTestCoverageCollectGCOV
  5. ------------------------
  6. .. versionadded:: 3.2
  7. This module is intended for use in CTest dashboard scripts and provides a
  8. command to generate a tarball containing code coverage reports.
  9. Load this module in a CTest script with:
  10. .. code-block:: cmake
  11. include(CTestCoverageCollectGCOV)
  12. Commands
  13. ^^^^^^^^
  14. This module provides the following command:
  15. .. command:: ctest_coverage_collect_gcov
  16. Runs ``gcov`` and packages a tar file for CDash:
  17. .. code-block:: cmake
  18. ctest_coverage_collect_gcov(
  19. TARBALL <tar-file>
  20. [TARBALL_COMPRESSION <compression>]
  21. [SOURCE <source-dir>]
  22. [BUILD <build-dir>]
  23. [GCOV_COMMAND <gcov-command>]
  24. [GCOV_OPTIONS <options>...]
  25. [GLOB]
  26. [DELETE]
  27. [QUIET]
  28. )
  29. This command runs ``gcov`` on all ``.gcda`` files found in the binary tree
  30. and packages the resulting ``.gcov`` files into a tar file, along with the
  31. following:
  32. * *data.json* file that defines the source and build directories for use
  33. by CDash.
  34. * *Labels.json* files that indicate any :prop_sf:`LABELS` that have been
  35. set on the source files.
  36. * The *uncovered* directory containing any uncovered files found by
  37. :variable:`CTEST_EXTRA_COVERAGE_GLOB`.
  38. The resulting tar file can be submitted to CDash for display using the
  39. :command:`ctest_submit(CDASH_UPLOAD)` command.
  40. The arguments are:
  41. ``TARBALL <tar-file>``
  42. Specify the location of the ``.tar`` file to be created for later
  43. upload to CDash. Relative paths will be interpreted with respect
  44. to the top-level build directory.
  45. ``TARBALL_COMPRESSION <compression>``
  46. .. versionadded:: 3.18
  47. Specify a compression algorithm for the
  48. ``TARBALL`` data file. Using this option reduces the size of the data file
  49. before it is submitted to CDash. ``<compression>`` must be one of ``GZIP``,
  50. ``BZIP2``, ``XZ``, ``ZSTD``, ``FROM_EXT``, or an expression that CMake
  51. evaluates as ``FALSE``. The default value is ``BZIP2``.
  52. If ``FROM_EXT`` is specified, the resulting file will be compressed based on
  53. the file extension of the ``<tar-file>`` (i.e. ``.tar.gz`` will use ``GZIP``
  54. compression). File extensions that will produce compressed output include
  55. ``.tar.gz``, ``.tgz``, ``.tar.bzip2``, ``.tbz``, ``.tar.xz``, and ``.txz``.
  56. ``SOURCE <source-dir>``
  57. Specify the top-level source directory for the build.
  58. Default is the value of :variable:`CTEST_SOURCE_DIRECTORY`.
  59. ``BUILD <build-dir>``
  60. Specify the top-level build directory for the build.
  61. Default is the value of :variable:`CTEST_BINARY_DIRECTORY`.
  62. ``GCOV_COMMAND <gcov-command>``
  63. Specify the full path to the ``gcov`` command on the machine.
  64. Default is the value of :variable:`CTEST_COVERAGE_COMMAND`.
  65. ``GCOV_OPTIONS <options>...``
  66. Specify options to be passed to gcov. The ``gcov`` command
  67. is run as ``gcov <options>... -o <gcov-dir> <file>.gcda``.
  68. If not specified, the default option is just ``-b -x``.
  69. ``GLOB``
  70. .. versionadded:: 3.6
  71. Recursively search for ``.gcda`` files in ``<build-dir>`` rather than
  72. determining search locations by reading ``CMakeFiles/TargetDirectories.txt``
  73. (file generated by CMake at the generation phase).
  74. ``DELETE``
  75. .. versionadded:: 3.6
  76. Delete coverage files after they've been packaged into the ``.tar``.
  77. ``QUIET``
  78. Suppress non-error messages that otherwise would have been
  79. printed out by this command.
  80. .. versionadded:: 3.3
  81. Added support for the :variable:`CTEST_CUSTOM_COVERAGE_EXCLUDE` variable.
  82. Examples
  83. ^^^^^^^^
  84. Generating code coverage data packaged as a ``.tar.gz`` file in a
  85. :option:`ctest -S` script:
  86. .. code-block:: cmake
  87. :caption: ``script.cmake``
  88. include(CTestCoverageCollectGCOV)
  89. ctest_coverage_collect_gcov(
  90. TARBALL "${CTEST_BINARY_DIRECTORY}/gcov.tar.gz"
  91. TARBALL_COMPRESSION "GZIP"
  92. )
  93. #]=======================================================================]
  94. function(ctest_coverage_collect_gcov)
  95. set(options QUIET GLOB DELETE)
  96. set(oneValueArgs TARBALL SOURCE BUILD GCOV_COMMAND TARBALL_COMPRESSION)
  97. set(multiValueArgs GCOV_OPTIONS)
  98. cmake_parse_arguments(GCOV "${options}" "${oneValueArgs}"
  99. "${multiValueArgs}" "" ${ARGN} )
  100. if(NOT DEFINED GCOV_TARBALL)
  101. message(FATAL_ERROR
  102. "TARBALL must be specified. for ctest_coverage_collect_gcov")
  103. endif()
  104. if(NOT DEFINED GCOV_SOURCE)
  105. set(source_dir "${CTEST_SOURCE_DIRECTORY}")
  106. else()
  107. set(source_dir "${GCOV_SOURCE}")
  108. endif()
  109. if(NOT DEFINED GCOV_BUILD)
  110. set(binary_dir "${CTEST_BINARY_DIRECTORY}")
  111. else()
  112. set(binary_dir "${GCOV_BUILD}")
  113. endif()
  114. if(NOT DEFINED GCOV_GCOV_COMMAND)
  115. set(gcov_command "${CTEST_COVERAGE_COMMAND}")
  116. else()
  117. set(gcov_command "${GCOV_GCOV_COMMAND}")
  118. endif()
  119. if(NOT DEFINED GCOV_TARBALL_COMPRESSION)
  120. set(GCOV_TARBALL_COMPRESSION "BZIP2")
  121. elseif( GCOV_TARBALL_COMPRESSION AND
  122. NOT GCOV_TARBALL_COMPRESSION MATCHES "^(GZIP|BZIP2|XZ|ZSTD|FROM_EXT)$")
  123. message(FATAL_ERROR "TARBALL_COMPRESSION must be one of OFF, GZIP, "
  124. "BZIP2, XZ, ZSTD, or FROM_EXT for ctest_coverage_collect_gcov")
  125. endif()
  126. # run gcov on each gcda file in the binary tree
  127. set(gcda_files)
  128. set(label_files)
  129. if (GCOV_GLOB)
  130. file(GLOB_RECURSE gfiles "${binary_dir}/*.gcda")
  131. list(LENGTH gfiles len)
  132. # if we have gcda files then also grab the labels file for that target
  133. if(${len} GREATER 0)
  134. file(GLOB_RECURSE lfiles RELATIVE ${binary_dir} "${binary_dir}/Labels.json")
  135. list(APPEND gcda_files ${gfiles})
  136. list(APPEND label_files ${lfiles})
  137. endif()
  138. else()
  139. # look for gcda files in the target directories
  140. # this will be faster and only look where the files will be
  141. file(STRINGS "${binary_dir}/CMakeFiles/TargetDirectories.txt" target_dirs
  142. ENCODING UTF-8)
  143. foreach(target_dir ${target_dirs})
  144. file(GLOB_RECURSE gfiles "${target_dir}/*.gcda")
  145. list(LENGTH gfiles len)
  146. # if we have gcda files then also grab the labels file for that target
  147. if(${len} GREATER 0)
  148. file(GLOB_RECURSE lfiles RELATIVE ${binary_dir}
  149. "${target_dir}/Labels.json")
  150. list(APPEND gcda_files ${gfiles})
  151. list(APPEND label_files ${lfiles})
  152. endif()
  153. endforeach()
  154. endif()
  155. # return early if no coverage files were found
  156. list(LENGTH gcda_files len)
  157. if(len EQUAL 0)
  158. if (NOT GCOV_QUIET)
  159. message("ctest_coverage_collect_gcov: No .gcda files found, "
  160. "ignoring coverage request.")
  161. endif()
  162. return()
  163. endif()
  164. # setup the dir for the coverage files
  165. set(coverage_dir "${binary_dir}/Testing/CoverageInfo")
  166. file(MAKE_DIRECTORY "${coverage_dir}")
  167. # run gcov, this will produce the .gcov files in the current
  168. # working directory
  169. if(NOT DEFINED GCOV_GCOV_OPTIONS)
  170. set(GCOV_GCOV_OPTIONS -b -x)
  171. endif()
  172. if (GCOV_QUIET)
  173. set(coverage_out_opts
  174. OUTPUT_QUIET
  175. ERROR_QUIET
  176. )
  177. else()
  178. set(coverage_out_opts
  179. OUTPUT_FILE "${coverage_dir}/gcov.log"
  180. ERROR_FILE "${coverage_dir}/gcov.log"
  181. )
  182. endif()
  183. execute_process(COMMAND
  184. ${gcov_command} ${GCOV_GCOV_OPTIONS} ${gcda_files}
  185. RESULT_VARIABLE res
  186. WORKING_DIRECTORY ${coverage_dir}
  187. ${coverage_out_opts}
  188. )
  189. if (GCOV_DELETE)
  190. file(REMOVE ${gcda_files})
  191. endif()
  192. if(NOT "${res}" EQUAL 0)
  193. if (NOT GCOV_QUIET)
  194. message(STATUS "Error running gcov: ${res}, see\n ${coverage_dir}/gcov.log")
  195. endif()
  196. endif()
  197. # create json file with project information
  198. file(WRITE ${coverage_dir}/data.json
  199. "{
  200. \"Source\": \"${source_dir}\",
  201. \"Binary\": \"${binary_dir}\"
  202. }")
  203. # collect the gcov files
  204. set(unfiltered_gcov_files)
  205. file(GLOB_RECURSE unfiltered_gcov_files RELATIVE ${binary_dir} "${coverage_dir}/*.gcov")
  206. # if CTEST_EXTRA_COVERAGE_GLOB was specified we search for files
  207. # that might be uncovered
  208. if (DEFINED CTEST_EXTRA_COVERAGE_GLOB)
  209. set(uncovered_files)
  210. foreach(search_entry IN LISTS CTEST_EXTRA_COVERAGE_GLOB)
  211. if(NOT GCOV_QUIET)
  212. message("Add coverage glob: ${search_entry}")
  213. endif()
  214. file(GLOB_RECURSE matching_files "${source_dir}/${search_entry}")
  215. if (matching_files)
  216. list(APPEND uncovered_files "${matching_files}")
  217. endif()
  218. endforeach()
  219. endif()
  220. set(gcov_files)
  221. foreach(gcov_file ${unfiltered_gcov_files})
  222. file(STRINGS ${binary_dir}/${gcov_file} first_line LIMIT_COUNT 1 ENCODING UTF-8)
  223. set(is_excluded false)
  224. if(first_line MATCHES "^ -: 0:Source:(.*)$")
  225. set(source_file ${CMAKE_MATCH_1})
  226. elseif(NOT GCOV_QUIET)
  227. message(STATUS "Could not determine source file corresponding to: ${gcov_file}")
  228. endif()
  229. foreach(exclude_entry IN LISTS CTEST_CUSTOM_COVERAGE_EXCLUDE)
  230. if(source_file MATCHES "${exclude_entry}")
  231. set(is_excluded true)
  232. if(NOT GCOV_QUIET)
  233. message("Excluding coverage for: ${source_file} which matches ${exclude_entry}")
  234. endif()
  235. break()
  236. endif()
  237. endforeach()
  238. get_filename_component(resolved_source_file "${source_file}" ABSOLUTE)
  239. foreach(uncovered_file IN LISTS uncovered_files)
  240. get_filename_component(resolved_uncovered_file "${uncovered_file}" ABSOLUTE)
  241. if (resolved_uncovered_file STREQUAL resolved_source_file)
  242. list(REMOVE_ITEM uncovered_files "${uncovered_file}")
  243. endif()
  244. endforeach()
  245. if(NOT is_excluded)
  246. list(APPEND gcov_files ${gcov_file})
  247. endif()
  248. endforeach()
  249. foreach (uncovered_file ${uncovered_files})
  250. # Check if this uncovered file should be excluded.
  251. set(is_excluded false)
  252. foreach(exclude_entry IN LISTS CTEST_CUSTOM_COVERAGE_EXCLUDE)
  253. if(uncovered_file MATCHES "${exclude_entry}")
  254. set(is_excluded true)
  255. if(NOT GCOV_QUIET)
  256. message("Excluding coverage for: ${uncovered_file} which matches ${exclude_entry}")
  257. endif()
  258. break()
  259. endif()
  260. endforeach()
  261. if(is_excluded)
  262. continue()
  263. endif()
  264. # Copy from source to binary dir, preserving any intermediate subdirectories.
  265. get_filename_component(filename "${uncovered_file}" NAME)
  266. get_filename_component(relative_path "${uncovered_file}" DIRECTORY)
  267. string(REPLACE "${source_dir}" "" relative_path "${relative_path}")
  268. if (relative_path)
  269. # Strip leading slash.
  270. string(SUBSTRING "${relative_path}" 1 -1 relative_path)
  271. endif()
  272. file(COPY ${uncovered_file} DESTINATION ${binary_dir}/uncovered/${relative_path})
  273. if(relative_path)
  274. list(APPEND uncovered_files_for_tar uncovered/${relative_path}/${filename})
  275. else()
  276. list(APPEND uncovered_files_for_tar uncovered/${filename})
  277. endif()
  278. endforeach()
  279. # tar up the coverage info with the same date so that the md5
  280. # sum will be the same for the tar file independent of file time
  281. # stamps
  282. string(REPLACE ";" "\n" gcov_files "${gcov_files}")
  283. string(REPLACE ";" "\n" label_files "${label_files}")
  284. string(REPLACE ";" "\n" uncovered_files_for_tar "${uncovered_files_for_tar}")
  285. file(WRITE "${coverage_dir}/coverage_file_list.txt"
  286. "${gcov_files}
  287. ${coverage_dir}/data.json
  288. ${label_files}
  289. ${uncovered_files_for_tar}
  290. ")
  291. # Prepare tar command line arguments
  292. set(tar_opts "")
  293. # Select data compression mode
  294. if( GCOV_TARBALL_COMPRESSION STREQUAL "FROM_EXT")
  295. if( GCOV_TARBALL MATCHES [[\.(tgz|tar.gz)$]] )
  296. string(APPEND tar_opts "z")
  297. elseif( GCOV_TARBALL MATCHES [[\.(txz|tar.xz)$]] )
  298. string(APPEND tar_opts "J")
  299. elseif( GCOV_TARBALL MATCHES [[\.(tbz|tar.bz)$]] )
  300. string(APPEND tar_opts "j")
  301. endif()
  302. elseif(GCOV_TARBALL_COMPRESSION STREQUAL "GZIP")
  303. string(APPEND tar_opts "z")
  304. elseif(GCOV_TARBALL_COMPRESSION STREQUAL "XZ")
  305. string(APPEND tar_opts "J")
  306. elseif(GCOV_TARBALL_COMPRESSION STREQUAL "BZIP2")
  307. string(APPEND tar_opts "j")
  308. elseif(GCOV_TARBALL_COMPRESSION STREQUAL "ZSTD")
  309. set(zstd_tar_opt "--zstd")
  310. endif()
  311. # Verbosity options
  312. if(NOT GCOV_QUIET AND NOT tar_opts MATCHES v)
  313. string(APPEND tar_opts "v")
  314. endif()
  315. # Prepend option 'c' specifying 'create'
  316. string(PREPEND tar_opts "c")
  317. # Append option 'f' so that the next argument is the filename
  318. string(APPEND tar_opts "f")
  319. execute_process(COMMAND
  320. ${CMAKE_COMMAND} -E tar ${tar_opts} ${GCOV_TARBALL} ${zstd_tar_opt}
  321. "--mtime=1970-01-01 0:0:0 UTC"
  322. "--format=gnutar"
  323. --files-from=${coverage_dir}/coverage_file_list.txt
  324. WORKING_DIRECTORY ${binary_dir})
  325. if (GCOV_DELETE)
  326. foreach(gcov_file ${unfiltered_gcov_files})
  327. file(REMOVE ${binary_dir}/${gcov_file})
  328. endforeach()
  329. file(REMOVE ${coverage_dir}/coverage_file_list.txt)
  330. file(REMOVE ${coverage_dir}/data.json)
  331. if (EXISTS ${binary_dir}/uncovered)
  332. file(REMOVE ${binary_dir}/uncovered)
  333. endif()
  334. endif()
  335. endfunction()