Skip to content

Commit 1cc96a7

Browse files
committed
Added branch coverage monintor script
newcov.py should be run in a git branch. It will show which lines you have touched in the branch but not covered with unit tests. It currenlty only knows how to run the unit tests for revs. Maybe we need a standard interface.
1 parent 741d973 commit 1cc96a7

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ install:
55
install -d $(DESTDIR)/usr/bin/
66
install -D -m 755 send_to_OBS.sh $(DESTDIR)/usr/bin/
77
install -D -m 755 project_status.pl $(DESTDIR)/usr/bin/
8+
install -D -m 755 newcov.py $(DESTDIR)/usr/bin/
89

910
all: default

newcov.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#! /usr/bin/python
2+
3+
# newcov - list new code that is not covered by the unit tests
4+
5+
import optparse
6+
import os
7+
import re
8+
import subprocess
9+
import sys
10+
11+
def parsecommandline(argv):
12+
parser = optparse.OptionParser(usage='%prog [options]')
13+
14+
parser.add_option('-b', '--base', type='string', dest='base',
15+
metavar='<commit or branch>',
16+
help='Base to diff from, defaults to branch point from master')
17+
parser.add_option('-f', '--file', type='string', dest='file',
18+
metavar='<coveragefile>',
19+
help='Name of the file containing the coverage report. Default is to '
20+
'generate the report by running the unit tests.')
21+
22+
(options, _) = parser.parse_args(argv)
23+
return options
24+
25+
def read_and_print(command, outf=sys.stdout):
26+
"""Run a shell command and collect the output file printing it."""
27+
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
28+
output = []
29+
for line in process.stdout:
30+
output.append(line)
31+
#outf.write(line)
32+
return ''.join(output)
33+
34+
def find_merge_base(commitspec=None):
35+
"""Find the best common ancestor between the working tree and base."""
36+
if commitspec:
37+
command = ['git', 'merge-base', 'HEAD', commitspec]
38+
else:
39+
command = ['git', 'merge-base', 'HEAD', 'master', 'origin/master']
40+
process = subprocess.Popen(command, stdout=subprocess.PIPE)
41+
(output, _) = process.communicate()
42+
return output.split()[0]
43+
44+
def get_diff(fromcommit):
45+
command = ['git', 'diff', fromcommit]
46+
process = subprocess.Popen(command, stdout=subprocess.PIPE)
47+
(output, _) = process.communicate()
48+
return output
49+
50+
def parse_coverage_report(report):
51+
"""Return a list of (filename, [linenr]) with data from this report."""
52+
delim = None
53+
started = False
54+
results = []
55+
for line in report.splitlines():
56+
line = line.strip()
57+
if line == delim:
58+
if not started:
59+
started = True
60+
continue
61+
else:
62+
return results
63+
if not started:
64+
if line.startswith('Name ') and line.endswith(' Missing'):
65+
delim = '-' * len(line)
66+
continue
67+
fields = line.split(None, 4)
68+
modulename = fields[0]
69+
filename = os.path.join(*modulename.split('.')) + '.py'
70+
linenrs = []
71+
if len(fields) > 4:
72+
for chunk in fields[4].split():
73+
if chunk.endswith(','):
74+
chunk = chunk[:-1]
75+
if '-' in chunk:
76+
first, last = chunk.split('-')
77+
linenrs.extend(range(int(first), int(last)+1))
78+
else:
79+
linenrs.append(int(chunk))
80+
results.append((filename, linenrs))
81+
return results
82+
83+
def find_new_lines(diff):
84+
filename = None
85+
linenr = 0
86+
results = []
87+
lines = []
88+
for line in diff.splitlines():
89+
if line.startswith('+++ '):
90+
if filename:
91+
results.append((filename, lines))
92+
lines = []
93+
filename = line[6:] # cut off "--- a/"
94+
linenr = 0
95+
elif line.startswith('@@ '):
96+
matchobj = re.match(r'^@@ -\d+(,\d+)? \+(\d+)(,\d+)? @@', line)
97+
linenr = int(matchobj.group(2))
98+
elif line.startswith('+'):
99+
lines.append(linenr)
100+
linenr += 1
101+
elif line.startswith(' '):
102+
linenr += 1
103+
if filename:
104+
results.append((filename, lines))
105+
return results
106+
107+
def compact_lines(linenrs):
108+
last = None
109+
in_run = False
110+
results = []
111+
for linenr in linenrs:
112+
if linenr - 1 == last:
113+
in_run = True
114+
else:
115+
if in_run:
116+
in_run = False
117+
results[-1] = "%s-%d" % (results[-1], last)
118+
results.append(str(linenr))
119+
last = linenr
120+
if in_run:
121+
results[-1] = "%s-%d" % (results[-1], last)
122+
return ', '.join(results)
123+
124+
def main(options):
125+
base = find_merge_base(options.base)
126+
127+
if options.file:
128+
with open(options.file) as inf:
129+
report = inf.read()
130+
else:
131+
report = read_and_print("python runtests.py")
132+
133+
coveredlines = parse_coverage_report(report)
134+
diff = get_diff(base)
135+
newcode = find_new_lines(diff)
136+
137+
cdict = dict(coveredlines)
138+
reportlines = []
139+
for filename, lines in newcode:
140+
if not filename.endswith('.py'):
141+
continue
142+
if filename not in cdict:
143+
uncovered_lines = []
144+
else:
145+
uncovered_lines = [linenr for linenr in cdict[filename]
146+
if linenr in lines]
147+
reportlines.append((filename, uncovered_lines))
148+
width = max([len(filename) for filename, _ in reportlines])
149+
total = 0
150+
for filename, lines in reportlines:
151+
if lines:
152+
print "%s %s" % (filename.ljust(width+2), compact_lines(lines))
153+
total += len(lines)
154+
print "%d new lines not covered" % total
155+
156+
if __name__ == '__main__':
157+
sys.exit(main(parsecommandline(sys.argv)))

0 commit comments

Comments
 (0)