coverage-info.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import argparse
  2. import os
  3. import sys
  4. import tarfile
  5. import collections
  6. import subprocess
  7. import re
  8. GCDA_EXT = '.gcda'
  9. GCNO_EXT = '.gcno'
  10. def suffixes(path):
  11. """
  12. >>> list(suffixes('/a/b/c'))
  13. ['c', 'b/c', '/a/b/c']
  14. >>> list(suffixes('/a/b/c/'))
  15. ['c', 'b/c', '/a/b/c']
  16. >>> list(suffixes('/a'))
  17. ['/a']
  18. >>> list(suffixes('/a/'))
  19. ['/a']
  20. >>> list(suffixes('/'))
  21. []
  22. """
  23. path = os.path.normpath(path)
  24. def up_dirs(cur_path):
  25. while os.path.dirname(cur_path) != cur_path:
  26. cur_path = os.path.dirname(cur_path)
  27. yield cur_path
  28. for x in up_dirs(path):
  29. yield path.replace(x + os.path.sep, '')
  30. def recast(in_file, out_file, probe_path, update_stat):
  31. PREFIX = 'SF:'
  32. probed_path = None
  33. any_payload = False
  34. with open(in_file, 'r') as input, open(out_file, 'w') as output:
  35. active = True
  36. for line in input:
  37. line = line.rstrip('\n')
  38. if line.startswith('TN:'):
  39. output.write(line + '\n')
  40. elif line.startswith(PREFIX):
  41. path = line[len(PREFIX) :]
  42. probed_path = probe_path(path)
  43. if probed_path:
  44. output.write(PREFIX + probed_path + '\n')
  45. active = bool(probed_path)
  46. else:
  47. if active:
  48. update_stat(probed_path, line)
  49. output.write(line + '\n')
  50. any_payload = True
  51. return any_payload
  52. def print_stat(da, fnda, teamcity_stat_output):
  53. lines_hit = sum(map(bool, da.values()))
  54. lines_total = len(da.values())
  55. lines_coverage = 100.0 * lines_hit / lines_total if lines_total else 0
  56. func_hit = sum(map(bool, fnda.values()))
  57. func_total = len(fnda.values())
  58. func_coverage = 100.0 * func_hit / func_total if func_total else 0
  59. print >> sys.stderr, '[[imp]]Lines[[rst]] {: >16} {: >16} {: >16.1f}%'.format(
  60. lines_hit, lines_total, lines_coverage
  61. )
  62. print >> sys.stderr, '[[imp]]Functions[[rst]] {: >16} {: >16} {: >16.1f}%'.format(
  63. func_hit, func_total, func_coverage
  64. )
  65. if teamcity_stat_output:
  66. with open(teamcity_stat_output, 'w') as tc_file:
  67. tc_file.write("##teamcity[blockOpened name='Code Coverage Summary']\n")
  68. tc_file.write(
  69. "##teamcity[buildStatisticValue key=\'CodeCoverageAbsLTotal\' value='{}']\n".format(lines_total)
  70. )
  71. tc_file.write(
  72. "##teamcity[buildStatisticValue key=\'CodeCoverageAbsLCovered\' value='{}']\n".format(lines_hit)
  73. )
  74. tc_file.write(
  75. "##teamcity[buildStatisticValue key=\'CodeCoverageAbsMTotal\' value='{}']\n".format(func_total)
  76. )
  77. tc_file.write(
  78. "##teamcity[buildStatisticValue key=\'CodeCoverageAbsMCovered\' value='{}']\n".format(func_hit)
  79. )
  80. tc_file.write("##teamcity[blockClosed name='Code Coverage Summary']\n")
  81. def chunks(l, n):
  82. """
  83. >>> list(chunks(range(10), 3))
  84. [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
  85. >>> list(chunks(range(10), 5))
  86. [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
  87. """
  88. for i in xrange(0, len(l), n):
  89. yield l[i : i + n]
  90. def combine_info_files(lcov, files, out_file):
  91. chunk_size = 50
  92. files = list(set(files))
  93. for chunk in chunks(files, chunk_size):
  94. combine_cmd = [lcov]
  95. if os.path.exists(out_file):
  96. chunk.append(out_file)
  97. for trace in chunk:
  98. assert os.path.exists(trace), "Trace file does not exist: {} (cwd={})".format(trace, os.getcwd())
  99. combine_cmd += ["-a", os.path.abspath(trace)]
  100. print >> sys.stderr, '## lcov', ' '.join(combine_cmd[1:])
  101. out_file_tmp = "combined.tmp"
  102. with open(out_file_tmp, "w") as stdout:
  103. subprocess.check_call(combine_cmd, stdout=stdout)
  104. if os.path.exists(out_file):
  105. os.remove(out_file)
  106. os.rename(out_file_tmp, out_file)
  107. def probe_path_global(path, source_root, prefix_filter, exclude_files):
  108. if path.endswith('_ut.cpp'):
  109. return None
  110. for suff in reversed(list(suffixes(path))):
  111. if (not prefix_filter or suff.startswith(prefix_filter)) and (
  112. not exclude_files or not exclude_files.match(suff)
  113. ):
  114. full_path = source_root + os.sep + suff
  115. if os.path.isfile(full_path):
  116. return full_path
  117. return None
  118. def update_stat_global(src_file, line, fnda, da):
  119. if line.startswith("FNDA:"):
  120. visits, func_name = line[len("FNDA:") :].split(',')
  121. fnda[src_file + func_name] += int(visits)
  122. if line.startswith("DA"):
  123. line_number, visits = line[len("DA:") :].split(',')
  124. if visits == '=====':
  125. visits = 0
  126. da[src_file + line_number] += int(visits)
  127. def gen_info_global(cmd, cov_info, probe_path, update_stat, lcov_args):
  128. print >> sys.stderr, '## geninfo', ' '.join(cmd)
  129. subprocess.check_call(cmd)
  130. if recast(cov_info + '.tmp', cov_info, probe_path, update_stat):
  131. lcov_args.append(cov_info)
  132. def init_all_coverage_files(
  133. gcno_archive, fname2gcno, fname2info, geninfo_executable, gcov_tool, gen_info, prefix_filter, exclude_files
  134. ):
  135. with tarfile.open(gcno_archive) as gcno_tf:
  136. for gcno_item in gcno_tf:
  137. if gcno_item.isfile() and gcno_item.name.endswith(GCNO_EXT):
  138. gcno_tf.extract(gcno_item)
  139. gcno_name = gcno_item.name
  140. source_fname = gcno_name[: -len(GCNO_EXT)]
  141. if prefix_filter and not source_fname.startswith(prefix_filter):
  142. sys.stderr.write("Skipping {} (doesn't match prefix '{}')\n".format(source_fname, prefix_filter))
  143. continue
  144. if exclude_files and exclude_files.search(source_fname):
  145. sys.stderr.write(
  146. "Skipping {} (matched exclude pattern '{}')\n".format(source_fname, exclude_files.pattern)
  147. )
  148. continue
  149. fname2gcno[source_fname] = gcno_name
  150. if os.path.getsize(gcno_name) > 0:
  151. coverage_info = source_fname + '.' + str(len(fname2info[source_fname])) + '.info'
  152. fname2info[source_fname].append(coverage_info)
  153. geninfo_cmd = [
  154. geninfo_executable,
  155. '--gcov-tool',
  156. gcov_tool,
  157. '-i',
  158. gcno_name,
  159. '-o',
  160. coverage_info + '.tmp',
  161. ]
  162. gen_info(geninfo_cmd, coverage_info)
  163. def process_all_coverage_files(gcda_archive, fname2gcno, fname2info, geninfo_executable, gcov_tool, gen_info):
  164. with tarfile.open(gcda_archive) as gcda_tf:
  165. for gcda_item in gcda_tf:
  166. if gcda_item.isfile() and gcda_item.name.endswith(GCDA_EXT):
  167. gcda_name = gcda_item.name
  168. source_fname = gcda_name[: -len(GCDA_EXT)]
  169. for suff in suffixes(source_fname):
  170. if suff in fname2gcno:
  171. gcda_new_name = suff + GCDA_EXT
  172. gcda_item.name = gcda_new_name
  173. gcda_tf.extract(gcda_item)
  174. if os.path.getsize(gcda_new_name) > 0:
  175. coverage_info = suff + '.' + str(len(fname2info[suff])) + '.info'
  176. fname2info[suff].append(coverage_info)
  177. geninfo_cmd = [
  178. geninfo_executable,
  179. '--gcov-tool',
  180. gcov_tool,
  181. gcda_new_name,
  182. '-o',
  183. coverage_info + '.tmp',
  184. ]
  185. gen_info(geninfo_cmd, coverage_info)
  186. def gen_cobertura(tool, output, combined_info):
  187. cmd = [tool, combined_info, '-b', '#hamster#', '-o', output]
  188. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  189. out, err = p.communicate()
  190. if p.returncode:
  191. raise Exception(
  192. 'lcov_cobertura failed with exit code {}\nstdout: {}\nstderr: {}'.format(p.returncode, out, err)
  193. )
  194. def main(
  195. source_root,
  196. output,
  197. gcno_archive,
  198. gcda_archive,
  199. gcov_tool,
  200. prefix_filter,
  201. exclude_regexp,
  202. teamcity_stat_output,
  203. coverage_report_path,
  204. gcov_report,
  205. lcov_cobertura,
  206. ):
  207. exclude_files = re.compile(exclude_regexp) if exclude_regexp else None
  208. fname2gcno = {}
  209. fname2info = collections.defaultdict(list)
  210. lcov_args = []
  211. geninfo_executable = os.path.join(source_root, 'devtools', 'lcov', 'geninfo')
  212. def probe_path(path):
  213. return probe_path_global(path, source_root, prefix_filter, exclude_files)
  214. fnda = collections.defaultdict(int)
  215. da = collections.defaultdict(int)
  216. def update_stat(src_file, line):
  217. update_stat_global(src_file, line, da, fnda)
  218. def gen_info(cmd, cov_info):
  219. gen_info_global(cmd, cov_info, probe_path, update_stat, lcov_args)
  220. init_all_coverage_files(
  221. gcno_archive, fname2gcno, fname2info, geninfo_executable, gcov_tool, gen_info, prefix_filter, exclude_files
  222. )
  223. process_all_coverage_files(gcda_archive, fname2gcno, fname2info, geninfo_executable, gcov_tool, gen_info)
  224. if coverage_report_path:
  225. output_dir = coverage_report_path
  226. else:
  227. output_dir = output + '.dir'
  228. if not os.path.exists(output_dir):
  229. os.makedirs(output_dir)
  230. teamcity_stat_file = None
  231. if teamcity_stat_output:
  232. teamcity_stat_file = os.path.join(output_dir, 'teamcity.out')
  233. print_stat(da, fnda, teamcity_stat_file)
  234. if lcov_args:
  235. output_trace = "combined.info"
  236. combine_info_files(os.path.join(source_root, 'devtools', 'lcov', 'lcov'), lcov_args, output_trace)
  237. cmd = [
  238. os.path.join(source_root, 'devtools', 'lcov', 'genhtml'),
  239. '-p',
  240. source_root,
  241. '--ignore-errors',
  242. 'source',
  243. '-o',
  244. output_dir,
  245. output_trace,
  246. ]
  247. print >> sys.stderr, '## genhtml', ' '.join(cmd)
  248. subprocess.check_call(cmd)
  249. if lcov_cobertura:
  250. gen_cobertura(lcov_cobertura, gcov_report, output_trace)
  251. with tarfile.open(output, 'w') as tar:
  252. tar.add(output_dir, arcname='.')
  253. if __name__ == '__main__':
  254. parser = argparse.ArgumentParser()
  255. parser.add_argument('--source-root', action='store')
  256. parser.add_argument('--output', action='store')
  257. parser.add_argument('--gcno-archive', action='store')
  258. parser.add_argument('--gcda-archive', action='store')
  259. parser.add_argument('--gcov-tool', action='store')
  260. parser.add_argument('--prefix-filter', action='store')
  261. parser.add_argument('--exclude-regexp', action='store')
  262. parser.add_argument('--teamcity-stat-output', action='store_const', const=True)
  263. parser.add_argument('--coverage-report-path', action='store')
  264. parser.add_argument('--gcov-report', action='store')
  265. parser.add_argument('--lcov-cobertura', action='store')
  266. args = parser.parse_args()
  267. main(**vars(args))