summaryrefslogtreecommitdiff
path: root/Modules/CTestCoverageCollectGCOV.cmake
blob: a6fa3a4c6cdc2599e81154407665dd3545686a2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.

#[=======================================================================[.rst:
CTestCoverageCollectGCOV
------------------------

.. versionadded:: 3.2

This module provides the ``ctest_coverage_collect_gcov`` function.

This function runs gcov on all .gcda files found in the binary tree
and packages the resulting .gcov files into a tar file.
This tarball also contains the following:

* *data.json* defines the source and build directories for use by CDash.
* *Labels.json* indicates any :prop_sf:`LABELS` that have been set on the
  source files.
* The *uncovered* directory holds any uncovered files found by
  :variable:`CTEST_EXTRA_COVERAGE_GLOB`.

After generating this tar file, it can be sent to CDash for display with the
:command:`ctest_submit(CDASH_UPLOAD)` command.

.. command:: ctest_coverage_collect_gcov

  ::

    ctest_coverage_collect_gcov(TARBALL <tarfile>
      [SOURCE <source_dir>][BUILD <build_dir>]
      [GCOV_COMMAND <gcov_command>]
      [GCOV_OPTIONS <options>...]
      )

  Run gcov and package a tar file for CDash.  The options are:

  ``TARBALL <tarfile>``
    Specify the location of the ``.tar`` file to be created for later
    upload to CDash.  Relative paths will be interpreted with respect
    to the top-level build directory.

  ``TARBALL_COMPRESSION <option>``
    .. versionadded:: 3.18

    Specify a compression algorithm for the
    ``TARBALL`` data file.  Using this option reduces the size of the data file
    before it is submitted to CDash.  ``<option>`` must be one of ``GZIP``,
    ``BZIP2``, ``XZ``, ``ZSTD``, ``FROM_EXT``, or an expression that CMake
    evaluates as ``FALSE``. The default value is ``BZIP2``.

    If ``FROM_EXT`` is specified, the resulting file will be compressed based on
    the file extension of the ``<tarfile>`` (i.e. ``.tar.gz`` will use ``GZIP``
    compression). File extensions that will produce compressed output include
    ``.tar.gz``, ``.tgz``, ``.tar.bzip2``, ``.tbz``, ``.tar.xz``, and ``.txz``.

  ``SOURCE <source_dir>``
    Specify the top-level source directory for the build.
    Default is the value of :variable:`CTEST_SOURCE_DIRECTORY`.

  ``BUILD <build_dir>``
    Specify the top-level build directory for the build.
    Default is the value of :variable:`CTEST_BINARY_DIRECTORY`.

  ``GCOV_COMMAND <gcov_command>``
    Specify the full path to the ``gcov`` command on the machine.
    Default is the value of :variable:`CTEST_COVERAGE_COMMAND`.

  ``GCOV_OPTIONS <options>...``
    Specify options to be passed to gcov.  The ``gcov`` command
    is run as ``gcov <options>... -o <gcov-dir> <file>.gcda``.
    If not specified, the default option is just ``-b -x``.

  ``GLOB``
    .. versionadded:: 3.6

    Recursively search for .gcda files in build_dir rather than
    determining search locations by reading TargetDirectories.txt.

  ``DELETE``
    .. versionadded:: 3.6

    Delete coverage files after they've been packaged into the .tar.

  ``QUIET``
    Suppress non-error messages that otherwise would have been
    printed out by this function.

  .. versionadded:: 3.3
    Added support for the :variable:`CTEST_CUSTOM_COVERAGE_EXCLUDE` variable.

#]=======================================================================]

