blob: 2c26bd917cf9c7261eeaa0aa90249f6ce71b3cef [file] [log] [blame]
Joe Hildebrand7c6c3562015-03-31 00:21:21 -06001#
2# Permission is hereby granted, free of charge, to any person obtaining a copy
3# of this software and associated documentation files (the "Software"), to deal
4# in the Software without restriction, including without limitation the rights
5# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6# copies of the Software, and to permit persons to whom the Software is
7# furnished to do so, subject to the following conditions:
8#
9# The above copyright notice and this permission notice shall be included in all
10# copies or substantial portions of the Software.
11#
12# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18# SOFTWARE.
19#
20# Copyright (C) 2014 Joakim Söderberg <joakim.soderberg@gmail.com>
21#
22# This is intended to be run by a custom target in a CMake project like this.
23# 0. Compile program with coverage support.
24# 1. Clear coverage data. (Recursively delete *.gcda in build dir)
25# 2. Run the unit tests.
26# 3. Run this script specifying which source files the coverage should be performed on.
27#
28# This script will then use gcov to generate .gcov files in the directory specified
29# via the COV_PATH var. This should probably be the same as your cmake build dir.
30#
31# It then parses the .gcov files to convert them into the Coveralls JSON format:
32# https://coveralls.io/docs/api
33#
34# Example for running as standalone CMake script from the command line:
35# (Note it is important the -P is at the end...)
36# $ cmake -DCOV_PATH=$(pwd)
37# -DCOVERAGE_SRCS="catcierge_rfid.c;catcierge_timer.c"
38# -P ../cmake/CoverallsGcovUpload.cmake
39#
40CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
41
42
43#
44# Make sure we have the needed arguments.
45#
46if (NOT COVERALLS_OUTPUT_FILE)
47 message(FATAL_ERROR "Coveralls: No coveralls output file specified. Please set COVERALLS_OUTPUT_FILE")
48endif()
49
50if (NOT COV_PATH)
51 message(FATAL_ERROR "Coveralls: Missing coverage directory path where gcov files will be generated. Please set COV_PATH")
52endif()
53
54if (NOT COVERAGE_SRCS)
55 message(FATAL_ERROR "Coveralls: Missing the list of source files that we should get the coverage data for COVERAGE_SRCS")
56endif()
57
58if (NOT PROJECT_ROOT)
59 message(FATAL_ERROR "Coveralls: Missing PROJECT_ROOT.")
60endif()
61
62# Since it's not possible to pass a CMake list properly in the
63# "1;2;3" format to an external process, we have replaced the
64# ";" with "*", so reverse that here so we get it back into the
65# CMake list format.
66string(REGEX REPLACE "\\*" ";" COVERAGE_SRCS ${COVERAGE_SRCS})
67
68find_program(GCOV_EXECUTABLE gcov)
69
70if (NOT GCOV_EXECUTABLE)
71 message(FATAL_ERROR "gcov not found! Aborting...")
72endif()
73
74find_package(Git)
75
76# TODO: Add these git things to the coveralls json.
77if (GIT_FOUND)
78 # Branch.
79 execute_process(
80 COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
81 WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
82 OUTPUT_VARIABLE GIT_BRANCH
83 OUTPUT_STRIP_TRAILING_WHITESPACE
84 )
85
86 macro (git_log_format FORMAT_CHARS VAR_NAME)
87 execute_process(
88 COMMAND ${GIT_EXECUTABLE} log -1 --pretty=format:%${FORMAT_CHARS}
89 WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
90 OUTPUT_VARIABLE ${VAR_NAME}
91 OUTPUT_STRIP_TRAILING_WHITESPACE
92 )
93 endmacro()
94
95 git_log_format(an GIT_AUTHOR_EMAIL)
96 git_log_format(ae GIT_AUTHOR_EMAIL)
97 git_log_format(cn GIT_COMMITTER_NAME)
98 git_log_format(ce GIT_COMMITTER_EMAIL)
99 git_log_format(B GIT_COMMIT_MESSAGE)
100
101 message("Git exe: ${GIT_EXECUTABLE}")
102 message("Git branch: ${GIT_BRANCH}")
103 message("Git author: ${GIT_AUTHOR_NAME}")
104 message("Git e-mail: ${GIT_AUTHOR_EMAIL}")
105 message("Git commiter name: ${GIT_COMMITTER_NAME}")
106 message("Git commiter e-mail: ${GIT_COMMITTER_EMAIL}")
107 message("Git commit message: ${GIT_COMMIT_MESSAGE}")
108
109endif()
110
111############################# Macros #########################################
112
113#
114# This macro converts from the full path format gcov outputs:
115#
116# /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
117#
118# to the original source file path the .gcov is for:
119#
120# /path/to/project/root/subdir/the_file.c
121#
122macro(get_source_path_from_gcov_filename _SRC_FILENAME _GCOV_FILENAME)
123
124 # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
125 # ->
126 # #path#to#project#root#subdir#the_file.c.gcov
127 get_filename_component(_GCOV_FILENAME_WEXT ${_GCOV_FILENAME} NAME)
128
129 # #path#to#project#root#subdir#the_file.c.gcov -> /path/to/project/root/subdir/the_file.c
130 string(REGEX REPLACE "\\.gcov$" "" SRC_FILENAME_TMP ${_GCOV_FILENAME_WEXT})
131 string(REGEX REPLACE "\#" "/" SRC_FILENAME_TMP ${SRC_FILENAME_TMP})
132 set(${_SRC_FILENAME} "${SRC_FILENAME_TMP}")
133endmacro()
134
135##############################################################################
136
137# Get the coverage data.
138file(GLOB_RECURSE GCDA_FILES "${COV_PATH}/*.gcda")
139message("GCDA files:")
140
141# Get a list of all the object directories needed by gcov
142# (The directories the .gcda files and .o files are found in)
143# and run gcov on those.
144foreach(GCDA ${GCDA_FILES})
145 message("Process: ${GCDA}")
146 message("------------------------------------------------------------------------------")
147 get_filename_component(GCDA_DIR ${GCDA} PATH)
148
149 #
150 # The -p below refers to "Preserve path components",
151 # This means that the generated gcov filename of a source file will
152 # keep the original files entire filepath, but / is replaced with #.
153 # Example:
154 #
155 # /path/to/project/root/build/CMakeFiles/the_file.dir/subdir/the_file.c.gcda
156 # ------------------------------------------------------------------------------
157 # File '/path/to/project/root/subdir/the_file.c'
158 # Lines executed:68.34% of 199
159 # /path/to/project/root/subdir/the_file.c:creating '#path#to#project#root#subdir#the_file.c.gcov'
160 #
161 # If -p is not specified then the file is named only "the_file.c.gcov"
162 #
163 execute_process(
Joe Hildebrandc3232622015-04-05 14:50:43 -0600164 COMMAND ${GCOV_EXECUTABLE} -c -p -o ${GCDA_DIR} ${GCDA}
Joe Hildebrand7c6c3562015-03-31 00:21:21 -0600165 WORKING_DIRECTORY ${COV_PATH}
166 )
167endforeach()
168
169# TODO: Make these be absolute path
170file(GLOB ALL_GCOV_FILES ${COV_PATH}/*.gcov)
171
172# Get only the filenames to use for filtering.
173#set(COVERAGE_SRCS_NAMES "")
174#foreach (COVSRC ${COVERAGE_SRCS})
175# get_filename_component(COVSRC_NAME ${COVSRC} NAME)
176# message("${COVSRC} -> ${COVSRC_NAME}")
177# list(APPEND COVERAGE_SRCS_NAMES "${COVSRC_NAME}")
178#endforeach()
179
180#
181# Filter out all but the gcov files we want.
182#
183# We do this by comparing the list of COVERAGE_SRCS filepaths that the
184# user wants the coverage data for with the paths of the generated .gcov files,
185# so that we only keep the relevant gcov files.
186#
187# Example:
188# COVERAGE_SRCS =
189# /path/to/project/root/subdir/the_file.c
190#
191# ALL_GCOV_FILES =
192# /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
193# /path/to/project/root/build/#path#to#project#root#subdir#other_file.c.gcov
194#
195# Result should be:
196# GCOV_FILES =
197# /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
198#
199set(GCOV_FILES "")
200#message("Look in coverage sources: ${COVERAGE_SRCS}")
201message("\nFilter out unwanted GCOV files:")
202message("===============================")
203
204set(COVERAGE_SRCS_REMAINING ${COVERAGE_SRCS})
205
206foreach (GCOV_FILE ${ALL_GCOV_FILES})
207
208 #
209 # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
210 # ->
211 # /path/to/project/root/subdir/the_file.c
212 get_source_path_from_gcov_filename(GCOV_SRC_PATH ${GCOV_FILE})
213
214 # Is this in the list of source files?
215 # TODO: We want to match against relative path filenames from the source file root...
216 list(FIND COVERAGE_SRCS ${GCOV_SRC_PATH} WAS_FOUND)
217
218 if (NOT WAS_FOUND EQUAL -1)
219 message("YES: ${GCOV_FILE}")
220 list(APPEND GCOV_FILES ${GCOV_FILE})
221
222 # We remove it from the list, so we don't bother searching for it again.
223 # Also files left in COVERAGE_SRCS_REMAINING after this loop ends should
224 # have coverage data generated from them (no lines are covered).
225 list(REMOVE_ITEM COVERAGE_SRCS_REMAINING ${GCOV_SRC_PATH})
226 else()
227 message("NO: ${GCOV_FILE}")
228 endif()
229endforeach()
230
231# TODO: Enable setting these
232set(JSON_SERVICE_NAME "travis-ci")
233set(JSON_SERVICE_JOB_ID $ENV{TRAVIS_JOB_ID})
234
235set(JSON_TEMPLATE
236"{
237 \"service_name\": \"\@JSON_SERVICE_NAME\@\",
238 \"service_job_id\": \"\@JSON_SERVICE_JOB_ID\@\",
239 \"source_files\": \@JSON_GCOV_FILES\@
240}"
241)
242
243set(SRC_FILE_TEMPLATE
244"{
245 \"name\": \"\@GCOV_SRC_REL_PATH\@\",
246 \"source_digest\": \"\@GCOV_CONTENTS_MD5\@\",
247 \"coverage\": \@GCOV_FILE_COVERAGE\@
248 }"
249)
250
251message("\nGenerate JSON for files:")
252message("=========================")
253
254set(JSON_GCOV_FILES "[")
255
256# Read the GCOV files line by line and get the coverage data.
257foreach (GCOV_FILE ${GCOV_FILES})
258
259 get_source_path_from_gcov_filename(GCOV_SRC_PATH ${GCOV_FILE})
260 file(RELATIVE_PATH GCOV_SRC_REL_PATH "${PROJECT_ROOT}" "${GCOV_SRC_PATH}")
261
262 # The new coveralls API doesn't need the entire source (Yay!)
263 # However, still keeping that part for now. Will cleanup in the future.
264 file(MD5 "${GCOV_SRC_PATH}" GCOV_CONTENTS_MD5)
265 message("MD5: ${GCOV_SRC_PATH} = ${GCOV_CONTENTS_MD5}")
266
267 # Loads the gcov file as a list of lines.
268 # (We first open the file and replace all occurences of [] with _
269 # because CMake will fail to parse a line containing unmatched brackets...
270 # also the \ to escaped \n in macros screws up things.)
271 # https://public.kitware.com/Bug/view.php?id=15369
272 file(READ ${GCOV_FILE} GCOV_CONTENTS)
273 string(REPLACE "[" "_" GCOV_CONTENTS "${GCOV_CONTENTS}")
274 string(REPLACE "]" "_" GCOV_CONTENTS "${GCOV_CONTENTS}")
275 string(REPLACE "\\" "_" GCOV_CONTENTS "${GCOV_CONTENTS}")
276 file(WRITE ${GCOV_FILE}_tmp "${GCOV_CONTENTS}")
277
278 file(STRINGS ${GCOV_FILE}_tmp GCOV_LINES)
279 list(LENGTH GCOV_LINES LINE_COUNT)
280
281 # Instead of trying to parse the source from the
282 # gcov file, simply read the file contents from the source file.
283 # (Parsing it from the gcov is hard because C-code uses ; in many places
284 # which also happens to be the same as the CMake list delimeter).
285 file(READ ${GCOV_SRC_PATH} GCOV_FILE_SOURCE)
286
287 string(REPLACE "\\" "\\\\" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
288 string(REGEX REPLACE "\"" "\\\\\"" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
289 string(REPLACE "\t" "\\\\t" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
290 string(REPLACE "\r" "\\\\r" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
291 string(REPLACE "\n" "\\\\n" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
292 # According to http://json.org/ these should be escaped as well.
293 # Don't know how to do that in CMake however...
294 #string(REPLACE "\b" "\\\\b" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
295 #string(REPLACE "\f" "\\\\f" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
296 #string(REGEX REPLACE "\u([a-fA-F0-9]{4})" "\\\\u\\1" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
297
298 # We want a json array of coverage data as a single string
299 # start building them from the contents of the .gcov
300 set(GCOV_FILE_COVERAGE "[")
301
302 set(GCOV_LINE_COUNT 1) # Line number for the .gcov.
303 set(DO_SKIP 0)
304 foreach (GCOV_LINE ${GCOV_LINES})
305 #message("${GCOV_LINE}")
306 # Example of what we're parsing:
307 # Hitcount |Line | Source
308 # " 8: 26: if (!allowed || (strlen(allowed) == 0))"
309 string(REGEX REPLACE
310 "^([^:]*):([^:]*):(.*)$"
311 "\\1;\\2;\\3"
312 RES
313 "${GCOV_LINE}")
314
315 # Check if we should exclude lines using the Lcov syntax.
316 string(REGEX MATCH "LCOV_EXCL_START" START_SKIP "${GCOV_LINE}")
317 string(REGEX MATCH "LCOV_EXCL_END" END_SKIP "${GCOV_LINE}")
318 string(REGEX MATCH "LCOV_EXCL_LINE" LINE_SKIP "${GCOV_LINE}")
319
320 set(RESET_SKIP 0)
321 if (LINE_SKIP AND NOT DO_SKIP)
322 set(DO_SKIP 1)
323 set(RESET_SKIP 1)
324 endif()
325
326 if (START_SKIP)
327 set(DO_SKIP 1)
328 message("${GCOV_LINE_COUNT}: Start skip")
329 endif()
330
331 if (END_SKIP)
332 set(DO_SKIP 0)
333 endif()
334
335 list(LENGTH RES RES_COUNT)
336
337 if (RES_COUNT GREATER 2)
338 list(GET RES 0 HITCOUNT)
339 list(GET RES 1 LINE)
340 list(GET RES 2 SOURCE)
341
342 string(STRIP ${HITCOUNT} HITCOUNT)
343 string(STRIP ${LINE} LINE)
344
345 # Lines with 0 line numbers are metadata and can be ignored.
346 if (NOT ${LINE} EQUAL 0)
347
348 if (DO_SKIP)
349 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}null, ")
350 else()
351 # Translate the hitcount into valid JSON values.
352 if (${HITCOUNT} STREQUAL "#####")
353 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}0, ")
354 elseif (${HITCOUNT} STREQUAL "-")
355 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}null, ")
356 else()
357 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}${HITCOUNT}, ")
358 endif()
359 endif()
360 endif()
361 else()
362 message(WARNING "Failed to properly parse line (RES_COUNT = ${RES_COUNT}) ${GCOV_FILE}:${GCOV_LINE_COUNT}\n-->${GCOV_LINE}")
363 endif()
364
365 if (RESET_SKIP)
366 set(DO_SKIP 0)
367 endif()
368 math(EXPR GCOV_LINE_COUNT "${GCOV_LINE_COUNT}+1")
369 endforeach()
370
371 message("${GCOV_LINE_COUNT} of ${LINE_COUNT} lines read!")
372
373 # Advanced way of removing the trailing comma in the JSON array.
374 # "[1, 2, 3, " -> "[1, 2, 3"
375 string(REGEX REPLACE ",[ ]*$" "" GCOV_FILE_COVERAGE ${GCOV_FILE_COVERAGE})
376
377 # Append the trailing ] to complete the JSON array.
378 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}]")
379
380 # Generate the final JSON for this file.
381 message("Generate JSON for file: ${GCOV_SRC_REL_PATH}...")
382 string(CONFIGURE ${SRC_FILE_TEMPLATE} FILE_JSON)
383
384 set(JSON_GCOV_FILES "${JSON_GCOV_FILES}${FILE_JSON}, ")
385endforeach()
386
387# Loop through all files we couldn't find any coverage for
388# as well, and generate JSON for those as well with 0% coverage.
389foreach(NOT_COVERED_SRC ${COVERAGE_SRCS_REMAINING})
390
391 # Loads the source file as a list of lines.
392 file(STRINGS ${NOT_COVERED_SRC} SRC_LINES)
393
394 set(GCOV_FILE_COVERAGE "[")
395 set(GCOV_FILE_SOURCE "")
396
397 foreach (SOURCE ${SRC_LINES})
398 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}0, ")
399
400 string(REPLACE "\\" "\\\\" SOURCE "${SOURCE}")
401 string(REGEX REPLACE "\"" "\\\\\"" SOURCE "${SOURCE}")
402 string(REPLACE "\t" "\\\\t" SOURCE "${SOURCE}")
403 string(REPLACE "\r" "\\\\r" SOURCE "${SOURCE}")
404 set(GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}${SOURCE}\\n")
405 endforeach()
406
407 # Remove trailing comma, and complete JSON array with ]
408 string(REGEX REPLACE ",[ ]*$" "" GCOV_FILE_COVERAGE ${GCOV_FILE_COVERAGE})
409 set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}]")
410
411 # Generate the final JSON for this file.
412 message("Generate JSON for non-gcov file: ${NOT_COVERED_SRC}...")
413 string(CONFIGURE ${SRC_FILE_TEMPLATE} FILE_JSON)
414 set(JSON_GCOV_FILES "${JSON_GCOV_FILES}${FILE_JSON}, ")
415endforeach()
416
417# Get rid of trailing comma.
418string(REGEX REPLACE ",[ ]*$" "" JSON_GCOV_FILES ${JSON_GCOV_FILES})
419set(JSON_GCOV_FILES "${JSON_GCOV_FILES}]")
420
421# Generate the final complete JSON!
422message("Generate final JSON...")
423string(CONFIGURE ${JSON_TEMPLATE} JSON)
424
425file(WRITE "${COVERALLS_OUTPUT_FILE}" "${JSON}")
426message("###########################################################################")
427message("Generated coveralls JSON containing coverage data:")
428message("${COVERALLS_OUTPUT_FILE}")
429message("###########################################################################")