summaryrefslogtreecommitdiff
path: root/src/mesh/assimp-master/cmake-modules/CoverallsGenerateGcov.cmake
blob: 1047371642ca0eed4f16c31d510aaeb489ab0427 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Copyright (C) 2014 Joakim Söderberg <joakim.soderberg@gmail.com>
#
# This is intended to be run by a custom target in a CMake project like this.
# 0. Compile program with coverage support.
# 1. Clear coverage data. (Recursively delete *.gcda in build dir)
# 2. Run the unit tests.
# 3. Run this script specifying which source files the coverage should be performed on.
#
# This script will then use gcov to generate .gcov files in the directory specified
# via the COV_PATH var. This should probably be the same as your cmake build dir.
#
# It then parses the .gcov files to convert them into the Coveralls JSON format:
# https://coveralls.io/docs/api
#
# Example for running as standalone CMake script from the command line:
# (Note it is important the -P is at the end...)
# $ cmake -DCOV_PATH=$(pwd)
#         -DCOVERAGE_SRCS="catcierge_rfid.c;catcierge_timer.c"
#         -P ../cmake/CoverallsGcovUpload.cmake
#
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)


#
# Make sure we have the needed arguments.
#
if (NOT COVERALLS_OUTPUT_FILE)
	message(FATAL_ERROR "Coveralls: No coveralls output file specified. Please set COVERALLS_OUTPUT_FILE")
endif()

if (NOT COV_PATH)
	message(FATAL_ERROR "Coveralls: Missing coverage directory path where gcov files will be generated. Please set COV_PATH")
endif()

if (NOT COVERAGE_SRCS)
	message(FATAL_ERROR "Coveralls: Missing the list of source files that we should get the coverage data for COVERAGE_SRCS")
endif()

if (NOT PROJECT_ROOT)
	message(FATAL_ERROR "Coveralls: Missing PROJECT_ROOT.")
endif()

# Since it's not possible to pass a CMake list properly in the
# "1;2;3" format to an external process, we have replaced the
# ";" with "*", so reverse that here so we get it back into the
# CMake list format.
string(REGEX REPLACE "\\*" ";" COVERAGE_SRCS ${COVERAGE_SRCS})

if (NOT DEFINED ENV{GCOV})
  find_program(GCOV_EXECUTABLE gcov)
else()
  find_program(GCOV_EXECUTABLE $ENV{GCOV})
endif()

# convert all paths in COVERAGE_SRCS to absolute paths
set(COVERAGE_SRCS_TMP "")
foreach (COVERAGE_SRC ${COVERAGE_SRCS})
	if (NOT "${COVERAGE_SRC}" MATCHES "^/")
		set(COVERAGE_SRC ${PROJECT_ROOT}/${COVERAGE_SRC})
	endif()
	list(APPEND COVERAGE_SRCS_TMP ${COVERAGE_SRC})
endforeach()
set(COVERAGE_SRCS ${COVERAGE_SRCS_TMP})
unset(COVERAGE_SRCS_TMP)

if (NOT GCOV_EXECUTABLE)
	message(FATAL_ERROR "gcov not found! Aborting...")
endif()

find_package(Git)

set(JSON_REPO_TEMPLATE
  "{
    \"head\": {
      \"id\": \"\@GIT_COMMIT_HASH\@\",
      \"author_name\": \"\@GIT_AUTHOR_NAME\@\",
      \"author_email\": \"\@GIT_AUTHOR_EMAIL\@\",
      \"committer_name\": \"\@GIT_COMMITTER_NAME\@\",
      \"committer_email\": \"\@GIT_COMMITTER_EMAIL\@\",
      \"message\": \"\@GIT_COMMIT_MESSAGE\@\"
    },
    \"branch\": \"@GIT_BRANCH@\",
    \"remotes\": []
  }"
)