function(ctest_coverage_collect_gcov)
  set(options QUIET GLOB DELETE)
  set(oneValueArgs TARBALL SOURCE BUILD GCOV_COMMAND TARBALL_COMPRESSION)
  set(multiValueArgs GCOV_OPTIONS)
  cmake_parse_arguments(GCOV  "${options}" "${oneValueArgs}"
    "${multiValueArgs}" "" ${ARGN} )
  if(NOT DEFINED GCOV_TARBALL)
    message(FATAL_ERROR
      "TARBALL must be specified. for ctest_coverage_collect_gcov")
  endif()
  if(NOT DEFINED GCOV_SOURCE)
    set(source_dir "${CTEST_SOURCE_DIRECTORY}")
  else()
    set(source_dir "${GCOV_SOURCE}")
  endif()
  if(NOT DEFINED GCOV_BUILD)
    set(binary_dir "${CTEST_BINARY_DIRECTORY}")
  else()
    set(binary_dir "${GCOV_BUILD}")
  endif()
  if(NOT DEFINED GCOV_GCOV_COMMAND)
    set(gcov_command "${CTEST_COVERAGE_COMMAND}")
  else()
    set(gcov_command "${GCOV_GCOV_COMMAND}")
  endif()
  if(NOT DEFINED GCOV_TARBALL_COMPRESSION)
    set(GCOV_TARBALL_COMPRESSION "BZIP2")
  elseif( GCOV_TARBALL_COMPRESSION AND
      NOT GCOV_TARBALL_COMPRESSION MATCHES "^(GZIP|BZIP2|XZ|ZSTD|FROM_EXT)$")
    message(FATAL_ERROR "TARBALL_COMPRESSION must be one of OFF, GZIP, "
      "BZIP2, XZ, ZSTD, or FROM_EXT for ctest_coverage_collect_gcov")
  endif()
  # run gcov on each gcda file in the binary tree
  set(gcda_files)
  set(label_files)
  if (GCOV_GLOB)
      file(GLOB_RECURSE gfiles "${binary_dir}/*.gcda")
      list(LENGTH gfiles len)
      # if we have gcda files then also grab the labels file for that target
      if(${len} GREATER 0)
        file(GLOB_RECURSE lfiles RELATIVE ${binary_dir} "${binary_dir}/Labels.json")
        list(APPEND gcda_files ${gfiles})
        list(APPEND label_files ${lfiles})
      endif()
  else()
    # look for gcda files in the target directories
    # this will be faster and only look where the files will be
    file(STRINGS "${binary_dir}/CMakeFiles/TargetDirectories.txt" target_dirs
         ENCODING UTF-8)
    foreach(target_dir ${target_dirs})
      file(GLOB_RECURSE gfiles "${target_dir}/*.gcda")
      list(LENGTH gfiles len)
      # if we have gcda files then also grab the labels file for that target
      if(${len} GREATER 0)
        file(GLOB_RECURSE lfiles RELATIVE ${binary_dir}
          "${target_dir}/Labels.json")
        list(APPEND gcda_files ${gfiles})
        list(APPEND label_files ${lfiles})
      endif()
    endforeach()
  endif()
  # return early if no coverage files were found
  list(LENGTH gcda_files len)
  if(len EQUAL 0)
    if (NOT GCOV_QUIET)
      message("ctest_coverage_collect_gcov: No .gcda files found, "
        "ignoring coverage request.")
    endif()
    return()
  endif()
  # setup the dir for the coverage files
  set(coverage_dir "${binary_dir}/Testing/CoverageInfo")
  file(MAKE_DIRECTORY  "${coverage_dir}")
  # run gcov, this will produce the .gcov files in the current
  # working directory
  if(NOT DEFINED GCOV_GCOV_OPTIONS)
    set(GCOV_GCOV_OPTIONS -b -x)
  endif()
  if (GCOV_QUIET)
    set(coverage_out_opts
      OUTPUT_QUIET
      ERROR_QUIET
      )
  else()
    set(coverage_out_opts
      OUTPUT_FILE "${coverage_dir}/gcov.log"
      ERROR_FILE  "${coverage_dir}/gcov.log"
      )
  endif()
  execute_process(COMMAND
    ${gcov_command} ${GCOV_GCOV_OPTIONS} ${gcda_files}
    RESULT_VARIABLE res
    WORKING_DIRECTORY ${coverage_dir}
    ${coverage_out_opts}
    )

  if (GCOV_DELETE)
    file(REMOVE ${gcda_files})
  endif()

  if(NOT "${res}" EQUAL 0)
    if (NOT GCOV_QUIET)
      message(STATUS "Error running gcov: ${res}, see\n  ${coverage_dir}/gcov.log")
    endif()
  endif()
  # create json file with project information
  file(WRITE ${coverage_dir}/data.json
    "{
    \"Source\": \"${source_dir}\",
    \"Binary\": \"${binary_dir}\"
}")
  # collect the gcov files
  set(unfiltered_gcov_files)
  file(GLOB_RECURSE unfiltered_gcov_files RELATIVE ${binary_dir} "${coverage_dir}/*.gcov")

  # if CTEST_EXTRA_COVERAGE_GLOB was specified we search for files
  # that might be uncovered
  if (DEFINED CTEST_EXTRA_COVERAGE_GLOB)
    set(uncovered_files)
    foreach(search_entry IN LISTS CTEST_EXTRA_COVERAGE_GLOB)
      if(NOT GCOV_QUIET)
        message("Add coverage glob: ${search_entry}")
      endif()
      file(GLOB_RECURSE matching_files "${source_dir}/${search_entry}")
      if (matching_files)
        list(APPEND uncovered_files "${matching_files}")
      endif()
    endforeach()
  endif()

  set(gcov_files)
  foreach(gcov_file ${unfiltered_gcov_files})
    file(STRINGS ${binary_dir}/${gcov_file} first_line LIMIT_COUNT 1 ENCODING UTF-8)

    set(is_excluded false)
    if(first_line MATCHES "^        -:    0:Source:(.*)$")
      set(source_file ${CMAKE_MATCH_1})
    elseif(NOT GCOV_QUIET)
      message(STATUS "Could not determine source file corresponding to: ${gcov_file}")
    endif()

    foreach(exclude_entry IN LISTS CTEST_CUSTOM_COVERAGE_EXCLUDE)
      if(source_file MATCHES "${exclude_entry}")
        set(is_excluded true)

        if(NOT GCOV_QUIET)
          message("Excluding coverage for: ${source_file} which matches ${exclude_entry}")
        endif()

        break()
      endif()
    endforeach()

    get_filename_component(resolved_source_file "${source_file}" ABSOLUTE)
    foreach(uncovered_file IN LISTS uncovered_files)
      get_filename_component(resolved_uncovered_file "${uncovered_file}" ABSOLUTE)
      if (resolved_uncovered_file STREQUAL resolved_source_file)
        list(REMOVE_ITEM uncovered_files "${uncovered_file}")
      endif()
    endforeach()

    if(NOT is_excluded)
      list(APPEND gcov_files ${gcov_file})
    endif()
  endforeach()

  foreach (uncovered_file ${uncovered_files})
    # Check if this uncovered file should be excluded.
    set(is_excluded false)
    foreach(exclude_entry IN LISTS CTEST_CUSTOM_COVERAGE_EXCLUDE)
      if(uncovered_file MATCHES "${exclude_entry}")
        set(is_excluded true)
        if(NOT GCOV_QUIET)
          message("Excluding coverage for: ${uncovered_file} which matches ${exclude_entry}")
        endif()
        break()
      endif()
    endforeach()
    if(is_excluded)
      continue()
    endif()

    # Copy from source to binary dir, preserving any intermediate subdirectories.
    get_filename_component(filename "${uncovered_file}" NAME)
    get_filename_component(relative_path "${uncovered_file}" DIRECTORY)
    string(REPLACE "${source_dir}" "" relative_path "${relative_path}")
    if (relative_path)
      # Strip leading slash.
      string(SUBSTRING "${relative_path}" 1 -1 relative_path)
    endif()
    file(COPY ${uncovered_file} DESTINATION ${binary_dir}/uncovered/${relative_path})
    if(relative_path)
      list(APPEND uncovered_files_for_tar uncovered/${relative_path}/${filename})
    else()
      list(APPEND uncovered_files_for_tar uncovered/${filename})
    endif()
  endforeach()

  # tar up the coverage info with the same date so that the md5
  # sum will be the same for the tar file independent of file time
  # stamps
  string(REPLACE ";" "\n" gcov_files "${gcov_files}")
  string(REPLACE ";" "\n" label_files "${label_files}")
  string(REPLACE ";" "\n" uncovered_files_for_tar "${uncovered_files_for_tar}")
  file(WRITE "${coverage_dir}/coverage_file_list.txt"
    "${gcov_files}
${coverage_dir}/data.json
${label_files}
${uncovered_files_for_tar}
")

  # Prepare tar command line arguments

  set(tar_opts "")
  # Select data compression mode
  if( GCOV_TARBALL_COMPRESSION STREQUAL "FROM_EXT")
    if( GCOV_TARBALL MATCHES [[\.(tgz|tar.gz)$]] )
      string(APPEND tar_opts "z")
    elseif( GCOV_TARBALL MATCHES [[\.(txz|tar.xz)$]] )
      string(APPEND tar_opts "J")
    elseif( GCOV_TARBALL MATCHES [[\.(tbz|tar.bz)$]] )
      string(APPEND tar_opts "j")
    endif()
  elseif(GCOV_TARBALL_COMPRESSION STREQUAL "GZIP")
    string(APPEND tar_opts "z")
  elseif(GCOV_TARBALL_COMPRESSION STREQUAL "XZ")
    string(APPEND tar_opts "J")
  elseif(GCOV_TARBALL_COMPRESSION STREQUAL "BZIP2")
    string(APPEND tar_opts "j")
  elseif(GCOV_TARBALL_COMPRESSION STREQUAL "ZSTD")
    set(zstd_tar_opt "--zstd")
  endif()
  # Verbosity options
  if(NOT GCOV_QUIET AND NOT tar_opts MATCHES v)
    string(APPEND tar_opts "v")
  endif()
  # Prepend option 'c' specifying 'create'
  string(PREPEND tar_opts "c")
  # Append option 'f' so that the next argument is the filename
  string(APPEND tar_opts "f")

  execute_process(COMMAND
    ${CMAKE_COMMAND} -E tar ${tar_opts} ${GCOV_TARBALL} ${zstd_tar_opt}
    "--mtime=1970-01-01 0:0:0 UTC"
    "--format=gnutar"
    --files-from=${coverage_dir}/coverage_file_list.txt
    WORKING_DIRECTORY ${binary_dir})

  if (GCOV_DELETE)
    foreach(gcov_file ${unfiltered_gcov_files})
      file(REMOVE ${binary_dir}/${gcov_file})
    endforeach()
    file(REMOVE ${coverage_dir}/coverage_file_list.txt)
    file(REMOVE ${coverage_dir}/data.json)
    if (EXISTS ${binary_dir}/uncovered)
      file(REMOVE ${binary_dir}/uncovered)
    endif()
  endif()

endfunction()