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