summaryrefslogtreecommitdiff
path: root/git-coverage
diff options
context:
space:
mode:
Diffstat (limited to 'git-coverage')
-rwxr-xr-xgit-coverage473
1 files changed, 272 insertions, 201 deletions
diff --git a/git-coverage b/git-coverage
index ac837c9..09c803d 100755
--- a/git-coverage
+++ b/git-coverage
@@ -8,6 +8,9 @@ import subprocess
import sys
COVERAGE_EXTENSIONS = [".c", ".cpp", ".cc"]
+SKIP_PATTERNS = [
+ 'assert_not_reached'
+]
# Portions of the code:
#
@@ -46,179 +49,155 @@ class BadPatch(Exception):
def __str__(self):
return self.message
-
-def get_patch_names(iter_lines):
- try:
- line = iter_lines.next()
- if not line.startswith("--- "):
- raise BadPatch("No orig name: %s" % line)
- else:
- orig_name = line[4:].rstrip("\n")
- except StopIteration:
- raise BadPatch("No orig line")
- try:
- line = iter_lines.next()
- if not line.startswith("+++ "):
- raise BadPatch("No mod name")
- else:
- mod_name = line[4:].rstrip("\n")
- except StopIteration:
- raise BadPatch("No mod line")
- return (orig_name, mod_name)
-
-
-def parse_range(textrange):
- tmp = textrange.split(',')
- if len(tmp) == 1:
- pos = tmp[0]
- range = "1"
- else:
- (pos, range) = tmp
- pos = int(pos)
- range = int(range)
- return (pos, range)
-
-
-def hunk_from_header(line):
- matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
- if matches is None:
- raise MalformedHunkHeader("Does not match format.", line)
- try:
- (orig, mod) = matches.group(1).split(" ")
- except (ValueError, IndexError), e:
- raise MalformedHunkHeader(str(e), line)
- if not orig.startswith('-') or not mod.startswith('+'):
- raise MalformedHunkHeader("Positions don't start with + or -.", line)
- try:
- (orig_pos, orig_range) = parse_range(orig[1:])
- (mod_pos, mod_range) = parse_range(mod[1:])
- except (ValueError, IndexError), e:
- raise MalformedHunkHeader(str(e), line)
- if mod_range < 0 or orig_range < 0:
- raise MalformedHunkHeader("Hunk range is negative", line)
- tail = matches.group(3)
- return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
-
-
-class HunkLine:
- def __init__(self, contents):
- self.contents = contents
-
-
-class ContextLine(HunkLine):
- def __init__(self, contents):
- HunkLine.__init__(self, contents)
-
-
-class InsertLine(HunkLine):
- def __init__(self, contents):
- HunkLine.__init__(self, contents)
-
-
-class RemoveLine(HunkLine):
- def __init__(self, contents):
- HunkLine.__init__(self, contents)
-
-
-def parse_line(line):
- if line.startswith("\n"):
- return ContextLine(line)
- elif line.startswith(" "):
- return ContextLine(line[1:])
- elif line.startswith("+"):
- return InsertLine(line[1:])
- elif line.startswith("-"):
- return RemoveLine(line[1:])
- else:
- raise BadPatch("Unknown line type" % line)
-
-
class Hunk:
- def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
+ def __init__(self, orig_pos, orig_range, mod_pos, mod_range, header=None):
self.orig_pos = orig_pos
self.orig_range = orig_range
self.mod_pos = mod_pos
self.mod_range = mod_range
- self.tail = tail
+ self.header = header
self.lines = []
-
-def iter_hunks(iter_lines, allow_dirty=False):
- hunk = None
- for line in iter_lines:
- if line == "\n":
+ @staticmethod
+ def parse_range(textrange):
+ tmp = textrange.split(',')
+ if len(tmp) == 1:
+ pos = tmp[0]
+ range = "1"
+ else:
+ (pos, range) = tmp
+ pos = int(pos)
+ range = int(range)
+ return (pos, range)
+
+ @staticmethod
+ def from_header(line):
+ matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
+ if matches is None:
+ raise MalformedHunkHeader("Does not match format.", line)
+ try:
+ (orig, mod) = matches.group(1).split(" ")
+ except (ValueError, IndexError), e:
+ raise MalformedHunkHeader(str(e), line)
+ if not orig.startswith('-') or not mod.startswith('+'):
+ raise MalformedHunkHeader("Positions don't start with + or -.", line)
+ try:
+ (orig_pos, orig_range) = Hunk.parse_range(orig[1:])
+ (mod_pos, mod_range) = Hunk.parse_range(mod[1:])
+ except (ValueError, IndexError), e:
+ raise MalformedHunkHeader(str(e), line)
+ if mod_range < 0 or orig_range < 0:
+ raise MalformedHunkHeader("Hunk range is negative", line)
+ tail = matches.group(3)
+ return Hunk(orig_pos, orig_range, mod_pos, mod_range, line)
+
+ @staticmethod
+ def parse(lines, allow_dirty=True):
+ hunk = None
+ lines = iter(lines)
+ for line in lines:
+ if line == "\n":
+ if hunk is not None:
+ yield hunk
+ hunk = None
+ continue
if hunk is not None:
yield hunk
- hunk = None
- continue
+ try:
+ hunk = Hunk.from_header(line)
+ except MalformedHunkHeader:
+ if allow_dirty:
+ # If the line isn't a hunk header, then we've reached the end
+ # of this patch and there's "junk" at the end. Ignore the
+ # rest of this patch.
+ return
+ raise
+ orig_size = 0
+ mod_size = 0
+ offset = hunk.mod_pos
+ while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
+ hunk_line = lines.next()
+ if hunk_line.startswith("-"):
+ orig_size += 1
+ elif hunk_line.startswith("\n"):
+ orig_size += 1
+ mod_size += 1
+ elif hunk_line.startswith(" "):
+ orig_size += 1
+ mod_size += 1
+ elif hunk_line.startswith("+"):
+ mod_size += 1
+ else:
+ raise BadPatch("Unknown line type: %s" % hunk_line)
+ hunk.lines.append((offset, hunk_line))
+ if not hunk_line.startswith("-"):
+ offset += 1
if hunk is not None:
yield hunk
- try:
- hunk = hunk_from_header(line)
- except MalformedHunkHeader:
- if allow_dirty:
- # If the line isn't a hunk header, then we've reached the end
- # of this patch and there's "junk" at the end. Ignore the
- # rest of this patch.
- return
- raise
- orig_size = 0
- mod_size = 0
- while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
- hunk_line = parse_line(iter_lines.next())
- hunk.lines.append(hunk_line)
- if isinstance(hunk_line, (RemoveLine, ContextLine)):
- orig_size += 1
- if isinstance(hunk_line, (InsertLine, ContextLine)):
- mod_size += 1
- if hunk is not None:
- yield hunk
class Patch(object):
def __init__(self, oldname, newname):
self.oldname = oldname
self.newname = newname
+ self.prefix = []
self.hunks = []
-
-def parse_patch(iter_lines, allow_dirty=False):
- (orig_name, mod_name) = get_patch_names(iter_lines)
- patch = Patch(orig_name, mod_name)
- for hunk in iter_hunks(iter_lines, allow_dirty):
- patch.hunks.append(hunk)
- return patch
-
-
-def iter_file_patch(iter_lines, allow_dirty=False):
- saved_lines = []
- orig_range = 0
- beginning = True
- for line in iter_lines:
- if BINARY_FILES_RE.match(line):
- continue
- if line.startswith('=== ') or line.startswith('*** '):
- continue
- if line.startswith('#'):
- continue
- elif orig_range > 0:
- if line.startswith('-') or line.startswith(' '):
- orig_range -= 1
- elif line.startswith('--- '):
- if allow_dirty and beginning:
- # Patches can have "junk" at the beginning
- # Stripping junk from the end of patches is handled when we
- # parse the patch
- beginning = False
- elif len(saved_lines) > 0:
- yield saved_lines
- saved_lines = []
- elif line.startswith('@@'):
- hunk = hunk_from_header(line)
- orig_range = hunk.orig_range
- saved_lines.append(line)
- if len(saved_lines) > 0:
- yield saved_lines
+ @staticmethod
+ def parse_one(lines, allow_dirty=True):
+ try:
+ first = lines.next()
+ if not first.startswith("--- "):
+ raise BadPatch("No orig name: %s" % first)
+ else:
+ orig_name = first[4:].rstrip("\n")
+ except StopIteration:
+ raise BadPatch("No orig line")
+ try:
+ second = lines.next()
+ if not second.startswith("+++ "):
+ raise BadPatch("No mod name")
+ else:
+ mod_name = second[4:].rstrip("\n")
+ except StopIteration:
+ raise BadPatch("No mod line")
+
+ patch = Patch(orig_name, mod_name)
+ for hunk in Hunk.parse(lines, allow_dirty):
+ patch.hunks.append(hunk)
+ patch.prefix = [first, second]
+ return patch
+
+ @staticmethod
+ def parse(lines, allow_dirty=True):
+ saved_lines = []
+ orig_range = 0
+ beginning = True
+ for line in lines:
+ if BINARY_FILES_RE.match(line):
+ continue
+ if line.startswith('=== ') or line.startswith('*** '):
+ continue
+ if line.startswith('#'):
+ continue
+ elif orig_range > 0:
+ if line.startswith('-') or line.startswith(' '):
+ orig_range -= 1
+ elif line.startswith('--- '):
+ if allow_dirty and beginning:
+ # Patches can have "junk" at the beginning
+ # Stripping junk from the end of patches is handled when we
+ # parse the patch
+ beginning = False
+ elif len(saved_lines) > 0:
+ yield Patch.parse_one(iter(saved_lines), allow_dirty)
+ saved_lines = []
+ elif line.startswith('@@'):
+ hunk = Hunk.from_header(line)
+ orig_range = hunk.orig_range
+ saved_lines.append(line)
+ if len(saved_lines) > 0:
+ yield Patch.parse_one(iter(saved_lines), allow_dirty)
#
@@ -234,23 +213,38 @@ def iter_process_lines(argv):
else:
return
-def iter_desired_coverage(iter_lines):
- for fil in iter_file_patch(iter_lines, allow_dirty=True):
- patch = parse_patch(fil.__iter__(), allow_dirty=True)
- filename = os.path.normpath(patch.newname.split("/", 1)[1])
- (root, ext) = os.path.splitext(filename)
- if ext not in COVERAGE_EXTENSIONS:
- continue
- for hunk in patch.hunks:
- offset = hunk.mod_pos
- for line in hunk.lines:
- if isinstance(line, RemoveLine):
- yield (filename, offset, line.contents)
- elif isinstance(line, InsertLine):
- yield (filename, offset, line.contents)
- offset += 1
- else:
- offset += 1
+def ensure_gcda_file(gcno):
+ (base, ext) = os.path.splitext(gcno)
+ gcda = base + ".gcda"
+
+ # From gcov-io.h
+ # The basic format of the files is
+ #
+ # file : int32:magic int32:version int32:stamp record*
+ #
+ # magic for gcno files is 'gcno' as an integer, big or little endian
+ # and for gcda files it is 'gcda'
+
+ if os.path.exists(gcda):
+ return True
+
+ return False
+
+ # with open(gcno, 'r') as fi:
+ # bytes = fi.read(12)
+ # if len(bytes) != 12:
+ # return False
+ # if bytes[0:4] == 'gcno':
+ # bytes = 'gcda' + bytes[4:]
+ # elif bytes[0:4] == 'oncg':
+ # bytes = 'adcg' + bytes[4:]
+ # else:
+ # print "bad", bytes[0:4]
+ # return False
+ # with open(gcda, 'w') as fo:
+ # fo.write(bytes)
+ # return True
+
def find_all_gcno_files(directory):
paths = []
@@ -261,10 +255,8 @@ def find_all_gcno_files(directory):
continue
if not fnmatch.fnmatch(name, "*.gcno"):
continue
- (base, ext) = os.path.splitext(path)
- if not os.path.exists(base + ".gcda"):
- continue
- paths.append(path)
+ if ensure_gcda_file(path):
+ paths.append(path)
os.path.walk(".", visit, paths)
return paths
@@ -343,42 +335,121 @@ def gcov_coverage_lines(gcov_iter):
pass
return coverage
-
-def main(argv):
- try:
- opts, args = getopt.getopt(argv[1:], "h", ["help"])
- except getopt.GetoptError, err:
- print str(err)
- return 2
- for o, a in opts:
- if o in ("-h", "--help"):
- usage()
- return 0
+class Output:
+ defaults = {
+ 'diff.new': 'green',
+ 'diff.meta': 'bold',
+ 'diff.plain': 'normal',
+ 'diff.frag': 'cyan',
+ 'diff.old': 'red',
+ 'diff.whitespace': 'normal red',
+ 'diff.problem': 'normal red'
+ }
+
+ def __init__(self, output):
+ self.output = output
+ self.escapes = { }
+
+ cmd = ['git', 'config', '--get-colorbool',
+ 'color.diff', output.isatty() and 'true' or 'false']
+ self.with_colors = subprocess.check_output(cmd).strip() == 'true'
+
+ def write_meta(self, data, meta):
+ if not self.with_colors:
+ pass
+ elif not meta:
+ self.output.write('\033[0m')
+ elif meta in self.escapes:
+ self.output.write(self.escapes[meta])
else:
- assert False, "unhandled option"
+ default = self.defaults.get(meta, 'normal')
+ cmd = ['git', 'config', '--get-color', "color.%s" % meta, default]
+ escape = subprocess.check_output(cmd)
+ self.output.write(escape)
+ self.escapes[meta] = escape
+ self.write(data)
+ if self.with_colors:
+ self.output.write('\033[0m')
+
+ def write(self, data):
+ self.output.write(data)
+
+def print_patch_hunks(patch, hunks, coverage, output):
+ for line in patch.prefix:
+ output.write_meta(line, 'diff.meta')
+ for hunk in hunks:
+ output.write_meta(hunk.header, 'diff.frag')
+ for (no, line) in hunk.lines:
+ if no in coverage:
+ output.write(" ")
+ else:
+ output.write_meta("!", 'diff.problem')
+ if line.startswith("-"):
+ output.write_meta(line, 'diff.old')
+ elif line.startswith("+"):
+ output.write_meta(line, 'diff.new')
+ else:
+ output.write_meta(line, 'diff.plain')
+
+def is_hunk_covered(hunk, coverage, patterns):
+ for (no, contents) in hunk.lines:
+ if no not in coverage:
+ for pattern in patterns:
+ data = contents[1:].strip()
+ if pattern.search(data):
+ break
+ else:
+ return False
+ return True
- if len(args) == 0:
- cmd = ['git', 'diff', 'HEAD']
+
+def main(argv):
+ have_target = False
+ for arg in argv[1:]:
+ if not arg.startswith("-"):
+ have_target = True
+ break;
+
+ if have_target:
+ cmd = ['git', 'diff'] + argv[1:]
else:
- cmd = ['git', 'show'] + args
+ cmd = ['git', 'diff', 'HEAD']
- # First process all the lines that we need
- needed_coverage = { }
- iterator = iter_process_lines(cmd)
- for (filename, no, content) in iter_desired_coverage(iterator):
- if filename not in needed_coverage:
- needed_coverage[filename] = { }
- needed_coverage[filename][no] = content
+ printed_any = 0
+ output = Output(sys.stdout)
+ patches_by_filename = { }
+ # Pull all the patches appart into the hunks that we need
+ for patch in Patch.parse(iter_process_lines(cmd)):
+ (name, ext) = os.path.splitext(patch.newname)
+ if ext not in COVERAGE_EXTENSIONS:
+ continue
+ filename = os.path.normpath(patch.newname.split("/", 1)[1])
+ if filename not in patches_by_filename:
+ patches_by_filename[filename] = []
+ patches_by_filename[filename].append(patch)
+
+ # Find all the gcno files in the directory
gcno_cache = find_all_gcno_files(".")
- for (filename, needed) in needed_coverage.items():
+ # Compile all the skip patterns
+ patterns = [re.compile(p) for p in SKIP_PATTERNS]
+
+ # Now go through and calculate coverage
+ for (filename, patches) in patches_by_filename.items():
gcov = gcov_lines_for_files(filename, gcno_cache)
coverage = gcov_coverage_lines(gcov)
- for (no, content) in needed.items():
- if no not in coverage:
- print filename, no, content,
+ for patch in patches:
+ to_print = []
+ for hunk in patch.hunks:
+ if not is_hunk_covered(hunk, coverage, patterns):
+ to_print.append(hunk)
+ if to_print:
+ print_patch_hunks(patch, to_print, coverage, output)
+ printed_any = 1
+
+ return printed_any
if __name__ == '__main__':
sys.exit(main(sys.argv))