# This file is part of CMake-codecov.
#
# Copyright (c)
#   2015-2020 RWTH Aachen University, Federal Republic of Germany
#
# See the LICENSE file in the package base directory for details
#
# Written by Alexander Haase, alexander.haase@rwth-aachen.de
#

# configuration
set(LCOV_DATA_PATH "${CMAKE_BINARY_DIR}/lcov/data")
set(LCOV_DATA_PATH_INIT "${LCOV_DATA_PATH}/init")
set(LCOV_DATA_PATH_CAPTURE "${LCOV_DATA_PATH}/capture")
set(LCOV_HTML_PATH "${CMAKE_BINARY_DIR}/lcov/html")

# Search for Gcov which is used by Lcov.
find_package(Gcov)

# This function will add lcov evaluation for target <TNAME>. Only sources of this target will be evaluated and no dependencies will be added. It will call geninfo on any source file of <TNAME> once
# and store the info file in the same directory.
#
# Note: This function is only a wrapper to define this function always, even if coverage is not supported by the compiler or disabled. This function must be defined here, because the module will be
# exited, if there is no coverage support by the compiler or it is disabled by the user.
function(add_lcov_target TNAME)
  if(LCOV_FOUND)
    # capture initial coverage data
    lcov_capture_initial_tgt(${TNAME})

    # capture coverage data after execution
    lcov_capture_tgt(${TNAME})
  endif()
endfunction(add_lcov_target)

# include required Modules
include(FindPackageHandleStandardArgs)

# Search for required lcov binaries.
find_program(LCOV_BIN lcov)
find_program(GENINFO_BIN geninfo)
find_program(GENHTML_BIN genhtml)
find_package_handle_standard_args(Lcov REQUIRED_VARS LCOV_BIN GENINFO_BIN GENHTML_BIN)

# enable genhtml C++ demangeling, if c++filt is found.
set(GENHTML_CPPFILT_FLAG "")
find_program(CPPFILT_BIN c++filt)
if(NOT CPPFILT_BIN STREQUAL "")
  set(GENHTML_CPPFILT_FLAG "--demangle-cpp")
endif(NOT CPPFILT_BIN STREQUAL "")

# enable no-external flag for lcov, if available.
if(GENINFO_BIN AND NOT DEFINED GENINFO_EXTERN_FLAG)
  set(FLAG "")
  execute_process(COMMAND ${GENINFO_BIN} --help OUTPUT_VARIABLE GENINFO_HELP)
  string(REGEX MATCH "external" GENINFO_RES "${GENINFO_HELP}")
  if(GENINFO_RES)
    set(FLAG "--no-external")
  endif()

  set(GENINFO_EXTERN_FLAG "${FLAG}" CACHE STRING "Geninfo flag to exclude system sources.")
endif()

# If Lcov was not found, exit module now.
if(NOT LCOV_FOUND)
  return()
endif(NOT LCOV_FOUND)

# Create directories to be used.
file(MAKE_DIRECTORY ${LCOV_DATA_PATH_INIT})
file(MAKE_DIRECTORY ${LCOV_DATA_PATH_CAPTURE})

set(LCOV_REMOVE_PATTERNS "")

# This function will merge lcov files to a single target file. Additional lcov flags may be set with setting LCOV_EXTRA_FLAGS before calling this function.
function(lcov_merge_files OUTFILE ...)
  # Remove ${OUTFILE} from ${ARGV} and generate lcov parameters with files.
  list(REMOVE_AT ARGV 0)

  # Generate merged file.
  string(REPLACE "${CMAKE_BINARY_DIR}/" "" FILE_REL "${OUTFILE}")
  add_custom_command(
    OUTPUT "${OUTFILE}.raw"
    COMMAND cat ${ARGV} > ${OUTFILE}.raw
    DEPENDS ${ARGV}
    COMMENT "Generating ${FILE_REL}"
    )

  add_custom_command(
    OUTPUT "${OUTFILE}"
    COMMAND ${LCOV_BIN} --quiet -a ${OUTFILE}.raw --output-file ${OUTFILE} --base-directory ${PROJECT_SOURCE_DIR} ${LCOV_EXTRA_FLAGS}
    COMMAND ${LCOV_BIN} --quiet -r ${OUTFILE} ${LCOV_REMOVE_PATTERNS} --output-file ${OUTFILE} ${LCOV_EXTRA_FLAGS}
    DEPENDS ${OUTFILE}.raw
    COMMENT "Post-processing ${FILE_REL}"
    )
