CoverallsGenerateGcov.cmake 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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 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. #
  40. CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
  41. #
  42. # Make sure we have the needed arguments.
  43. #
  44. if (NOT COVERALLS_OUTPUT_FILE)
  45. message(FATAL_ERROR "Coveralls: No coveralls output file specified. Please set COVERALLS_OUTPUT_FILE")
  46. endif()
  47. if (NOT COV_PATH)
  48. message(FATAL_ERROR "Coveralls: Missing coverage directory path where gcov files will be generated. Please set COV_PATH")
  49. endif()
  50. if (NOT COVERAGE_SRCS)
  51. message(FATAL_ERROR "Coveralls: Missing the list of source files that we should get the coverage data for COVERAGE_SRCS")
  52. endif()
  53. if (NOT PROJECT_ROOT)
  54. message(FATAL_ERROR "Coveralls: Missing PROJECT_ROOT.")
  55. endif()
  56. # Since it's not possible to pass a CMake list properly in the
  57. # "1;2;3" format to an external process, we have replaced the
  58. # ";" with "*", so reverse that here so we get it back into the
  59. # CMake list format.
  60. string(REGEX REPLACE "\\*" ";" COVERAGE_SRCS ${COVERAGE_SRCS})
  61. find_program(GCOV_EXECUTABLE gcov)
  62. if (NOT GCOV_EXECUTABLE)
  63. message(FATAL_ERROR "gcov not found! Aborting...")
  64. endif()
  65. find_package(Git)
  66. # TODO: Add these git things to the coveralls json.
  67. if (GIT_FOUND)
  68. # Branch.
  69. execute_process(
  70. COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
  71. WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  72. OUTPUT_VARIABLE GIT_BRANCH
  73. OUTPUT_STRIP_TRAILING_WHITESPACE
  74. )
  75. macro (git_log_format FORMAT_CHARS VAR_NAME)
  76. execute_process(
  77. COMMAND ${GIT_EXECUTABLE} log -1 --pretty=format:%${FORMAT_CHARS}
  78. WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  79. OUTPUT_VARIABLE ${VAR_NAME}
  80. OUTPUT_STRIP_TRAILING_WHITESPACE
  81. )
  82. endmacro()
  83. git_log_format(an GIT_AUTHOR_EMAIL)
  84. git_log_format(ae GIT_AUTHOR_EMAIL)
  85. git_log_format(cn GIT_COMMITTER_NAME)
  86. git_log_format(ce GIT_COMMITTER_EMAIL)
  87. git_log_format(B GIT_COMMIT_MESSAGE)
  88. message("Git exe: ${GIT_EXECUTABLE}")
  89. message("Git branch: ${GIT_BRANCH}")
  90. message("Git author: ${GIT_AUTHOR_NAME}")
  91. message("Git e-mail: ${GIT_AUTHOR_EMAIL}")
  92. message("Git commiter name: ${GIT_COMMITTER_NAME}")
  93. message("Git commiter e-mail: ${GIT_COMMITTER_EMAIL}")
  94. message("Git commit message: ${GIT_COMMIT_MESSAGE}")
  95. endif()
  96. ############################# Macros #########################################
  97. #
  98. # This macro converts from the full path format gcov outputs:
  99. #
  100. # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
  101. #
  102. # to the original source file path the .gcov is for:
  103. #
  104. # /path/to/project/root/subdir/the_file.c
  105. #
  106. macro(get_source_path_from_gcov_filename _SRC_FILENAME _GCOV_FILENAME)
  107. # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
  108. # ->
  109. # #path#to#project#root#subdir#the_file.c.gcov
  110. get_filename_component(_GCOV_FILENAME_WEXT ${_GCOV_FILENAME} NAME)
  111. # #path#to#project#root#subdir#the_file.c.gcov -> /path/to/project/root/subdir/the_file.c
  112. string(REGEX REPLACE "\\.gcov$" "" SRC_FILENAME_TMP ${_GCOV_FILENAME_WEXT})
  113. string(REGEX REPLACE "\#" "/" SRC_FILENAME_TMP ${SRC_FILENAME_TMP})
  114. set(${_SRC_FILENAME} "${SRC_FILENAME_TMP}")
  115. endmacro()
  116. ##############################################################################
  117. # Get the coverage data.
  118. file(GLOB_RECURSE GCDA_FILES "${COV_PATH}/*.gcda")
  119. message("GCDA files:")
  120. # Get a list of all the object directories needed by gcov
  121. # (The directories the .gcda files and .o files are found in)
  122. # and run gcov on those.
  123. foreach(GCDA ${GCDA_FILES})
  124. message("Process: ${GCDA}")
  125. message("------------------------------------------------------------------------------")
  126. get_filename_component(GCDA_DIR ${GCDA} PATH)
  127. #
  128. # The -p below refers to "Preserve path components",
  129. # This means that the generated gcov filename of a source file will
  130. # keep the original files entire filepath, but / is replaced with #.
  131. # Example:
  132. #
  133. # /path/to/project/root/build/CMakeFiles/the_file.dir/subdir/the_file.c.gcda
  134. # ------------------------------------------------------------------------------
  135. # File '/path/to/project/root/subdir/the_file.c'
  136. # Lines executed:68.34% of 199
  137. # /path/to/project/root/subdir/the_file.c:creating '#path#to#project#root#subdir#the_file.c.gcov'
  138. #
  139. # If -p is not specified then the file is named only "the_file.c.gcov"
  140. #
  141. execute_process(
  142. COMMAND ${GCOV_EXECUTABLE} -p -o ${GCDA_DIR} ${GCDA}
  143. WORKING_DIRECTORY ${COV_PATH}
  144. )
  145. endforeach()
  146. # TODO: Make these be absolute path
  147. file(GLOB ALL_GCOV_FILES ${COV_PATH}/*.gcov)
  148. # Get only the filenames to use for filtering.
  149. #set(COVERAGE_SRCS_NAMES "")
  150. #foreach (COVSRC ${COVERAGE_SRCS})
  151. # get_filename_component(COVSRC_NAME ${COVSRC} NAME)
  152. # message("${COVSRC} -> ${COVSRC_NAME}")
  153. # list(APPEND COVERAGE_SRCS_NAMES "${COVSRC_NAME}")
  154. #endforeach()
  155. #
  156. # Filter out all but the gcov files we want.
  157. #
  158. # We do this by comparing the list of COVERAGE_SRCS filepaths that the
  159. # user wants the coverage data for with the paths of the generated .gcov files,
  160. # so that we only keep the relevant gcov files.
  161. #
  162. # Example:
  163. # COVERAGE_SRCS =
  164. # /path/to/project/root/subdir/the_file.c
  165. #
  166. # ALL_GCOV_FILES =
  167. # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
  168. # /path/to/project/root/build/#path#to#project#root#subdir#other_file.c.gcov
  169. #
  170. # Result should be:
  171. # GCOV_FILES =
  172. # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
  173. #
  174. set(GCOV_FILES "")
  175. #message("Look in coverage sources: ${COVERAGE_SRCS}")
  176. message("\nFilter out unwanted GCOV files:")
  177. message("===============================")
  178. set(COVERAGE_SRCS_REMAINING ${COVERAGE_SRCS})
  179. foreach (GCOV_FILE ${ALL_GCOV_FILES})
  180. #
  181. # /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
  182. # ->
  183. # /path/to/project/root/subdir/the_file.c
  184. get_source_path_from_gcov_filename(GCOV_SRC_PATH ${GCOV_FILE})
  185. # Is this in the list of source files?
  186. # TODO: We want to match against relative path filenames from the source file root...
  187. list(FIND COVERAGE_SRCS ${GCOV_SRC_PATH} WAS_FOUND)
  188. if (NOT WAS_FOUND EQUAL -1)
  189. message("YES: ${GCOV_FILE}")
  190. list(APPEND GCOV_FILES ${GCOV_FILE})
  191. # We remove it from the list, so we don't bother searching for it again.
  192. # Also files left in COVERAGE_SRCS_REMAINING after this loop ends should
  193. # have coverage data generated from them (no lines are covered).
  194. list(REMOVE_ITEM COVERAGE_SRCS_REMAINING ${GCOV_SRC_PATH})
  195. else()
  196. message("NO: ${GCOV_FILE}")
  197. endif()
  198. endforeach()
  199. # TODO: Enable setting these
  200. set(JSON_SERVICE_NAME "travis-ci")
  201. set(JSON_SERVICE_JOB_ID $ENV{TRAVIS_JOB_ID})
  202. set(JSON_TEMPLATE
  203. "{
  204. \"service_name\": \"\@JSON_SERVICE_NAME\@\",
  205. \"service_job_id\": \"\@JSON_SERVICE_JOB_ID\@\",
  206. \"source_files\": \@JSON_GCOV_FILES\@
  207. }"
  208. )
  209. set(SRC_FILE_TEMPLATE
  210. "{
  211. \"name\": \"\@GCOV_SRC_REL_PATH\@\",
  212. \"source\": \"\@GCOV_FILE_SOURCE\@\",
  213. \"coverage\": \@GCOV_FILE_COVERAGE\@
  214. }"
  215. )
  216. message("\nGenerate JSON for files:")
  217. message("=========================")
  218. set(JSON_GCOV_FILES "[")
  219. # Read the GCOV files line by line and get the coverage data.
  220. foreach (GCOV_FILE ${GCOV_FILES})
  221. get_source_path_from_gcov_filename(GCOV_SRC_PATH ${GCOV_FILE})
  222. file(RELATIVE_PATH GCOV_SRC_REL_PATH "${PROJECT_ROOT}" "${GCOV_SRC_PATH}")
  223. # Loads the gcov file as a list of lines.
  224. file(STRINGS ${GCOV_FILE} GCOV_LINES)
  225. # Instead of trying to parse the source from the
  226. # gcov file, simply read the file contents from the source file.
  227. # (Parsing it from the gcov is hard because C-code uses ; in many places
  228. # which also happens to be the same as the CMake list delimeter).
  229. file(READ ${GCOV_SRC_PATH} GCOV_FILE_SOURCE)
  230. string(REPLACE "\\" "\\\\" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  231. string(REGEX REPLACE "\"" "\\\\\"" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  232. string(REPLACE "\t" "\\\\t" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  233. string(REPLACE "\r" "\\\\r" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  234. string(REPLACE "\n" "\\\\n" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  235. # According to http://json.org/ these should be escaped as well.
  236. # Don't know how to do that in CMake however...
  237. #string(REPLACE "\b" "\\\\b" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  238. #string(REPLACE "\f" "\\\\f" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  239. #string(REGEX REPLACE "\u([a-fA-F0-9]{4})" "\\\\u\\1" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
  240. # We want a json array of coverage data as a single string
  241. # start building them from the contents of the .gcov
  242. set(GCOV_FILE_COVERAGE "[")
  243. foreach (GCOV_LINE ${GCOV_LINES})
  244. # Example of what we're parsing:
  245. # Hitcount |Line | Source
  246. # " 8: 26: if (!allowed || (strlen(allowed) == 0))"
  247. string(REGEX REPLACE
  248. "^([^:]*):([^:]*):(.*)$"
  249. "\\1;\\2;\\3"
  250. RES
  251. "${GCOV_LINE}")
  252. list(LENGTH RES RES_COUNT)
  253. if (RES_COUNT GREATER 2)
  254. list(GET RES 0 HITCOUNT)
  255. list(GET RES 1 LINE)
  256. list(GET RES 2 SOURCE)
  257. string(STRIP ${HITCOUNT} HITCOUNT)
  258. string(STRIP ${LINE} LINE)
  259. # Lines with 0 line numbers are metadata and can be ignored.
  260. if (NOT ${LINE} EQUAL 0)
  261. # Translate the hitcount into valid JSON values.
  262. if (${HITCOUNT} STREQUAL "#####")
  263. set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}0, ")
  264. elseif (${HITCOUNT} STREQUAL "-")
  265. set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}null, ")
  266. else()
  267. set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}${HITCOUNT}, ")
  268. endif()
  269. # TODO: Look for LCOV_EXCL_LINE in SOURCE to get rid of false positives.
  270. endif()
  271. else()
  272. message(WARNING "Failed to properly parse line --> ${GCOV_LINE}")
  273. endif()
  274. endforeach()
  275. # Advanced way of removing the trailing comma in the JSON array.
  276. # "[1, 2, 3, " -> "[1, 2, 3"
  277. string(REGEX REPLACE ",[ ]*$" "" GCOV_FILE_COVERAGE ${GCOV_FILE_COVERAGE})
  278. # Append the trailing ] to complete the JSON array.
  279. set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}]")
  280. # Generate the final JSON for this file.
  281. message("Generate JSON for file: ${GCOV_SRC_REL_PATH}...")
  282. string(CONFIGURE ${SRC_FILE_TEMPLATE} FILE_JSON)
  283. set(JSON_GCOV_FILES "${JSON_GCOV_FILES}${FILE_JSON}, ")
  284. endforeach()
  285. # Loop through all files we couldn't find any coverage for
  286. # as well, and generate JSON for those as well with 0% coverage.
  287. foreach(NOT_COVERED_SRC ${COVERAGE_SRCS_REMAINING})
  288. # Loads the source file as a list of lines.
  289. file(STRINGS ${NOT_COVERED_SRC} SRC_LINES)
  290. set(GCOV_FILE_COVERAGE "[")
  291. set(GCOV_FILE_SOURCE "")
  292. foreach (SOURCE ${SRC_LINES})
  293. set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}0, ")
  294. string(REPLACE "\\" "\\\\" SOURCE "${SOURCE}")
  295. string(REGEX REPLACE "\"" "\\\\\"" SOURCE "${SOURCE}")
  296. string(REPLACE "\t" "\\\\t" SOURCE "${SOURCE}")
  297. string(REPLACE "\r" "\\\\r" SOURCE "${SOURCE}")
  298. set(GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}${SOURCE}\\n")
  299. endforeach()
  300. # Remove trailing comma, and complete JSON array with ]
  301. string(REGEX REPLACE ",[ ]*$" "" GCOV_FILE_COVERAGE ${GCOV_FILE_COVERAGE})
  302. set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}]")
  303. # Generate the final JSON for this file.
  304. message("Generate JSON for non-gcov file: ${NOT_COVERED_SRC}...")
  305. string(CONFIGURE ${SRC_FILE_TEMPLATE} FILE_JSON)
  306. set(JSON_GCOV_FILES "${JSON_GCOV_FILES}${FILE_JSON}, ")
  307. endforeach()
  308. # Get rid of trailing comma.
  309. string(REGEX REPLACE ",[ ]*$" "" JSON_GCOV_FILES ${JSON_GCOV_FILES})
  310. set(JSON_GCOV_FILES "${JSON_GCOV_FILES}]")
  311. # Generate the final complete JSON!
  312. message("Generate final JSON...")
  313. string(CONFIGURE ${JSON_TEMPLATE} JSON)
  314. file(WRITE "${COVERALLS_OUTPUT_FILE}" "${JSON}")
  315. message("###########################################################################")
  316. message("Generated coveralls JSON containing coverage data:")
  317. message("${COVERALLS_OUTPUT_FILE}")
  318. message("###########################################################################")