# CMake targets for generating code coverage reports
#
# Note that the targets in here are not needed to enable code coverage on
# builds. To have builds generate code coverage metadata, one only has to enable
# the --coverage option for the compiler and this is done in the top-level
# CMakeLists.txt so that the option covers all build targets in the project.
#
# Given that all dependencies (lcov, gcov, genhtml) are installed, and CMake is
# initialized with -DCODECOVERAGE=ON, it should be possible to generate a code
# coverage report by running:
#
# $ make coverage
#
# The report is generated in REPORT_DIR and can be viewed in a web browser.

# If we use clang, prefer the LLVM gcov. The "normal" gcov segfaults with the
# coverage info generated by clang, and the LLVM gcov with GCC-generated
# coverage gives weird errors like GCOV_TAG_COUNTER_ARCS mismatch.
set(GCOV_NAMES "gcov")
if(CMAKE_C_COMPILER_ID MATCHES "Clang|AppleClang")
  string(REGEX MATCH "^[0-9]+" CMAKE_C_COMPILER_VERSION_MAJOR
               ${CMAKE_C_COMPILER_VERSION})
  list(PREPEND GCOV_NAMES "llvm-cov-${CMAKE_C_COMPILER_VERSION_MAJOR}")
endif()

find_program(GCOV NAMES ${GCOV_NAMES})

# Find lcov for html output
find_program(LCOV lcov)

if(NOT GCOV)
  message(STATUS "Install gcov to generate code coverage reports")
elseif(NOT LCOV)
  message(STATUS "Install lcov to generate code coverage reports")