endfunction()

# Add a new global target to generate initial coverage reports for all targets. This target will be used to generate the global initial info file, which is used to gather even empty report data.
if(NOT TARGET lcov-capture-init)
  add_custom_target(lcov-capture-init)
  set(LCOV_CAPTURE_INIT_FILES "" CACHE INTERNAL "")
endif(NOT TARGET lcov-capture-init)

# This function will add initial capture of coverage data for target <TNAME>, which is needed to get also data for objects, which were not loaded at execution time. It will call geninfo for every
# source file of <TNAME> once and store the info file in the same directory.
function(lcov_capture_initial_tgt TNAME)
  # We don't have to check, if the target has support for coverage, thus this will be checked by add_coverage_target in Findcoverage.cmake. Instead we have to determine which gcov binary to use.
  get_target_property(TSOURCES ${TNAME} SOURCES)
  set(SOURCES "")
  set(TCOMPILER "")
  foreach(FILE ${TSOURCES})
    codecov_path_of_source(${FILE} FILE)
    if(NOT "${FILE}" STREQUAL "")
      codecov_lang_of_source(${FILE} LANG)
      if(NOT "${LANG}" STREQUAL "")
        list(APPEND SOURCES "${FILE}")
        set(TCOMPILER ${CMAKE_${LANG}_COMPILER_ID})
      endif()
    endif()
  endforeach()

  # If no gcov binary was found, coverage data can't be evaluated.
  if(NOT GCOV_${TCOMPILER}_BIN)
    message(WARNING "No coverage evaluation binary found for ${TCOMPILER}.")
    return()
  endif()

  set(GCOV_BIN "${GCOV_${TCOMPILER}_BIN}")
  set(GCOV_ENV "${GCOV_${TCOMPILER}_ENV}")

  get_target_property(TBIN_DIR ${TNAME} BINARY_DIR)
  set(TDIR ${TBIN_DIR}/CMakeFiles/${TNAME}.dir)

  set(GENINFO_FILES "")
  foreach(FILE ${SOURCES})
    # generate empty coverage files
    set(OUTFILE "${TDIR}/${FILE}.info.init")
    list(APPEND GENINFO_FILES ${OUTFILE})

    add_custom_command(
      OUTPUT ${OUTFILE}
      COMMAND ${GCOV_ENV} ${GENINFO_BIN} --quiet --base-directory ${PROJECT_SOURCE_DIR} --initial --gcov-tool ${GCOV_BIN} --output-filename ${OUTFILE} ${GENINFO_EXTERN_FLAG} ${TDIR}/${FILE}.gcno
      DEPENDS ${TNAME}
      COMMENT "Capturing initial coverage data for ${FILE}"
      )
  endforeach()

  # Concatenate all files generated by geninfo to a single file per target.
  set(OUTFILE "${LCOV_DATA_PATH_INIT}/${TNAME}.info")
  set(LCOV_EXTRA_FLAGS "--initial")
  lcov_merge_files("${OUTFILE}" ${GENINFO_FILES})
  add_custom_target(${TNAME}-capture-init ALL DEPENDS ${OUTFILE})

  # add geninfo file generation to global lcov-geninfo target
  add_dependencies(lcov-capture-init ${TNAME}-capture-init)
  set(LCOV_CAPTURE_INIT_FILES "${LCOV_CAPTURE_INIT_FILES}" "${OUTFILE}" CACHE INTERNAL "")
endfunction(lcov_capture_initial_tgt)