# TODO: Fill in git remote data
if (GIT_FOUND)
	# Branch.
	execute_process(
		COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
		WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
		OUTPUT_VARIABLE GIT_BRANCH
		OUTPUT_STRIP_TRAILING_WHITESPACE
	)

	macro (git_log_format FORMAT_CHARS VAR_NAME)
		execute_process(
			COMMAND ${GIT_EXECUTABLE} log -1 --pretty=format:%${FORMAT_CHARS}
			WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
			OUTPUT_VARIABLE ${VAR_NAME}
			OUTPUT_STRIP_TRAILING_WHITESPACE
		)
	endmacro()

	git_log_format(an GIT_AUTHOR_NAME)
	git_log_format(ae GIT_AUTHOR_EMAIL)
	git_log_format(cn GIT_COMMITTER_NAME)
	git_log_format(ce GIT_COMMITTER_EMAIL)
	git_log_format(B GIT_COMMIT_MESSAGE)
	git_log_format(H GIT_COMMIT_HASH)

	if(GIT_COMMIT_MESSAGE)
		string(REPLACE "\n" "\\n" GIT_COMMIT_MESSAGE ${GIT_COMMIT_MESSAGE})
	endif()

	message("Git exe: ${GIT_EXECUTABLE}")
	message("Git branch: ${GIT_BRANCH}")
	message("Git author: ${GIT_AUTHOR_NAME}")
	message("Git e-mail: ${GIT_AUTHOR_EMAIL}")
	message("Git commiter name: ${GIT_COMMITTER_NAME}")
	message("Git commiter e-mail: ${GIT_COMMITTER_EMAIL}")
	message("Git commit hash: ${GIT_COMMIT_HASH}")
	message("Git commit message: ${GIT_COMMIT_MESSAGE}")

	string(CONFIGURE ${JSON_REPO_TEMPLATE} JSON_REPO_DATA)
else()
	set(JSON_REPO_DATA "{}")
endif()

############################# Macros #########################################

#
# This macro converts from the full path format gcov outputs:
#
#    /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
#
# to the original source file path the .gcov is for:
#
#   /path/to/project/root/subdir/the_file.c
#
macro(get_source_path_from_gcov_filename _SRC_FILENAME _GCOV_FILENAME)

	# /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
	# ->
	# #path#to#project#root#subdir#the_file.c.gcov
	get_filename_component(_GCOV_FILENAME_WEXT ${_GCOV_FILENAME} NAME)

	# #path#to#project#root#subdir#the_file.c.gcov -> /path/to/project/root/subdir/the_file.c
	string(REGEX REPLACE "\\.gcov$" "" SRC_FILENAME_TMP ${_GCOV_FILENAME_WEXT})
	string(REGEX REPLACE "\#" "/" SRC_FILENAME_TMP ${SRC_FILENAME_TMP})
	set(${_SRC_FILENAME} "${SRC_FILENAME_TMP}")
endmacro()

##############################################################################

# Get the coverage data.
file(GLOB_RECURSE GCDA_FILES "${COV_PATH}/*.gcda")
message("GCDA files:")

# Get a list of all the object directories needed by gcov
# (The directories the .gcda files and .o files are found in)
# and run gcov on those.
foreach(GCDA ${GCDA_FILES})
	message("Process: ${GCDA}")
	message("------------------------------------------------------------------------------")
	get_filename_component(GCDA_DIR ${GCDA} PATH)

	#
	# The -p below refers to "Preserve path components",
	# This means that the generated gcov filename of a source file will
	# keep the original files entire filepath, but / is replaced with #.
	# Example:
	#
	# /path/to/project/root/build/CMakeFiles/the_file.dir/subdir/the_file.c.gcda
	# ------------------------------------------------------------------------------
	# File '/path/to/project/root/subdir/the_file.c'
	# Lines executed:68.34% of 199
	# /path/to/project/root/subdir/the_file.c:creating '#path#to#project#root#subdir#the_file.c.gcov'
	#
	# If -p is not specified then the file is named only "the_file.c.gcov"
	#
	execute_process(
		COMMAND ${GCOV_EXECUTABLE} -p -o ${GCDA_DIR} ${GCDA}
		WORKING_DIRECTORY ${COV_PATH}
	)
endforeach()

