Home | History | Annotate | Download | only in cmake
      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