else()
  execute_process(
    COMMAND ${LCOV} --version
    OUTPUT_VARIABLE LCOV_VERSION_OUTPUT
    RESULT_VARIABLE LCOV_VERSION_RESULT
    OUTPUT_STRIP_TRAILING_WHITESPACE)
  if(LCOV_VERSION_RESULT EQUAL 0)
    # You might need to parse LCOV_VERSION_OUTPUT to extract just the version
    # number For example, if the output is "lcov: LCOV version 1.15", you'd
    # extract "1.15"
    string(REGEX MATCH "LCOV version ([0-9]+\\.[0-9]+)" LCOV_VERSION_MATCH
                 "${LCOV_VERSION_OUTPUT}")
    if(LCOV_VERSION_MATCH)
      set(LCOV_VERSION ${CMAKE_MATCH_1})
      message(STATUS "Using lcov ${LCOV}: ${LCOV_VERSION}")
    else()
      message(
        FATAL_ERROR
          "Could not parse LCOV version from output: ${LCOV_VERSION_OUTPUT}")
    endif()
  else()
    message(
      FATAL_ERROR
        "Failed to get LCOV version. Error code: ${LCOV_VERSION_RESULT}")
  endif()

  if(${LCOV_VERSION} GREATER_EQUAL "2.0")
    set(LCOV_EXTRA_OPTIONS "--ignore-errors mismatch,mismatch")
  else()
    set(LCOV_EXTRA_OPTIONS "")
  endif()

  message(STATUS "Using gcov ${GCOV}:")
  execute_process(COMMAND ${GCOV} --version)

  # Final tracefile for code coverage
  set(OUTPUT_FILE "timescaledb-coverage.info")

  # Directory where to generate the HTML report
  set(REPORT_DIR "lcov-report")

  # We can't directly use llvm-cov as --gcov-tool, because it has to be called
  # like "llvm-cov gcov <gcov args>" for that. Thankfully, if its $0 is gcov, it
  # will understand that we want it to operate like gcov. Just create a symlink.
  add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/gcov
                     COMMAND ln -s ${GCOV} ${CMAKE_CURRENT_BINARY_DIR}/gcov)

  # The baseline run needs to run before tests to learn what zero coverage looks
  # like
  add_custom_command(
    OUTPUT ${OUTPUT_FILE}.base
    COMMENT "Generating code coverage base file"
    COMMAND
      ${LCOV} --rc lcov_branch_coverage=1 --capture --initial # Initial run
      --no-external # Do not include external source files
      --base-directory ${CMAKE_SOURCE_DIR} --directory ${CMAKE_BINARY_DIR}
      --output-file ${OUTPUT_FILE}.base --gcov-tool
      ${CMAKE_CURRENT_BINARY_DIR}/gcov ${LCOV_EXTRA_OPTIONS}
    DEPENDS timescaledb-tsl timescaledb timescaledb-loader
            ${CMAKE_CURRENT_BINARY_DIR}/gcov)

  add_custom_target(coverage_base DEPENDS ${OUTPUT_FILE}.base)

  # Ensure baseline file is generated before tests
  add_dependencies(installcheck coverage_base)

  # The test run needs to run after tests to analyze the test coverage
  add_custom_command(
    OUTPUT ${OUTPUT_FILE}.test
    COMMENT "Generating code coverage test file"
    COMMAND
      ${LCOV} --rc lcov_branch_coverage=1 --capture --no-external
      --base-directory ${CMAKE_SOURCE_DIR} --directory ${CMAKE_BINARY_DIR}
      --output-file ${OUTPUT_FILE}.test --gcov-tool
      ${CMAKE_CURRENT_BINARY_DIR}/gcov ${LCOV_EXTRA_OPTIONS}
    DEPENDS ${OUTPUT_FILE}.base coverage_base)

  # Make sure coverage_test runs after tests (installcheck) finish
  add_custom_target(coverage_test DEPENDS ${OUTPUT_FILE}.test)
  add_dependencies(installcheck-post-hook coverage_test)

  # Generate the final coverage file by combining the pre and post test
  # tracefiles
  add_custom_command(
    OUTPUT ${OUTPUT_FILE}
    COMMENT "Generating final code coverage file"
    COMMAND
      ${LCOV} --rc lcov_branch_coverage=1 --add-tracefile ${OUTPUT_FILE}.base
      --add-tracefile ${OUTPUT_FILE}.test --output-file ${OUTPUT_FILE}
      --gcov-tool ${CMAKE_CURRENT_BINARY_DIR}/gcov ${LCOV_EXTRA_OPTIONS}
    DEPENDS ${OUTPUT_FILE}.test coverage_test)

  add_custom_target(coverage_final DEPENDS ${OUTPUT_FILE})
  add_dependencies(coverage_final coverage_test)

  # Look for genhtml to produce HTML report. This tool is part of the lcov
  # suite, so should be installed if lcov is installed. Thus, this is an
  # over-cautious check just in case some distributions break these tools up in
  # separate packages.
  find_program(GENHTML genhtml)

  if(GENHTML)
    message(
      STATUS
        "Generate a code coverage report using the 'coverage' target after tests have run."
    )

    add_custom_command(
      OUTPUT ${REPORT_DIR}/index.html
      COMMENT
        "Generating HTML code coverage report in ${CMAKE_CURRENT_BINARY_DIR}/${REPORT_DIR}"
      COMMAND
        ${GENHTML} --prefix ${CMAKE_SOURCE_DIR} --branch-coverage
        --ignore-errors source --legend --title "TimescaleDB" --output-directory
        ${REPORT_DIR} ${OUTPUT_FILE}
      DEPENDS ${OUTPUT_FILE})
    add_custom_target(coverage DEPENDS ${REPORT_DIR}/index.html)
    add_dependencies(coverage coverage_final)

    add_custom_command(
      COMMAND
        echo
        "Open file://${CMAKE_CURRENT_BINARY_DIR}/${REPORT_DIR}/index.html in a browser to view the report"
        TARGET coverage POST_BUILD
      COMMENT)
  else()
    message(STATUS "Install genhtml to generate code coverage reports")
  endif(GENHTML)
endif()