# TODO: Make these be absolute path
file(GLOB ALL_GCOV_FILES ${COV_PATH}/*.gcov)

# Get only the filenames to use for filtering.
#set(COVERAGE_SRCS_NAMES "")
#foreach (COVSRC ${COVERAGE_SRCS})
#	get_filename_component(COVSRC_NAME ${COVSRC} NAME)
#	message("${COVSRC} -> ${COVSRC_NAME}")
#	list(APPEND COVERAGE_SRCS_NAMES "${COVSRC_NAME}")
#endforeach()

#
# Filter out all but the gcov files we want.
#
# We do this by comparing the list of COVERAGE_SRCS filepaths that the
# user wants the coverage data for with the paths of the generated .gcov files,
# so that we only keep the relevant gcov files.
#
# Example:
# COVERAGE_SRCS =
#				/path/to/project/root/subdir/the_file.c
#
# ALL_GCOV_FILES =
#				/path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
#				/path/to/project/root/build/#path#to#project#root#subdir#other_file.c.gcov
#
# Result should be:
# GCOV_FILES =
#				/path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
#
set(GCOV_FILES "")
#message("Look in coverage sources: ${COVERAGE_SRCS}")
message("\nFilter out unwanted GCOV files:")
message("===============================")

set(COVERAGE_SRCS_REMAINING ${COVERAGE_SRCS})

foreach (GCOV_FILE ${ALL_GCOV_FILES})

	#
	# /path/to/project/root/build/#path#to#project#root#subdir#the_file.c.gcov
	# ->
	# /path/to/project/root/subdir/the_file.c
	get_source_path_from_gcov_filename(GCOV_SRC_PATH ${GCOV_FILE})
	file(RELATIVE_PATH GCOV_SRC_REL_PATH "${PROJECT_ROOT}" "${GCOV_SRC_PATH}")

	# Is this in the list of source files?
	# TODO: We want to match against relative path filenames from the source file root...
	list(FIND COVERAGE_SRCS ${GCOV_SRC_PATH} WAS_FOUND)

	if (NOT WAS_FOUND EQUAL -1)
		message("YES: ${GCOV_FILE}")
		list(APPEND GCOV_FILES ${GCOV_FILE})

		# We remove it from the list, so we don't bother searching for it again.
		# Also files left in COVERAGE_SRCS_REMAINING after this loop ends should
		# have coverage data generated from them (no lines are covered).
		list(REMOVE_ITEM COVERAGE_SRCS_REMAINING ${GCOV_SRC_PATH})
	else()
		message("NO:  ${GCOV_FILE}")
	endif()
endforeach()

# TODO: Enable setting these
set(JSON_SERVICE_NAME "travis-ci")
set(JSON_SERVICE_JOB_ID $ENV{TRAVIS_JOB_ID})
set(JSON_REPO_TOKEN $ENV{COVERALLS_REPO_TOKEN})

set(JSON_TEMPLATE
"{
  \"repo_token\": \"\@JSON_REPO_TOKEN\@\",
  \"service_name\": \"\@JSON_SERVICE_NAME\@\",
  \"service_job_id\": \"\@JSON_SERVICE_JOB_ID\@\",
  \"source_files\": \@JSON_GCOV_FILES\@,
  \"git\": \@JSON_REPO_DATA\@
}"
)

set(SRC_FILE_TEMPLATE
"{
      \"name\": \"\@GCOV_SRC_REL_PATH\@\",
      \"source_digest\": \"\@GCOV_CONTENTS_MD5\@\",
      \"coverage\": \@GCOV_FILE_COVERAGE\@
  }"
)

message("\nGenerate JSON for files:")
message("=========================")

set(JSON_GCOV_FILES "[")

# Read the GCOV files line by line and get the coverage data.
foreach (GCOV_FILE ${GCOV_FILES})

	get_source_path_from_gcov_filename(GCOV_SRC_PATH ${GCOV_FILE})
	file(RELATIVE_PATH GCOV_SRC_REL_PATH "${PROJECT_ROOT}" "${GCOV_SRC_PATH}")

	# The new coveralls API doesn't need the entire source (Yay!)
	# However, still keeping that part for now. Will cleanup in the future.
	file(MD5 "${GCOV_SRC_PATH}" GCOV_CONTENTS_MD5)
	message("MD5: ${GCOV_SRC_PATH} = ${GCOV_CONTENTS_MD5}")

	# Loads the gcov file as a list of lines.
	# (We first open the file and replace all occurrences of [] with _
	#  because CMake will fail to parse a line containing unmatched brackets...
	#  also the \ to escaped \n in macros screws up things.)
	# https://public.kitware.com/Bug/view.php?id=15369
	file(READ ${GCOV_FILE} GCOV_CONTENTS)
	string(REPLACE "[" "_" GCOV_CONTENTS "${GCOV_CONTENTS}")
	string(REPLACE "]" "_" GCOV_CONTENTS "${GCOV_CONTENTS}")
	string(REPLACE "\\" "_" GCOV_CONTENTS "${GCOV_CONTENTS}")

	# Remove file contents to avoid encoding issues (cmake 2.8 has no ENCODING option)
	string(REGEX REPLACE "([^:]*):([^:]*):([^\n]*)\n" "\\1:\\2: \n" GCOV_CONTENTS "${GCOV_CONTENTS}")
	file(WRITE ${GCOV_FILE}_tmp "${GCOV_CONTENTS}")

	file(STRINGS ${GCOV_FILE}_tmp GCOV_LINES)
	list(LENGTH GCOV_LINES LINE_COUNT)

	# Instead of trying to parse the source from the
	# gcov file, simply read the file contents from the source file.
	# (Parsing it from the gcov is hard because C-code uses ; in many places
	#  which also happens to be the same as the CMake list delimiter).
	file(READ ${GCOV_SRC_PATH} GCOV_FILE_SOURCE)

	string(REPLACE "\\" "\\\\" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	string(REGEX REPLACE "\"" "\\\\\"" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	string(REPLACE "\t" "\\\\t" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	string(REPLACE "\r" "\\\\r" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	string(REPLACE "\n" "\\\\n" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	# According to http://json.org/ these should be escaped as well.
	# Don't know how to do that in CMake however...
	#string(REPLACE "\b" "\\\\b" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	#string(REPLACE "\f" "\\\\f" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")
	#string(REGEX REPLACE "\u([a-fA-F0-9]{4})" "\\\\u\\1" GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}")

	# We want a json array of coverage data as a single string
	# start building them from the contents of the .gcov
	set(GCOV_FILE_COVERAGE "[")

	set(GCOV_LINE_COUNT 1) # Line number for the .gcov.
	set(DO_SKIP 0)
	foreach (GCOV_LINE ${GCOV_LINES})
		#message("${GCOV_LINE}")
		# Example of what we're parsing:
		# Hitcount  |Line | Source
		# "        8:   26:        if (!allowed || (strlen(allowed) == 0))"
		string(REGEX REPLACE
			"^([^:]*):([^:]*):(.*)$"
			"\\1;\\2;\\3"
			RES
			"${GCOV_LINE}")

		# Check if we should exclude lines using the Lcov syntax.
		string(REGEX MATCH "LCOV_EXCL_START" START_SKIP "${GCOV_LINE}")
		string(REGEX MATCH "LCOV_EXCL_END" END_SKIP "${GCOV_LINE}")
		string(REGEX MATCH "LCOV_EXCL_LINE" LINE_SKIP "${GCOV_LINE}")

		set(RESET_SKIP 0)
		if (LINE_SKIP AND NOT DO_SKIP)
			set(DO_SKIP 1)
			set(RESET_SKIP 1)
		endif()

		if (START_SKIP)
			set(DO_SKIP 1)
			message("${GCOV_LINE_COUNT}: Start skip")
		endif()

		if (END_SKIP)
			set(DO_SKIP 0)
		endif()

		list(LENGTH RES RES_COUNT)

		if (RES_COUNT GREATER 2)
			list(GET RES 0 HITCOUNT)
			list(GET RES 1 LINE)
			list(GET RES 2 SOURCE)

			string(STRIP ${HITCOUNT} HITCOUNT)
			string(STRIP ${LINE} LINE)

			# Lines with 0 line numbers are metadata and can be ignored.
			if (NOT ${LINE} EQUAL 0)

				if (DO_SKIP)
					set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}null, ")
				else()
					# Translate the hitcount into valid JSON values.
					if (${HITCOUNT} STREQUAL "#####" OR ${HITCOUNT} STREQUAL "=====")
						set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}0, ")
					elseif (${HITCOUNT} STREQUAL "-")
						set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}null, ")
					else()
						set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}${HITCOUNT}, ")
					endif()
				endif()
			endif()
		else()
			message(WARNING "Failed to properly parse line (RES_COUNT = ${RES_COUNT}) ${GCOV_FILE}:${GCOV_LINE_COUNT}\n-->${GCOV_LINE}")
		endif()

		if (RESET_SKIP)
			set(DO_SKIP 0)
		endif()
		math(EXPR GCOV_LINE_COUNT "${GCOV_LINE_COUNT}+1")
	endforeach()

	message("${GCOV_LINE_COUNT} of ${LINE_COUNT} lines read!")

	# Advanced way of removing the trailing comma in the JSON array.
	# "[1, 2, 3, " -> "[1, 2, 3"
	string(REGEX REPLACE ",[ ]*$" "" GCOV_FILE_COVERAGE ${GCOV_FILE_COVERAGE})

	# Append the trailing ] to complete the JSON array.
	set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}]")

	# Generate the final JSON for this file.
	message("Generate JSON for file: ${GCOV_SRC_REL_PATH}...")
	string(CONFIGURE ${SRC_FILE_TEMPLATE} FILE_JSON)

	set(JSON_GCOV_FILES "${JSON_GCOV_FILES}${FILE_JSON}, ")
endforeach()

# Loop through all files we couldn't find any coverage for
# as well, and generate JSON for those as well with 0% coverage.
foreach(NOT_COVERED_SRC ${COVERAGE_SRCS_REMAINING})

	# Set variables for json replacement
	set(GCOV_SRC_PATH ${NOT_COVERED_SRC})
	file(MD5 "${GCOV_SRC_PATH}" GCOV_CONTENTS_MD5)
	file(RELATIVE_PATH GCOV_SRC_REL_PATH "${PROJECT_ROOT}" "${GCOV_SRC_PATH}")

	# Loads the source file as a list of lines.
	file(STRINGS ${NOT_COVERED_SRC} SRC_LINES)

	set(GCOV_FILE_COVERAGE "[")
	set(GCOV_FILE_SOURCE "")

	foreach (SOURCE ${SRC_LINES})
		set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}null, ")

		string(REPLACE "\\" "\\\\" SOURCE "${SOURCE}")
		string(REGEX REPLACE "\"" "\\\\\"" SOURCE "${SOURCE}")
		string(REPLACE "\t" "\\\\t" SOURCE "${SOURCE}")
		string(REPLACE "\r" "\\\\r" SOURCE "${SOURCE}")
		set(GCOV_FILE_SOURCE "${GCOV_FILE_SOURCE}${SOURCE}\\n")
	endforeach()

	# Remove trailing comma, and complete JSON array with ]
	string(REGEX REPLACE ",[ ]*$" "" GCOV_FILE_COVERAGE ${GCOV_FILE_COVERAGE})
	set(GCOV_FILE_COVERAGE "${GCOV_FILE_COVERAGE}]")

	# Generate the final JSON for this file.
	message("Generate JSON for non-gcov file: ${NOT_COVERED_SRC}...")
	string(CONFIGURE ${SRC_FILE_TEMPLATE} FILE_JSON)
	set(JSON_GCOV_FILES "${JSON_GCOV_FILES}${FILE_JSON}, ")
endforeach()

# Get rid of trailing comma.
string(REGEX REPLACE ",[ ]*$" "" JSON_GCOV_FILES ${JSON_GCOV_FILES})
set(JSON_GCOV_FILES "${JSON_GCOV_FILES}]")

# Generate the final complete JSON!
message("Generate final JSON...")
string(CONFIGURE ${JSON_TEMPLATE} JSON)

file(WRITE "${COVERALLS_OUTPUT_FILE}" "${JSON}")
message("###########################################################################")
message("Generated coveralls JSON containing coverage data:")
message("${COVERALLS_OUTPUT_FILE}")
message("###########################################################################")