|
|
@@ -0,0 +1,290 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+#
|
|
|
+# Script to summarize the outputs of other scripts. Operates on CSV files.
|
|
|
+#
|
|
|
+
|
|
|
+import functools as ft
|
|
|
+import collections as co
|
|
|
+import os
|
|
|
+import csv
|
|
|
+import re
|
|
|
+import math as m
|
|
|
+
|
|
|
+# displayable fields
|
|
|
+Field = co.namedtuple('Field', 'name,parse,acc,key,fmt,repr,null,ratio')
|
|
|
+FIELDS = [
|
|
|
+ # name, parse, accumulate, fmt, print, null
|
|
|
+ Field('code',
|
|
|
+ lambda r: int(r['code_size']),
|
|
|
+ sum,
|
|
|
+ lambda r: r,
|
|
|
+ '%7s',
|
|
|
+ lambda r: r,
|
|
|
+ '-',
|
|
|
+ lambda old, new: (new-old)/old),
|
|
|
+ Field('data',
|
|
|
+ lambda r: int(r['data_size']),
|
|
|
+ sum,
|
|
|
+ lambda r: r,
|
|
|
+ '%7s',
|
|
|
+ lambda r: r,
|
|
|
+ '-',
|
|
|
+ lambda old, new: (new-old)/old),
|
|
|
+ Field('stack',
|
|
|
+ lambda r: float(r['stack_limit']),
|
|
|
+ max,
|
|
|
+ lambda r: r,
|
|
|
+ '%7s',
|
|
|
+ lambda r: '∞' if m.isinf(r) else int(r),
|
|
|
+ '-',
|
|
|
+ lambda old, new: (new-old)/old),
|
|
|
+ Field('structs',
|
|
|
+ lambda r: int(r['struct_size']),
|
|
|
+ sum,
|
|
|
+ lambda r: r,
|
|
|
+ '%8s',
|
|
|
+ lambda r: r,
|
|
|
+ '-',
|
|
|
+ lambda old, new: (new-old)/old),
|
|
|
+ Field('coverage',
|
|
|
+ lambda r: (int(r['coverage_hits']), int(r['coverage_count'])),
|
|
|
+ lambda rs: ft.reduce(lambda a, b: (a[0]+b[0], a[1]+b[1]), rs),
|
|
|
+ lambda r: r[0]/r[1],
|
|
|
+ '%19s',
|
|
|
+ lambda r: '%11s %7s' % ('%d/%d' % (r[0], r[1]), '%.1f%%' % (100*r[0]/r[1])),
|
|
|
+ '%11s %7s' % ('-', '-'),
|
|
|
+ lambda old, new: ((new[0]/new[1]) - (old[0]/old[1])))
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+def main(**args):
|
|
|
+ def openio(path, mode='r'):
|
|
|
+ if path == '-':
|
|
|
+ if 'r' in mode:
|
|
|
+ return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
|
|
|
+ else:
|
|
|
+ return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
|
|
|
+ else:
|
|
|
+ return open(path, mode)
|
|
|
+
|
|
|
+ # find results
|
|
|
+ results = co.defaultdict(lambda: {})
|
|
|
+ for path in args.get('csv_paths', '-'):
|
|
|
+ try:
|
|
|
+ with openio(path) as f:
|
|
|
+ r = csv.DictReader(f)
|
|
|
+ for result in r:
|
|
|
+ file = result.pop('file', '')
|
|
|
+ name = result.pop('name', '')
|
|
|
+ prev = results[(file, name)]
|
|
|
+ for field in FIELDS:
|
|
|
+ try:
|
|
|
+ r = field.parse(result)
|
|
|
+ if field.name in prev:
|
|
|
+ results[(file, name)][field.name] = field.acc(
|
|
|
+ [prev[field.name], r])
|
|
|
+ else:
|
|
|
+ results[(file, name)][field.name] = r
|
|
|
+ except (KeyError, ValueError):
|
|
|
+ pass
|
|
|
+ except FileNotFoundError:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # find fields
|
|
|
+ if args.get('all_fields'):
|
|
|
+ fields = FIELDS
|
|
|
+ elif args.get('fields') is not None:
|
|
|
+ fields_dict = {field.name: field for field in FIELDS}
|
|
|
+ fields = [fields_dict[f] for f in args['fields']]
|
|
|
+ else:
|
|
|
+ fields = []
|
|
|
+ for field in FIELDS:
|
|
|
+ if any(field.name in result for result in results.values()):
|
|
|
+ fields.append(field)
|
|
|
+
|
|
|
+ # find total for every field
|
|
|
+ total = {}
|
|
|
+ for result in results.values():
|
|
|
+ for field in fields:
|
|
|
+ if field.name in result and field.name in total:
|
|
|
+ total[field.name] = field.acc(
|
|
|
+ [total[field.name], result[field.name]])
|
|
|
+ elif field.name in result:
|
|
|
+ total[field.name] = result[field.name]
|
|
|
+
|
|
|
+ # find previous results?
|
|
|
+ if args.get('diff'):
|
|
|
+ prev_results = co.defaultdict(lambda: {})
|
|
|
+ try:
|
|
|
+ with openio(args['diff']) as f:
|
|
|
+ r = csv.DictReader(f)
|
|
|
+ for result in r:
|
|
|
+ file = result.pop('file', '')
|
|
|
+ name = result.pop('name', '')
|
|
|
+ prev = prev_results[(file, name)]
|
|
|
+ for field in FIELDS:
|
|
|
+ try:
|
|
|
+ r = field.parse(result)
|
|
|
+ if field.name in prev:
|
|
|
+ prev_results[(file, name)][field.name] = field.acc(
|
|
|
+ [prev[field.name], r])
|
|
|
+ else:
|
|
|
+ prev_results[(file, name)][field.name] = r
|
|
|
+ except (KeyError, ValueError):
|
|
|
+ pass
|
|
|
+ except FileNotFoundError:
|
|
|
+ pass
|
|
|
+
|
|
|
+ if args.get('all_fields'):
|
|
|
+ fields = FIELDS
|
|
|
+ elif args.get('fields') is not None:
|
|
|
+ fields_dict = {field.name: field for field in FIELDS}
|
|
|
+ fields = [fields_dict[f] for f in args['fields']]
|
|
|
+ else:
|
|
|
+ fields = []
|
|
|
+ for field in FIELDS:
|
|
|
+ if any(field.name in result for result in prev_results.values()):
|
|
|
+ fields.append(field)
|
|
|
+
|
|
|
+ prev_total = {}
|
|
|
+ for result in prev_results.values():
|
|
|
+ for field in fields:
|
|
|
+ if field.name in result and field.name in prev_total:
|
|
|
+ prev_total[field.name] = field.acc(
|
|
|
+ [prev_total[field.name], result[field.name]])
|
|
|
+ elif field.name in result:
|
|
|
+ prev_total[field.name] = result[field.name]
|
|
|
+
|
|
|
+ # print results
|
|
|
+ def dedup_entries(results, by='name'):
|
|
|
+ entries = co.defaultdict(lambda: {})
|
|
|
+ for (file, func), result in results.items():
|
|
|
+ entry = (file if by == 'file' else func)
|
|
|
+ prev = entries[entry]
|
|
|
+ for field in fields:
|
|
|
+ if field.name in result and field.name in prev:
|
|
|
+ entries[entry][field.name] = field.acc(
|
|
|
+ [prev[field.name], result[field.name]])
|
|
|
+ elif field.name in result:
|
|
|
+ entries[entry][field.name] = result[field.name]
|
|
|
+ return entries
|
|
|
+
|
|
|
+ def sorted_entries(entries):
|
|
|
+ if args.get('sort') is not None:
|
|
|
+ field = {field.name: field for field in FIELDS}[args['sort']]
|
|
|
+ return sorted(entries, key=lambda x: (
|
|
|
+ -(field.key(x[1][field.name])) if field.name in x[1] else -1, x))
|
|
|
+ elif args.get('reverse_sort') is not None:
|
|
|
+ field = {field.name: field for field in FIELDS}[args['reverse_sort']]
|
|
|
+ return sorted(entries, key=lambda x: (
|
|
|
+ +(field.key(x[1][field.name])) if field.name in x[1] else -1, x))
|
|
|
+ else:
|
|
|
+ return sorted(entries)
|
|
|
+
|
|
|
+ def print_header(by=''):
|
|
|
+ if not args.get('diff'):
|
|
|
+ print('%-36s' % by, end='')
|
|
|
+ for field in fields:
|
|
|
+ print((' '+field.fmt) % field.name, end='')
|
|
|
+ print()
|
|
|
+ else:
|
|
|
+ print('%-36s' % by, end='')
|
|
|
+ for field in fields:
|
|
|
+ print((' '+field.fmt) % field.name, end='')
|
|
|
+ print(' %-9s' % '', end='')
|
|
|
+ print()
|
|
|
+
|
|
|
+ def print_entry(name, result):
|
|
|
+ print('%-36s' % name, end='')
|
|
|
+ for field in fields:
|
|
|
+ r = result.get(field.name)
|
|
|
+ if r is not None:
|
|
|
+ print((' '+field.fmt) % field.repr(r), end='')
|
|
|
+ else:
|
|
|
+ print((' '+field.fmt) % '-', end='')
|
|
|
+ print()
|
|
|
+
|
|
|
+ def print_diff_entry(name, old, new):
|
|
|
+ print('%-36s' % name, end='')
|
|
|
+ for field in fields:
|
|
|
+ n = new.get(field.name)
|
|
|
+ if n is not None:
|
|
|
+ print((' '+field.fmt) % field.repr(n), end='')
|
|
|
+ else:
|
|
|
+ print((' '+field.fmt) % '-', end='')
|
|
|
+ o = old.get(field.name)
|
|
|
+ ratio = (
|
|
|
+ 0.0 if m.isinf(o or 0) and m.isinf(n or 0)
|
|
|
+ else +float('inf') if m.isinf(n or 0)
|
|
|
+ else -float('inf') if m.isinf(o or 0)
|
|
|
+ else 0.0 if not o and not n
|
|
|
+ else +1.0 if not o
|
|
|
+ else -1.0 if not n
|
|
|
+ else field.ratio(o, n))
|
|
|
+ print(' %-9s' % (
|
|
|
+ '' if not ratio
|
|
|
+ else '(+∞%)' if ratio > 0 and m.isinf(ratio)
|
|
|
+ else '(-∞%)' if ratio < 0 and m.isinf(ratio)
|
|
|
+ else '(%+.1f%%)' % (100*ratio)), end='')
|
|
|
+ print()
|
|
|
+
|
|
|
+ def print_entries(by='name'):
|
|
|
+ entries = dedup_entries(results, by=by)
|
|
|
+
|
|
|
+ if not args.get('diff'):
|
|
|
+ print_header(by=by)
|
|
|
+ for name, result in sorted_entries(entries.items()):
|
|
|
+ print_entry(name, result)
|
|
|
+ else:
|
|
|
+ prev_entries = dedup_entries(prev_results, by=by)
|
|
|
+ print_header(by='%s (%d added, %d removed)' % (by,
|
|
|
+ sum(1 for name in entries if name not in prev_entries),
|
|
|
+ sum(1 for name in prev_entries if name not in entries)))
|
|
|
+ for name, result in sorted_entries(entries.items()):
|
|
|
+ if args.get('all') or result != prev_entries.get(name, {}):
|
|
|
+ print_diff_entry(name, prev_entries.get(name, {}), result)
|
|
|
+
|
|
|
+ def print_totals():
|
|
|
+ if not args.get('diff'):
|
|
|
+ print_entry('TOTAL', total)
|
|
|
+ else:
|
|
|
+ print_diff_entry('TOTAL', prev_total, total)
|
|
|
+
|
|
|
+ if args.get('summary'):
|
|
|
+ print_header()
|
|
|
+ print_totals()
|
|
|
+ elif args.get('files'):
|
|
|
+ print_entries(by='file')
|
|
|
+ print_totals()
|
|
|
+ else:
|
|
|
+ print_entries(by='name')
|
|
|
+ print_totals()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ import argparse
|
|
|
+ import sys
|
|
|
+ parser = argparse.ArgumentParser(
|
|
|
+ description="Summarize measurements")
|
|
|
+ parser.add_argument('csv_paths', nargs='*', default='-',
|
|
|
+ help="Description of where to find *.csv files. May be a directory \
|
|
|
+ or list of paths. *.csv files will be merged to show the total \
|
|
|
+ coverage.")
|
|
|
+ parser.add_argument('-d', '--diff',
|
|
|
+ help="Specify CSV file to diff against.")
|
|
|
+ parser.add_argument('-a', '--all', action='store_true',
|
|
|
+ help="Show all objects, not just the ones that changed.")
|
|
|
+ parser.add_argument('-e', '--all-fields', action='store_true',
|
|
|
+ help="Show all fields, even those with no results.")
|
|
|
+ parser.add_argument('-f', '--fields', type=lambda x: re.split('\s*,\s*', x),
|
|
|
+ help="Comma separated list of fields to print, by default all fields \
|
|
|
+ that are found in the CSV files are printed.")
|
|
|
+ parser.add_argument('-s', '--sort',
|
|
|
+ help="Sort by this field.")
|
|
|
+ parser.add_argument('-S', '--reverse-sort',
|
|
|
+ help="Sort by this field, but backwards.")
|
|
|
+ parser.add_argument('-F', '--files', action='store_true',
|
|
|
+ help="Show file-level calls.")
|
|
|
+ parser.add_argument('-Y', '--summary', action='store_true',
|
|
|
+ help="Only show the totals.")
|
|
|
+ sys.exit(main(**vars(parser.parse_args())))
|