[Checkins] SVN: z3c.coverage/trunk/ Add a coveragediff script.
Marius Gedminas
marius at pov.lt
Wed Jul 18 14:52:36 EDT 2007
Log message for revision 78120:
Add a coveragediff script.
Changed:
U z3c.coverage/trunk/README.txt
U z3c.coverage/trunk/setup.py
A z3c.coverage/trunk/src/z3c/coverage/coveragediff.py
-=-
Modified: z3c.coverage/trunk/README.txt
===================================================================
--- z3c.coverage/trunk/README.txt 2007-07-18 17:18:58 UTC (rev 78119)
+++ z3c.coverage/trunk/README.txt 2007-07-18 18:52:35 UTC (rev 78120)
@@ -1,2 +1,5 @@
This package produces a nice HTML representation of the coverage data
generated by the Zope test runner.
+
+It also has a script to check for differences in coverage and report
+any regressions (increases in the number of untested lines).
Modified: z3c.coverage/trunk/setup.py
===================================================================
--- z3c.coverage/trunk/setup.py 2007-07-18 17:18:58 UTC (rev 78119)
+++ z3c.coverage/trunk/setup.py 2007-07-18 18:52:35 UTC (rev 78120)
@@ -23,7 +23,7 @@
setup (
name='z3c.coverage',
- version='0.1.0',
+ version='0.2.0',
author = "Zope Community",
author_email = "zope3-dev at zope.org",
description = "A script to visualize coverage reports via HTML",
@@ -56,6 +56,7 @@
entry_points = """
[console_scripts]
coverage = z3c.coverage.coveragereport:main
+ coveragediff = z3c.coverage.coveragediff:main
""",
dependency_links = ['http://download.zope.org/distribution'],
zip_safe = False,
Added: z3c.coverage/trunk/src/z3c/coverage/coveragediff.py
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/coveragediff.py (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/coveragediff.py 2007-07-18 18:52:35 UTC (rev 78120)
@@ -0,0 +1,276 @@
+#!/usr/bin/env python
+##############################################################################
+#
+# Copyright (c) 2007 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Compare the test coverage between two versions. The promary goal is to find
+regressions in test coverage (newly added lines of code without tests, or
+old lines of code that used to have tests but don't any more).
+
+Usage: coverage_diff.py [options] old-dir new-dir
+
+The directories are expected to contain files named '<package>.<module>.cover'
+with the format that Python's trace.py produces.
+"""
+
+import os
+import re
+import sys
+import smtplib
+import optparse
+from email.MIMEText import MIMEText
+
+
+try:
+ any
+except NameError:
+ # python 2.4 compatibility
+ def any(list):
+ for item in list:
+ if item:
+ return True
+ return False
+
+
+def matches(string, list_of_regexes):
+ """Check whether a string matches any of a list of regexes.
+
+ >>> matches('foo', map(re.compile, ['x', 'o']))
+ True
+ >>> matches('foo', map(re.compile, ['x', 'f$']))
+ False
+ >>> matches('foo', [])
+ False
+
+ """
+ return any(regex.search(string) for regex in list_of_regexes)
+
+
+def filter_files(files, include=(), exclude=()):
+ """Filters a file list by considering only the include patterns, then
+ excluding exclude patterns. Patterns are regular expressions.
+
+ Examples:
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'],
+ ... include=['^ivija'], exclude=['tests'])
+ ['ivija.food']
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'],
+ ... exclude=['tests'])
+ ['ivija.food', 'other.ivija']
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'],
+ ... include=['^ivija'])
+ ['ivija.food', 'ivija.food.tests']
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'])
+ ['ivija.food', 'ivija.food.tests', 'other.ivija']
+
+ """
+ if not include:
+ include = ['.'] # include everything by default
+ if not exclude:
+ exclude = [] # exclude nothing by default
+ include = map(re.compile, include)
+ exclude = map(re.compile, exclude)
+ return [fn for fn in files
+ if matches(fn, include) and not matches(fn, exclude)]
+
+
+def find_coverage_files(dir):
+ """Find all test coverage files in a given directory.
+
+ The files are expected to end in '.cover'. Weird filenames produced
+ by tracing "fake" code (like '<doctest ...>') are ignored.
+ """
+ return [fn for fn in os.listdir(dir)
+ if fn.endswith('.cover') and not fn.startswith('<')]
+
+
+def filter_coverage_files(dir, include=(), exclude=()):
+ """Find test coverage files in a given directory matching given patterns.
+
+ The files are expected to end in '.cover'. Weird filenames produced
+ by tracing "fake" code (like '<doctest ...>') are ignored.
+
+ Include/exclude patterns are regular expressions. Include patterns
+ are considered first, then the results are trimmed by the exclude
+ patterns.
+ """
+ return filter_files(find_coverage_files(dir), include, exclude)
+
+
+def warn(filename, message):
+ """Warn about test coverage regression."""
+ module = strip(os.path.basename(filename), '.cover')
+ print '%s: %s' % (module, message)
+
+
+def compare_dirs(olddir, newdir, include=(), exclude=(), warn=warn):
+ """Compare two directories of coverage files."""
+ old_coverage_files = filter_coverage_files(olddir, include, exclude)
+ new_coverage_files = filter_coverage_files(newdir, include, exclude)
+
+ old_coverage_set = set(old_coverage_files)
+ for fn in sorted(new_coverage_files):
+ if fn in old_coverage_files:
+ compare_file(os.path.join(olddir, fn),
+ os.path.join(newdir, fn), warn=warn)
+ else:
+ new_file(os.path.join(newdir, fn), warn=warn)
+
+
+def count_coverage(filename):
+ """Count the number of covered and uncovered lines in a file."""
+ covered = uncovered = 0
+ for line in file(filename):
+ if line.startswith('>>>>>>'):
+ uncovered += 1
+ elif len(line) >= 7 and not line.startswith(' '*7):
+ covered += 1
+ return covered, uncovered
+
+
+def compare_file(oldfile, newfile, warn=warn):
+ """Compare two coverage files."""
+ old_covered, old_uncovered = count_coverage(oldfile)
+ new_covered, new_uncovered = count_coverage(newfile)
+ if new_uncovered > old_uncovered:
+ increase = new_uncovered - old_uncovered
+ warn(newfile, "%d new lines of untested code" % increase)
+
+
+def new_file(newfile, warn=warn):
+ """Look for uncovered lines in a new coverage file."""
+ covered, uncovered = count_coverage(newfile)
+ if uncovered:
+ total = covered + uncovered
+ msg = "new file with %d lines of untested code (out of %d)" % (
+ uncovered, total)
+ warn(newfile, msg)
+
+
+def strip(string, suffix):
+ """Strip a suffix from a string if it exists:
+
+ >>> strip('go bar a foobar', 'bar')
+ 'go bar a foo'
+ >>> strip('go bar a foobar', 'baz')
+ 'go bar a foobar'
+ >>> strip('allofit', 'allofit')
+ ''
+
+ """
+ if string.endswith(suffix):
+ string = string[:-len(suffix)]
+ return string
+
+
+class MailSender(object):
+ """Send emails over SMTP"""
+
+ def __init__(self, smtp_host='localhost', smtp_port=25):
+ self.smtp_host = smtp_host
+ self.smtp_port = smtp_port
+
+ def send_email(self, from_addr, to_addr, subject, body):
+ """Send an email."""
+ # Note that this won't handle non-ASCII characters correctly.
+ # See http://mg.pov.lt/blog/unicode-emails-in-python.html
+ msg = MIMEText(body)
+ if from_addr:
+ msg['From'] = from_addr
+ if to_addr:
+ msg['To'] = to_addr
+ msg['Subject'] = subject
+ smtp = smtplib.SMTP(self.smtp_host, self.smtp_port)
+ smtp.sendmail(from_addr, to_addr, msg.as_string())
+ smtp.quit()
+
+
+class ReportEmailer(object):
+ """Warning collector and emailer."""
+
+ def __init__(self, from_addr, to_addr, subject, mailer=None):
+ if not mailer:
+ mailer = MailSender()
+ self.from_addr = from_addr
+ self.to_addr = to_addr
+ self.subject = subject
+ self.mailer = mailer
+ self.warnings = []
+
+ def warn(self, filename, message):
+ """Warn about test coverage regression."""
+ module = strip(os.path.basename(filename), '.cover')
+ self.warnings.append('%s: %s' % (module, message))
+
+ def send(self):
+ """Send the warnings (if any)."""
+ if self.warnings:
+ body = '\n'.join(self.warnings)
+ self.mailer.send_email(self.from_addr, self.to_addr, self.subject,
+ body)
+
+
+
+def selftest():
+ """Run all unit tests in this module."""
+ import doctest
+ nfail, ntests = doctest.testmod()
+ if nfail == 0:
+ print "All %d tests passed." % ntests
+
+
+def main():
+ """Parse command line arguments and do stuff."""
+ progname = os.path.basename(sys.argv[0])
+ parser = optparse.OptionParser("usage: %prog olddir newdir",
+ prog=progname)
+ parser.add_option('--include', metavar='REGEX',
+ help='only consider files matching REGEX',
+ action='append')
+ parser.add_option('--exclude', metavar='REGEX',
+ help='ignore files matching REGEX',
+ action='append')
+ parser.add_option('--email', metavar='ADDR',
+ help='send the report to a given email address'
+ ' (only if regressions were found)',)
+ parser.add_option('--from', metavar='ADDR', dest='sender',
+ help='set the email sender address')
+ parser.add_option('--subject', metavar='SUBJECT',
+ default='Unit test coverage regression',
+ help='set the email subject')
+ parser.add_option('--selftest', help='run integrity tests',
+ action='store_true')
+ opts, args = parser.parse_args()
+ if opts.selftest:
+ selftest()
+ return
+ if len(args) != 2:
+ parser.error("wrong number of arguments")
+ olddir, newdir = args
+ if opts.email:
+ mailer = ReportEmailer(opts.sender, opts.email, opts.subject)
+ warnfunc = mailer.warn
+ else:
+ warnfunc = warn
+ compare_dirs(olddir, newdir, include=opts.include, exclude=opts.exclude,
+ warn=warnfunc)
+ if opts.email:
+ mailer.send()
+
+
+if __name__ == '__main__':
+ main()
Property changes on: z3c.coverage/trunk/src/z3c/coverage/coveragediff.py
___________________________________________________________________
Name: svn:executable
+ *
Name: svn:keywords
+ Id
More information about the Checkins
mailing list