# This function will generate the global info file for all targets. It has to be called after all other CMake functions in the root CMakeLists.txt file, to get a full list of all targets that generate
# coverage data.
function(lcov_capture_initial)
  # Skip this function (and do not create the following targets), if there are no input files.
  if("${LCOV_CAPTURE_INIT_FILES}" STREQUAL "")
    return()
  endif()

  # Add a new target to merge the files of all targets.
  set(OUTFILE "${LCOV_DATA_PATH_INIT}/all_targets.info")
  lcov_merge_files("${OUTFILE}" ${LCOV_CAPTURE_INIT_FILES})
  add_custom_target(lcov-geninfo-init ALL DEPENDS ${OUTFILE} lcov-capture-init)
endfunction(lcov_capture_initial)

# Add a new global target to generate coverage reports for all targets. This target will be used to generate the global info file.
if(NOT TARGET lcov-capture)
  add_custom_target(lcov-capture)
  set(LCOV_CAPTURE_FILES "" CACHE INTERNAL "")
endif(NOT TARGET lcov-capture)

# This function will add capture of coverage data for target <TNAME>, which is needed to get also data for objects, which were not loaded at execution time. It will call geninfo for every source file
# of <TNAME> once and store the info file in the same directory.
function(lcov_capture_tgt TNAME)
  # We don't have to check, if the target has support for coverage, thus this will be checked by add_coverage_target in Findcoverage.cmake. Instead we have to determine which gcov binary to use.
  get_target_property(TSOURCES ${TNAME} SOURCES)
  set(SOURCES "")
  set(TCOMPILER "")
  foreach(FILE ${TSOURCES})
    codecov_path_of_source(${FILE} FILE)
    if(NOT "${FILE}" STREQUAL "")
      codecov_lang_of_source(${FILE} LANG)
      if(NOT "${LANG}" STREQUAL "")
        list(APPEND SOURCES "${FILE}")
        set(TCOMPILER ${CMAKE_${LANG}_COMPILER_ID})
      endif()
    endif()
  endforeach()

  # If no gcov binary was found, coverage data can't be evaluated.
  if(NOT GCOV_${TCOMPILER}_BIN)
    message(WARNING "No coverage evaluation binary found for ${TCOMPILER}.")
    return()
  endif()

  set(GCOV_BIN "${GCOV_${TCOMPILER}_BIN}")
  set(GCOV_ENV "${GCOV_${TCOMPILER}_ENV}")

  get_target_property(TBIN_DIR ${TNAME} BINARY_DIR)
  set(TDIR ${TBIN_DIR}/CMakeFiles/${TNAME}.dir)

  set(GENINFO_FILES "")
  foreach(FILE ${SOURCES})
    # Generate coverage files. If no .gcda file was generated during execution, the empty coverage file will be used instead.
    set(OUTFILE "${TDIR}/${FILE}.info")
    list(APPEND GENINFO_FILES ${OUTFILE})

    # Create an empty .gcda file, so the target capture file can have a dependency on it. The capture file will only use this .gcda if it has a non-zero size (test -s).
    add_custom_command(OUTPUT "${TDIR}/${FILE}.gcda" COMMAND "${CMAKE_COMMAND}" -E touch "${TDIR}/${FILE}.gcda")

    add_custom_command(
      OUTPUT ${OUTFILE}
      COMMAND test -s "${TDIR}/${FILE}.gcda" && ${GCOV_ENV} ${GENINFO_BIN} --quiet --base-directory ${PROJECT_SOURCE_DIR} --gcov-tool ${GCOV_BIN} --output-filename ${OUTFILE} ${GENINFO_EXTERN_FLAG}
              ${TDIR}/${FILE}.gcda || cp ${OUTFILE}.init ${OUTFILE}
      DEPENDS ${TNAME} ${TNAME}-capture-init "${TDIR}/${FILE}.gcda"
      COMMENT "Capturing coverage data for ${FILE}"
      )
  endforeach()

  # Concatenate all files generated by geninfo to a single file per target.
  set(OUTFILE "${LCOV_DATA_PATH_CAPTURE}/${TNAME}.info")
  lcov_merge_files("${OUTFILE}" ${GENINFO_FILES})
  add_custom_target(${TNAME}-geninfo DEPENDS ${OUTFILE})

  # add geninfo file generation to global lcov-capture target
  add_dependencies(lcov-capture ${TNAME}-geninfo)
  set(LCOV_CAPTURE_FILES "${LCOV_CAPTURE_FILES}" "${OUTFILE}" CACHE INTERNAL "")

  # Add target for generating html output for this target only.
  file(MAKE_DIRECTORY ${LCOV_HTML_PATH}/${TNAME})
  add_custom_target(
    ${TNAME}-genhtml COMMAND ${GENHTML_BIN} --quiet --sort --prefix ${PROJECT_SOURCE_DIR} --baseline-file ${LCOV_DATA_PATH_INIT}/${TNAME}.info --output-directory ${LCOV_HTML_PATH}/${TNAME} --title
                             "${CMAKE_PROJECT_NAME} - target ${TNAME}" ${GENHTML_CPPFILT_FLAG} ${OUTFILE} DEPENDS ${TNAME}-geninfo ${TNAME}-capture-init
    )
