xref: /aosp_15_r20/external/skia/tools/parse_llvm_coverage.py (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1*c8dee2aaSAndroid Build Coastguard Worker#!/usr/bin/env python
2*c8dee2aaSAndroid Build Coastguard Worker# Copyright (c) 2015 The Chromium Authors. All rights reserved.
3*c8dee2aaSAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
4*c8dee2aaSAndroid Build Coastguard Worker# found in the LICENSE file.
5*c8dee2aaSAndroid Build Coastguard Worker
6*c8dee2aaSAndroid Build Coastguard Worker
7*c8dee2aaSAndroid Build Coastguard Worker"""Parse an LLVM coverage report to generate useable results."""
8*c8dee2aaSAndroid Build Coastguard Worker
9*c8dee2aaSAndroid Build Coastguard Worker
10*c8dee2aaSAndroid Build Coastguard Workerimport argparse
11*c8dee2aaSAndroid Build Coastguard Workerimport json
12*c8dee2aaSAndroid Build Coastguard Workerimport os
13*c8dee2aaSAndroid Build Coastguard Workerimport re
14*c8dee2aaSAndroid Build Coastguard Workerimport subprocess
15*c8dee2aaSAndroid Build Coastguard Workerimport sys
16*c8dee2aaSAndroid Build Coastguard Worker
17*c8dee2aaSAndroid Build Coastguard Worker
18*c8dee2aaSAndroid Build Coastguard Workerdef _fix_filename(filename):
19*c8dee2aaSAndroid Build Coastguard Worker  """Return a filename which we can use to identify the file.
20*c8dee2aaSAndroid Build Coastguard Worker
21*c8dee2aaSAndroid Build Coastguard Worker  The file paths printed by llvm-cov take the form:
22*c8dee2aaSAndroid Build Coastguard Worker
23*c8dee2aaSAndroid Build Coastguard Worker      /path/to/repo/out/dir/../../src/filename.cpp
24*c8dee2aaSAndroid Build Coastguard Worker
25*c8dee2aaSAndroid Build Coastguard Worker  And then they're truncated to 22 characters with leading ellipses:
26*c8dee2aaSAndroid Build Coastguard Worker
27*c8dee2aaSAndroid Build Coastguard Worker      ...../../src/filename.cpp
28*c8dee2aaSAndroid Build Coastguard Worker
29*c8dee2aaSAndroid Build Coastguard Worker  This makes it really tough to determine whether the file actually belongs in
30*c8dee2aaSAndroid Build Coastguard Worker  the Skia repo.  This function strips out the leading junk so that, if the file
31*c8dee2aaSAndroid Build Coastguard Worker  exists in the repo, the returned string matches the end of some relative path
32*c8dee2aaSAndroid Build Coastguard Worker  in the repo. This doesn't guarantee correctness, but it's about as close as
33*c8dee2aaSAndroid Build Coastguard Worker  we can get.
34*c8dee2aaSAndroid Build Coastguard Worker  """
35*c8dee2aaSAndroid Build Coastguard Worker  return filename.split('..')[-1].lstrip('./')
36*c8dee2aaSAndroid Build Coastguard Worker
37*c8dee2aaSAndroid Build Coastguard Worker
38*c8dee2aaSAndroid Build Coastguard Workerdef _file_in_repo(filename, all_files):
39*c8dee2aaSAndroid Build Coastguard Worker  """Return the name of the checked-in file matching the given filename.
40*c8dee2aaSAndroid Build Coastguard Worker
41*c8dee2aaSAndroid Build Coastguard Worker  Use suffix matching to determine which checked-in files the given filename
42*c8dee2aaSAndroid Build Coastguard Worker  matches. If there are no matches or multiple matches, return None.
43*c8dee2aaSAndroid Build Coastguard Worker  """
44*c8dee2aaSAndroid Build Coastguard Worker  new_file = _fix_filename(filename)
45*c8dee2aaSAndroid Build Coastguard Worker  matched = []
46*c8dee2aaSAndroid Build Coastguard Worker  for f in all_files:
47*c8dee2aaSAndroid Build Coastguard Worker    if f.endswith(new_file):
48*c8dee2aaSAndroid Build Coastguard Worker      matched.append(f)
49*c8dee2aaSAndroid Build Coastguard Worker  if len(matched) == 1:
50*c8dee2aaSAndroid Build Coastguard Worker    return matched[0]
51*c8dee2aaSAndroid Build Coastguard Worker  elif len(matched) > 1:
52*c8dee2aaSAndroid Build Coastguard Worker    print >> sys.stderr, ('WARNING: multiple matches for %s; skipping:\n\t%s'
53*c8dee2aaSAndroid Build Coastguard Worker                          % (new_file, '\n\t'.join(matched)))
54*c8dee2aaSAndroid Build Coastguard Worker  return None
55*c8dee2aaSAndroid Build Coastguard Worker
56*c8dee2aaSAndroid Build Coastguard Worker
57*c8dee2aaSAndroid Build Coastguard Workerdef _get_per_file_per_line_coverage(report):
58*c8dee2aaSAndroid Build Coastguard Worker  """Return a dict whose keys are file names and values are coverage data.
59*c8dee2aaSAndroid Build Coastguard Worker
60*c8dee2aaSAndroid Build Coastguard Worker  Values are lists which take the form (lineno, coverage, code).
61*c8dee2aaSAndroid Build Coastguard Worker  """
62*c8dee2aaSAndroid Build Coastguard Worker  all_files = []
63*c8dee2aaSAndroid Build Coastguard Worker  for root, dirs, files in os.walk(os.getcwd()):
64*c8dee2aaSAndroid Build Coastguard Worker    if 'third_party/externals' in root:
65*c8dee2aaSAndroid Build Coastguard Worker      continue
66*c8dee2aaSAndroid Build Coastguard Worker    files = [f for f in files if not (f[0] == '.' or f.endswith('.pyc'))]
67*c8dee2aaSAndroid Build Coastguard Worker    dirs[:] = [d for d in dirs if not d[0] == '.']
68*c8dee2aaSAndroid Build Coastguard Worker    for name in files:
69*c8dee2aaSAndroid Build Coastguard Worker      all_files.append(os.path.join(root[(len(os.getcwd()) + 1):], name))
70*c8dee2aaSAndroid Build Coastguard Worker  all_files.sort()
71*c8dee2aaSAndroid Build Coastguard Worker
72*c8dee2aaSAndroid Build Coastguard Worker  lines = report.splitlines()
73*c8dee2aaSAndroid Build Coastguard Worker  current_file = None
74*c8dee2aaSAndroid Build Coastguard Worker  file_lines = []
75*c8dee2aaSAndroid Build Coastguard Worker  files = {}
76*c8dee2aaSAndroid Build Coastguard Worker  not_checked_in = '%' # Use this as the file name for not-checked-in files.
77*c8dee2aaSAndroid Build Coastguard Worker  for line in lines:
78*c8dee2aaSAndroid Build Coastguard Worker    m = re.match('([a-zA-Z0-9\./_-]+):', line)
79*c8dee2aaSAndroid Build Coastguard Worker    if m:
80*c8dee2aaSAndroid Build Coastguard Worker      if current_file and current_file != not_checked_in:
81*c8dee2aaSAndroid Build Coastguard Worker        files[current_file] = file_lines
82*c8dee2aaSAndroid Build Coastguard Worker      match_filename = _file_in_repo(m.groups()[0], all_files)
83*c8dee2aaSAndroid Build Coastguard Worker      current_file = match_filename or not_checked_in
84*c8dee2aaSAndroid Build Coastguard Worker      file_lines = []
85*c8dee2aaSAndroid Build Coastguard Worker    else:
86*c8dee2aaSAndroid Build Coastguard Worker      if current_file != not_checked_in:
87*c8dee2aaSAndroid Build Coastguard Worker        skip = re.match('^\s{2}-+$|^\s{2}\|.+$', line)
88*c8dee2aaSAndroid Build Coastguard Worker        if line and not skip:
89*c8dee2aaSAndroid Build Coastguard Worker          cov, linenum, code = line.split('|', 2)
90*c8dee2aaSAndroid Build Coastguard Worker          cov = cov.strip()
91*c8dee2aaSAndroid Build Coastguard Worker          if cov:
92*c8dee2aaSAndroid Build Coastguard Worker            cov = int(cov)
93*c8dee2aaSAndroid Build Coastguard Worker          else:
94*c8dee2aaSAndroid Build Coastguard Worker            cov = None # We don't care about coverage for this line.
95*c8dee2aaSAndroid Build Coastguard Worker          linenum = int(linenum.strip())
96*c8dee2aaSAndroid Build Coastguard Worker          assert linenum == len(file_lines) + 1
97*c8dee2aaSAndroid Build Coastguard Worker          file_lines.append((linenum, cov, code.decode('utf-8', 'replace')))
98*c8dee2aaSAndroid Build Coastguard Worker  return files
99*c8dee2aaSAndroid Build Coastguard Worker
100*c8dee2aaSAndroid Build Coastguard Worker
101*c8dee2aaSAndroid Build Coastguard Worker
102*c8dee2aaSAndroid Build Coastguard Workerdef _testname(filename):
103*c8dee2aaSAndroid Build Coastguard Worker  """Transform the file name into an ingestible test name."""
104*c8dee2aaSAndroid Build Coastguard Worker  return re.sub(r'[^a-zA-Z0-9]', '_', filename)
105*c8dee2aaSAndroid Build Coastguard Worker
106*c8dee2aaSAndroid Build Coastguard Worker
107*c8dee2aaSAndroid Build Coastguard Workerdef _nanobench_json(results, properties, key):
108*c8dee2aaSAndroid Build Coastguard Worker  """Return the results in JSON format like that produced by nanobench."""
109*c8dee2aaSAndroid Build Coastguard Worker  rv = {}
110*c8dee2aaSAndroid Build Coastguard Worker  # Copy over the properties first, then set the 'key' and 'results' keys,
111*c8dee2aaSAndroid Build Coastguard Worker  # in order to avoid bad formatting in case the user passes in a properties
112*c8dee2aaSAndroid Build Coastguard Worker  # dict containing those keys.
113*c8dee2aaSAndroid Build Coastguard Worker  rv.update(properties)
114*c8dee2aaSAndroid Build Coastguard Worker  rv['key'] = key
115*c8dee2aaSAndroid Build Coastguard Worker  rv['results'] = {
116*c8dee2aaSAndroid Build Coastguard Worker    _testname(f): {
117*c8dee2aaSAndroid Build Coastguard Worker      'coverage': {
118*c8dee2aaSAndroid Build Coastguard Worker        'percent': percent,
119*c8dee2aaSAndroid Build Coastguard Worker        'lines_not_covered': not_covered_lines,
120*c8dee2aaSAndroid Build Coastguard Worker        'options': {
121*c8dee2aaSAndroid Build Coastguard Worker          'fullname': f,
122*c8dee2aaSAndroid Build Coastguard Worker          'dir': os.path.dirname(f),
123*c8dee2aaSAndroid Build Coastguard Worker          'source_type': 'coverage',
124*c8dee2aaSAndroid Build Coastguard Worker        },
125*c8dee2aaSAndroid Build Coastguard Worker      },
126*c8dee2aaSAndroid Build Coastguard Worker    } for percent, not_covered_lines, f in results
127*c8dee2aaSAndroid Build Coastguard Worker  }
128*c8dee2aaSAndroid Build Coastguard Worker  return rv
129*c8dee2aaSAndroid Build Coastguard Worker
130*c8dee2aaSAndroid Build Coastguard Worker
131*c8dee2aaSAndroid Build Coastguard Workerdef _parse_key_value(kv_list):
132*c8dee2aaSAndroid Build Coastguard Worker  """Return a dict whose key/value pairs are derived from the given list.
133*c8dee2aaSAndroid Build Coastguard Worker
134*c8dee2aaSAndroid Build Coastguard Worker  For example:
135*c8dee2aaSAndroid Build Coastguard Worker
136*c8dee2aaSAndroid Build Coastguard Worker      ['k1', 'v1', 'k2', 'v2']
137*c8dee2aaSAndroid Build Coastguard Worker  becomes:
138*c8dee2aaSAndroid Build Coastguard Worker
139*c8dee2aaSAndroid Build Coastguard Worker      {'k1': 'v1',
140*c8dee2aaSAndroid Build Coastguard Worker       'k2': 'v2'}
141*c8dee2aaSAndroid Build Coastguard Worker  """
142*c8dee2aaSAndroid Build Coastguard Worker  if len(kv_list) % 2 != 0:
143*c8dee2aaSAndroid Build Coastguard Worker    raise Exception('Invalid key/value pairs: %s' % kv_list)
144*c8dee2aaSAndroid Build Coastguard Worker
145*c8dee2aaSAndroid Build Coastguard Worker  rv = {}
146*c8dee2aaSAndroid Build Coastguard Worker  for i in xrange(len(kv_list) / 2):
147*c8dee2aaSAndroid Build Coastguard Worker    rv[kv_list[i*2]] = kv_list[i*2+1]
148*c8dee2aaSAndroid Build Coastguard Worker  return rv
149*c8dee2aaSAndroid Build Coastguard Worker
150*c8dee2aaSAndroid Build Coastguard Worker
151*c8dee2aaSAndroid Build Coastguard Workerdef _get_per_file_summaries(line_by_line):
152*c8dee2aaSAndroid Build Coastguard Worker  """Summarize the full line-by-line coverage report by file."""
153*c8dee2aaSAndroid Build Coastguard Worker  per_file = []
154*c8dee2aaSAndroid Build Coastguard Worker  for filepath, lines in line_by_line.iteritems():
155*c8dee2aaSAndroid Build Coastguard Worker    total_lines = 0
156*c8dee2aaSAndroid Build Coastguard Worker    covered_lines = 0
157*c8dee2aaSAndroid Build Coastguard Worker    for _, cov, _ in lines:
158*c8dee2aaSAndroid Build Coastguard Worker      if cov is not None:
159*c8dee2aaSAndroid Build Coastguard Worker        total_lines += 1
160*c8dee2aaSAndroid Build Coastguard Worker        if cov > 0:
161*c8dee2aaSAndroid Build Coastguard Worker          covered_lines += 1
162*c8dee2aaSAndroid Build Coastguard Worker    if total_lines > 0:
163*c8dee2aaSAndroid Build Coastguard Worker      per_file.append((float(covered_lines)/float(total_lines)*100.0,
164*c8dee2aaSAndroid Build Coastguard Worker                       total_lines - covered_lines,
165*c8dee2aaSAndroid Build Coastguard Worker                       filepath))
166*c8dee2aaSAndroid Build Coastguard Worker  return per_file
167*c8dee2aaSAndroid Build Coastguard Worker
168*c8dee2aaSAndroid Build Coastguard Worker
169*c8dee2aaSAndroid Build Coastguard Workerdef main():
170*c8dee2aaSAndroid Build Coastguard Worker  """Generate useful data from a coverage report."""
171*c8dee2aaSAndroid Build Coastguard Worker  # Parse args.
172*c8dee2aaSAndroid Build Coastguard Worker  parser = argparse.ArgumentParser()
173*c8dee2aaSAndroid Build Coastguard Worker  parser.add_argument('--report', help='input file; an llvm coverage report.',
174*c8dee2aaSAndroid Build Coastguard Worker                      required=True)
175*c8dee2aaSAndroid Build Coastguard Worker  parser.add_argument('--nanobench', help='output file for nanobench data.')
176*c8dee2aaSAndroid Build Coastguard Worker  parser.add_argument(
177*c8dee2aaSAndroid Build Coastguard Worker      '--key', metavar='key_or_value', nargs='+',
178*c8dee2aaSAndroid Build Coastguard Worker      help='key/value pairs identifying this bot.')
179*c8dee2aaSAndroid Build Coastguard Worker  parser.add_argument(
180*c8dee2aaSAndroid Build Coastguard Worker      '--properties', metavar='key_or_value', nargs='+',
181*c8dee2aaSAndroid Build Coastguard Worker      help='key/value pairs representing properties of this build.')
182*c8dee2aaSAndroid Build Coastguard Worker  parser.add_argument('--linebyline',
183*c8dee2aaSAndroid Build Coastguard Worker                      help='output file for line-by-line JSON data.')
184*c8dee2aaSAndroid Build Coastguard Worker  args = parser.parse_args()
185*c8dee2aaSAndroid Build Coastguard Worker
186*c8dee2aaSAndroid Build Coastguard Worker  if args.nanobench and not (args.key and args.properties):
187*c8dee2aaSAndroid Build Coastguard Worker    raise Exception('--key and --properties are required with --nanobench')
188*c8dee2aaSAndroid Build Coastguard Worker
189*c8dee2aaSAndroid Build Coastguard Worker  with open(args.report) as f:
190*c8dee2aaSAndroid Build Coastguard Worker    report = f.read()
191*c8dee2aaSAndroid Build Coastguard Worker
192*c8dee2aaSAndroid Build Coastguard Worker  line_by_line = _get_per_file_per_line_coverage(report)
193*c8dee2aaSAndroid Build Coastguard Worker
194*c8dee2aaSAndroid Build Coastguard Worker  if args.linebyline:
195*c8dee2aaSAndroid Build Coastguard Worker    with open(args.linebyline, 'w') as f:
196*c8dee2aaSAndroid Build Coastguard Worker      json.dump(line_by_line, f)
197*c8dee2aaSAndroid Build Coastguard Worker
198*c8dee2aaSAndroid Build Coastguard Worker  if args.nanobench:
199*c8dee2aaSAndroid Build Coastguard Worker    # Parse the key and properties for use in the nanobench JSON output.
200*c8dee2aaSAndroid Build Coastguard Worker    key = _parse_key_value(args.key)
201*c8dee2aaSAndroid Build Coastguard Worker    properties = _parse_key_value(args.properties)
202*c8dee2aaSAndroid Build Coastguard Worker
203*c8dee2aaSAndroid Build Coastguard Worker    # Get per-file summaries.
204*c8dee2aaSAndroid Build Coastguard Worker    per_file = _get_per_file_summaries(line_by_line)
205*c8dee2aaSAndroid Build Coastguard Worker
206*c8dee2aaSAndroid Build Coastguard Worker    # Write results.
207*c8dee2aaSAndroid Build Coastguard Worker    format_results = _nanobench_json(per_file, properties, key)
208*c8dee2aaSAndroid Build Coastguard Worker    with open(args.nanobench, 'w') as f:
209*c8dee2aaSAndroid Build Coastguard Worker      json.dump(format_results, f)
210*c8dee2aaSAndroid Build Coastguard Worker
211*c8dee2aaSAndroid Build Coastguard Worker
212*c8dee2aaSAndroid Build Coastguard Workerif __name__ == '__main__':
213*c8dee2aaSAndroid Build Coastguard Worker  main()
214