# Copyright (c) 2020 The Bitcoin developers
###############################################################################
# Provide support for building test coverage reports.
#
# Dependencies:
#  - c++filt
#  - gcov
#  - genhtml
#  - lcov
#  - python3
#
# The basic workflow for building coverage report is as follow:
#  1/ Build the instrumented code by passing the --coverage flag to the
#     compiler. For better accuracy there should be no optimization.
#  2/ Build the code, prior to run any test. Capture an initial state with
#     `lcov` to get a baseline for the coverage information (function names,
#     number of lines). This is achieved by running the `coverage-baseline`
#     target.
#  3/ The coverage data is filtered to remove coverage info from code which is
#     not of interest (such as 3rd party libs, generated files, etc.).
#  4/ Run the tests, and capture the generated coverage data with `lcov`. Again,
#     the coverage data is filtered. After the test target has run, the
#     coverage counters are reset in order to make each test run produce an
#     independent output.
#  5/ Combine the test coverage data with the baseline data using `lcov` to
#     produce the final coverage information for the test.
#  6/ Use `genhtml` to generate a HTML report from the combined coverage data.
#
# How to use:
#  1/ Enable coverage with the `enable_coverage()` function. The branch coverage
#     can be enabled by setting the `ENABLE_BRANCH_COVERAGE` parameter to true.
#  2/ Call `exclude_from_coverage()` to exclude paths from the coverage report.
#  3/ Use `add_custom_target_coverage()` to enable coverage reporting for your
#     test target. This generates a new `coverage-<target>` target that will run
#     the tests and create the HTML report in a `<target.coverage>` directory.
###############################################################################

include(SanitizeHelper)

# Exclude directories (and subdirectories) from the coverage report.
function(exclude_from_coverage)
	foreach(_dir ${ARGN})
		get_filename_component(_abspath "${_dir}" ABSOLUTE)
		set_property(GLOBAL APPEND_STRING PROPERTY LCOV_FILTER_PATTERN " -p ${_abspath}")
	endforeach()
endfunction()

function(enable_coverage ENABLE_BRANCH_COVERAGE)
	set(__ENABLE_COVERAGE ON CACHE INTERNAL "Coverage is enabled")

	# Required dependencies.
	# c++filt is needed to demangle c++ function names at HTML generation time.
	include(DoOrFail)
	find_program_or_fail(LCOV_EXECUTABLE lcov)
	find_program_or_fail(GCOV_EXECUTABLE gcov)
	find_program_or_fail(GENHTML_EXECUTABLE genhtml)
	find_program_or_fail(CXXFILT_EXECUTABLE c++filt)
	find_package(Python3 COMPONENTS Interpreter REQUIRED)
	set(__COVERAGE_PYTHON "${Python3_EXECUTABLE}" CACHE PATH "Path to the Python interpreter")

	get_property(_project_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
	set(COVERAGE_FLAG --coverage)

	foreach(_language ${_project_languages})
		sanitize_c_cxx_definition(
			"supports_${_language}_"
			${COVERAGE_FLAG}
			SUPPORTS_COVERAGE
		)

		set(_save_linker_flags ${CMAKE_EXE_LINKER_FLAGS})
		string(APPEND CMAKE_EXE_LINKER_FLAGS " ${COVERAGE_FLAG}")

		if("${_language}" STREQUAL "C")
			include(CheckCCompilerFlag)
			CHECK_C_COMPILER_FLAG(${COVERAGE_FLAG} ${SUPPORTS_COVERAGE})
		elseif("${_language}" STREQUAL "CXX")
			include(CheckCXXCompilerFlag)
			CHECK_CXX_COMPILER_FLAG(${COVERAGE_FLAG} ${SUPPORTS_COVERAGE})
		else()
			message(WARNING "Coverage is not supported for the ${_language} language")
		endif()

		set(CMAKE_EXE_LINKER_FLAGS ${_save_linker_flags})

		if(NOT ${SUPPORTS_COVERAGE})
			message(FATAL_ERROR "The ${COVERAGE_FLAG} option is not supported by your ${_language} compiler")
		endif()

		add_compile_options($<$<COMPILE_LANGUAGE:${_language}>:${COVERAGE_FLAG}>)
		add_link_options($<$<COMPILE_LANGUAGE:${_language}>:${COVERAGE_FLAG}>)
	endforeach()

	# Exclude some path by default, such as system headers and generated files.
	exclude_from_coverage(
		"${CMAKE_BINARY_DIR}"
		"/usr/include"
		"/usr/lib"
		"/usr/lib64"
	)

	if(ENABLE_BRANCH_COVERAGE)
		string(APPEND LCOV_OPTIONS "--rc lcov_branch_coverage=1")
		set(LCOV_OPTIONS "${LCOV_OPTIONS}" CACHE STRING "Lcov options")
	endif()
endfunction()

function(add_custom_target_coverage TARGET)
	# Coverage should have been enabled in order to create the new coverage-*
	# targets.
	if(NOT __ENABLE_COVERAGE)
		return()
	endif()

	get_property(LCOV_FILTER_PATTERN GLOBAL PROPERTY LCOV_FILTER_PATTERN)

	# Make sure we generate the base coverage data before building this target.
	if(NOT TARGET coverage-baseline)
		configure_file(
			"${${PROJECT_NAME}_SOURCE_DIR}/cmake/templates/CoverageBaseline.sh.in"
			"${CMAKE_BINARY_DIR}/CoverageBaseline.sh"
		)

		add_custom_command(
			COMMENT "Generating baseline coverage info"
			OUTPUT "${CMAKE_BINARY_DIR}/baseline.info"
			COMMAND "${CMAKE_BINARY_DIR}/CoverageBaseline.sh"
			VERBATIM USES_TERMINAL
		)

		add_custom_target(coverage-baseline
			DEPENDS "${CMAKE_BINARY_DIR}/baseline.info"
		)
	endif()

	sanitize_c_cxx_definition("" "${TARGET}" SANITIZED_TARGET)

	configure_file(
		"${${PROJECT_NAME}_SOURCE_DIR}/cmake/templates/CoverageTest.sh.in"
		"${CMAKE_BINARY_DIR}/Coverage-${TARGET}.sh"
	)

	add_custom_target(coverage-${TARGET}
		DEPENDS coverage-baseline ${TARGET}
		COMMENT "Generating ${TARGET} coverage report"
		COMMAND "${CMAKE_BINARY_DIR}/Coverage-${TARGET}.sh"
		VERBATIM USES_TERMINAL
	)
endfunction()
