# This file is part of CMake-codecov.
#
# Copyright 2015-2017 RWTH Aachen University, Federal Republic of Germany
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the University of California, Berkeley nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#     * Neither the name of the copyright holder nor the names of its
#       contributors may be used to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# 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}")


	set(TDIR ${CMAKE_CURRENT_BINARY_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}")


	set(TDIR ${CMAKE_CURRENT_BINARY_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})

		add_custom_command(OUTPUT ${OUTFILE}
			COMMAND test -f "${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
			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)
