You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

gcov_coveralls.py 6.9KB

Fix coverage (#2603) * Add utility to prepare C coverage for upload to coveralls.io It turned out that it is more easy to write own script then debug and fix coveralls utility (https://github.com/eddyxu/cpp-coveralls). gcov-coveralls.py can be used as a replacement for coveralls. * Save coverage data from .gcda files only once Coverage data in .gcda files is merged after each binary invocation, so we can run all test and then gather coverage data. If we dump them two times execution counts will be more then they be. * Switch from coveralls (cpp-coveralls) to own script Problem with coveralls was, that coverage for source files outside build directory was not added to the report. * Add tool to dump info from json for coveralls.io * Add debug * Fix: don't die if there is no service_job_id in json * Debug * Fix dump_coveralls.py * Rename to gcov_coveralls.py (s/-/_/) For most files in this repo '_' is used as separator. * Don't add source code to coveralls JSON According to https://docs.coveralls.io/api-introduction Coverals don't need source code, only MD5 digest to tracks changes. Anyway source code is already added by luacov-coveralls and source_digest is added by cpp-coveralls and gcov_coveralls.py Both options seems to work for now. * Provide path to source directory to merge_coveralls.py merge_coveralls.py has code to filter files and remove prefixes. When --root points to source directory merge_coveralls.py can strip prefix from absolute path in JSONs generated by luacov-coveralls. * Style Don't add parameters with default values. * Make --output optional It useful mainly for debugging. We can send report without saving it. * Log CI_COMMIT_AUTHOR env var It is not clear from drone.io source how CI_COMMIT_AUTHOR variable is set. Log it to see what it means. * Move merge_coveralls.py to test/tools This script is used not only for funcional test coverage, but for rspamd-test coverage too. * Remove debug * Style Use more compact formatting. * Write comment about parallel tests running [SKIP CI] Document why running tests in parallel may be bad idea (but still do so). * Fix typo [SKIP CI]
5 years ago
Fix coverage (#2603) * Add utility to prepare C coverage for upload to coveralls.io It turned out that it is more easy to write own script then debug and fix coveralls utility (https://github.com/eddyxu/cpp-coveralls). gcov-coveralls.py can be used as a replacement for coveralls. * Save coverage data from .gcda files only once Coverage data in .gcda files is merged after each binary invocation, so we can run all test and then gather coverage data. If we dump them two times execution counts will be more then they be. * Switch from coveralls (cpp-coveralls) to own script Problem with coveralls was, that coverage for source files outside build directory was not added to the report. * Add tool to dump info from json for coveralls.io * Add debug * Fix: don't die if there is no service_job_id in json * Debug * Fix dump_coveralls.py * Rename to gcov_coveralls.py (s/-/_/) For most files in this repo '_' is used as separator. * Don't add source code to coveralls JSON According to https://docs.coveralls.io/api-introduction Coverals don't need source code, only MD5 digest to tracks changes. Anyway source code is already added by luacov-coveralls and source_digest is added by cpp-coveralls and gcov_coveralls.py Both options seems to work for now. * Provide path to source directory to merge_coveralls.py merge_coveralls.py has code to filter files and remove prefixes. When --root points to source directory merge_coveralls.py can strip prefix from absolute path in JSONs generated by luacov-coveralls. * Style Don't add parameters with default values. * Make --output optional It useful mainly for debugging. We can send report without saving it. * Log CI_COMMIT_AUTHOR env var It is not clear from drone.io source how CI_COMMIT_AUTHOR variable is set. Log it to see what it means. * Move merge_coveralls.py to test/tools This script is used not only for funcional test coverage, but for rspamd-test coverage too. * Remove debug * Style Use more compact formatting. * Write comment about parallel tests running [SKIP CI] Document why running tests in parallel may be bad idea (but still do so). * Fix typo [SKIP CI]
5 years ago
Fix coverage (#2603) * Add utility to prepare C coverage for upload to coveralls.io It turned out that it is more easy to write own script then debug and fix coveralls utility (https://github.com/eddyxu/cpp-coveralls). gcov-coveralls.py can be used as a replacement for coveralls. * Save coverage data from .gcda files only once Coverage data in .gcda files is merged after each binary invocation, so we can run all test and then gather coverage data. If we dump them two times execution counts will be more then they be. * Switch from coveralls (cpp-coveralls) to own script Problem with coveralls was, that coverage for source files outside build directory was not added to the report. * Add tool to dump info from json for coveralls.io * Add debug * Fix: don't die if there is no service_job_id in json * Debug * Fix dump_coveralls.py * Rename to gcov_coveralls.py (s/-/_/) For most files in this repo '_' is used as separator. * Don't add source code to coveralls JSON According to https://docs.coveralls.io/api-introduction Coverals don't need source code, only MD5 digest to tracks changes. Anyway source code is already added by luacov-coveralls and source_digest is added by cpp-coveralls and gcov_coveralls.py Both options seems to work for now. * Provide path to source directory to merge_coveralls.py merge_coveralls.py has code to filter files and remove prefixes. When --root points to source directory merge_coveralls.py can strip prefix from absolute path in JSONs generated by luacov-coveralls. * Style Don't add parameters with default values. * Make --output optional It useful mainly for debugging. We can send report without saving it. * Log CI_COMMIT_AUTHOR env var It is not clear from drone.io source how CI_COMMIT_AUTHOR variable is set. Log it to see what it means. * Move merge_coveralls.py to test/tools This script is used not only for funcional test coverage, but for rspamd-test coverage too. * Remove debug * Style Use more compact formatting. * Write comment about parallel tests running [SKIP CI] Document why running tests in parallel may be bad idea (but still do so). * Fix typo [SKIP CI]
5 years ago
Fix coverage (#2603) * Add utility to prepare C coverage for upload to coveralls.io It turned out that it is more easy to write own script then debug and fix coveralls utility (https://github.com/eddyxu/cpp-coveralls). gcov-coveralls.py can be used as a replacement for coveralls. * Save coverage data from .gcda files only once Coverage data in .gcda files is merged after each binary invocation, so we can run all test and then gather coverage data. If we dump them two times execution counts will be more then they be. * Switch from coveralls (cpp-coveralls) to own script Problem with coveralls was, that coverage for source files outside build directory was not added to the report. * Add tool to dump info from json for coveralls.io * Add debug * Fix: don't die if there is no service_job_id in json * Debug * Fix dump_coveralls.py * Rename to gcov_coveralls.py (s/-/_/) For most files in this repo '_' is used as separator. * Don't add source code to coveralls JSON According to https://docs.coveralls.io/api-introduction Coverals don't need source code, only MD5 digest to tracks changes. Anyway source code is already added by luacov-coveralls and source_digest is added by cpp-coveralls and gcov_coveralls.py Both options seems to work for now. * Provide path to source directory to merge_coveralls.py merge_coveralls.py has code to filter files and remove prefixes. When --root points to source directory merge_coveralls.py can strip prefix from absolute path in JSONs generated by luacov-coveralls. * Style Don't add parameters with default values. * Make --output optional It useful mainly for debugging. We can send report without saving it. * Log CI_COMMIT_AUTHOR env var It is not clear from drone.io source how CI_COMMIT_AUTHOR variable is set. Log it to see what it means. * Move merge_coveralls.py to test/tools This script is used not only for funcional test coverage, but for rspamd-test coverage too. * Remove debug * Style Use more compact formatting. * Write comment about parallel tests running [SKIP CI] Document why running tests in parallel may be bad idea (but still do so). * Fix typo [SKIP CI]
5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #!/usr/bin/env python3
  2. """
  3. Script to save coverage info for C source files in JSON for coveralls.io
  4. When C code compiled with --coverage flag, for each object files *.gcno is
  5. generated, it contains information to reconstruct the basic block graphs and
  6. assign source line numbers to blocks
  7. When binary executed *.gcda file is written on exit, with same base name as
  8. corresponding *.gcno file. It contains some summary information, counters, e.t.c.
  9. gcov(1) utility can be used to get information from *.gcda file and write text
  10. reports to *.gocov file (one file for each source file from which object was compiled).
  11. The script finds *.gcno files, uses gcov to generate *.gcov files, parses them
  12. and accomulates statistics for all source files.
  13. This script was written with quite a few assumptions:
  14. * Code was build using absolute path to source directory (and absolute path
  15. stored in object file debug sylmbols).
  16. * Current directory is writable and there is no useful *.gcov files in it
  17. (becase they will be deleted).
  18. * Object file has same base name as *.gcno file (e. g. foo.c.gcno and foo.c.o).
  19. This is the case for cmake builds, but probably not for other build systems
  20. * Source file names contain only ASCII characters.
  21. """
  22. import argparse
  23. from collections import defaultdict
  24. from glob import glob
  25. import hashlib
  26. import json
  27. import os
  28. from os.path import isabs, join, normpath, relpath
  29. import os.path
  30. import subprocess
  31. import sys
  32. def warn(*args, **kwargs):
  33. print(*args, file=sys.stderr, **kwargs)
  34. def parse_gcov_file(gcov_file):
  35. """Parses the content of .gcov file written by gcov -i
  36. Returns:
  37. str: Source file name
  38. dict: coverage info { line_number: hits }
  39. """
  40. count = {}
  41. with open(gcov_file) as fh:
  42. for line in fh:
  43. tag, value = line.split(':')
  44. if tag == 'file':
  45. src_file = value.rstrip()
  46. elif tag == 'lcount':
  47. line_num, exec_count = value.split(',')
  48. count[int(line_num)] = int(exec_count)
  49. return src_file, count
  50. def run_gcov(filename, coverage, args):
  51. """ * run gcov on given file
  52. * parse generated .gcov files and update coverage structure
  53. * store source file md5 (if not yet stored)
  54. * delete .gcov files
  55. """
  56. if args.verbose:
  57. warn("calling:", 'gcov', '-i', filename)
  58. stdout = None
  59. else:
  60. # gcov is noisy and don't have quit flag so redirect stdout to /dev/null
  61. stdout = subprocess.DEVNULL
  62. subprocess.check_call(['gcov', '-i', filename], stdout=stdout)
  63. for gcov_file in glob('*.gcov'):
  64. if args.verbose:
  65. warn('parsing', gcov_file)
  66. src_file, count = parse_gcov_file(gcov_file)
  67. os.remove(gcov_file)
  68. if src_file not in coverage:
  69. coverage[src_file] = defaultdict(int, count)
  70. else:
  71. # sum execution counts
  72. for line, exe_cnt in count.items():
  73. coverage[src_file][line] += exe_cnt
  74. def main():
  75. parser = argparse.ArgumentParser(
  76. description='Save gcov coverage results in JSON file for coveralls.io.')
  77. parser.add_argument(
  78. '-v',
  79. '--verbose',
  80. action="store_true",
  81. help='Display additional informaton and gcov command output.')
  82. parser.add_argument(
  83. '-e',
  84. '--exclude',
  85. action='append',
  86. metavar='DIR',
  87. help=
  88. ("Don't look for .gcno/.gcda files in this directories (repeat option to skip several directories). "
  89. "Path is relative to the dirictory where script was started, e. g. '.git'"))
  90. parser.add_argument(
  91. '-p',
  92. '--prefix',
  93. action='append',
  94. help=
  95. ("Strip this prefix from absolute path to source file. "
  96. "If this option is provided, then only files with given prefixex in absolute path "
  97. "will be added to coverage (option can be repeated)."))
  98. parser.add_argument(
  99. '--out',
  100. type=argparse.FileType('w'),
  101. required=True,
  102. metavar='FILE',
  103. help='Save JSON payload to this file')
  104. args = parser.parse_args()
  105. # ensure that there is no unrelated .gcov files in current directory
  106. for gcov_file in glob('*.gcov'):
  107. os.remove(gcov_file)
  108. warn("Warning: {} deleted".format(gcov_file))
  109. # dict { src_file_name: {line1: exec_count1, line2: exec_count2, ...} }
  110. coverage = {}
  111. # find . -name '*.gcno' (respecting args.exclude)
  112. for root, dirs, files in os.walk('.'):
  113. for f in files:
  114. # Usually gcov called with a source file as an argument, but this
  115. # name used only to find .gcno and .gcda files. To find source
  116. # file information from debug symbols is used. So we can call gcov
  117. # on .gcno file.
  118. if f.endswith('.gcno'):
  119. run_gcov(join(root, f), coverage, args)
  120. # don't look into excluded dirs
  121. for subdir in dirs:
  122. # path relative to start dir
  123. path = normpath(join(root, subdir))
  124. if path in args.exclude:
  125. if args.verbose:
  126. warn('directory "{}" excluded'.format(path))
  127. dirs.remove(subdir)
  128. # prepare JSON pyload for coveralls.io API
  129. # https://docs.coveralls.io/api-introduction
  130. coveralls_data = {'source_files': []}
  131. for src_file in coverage:
  132. # filter by prefix and save path with stripped prefix
  133. src_file_rel = src_file
  134. if args.prefix and isabs(src_file):
  135. for prefix in args.prefix:
  136. if src_file.startswith(prefix):
  137. src_file_rel = relpath(src_file, start=prefix)
  138. break
  139. else:
  140. # skip file outside given prefixes
  141. # it can be e. g. library include file
  142. if args.verbose:
  143. warn('file "{}" is not mathced by prefix, skipping'.format(src_file))
  144. continue
  145. try:
  146. with open(src_file, mode='rb') as fh:
  147. line_count = sum(1 for _ in fh)
  148. fh.seek(0)
  149. md5 = hashlib.md5(fh.read()).hexdigest()
  150. except OSError as err:
  151. # skip files for which source file is not available
  152. warn(err, 'not adding to coverage')
  153. continue
  154. coverage_array = [None] * line_count
  155. for line_num, exe_cnt in coverage[src_file].items():
  156. # item at index 0 representing the coverage for line 1 of the source code
  157. assert 1 <= line_num <= line_count
  158. coverage_array[line_num - 1] = exe_cnt
  159. coveralls_data['source_files'].append({
  160. 'name': src_file_rel,
  161. 'coverage': coverage_array,
  162. 'source_digest': md5
  163. })
  164. args.out.write(json.dumps(coveralls_data))
  165. if args.verbose:
  166. warn('Coverage for {} source files was written'.format(
  167. len(coveralls_data['source_files'])))
  168. if __name__ == '__main__':
  169. main()