Added support for annotated source in coverage.py

On one hand this isn't very different than the source annotation in
gcov, on the other hand I find it a bit more readable after a bit of
experimentation.
This commit is contained in:
Christopher Haster
2022-05-23 01:35:00 -05:00
parent 5b0a6d4747
commit 46cc6d4450

View File

@@ -164,6 +164,14 @@ def openio(path, mode='r'):
else:
return open(path, mode)
def color(**args):
if args.get('color') == 'auto':
return sys.stdout.isatty()
elif args.get('color') == 'always':
return True
else:
return False
def collect(paths, **args):
results = {}
for path in paths:
@@ -221,6 +229,81 @@ def collect(paths, **args):
return func_results, results
def annotate(paths, results, **args):
for path in paths:
# map to source file
src_path = re.sub('\.t\.a\.gcda$', '.c', path)
# TODO test this
if args.get('build_dir'):
src_path = re.sub('%s/*' % re.escape(args['build_dir']), '',
src_path)
# flatten to line info
line_results = {line: (hits, result)
for (_, _, line), (hits, result) in results.items()}
# calculate spans to show
if not args.get('annotate'):
spans = []
last = None
for line, (hits, result) in sorted(line_results.items()):
if ((args.get('lines') and hits == 0)
or (args.get('branches')
and result.coverage_branch_hits
< result.coverage_branch_count)):
if last is not None and line - last.stop <= args['context']:
last = range(
last.start,
line+1+args['context'])
else:
if last is not None:
spans.append(last)
last = range(
line-args['context'],
line+1+args['context'])
if last is not None:
spans.append(last)
with open(src_path) as f:
skipped = False
for i, line in enumerate(f):
# skip lines not in spans?
if (not args.get('annotate')
and not any(i+1 in s for s in spans)):
skipped = True
continue
if skipped:
skipped = False
print('%s@@ %s:%d @@%s' % (
'\x1b[36m' if color(**args) else '',
src_path,
i+1,
'\x1b[m' if color(**args) else ''))
# build line
if line.endswith('\n'):
line = line[:-1]
if i+1 in line_results:
hits, result = line_results[i+1]
line = '%-*s // %d hits, %d/%d branches' % (
args['width'],
line,
hits,
result.coverage_branch_hits,
result.coverage_branch_count)
if color(**args):
if args.get('lines') and hits == 0:
line = '\x1b[1;31m%s\x1b[m' % line
elif (args.get('branches') and
result.coverage_branch_hits
< result.coverage_branch_count):
line = '\x1b[35m%s\x1b[m' % line
print(line)
def main(**args):
# find sizes
if not args.get('use', None):
@@ -246,7 +329,10 @@ def main(**args):
*(result[f] for f in CoverageResult._fields))
for result in r
if all(result.get(f) not in {None, ''}
for f in CoverageResult._fields)}
paths = []
line_results = {}
# find previous results?
if args.get('diff'):
@@ -344,6 +430,10 @@ def main(**args):
if args.get('quiet'):
pass
elif (args.get('annotate')
or args.get('lines')
or args.get('branches')):
annotate(paths, line_results, **args)
elif args.get('summary'):
print_header('')
print_entries('total')
@@ -403,6 +493,20 @@ if __name__ == "__main__":
help="Show file-level coverage.")
parser.add_argument('-Y', '--summary', action='store_true',
help="Only show the total coverage.")
parser.add_argument('-p', '--annotate', action='store_true',
help="Show source files annotated with coverage info.")
parser.add_argument('-l', '--lines', action='store_true',
help="Show uncovered lines.")
parser.add_argument('-b', '--branches', action='store_true',
help="Show uncovered branches.")
parser.add_argument('-c', '--context', type=lambda x: int(x, 0), default=3,
help="Show a additional lines of context. Defaults to 3.")
parser.add_argument('-w', '--width', type=lambda x: int(x, 0), default=80,
help="Assume source is styled with this many columns. Defaults to 80.")
# TODO add this to test.py?
parser.add_argument('--color',
choices=['never', 'always', 'auto'], default='auto',
help="When to use terminal colors.")
parser.add_argument('-e', '--error-on-lines', action='store_true',
help="Error if any lines are not covered.")
parser.add_argument('-E', '--error-on-branches', action='store_true',