1# Copyright (c) 2012 - 2017, Lars Bilke
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without modification,
5# are permitted provided that the following conditions are met:
6#
7# 1. Redistributions of source code must retain the above copyright notice, this
8#    list of conditions and the following disclaimer.
9#
10# 2. Redistributions in binary form must reproduce the above copyright notice,
11#    this list of conditions and the following disclaimer in the documentation
12#    and/or other materials provided with the distribution.
13#
14# 3. Neither the name of the copyright holder nor the names of its contributors
15#    may be used to endorse or promote products derived from this software without
16#    specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28#
29# CHANGES:
30#
31# 2012-01-31, Lars Bilke
32# - Enable Code Coverage
33#
34# 2013-09-17, Joakim Söderberg
35# - Added support for Clang.
36# - Some additional usage instructions.
37#
38# 2016-02-03, Lars Bilke
39# - Refactored functions to use named parameters
40#
41# 2017-06-02, Lars Bilke
42# - Merged with modified version from github.com/ufz/ogs
43#
44#
45# USAGE:
46#
47# 1. Copy this file into your cmake modules path.
48#
49# 2. Add the following line to your CMakeLists.txt:
50#      include(CodeCoverage)
51#
52# 3. Append necessary compiler flags:
53#      APPEND_COVERAGE_COMPILER_FLAGS()
54#
55# 4. If you need to exclude additional directories from the report, specify them
56#    using the COVERAGE_LCOV_EXCLUDES variable before calling SETUP_TARGET_FOR_COVERAGE_LCOV.
57#    Example:
58#      set(COVERAGE_LCOV_EXCLUDES 'dir1/*' 'dir2/*')
59#
60# 5. Use the functions described below to create a custom make target which
61#    runs your test executable and produces a code coverage report.
62#
63# 6. Build a Debug build:
64#      cmake -DCMAKE_BUILD_TYPE=Debug ..
65#      make
66#      make my_coverage_target
67#
68
69include(CMakeParseArguments)
70
71# Check prereqs
72find_program( GCOV_PATH gcov )
73find_program( LCOV_PATH  NAMES lcov lcov.bat lcov.exe lcov.perl)
74find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat )
75find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
76find_program( SIMPLE_PYTHON_EXECUTABLE python )
77
78if(NOT GCOV_PATH)
79    message(FATAL_ERROR "gcov not found! Aborting...")
80endif() # NOT GCOV_PATH
81
82if("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
83    if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 3)
84        message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...")
85    endif()
86elseif(NOT CMAKE_COMPILER_IS_GNUCXX)
87    message(FATAL_ERROR "Compiler is not GNU gcc! Aborting...")
88endif()
89
90set(COVERAGE_COMPILER_FLAGS "-g -O0 --coverage -fprofile-arcs -ftest-coverage"
91    CACHE INTERNAL "")
92
93set(CMAKE_CXX_FLAGS_COVERAGE
94    ${COVERAGE_COMPILER_FLAGS}
95    CACHE STRING "Flags used by the C++ compiler during coverage builds."
96    FORCE )
97set(CMAKE_C_FLAGS_COVERAGE
98    ${COVERAGE_COMPILER_FLAGS}
99    CACHE STRING "Flags used by the C compiler during coverage builds."
100    FORCE )
101set(CMAKE_EXE_LINKER_FLAGS_COVERAGE
102    ""
103    CACHE STRING "Flags used for linking binaries during coverage builds."
104    FORCE )
105set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
106    ""
107    CACHE STRING "Flags used by the shared libraries linker during coverage builds."
108    FORCE )
109mark_as_advanced(
110    CMAKE_CXX_FLAGS_COVERAGE
111    CMAKE_C_FLAGS_COVERAGE
112    CMAKE_EXE_LINKER_FLAGS_COVERAGE
113    CMAKE_SHARED_LINKER_FLAGS_COVERAGE )
114
115if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
116    message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
117endif() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug"
118
119if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
120    link_libraries(gcov)
121else()
122    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
123endif()
124
125# Defines a target for running and collection code coverage information
126# Builds dependencies, runs the given executable and outputs reports.
127# NOTE! The executable should always have a ZERO as exit code otherwise
128# the coverage generation will not complete.
129#
130# SETUP_TARGET_FOR_COVERAGE_LCOV(
131#     NAME testrunner_coverage                    # New target name
132#     EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
133#     DEPENDENCIES testrunner                     # Dependencies to build first
134# )
135function(SETUP_TARGET_FOR_COVERAGE_LCOV)
136
137    set(options NONE)
138    set(oneValueArgs NAME)
139    set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
140    cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
141
142    if(NOT LCOV_PATH)
143        message(FATAL_ERROR "lcov not found! Aborting...")
144    endif() # NOT LCOV_PATH
145
146    if(NOT GENHTML_PATH)
147        message(FATAL_ERROR "genhtml not found! Aborting...")
148    endif() # NOT GENHTML_PATH
149
150    # Setup target
151    add_custom_target(${Coverage_NAME}
152
153        # Cleanup lcov
154        COMMAND ${LCOV_PATH} --directory . --zerocounters
155        # Create baseline to make sure untouched files show up in the report
156        COMMAND ${LCOV_PATH} -c -i -d . -o ${Coverage_NAME}.base
157
158        # Run tests
159        COMMAND ${Coverage_EXECUTABLE}
160
161        # Capturing lcov counters and generating report
162        COMMAND ${LCOV_PATH} --directory . --capture --output-file ${Coverage_NAME}.info
163        # add baseline counters
164        COMMAND ${LCOV_PATH} -a ${Coverage_NAME}.base -a ${Coverage_NAME}.info --output-file ${Coverage_NAME}.total
165        COMMAND ${LCOV_PATH} --remove ${Coverage_NAME}.total ${COVERAGE_LCOV_EXCLUDES} --output-file ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned
166        COMMAND ${GENHTML_PATH} -o ${Coverage_NAME} ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned
167        COMMAND ${CMAKE_COMMAND} -E remove ${Coverage_NAME}.base ${Coverage_NAME}.total ${PROJECT_BINARY_DIR}/${Coverage_NAME}.info.cleaned
168
169        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
170        DEPENDS ${Coverage_DEPENDENCIES}
171        COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
172    )
173
174    # Show where to find the lcov info report
175    add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
176        COMMAND ;
177        COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info."
178    )
179
180    # Show info where to find the report
181    add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
182        COMMAND ;
183        COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
184    )
185
186endfunction() # SETUP_TARGET_FOR_COVERAGE_LCOV
187
188# Defines a target for running and collection code coverage information
189# Builds dependencies, runs the given executable and outputs reports.
190# NOTE! The executable should always have a ZERO as exit code otherwise
191# the coverage generation will not complete.
192#
193# SETUP_TARGET_FOR_COVERAGE_GCOVR_XML(
194#     NAME ctest_coverage                    # New target name
195#     EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
196#     DEPENDENCIES executable_target         # Dependencies to build first
197# )
198function(SETUP_TARGET_FOR_COVERAGE_GCOVR_XML)
199
200    set(options NONE)
201    set(oneValueArgs NAME)
202    set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
203    cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
204
205    if(NOT SIMPLE_PYTHON_EXECUTABLE)
206        message(FATAL_ERROR "python not found! Aborting...")
207    endif() # NOT SIMPLE_PYTHON_EXECUTABLE
208
209    if(NOT GCOVR_PATH)
210        message(FATAL_ERROR "gcovr not found! Aborting...")
211    endif() # NOT GCOVR_PATH
212
213    # Combine excludes to several -e arguments
214    set(GCOVR_EXCLUDES "")
215    foreach(EXCLUDE ${COVERAGE_GCOVR_EXCLUDES})
216        list(APPEND GCOVR_EXCLUDES "-e")
217        list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
218    endforeach()
219
220    add_custom_target(${Coverage_NAME}
221        # Run tests
222        ${Coverage_EXECUTABLE}
223
224        # Running gcovr
225        COMMAND ${GCOVR_PATH} --xml
226            -r ${PROJECT_SOURCE_DIR} ${GCOVR_EXCLUDES}
227            --object-directory=${PROJECT_BINARY_DIR}
228            -o ${Coverage_NAME}.xml
229        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
230        DEPENDS ${Coverage_DEPENDENCIES}
231        COMMENT "Running gcovr to produce Cobertura code coverage report."
232    )
233
234    # Show info where to find the report
235    add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
236        COMMAND ;
237        COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml."
238    )
239
240endfunction() # SETUP_TARGET_FOR_COVERAGE_GCOVR_XML
241
242# Defines a target for running and collection code coverage information
243# Builds dependencies, runs the given executable and outputs reports.
244# NOTE! The executable should always have a ZERO as exit code otherwise
245# the coverage generation will not complete.
246#
247# SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML(
248#     NAME ctest_coverage                    # New target name
249#     EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
250#     DEPENDENCIES executable_target         # Dependencies to build first
251# )
252function(SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML)
253
254    set(options NONE)
255    set(oneValueArgs NAME)
256    set(multiValueArgs EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
257    cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
258
259    if(NOT SIMPLE_PYTHON_EXECUTABLE)
260        message(FATAL_ERROR "python not found! Aborting...")
261    endif() # NOT SIMPLE_PYTHON_EXECUTABLE
262
263    if(NOT GCOVR_PATH)
264        message(FATAL_ERROR "gcovr not found! Aborting...")
265    endif() # NOT GCOVR_PATH
266
267    # Combine excludes to several -e arguments
268    set(GCOVR_EXCLUDES "")
269    foreach(EXCLUDE ${COVERAGE_GCOVR_EXCLUDES})
270        list(APPEND GCOVR_EXCLUDES "-e")
271        list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
272    endforeach()
273
274    add_custom_target(${Coverage_NAME}
275        # Run tests
276        ${Coverage_EXECUTABLE}
277
278        # Create folder
279        COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME}
280
281        # Running gcovr
282        COMMAND ${GCOVR_PATH} --html --html-details
283            -r ${PROJECT_SOURCE_DIR} ${GCOVR_EXCLUDES}
284            --object-directory=${PROJECT_BINARY_DIR}
285            -o ${Coverage_NAME}/index.html
286        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
287        DEPENDS ${Coverage_DEPENDENCIES}
288        COMMENT "Running gcovr to produce HTML code coverage report."
289    )
290
291    # Show info where to find the report
292    add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
293        COMMAND ;
294        COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
295    )
296
297endfunction() # SETUP_TARGET_FOR_COVERAGE_GCOVR_HTML
298
299function(APPEND_COVERAGE_COMPILER_FLAGS)
300    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
301    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
302    message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
303endfunction() # APPEND_COVERAGE_COMPILER_FLAGS
304