endfunction(lcov_capture_tgt)

# This function will generate the global info file for all targets. It has to be called after all other CMake functions in the root CMakeLists.txt file, to get a full list of all targets that generate
# coverage data.
function(lcov_capture)
  # Skip this function (and do not create the following targets), if there are no input files.
  if("${LCOV_CAPTURE_FILES}" STREQUAL "")
    return()
  endif()

  # Add a new target to merge the files of all targets.
  set(OUTFILE "${LCOV_DATA_PATH_CAPTURE}/all_targets.info")
  lcov_merge_files("${OUTFILE}" ${LCOV_CAPTURE_FILES})
  add_custom_target(lcov-geninfo DEPENDS ${OUTFILE} lcov-capture)

  # Add a new global target for all lcov targets. This target could be used to generate the lcov html output for the whole project instead of calling <TARGET>-geninfo and <TARGET>-genhtml for each
  # target. It will also be used to generate a html site for all project data together instead of one for each target.
  if(NOT TARGET lcov)
    file(MAKE_DIRECTORY ${LCOV_HTML_PATH}/all_targets)
    add_custom_target(
      lcov COMMAND ${GENHTML_BIN} --quiet --sort --baseline-file ${LCOV_DATA_PATH_INIT}/all_targets.info --output-directory ${LCOV_HTML_PATH}/all_targets --title "${CMAKE_PROJECT_NAME}" --prefix
                   "${PROJECT_SOURCE_DIR}" ${GENHTML_CPPFILT_FLAG} ${OUTFILE} DEPENDS lcov-geninfo-init lcov-geninfo
      )
  endif()
endfunction(lcov_capture)

# Add a new global target to generate the lcov html report for the whole project instead of calling <TARGET>-genhtml for each target (to create an own report for each target). Instead of the lcov
# target it does not require geninfo for all targets, so you have to call <TARGET>-geninfo to generate the info files the targets you'd like to have in your report or lcov-geninfo for generating info
# files for all targets before calling lcov-genhtml.
file(MAKE_DIRECTORY ${LCOV_HTML_PATH}/selected_targets)
if(NOT TARGET lcov-genhtml)
  add_custom_target(
    lcov-genhtml
    COMMAND ${GENHTML_BIN} --quiet --output-directory ${LCOV_HTML_PATH}/selected_targets --title \"${CMAKE_PROJECT_NAME} - targets `find ${LCOV_DATA_PATH_CAPTURE} -name \"*.info\" ! -name
            \"all_targets.info\" -exec basename {} .info \\\;`\" --prefix ${PROJECT_SOURCE_DIR} --sort ${GENHTML_CPPFILT_FLAG} `find ${LCOV_DATA_PATH_CAPTURE} -name \"*.info\" ! -name
            \"all_targets.info\"`
    )
endif(NOT TARGET lcov-genhtml)
