1 # 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 Sderberg <joakim.soderberg (at] 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 # 40 CMAKE_MINIMUM_REQUIRED(VERSION 2.8) 41 42 43 # 44 # Make sure we have the needed arguments. 45 # 46 if (NOT COVERALLS_OUTPUT_FILE) 47 message(FATAL_ERROR "Coveralls: No coveralls output file specified. Please set COVERALLS_OUTPUT_FILE") 48 endif() 49 50 if (NOT COV_PATH) 51 message(FATAL_ERROR "Coveralls: Missing coverage directory path where gcov files will be generated. Please set COV_PATH") 52 endif() 53 54 if (NOT COVERAGE_SRCS) 55 message(FATAL_ERROR "Coveralls: Missing the list of source files that we should get the coverage data for COVERAGE_SRCS") 56 endif() 57 58 if (NOT PROJECT_ROOT) 59 message(FATAL_ERROR "Coveralls: Missing PROJECT_ROOT.") 60 endif() 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. 66 string(REGEX REPLACE "\\*" ";" COVERAGE_SRCS ${COVERAGE_SRCS}) 67 68 find_program(GCOV_EXECUTABLE gcov) 69 70 if (NOT GCOV_EXECUTABLE) 71 message(FATAL_ERROR "gcov not found! Aborting...") 72 endif() 73 74 find_package(Git) 75 76 # TODO: Add these git things to the coveralls json. 77 if (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 109 endif() 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 # 122 macro(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}") 133 endmacro() 134 135 ############################################################################## 136 137 # Get the coverage data. 138 file(GLOB_RECURSE GCDA_FILES "${COV_PATH}/*.gcda") 139 message("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. 144 foreach(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( 164 COMMAND ${GCOV_EXECUTABLE} -c -p -o ${GCDA_DIR} ${GCDA} 165 WORKING_DIRECTORY ${COV_PATH} 166 ) 167 endforeach() 168 169 # TODO: Make these be absolute path 170 file(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 # 199 set(GCOV_FILES "") 200 #message("Look in coverage sources: ${COVERAGE_SRCS}") 201 message("\nFilter out unwanted GCOV files:") 202 message("===============================") 203 204 set(COVERAGE_SRCS_REMAINING ${COVERAGE_SRCS}) 205 206 foreach (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() 229 endforeach() 230 231 # TODO: Enable setting these 232 set(JSON_SERVICE_NAME "travis-ci") 233 set(JSON_SERVICE_JOB_ID $ENV{TRAVIS_JOB_ID}) 234 235 set(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 243 set(SRC_FILE_TEMPLATE 244 "{ 245 \"name\": \"\@GCOV_SRC_REL_PATH\@\", 246 \"source_digest\": \"\@GCOV_CONTENTS_MD5\@\", 247 \"coverage\": \@GCOV_FILE_COVERAGE\@ 248 }" 249 ) 250 251 message("\nGenerate JSON for files:") 252 message("=========================") 253 254 set(JSON_GCOV_FILES "[") 255 256 # Read the GCOV files line by line and get the coverage data. 257 foreach (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}, ") 385 endforeach() 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. 389 foreach(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}, ") 415 endforeach() 416 417 # Get rid of trailing comma. 418 string(REGEX REPLACE ",[ ]*$" "" JSON_GCOV_FILES ${JSON_GCOV_FILES}) 419 set(JSON_GCOV_FILES "${JSON_GCOV_FILES}]") 420 421 # Generate the final complete JSON! 422 message("Generate final JSON...") 423 string(CONFIGURE ${JSON_TEMPLATE} JSON) 424 425 file(WRITE "${COVERALLS_OUTPUT_FILE}" "${JSON}") 426 message("###########################################################################") 427 message("Generated coveralls JSON containing coverage data:") 428 message("${COVERALLS_OUTPUT_FILE}") 429 message("###########################################################################") 430