|  | # Distributed under the OSI-approved BSD 3-Clause License.  See accompanying | 
|  | # file LICENSE.rst or https://cmake.org/licensing for details. | 
|  |  | 
|  | #[=======================================================================[.rst: | 
|  | CTestCoverageCollectGCOV | 
|  | ------------------------ | 
|  |  | 
|  | .. versionadded:: 3.2 | 
|  |  | 
|  | This module is intended for use in CTest dashboard scripts and provides a | 
|  | command to generate a tarball containing code coverage reports. | 
|  |  | 
|  | Load this module in a CTest script with: | 
|  |  | 
|  | .. code-block:: cmake | 
|  |  | 
|  | include(CTestCoverageCollectGCOV) | 
|  |  | 
|  | Commands | 
|  | ^^^^^^^^ | 
|  |  | 
|  | This module provides the following command: | 
|  |  | 
|  | .. command:: ctest_coverage_collect_gcov | 
|  |  | 
|  | Runs ``gcov`` and packages a tar file for CDash: | 
|  |  | 
|  | .. code-block:: cmake | 
|  |  | 
|  | ctest_coverage_collect_gcov( | 
|  | TARBALL <tar-file> | 
|  | [TARBALL_COMPRESSION <compression>] | 
|  | [SOURCE <source-dir>] | 
|  | [BUILD <build-dir>] | 
|  | [GCOV_COMMAND <gcov-command>] | 
|  | [GCOV_OPTIONS <options>...] | 
|  | [GLOB] | 
|  | [DELETE] | 
|  | [QUIET] | 
|  | ) | 
|  |  | 
|  | This command runs ``gcov`` on all ``.gcda`` files found in the binary tree | 
|  | and packages the resulting ``.gcov`` files into a tar file, along with the | 
|  | following: | 
|  |  | 
|  | * *data.json* file that defines the source and build directories for use | 
|  | by CDash. | 
|  | * *Labels.json* files that indicate any :prop_sf:`LABELS` that have been | 
|  | set on the source files. | 
|  | * The *uncovered* directory containing any uncovered files found by | 
|  | :variable:`CTEST_EXTRA_COVERAGE_GLOB`. | 
|  |  | 
|  | The resulting tar file can be submitted to CDash for display using the | 
|  | :command:`ctest_submit(CDASH_UPLOAD)` command. | 
|  |  | 
|  | The arguments are: | 
|  |  | 
|  | ``TARBALL <tar-file>`` | 
|  | 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 <compression>`` | 
|  | .. 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.  ``<compression>`` 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 ``<tar-file>`` (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 ``CMakeFiles/TargetDirectories.txt`` | 
|  | (file generated by CMake at the generation phase). | 
|  |  | 
|  | ``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 command. | 
|  |  | 
|  | .. versionadded:: 3.3 | 
|  | Added support for the :variable:`CTEST_CUSTOM_COVERAGE_EXCLUDE` variable. | 
|  |  | 
|  | Examples | 
|  | ^^^^^^^^ | 
|  |  | 
|  | Generating code coverage data packaged as a ``.tar.gz`` file in a | 
|  | :option:`ctest -S` script: | 
|  |  | 
|  | .. code-block:: cmake | 
|  | :caption: ``script.cmake`` | 
|  |  | 
|  | include(CTestCoverageCollectGCOV) | 
|  |  | 
|  | ctest_coverage_collect_gcov( | 
|  | TARBALL "${CTEST_BINARY_DIRECTORY}/gcov.tar.gz" | 
|  | TARBALL_COMPRESSION "GZIP" | 
|  | ) | 
|  | #]=======================================================================] | 
|  |  | 
|  | 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() |