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