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