diff --git a/contrib/pytest_coverage/__init__.py b/contrib/pytest_coverage/__init__.py
new file mode 100644
index 000000000..0c4d2134c
--- /dev/null
+++ b/contrib/pytest_coverage/__init__.py
@@ -0,0 +1,346 @@
+"""
+Tested with coverage 2.85 and pygments 1.0
+
+TODO:
+ + 'html-output/*,cover' should be deleted
+ + credits for coverage
+ + credits for pygments
+ + 'Install pygments' after ImportError is to less
+ + is the way of determining DIR_CSS_RESOURCE ok?
+ + write plugin test
+ + '.coverage' still exists in py.test execution dir
+"""
+
+import os
+import sys
+import re
+import shutil
+from StringIO import StringIO
+
+import py
+
+try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters import HtmlFormatter
+except ImportError:
+ print "Install pygments" # XXX
+ sys.exit(0)
+
+
+DIR_CUR = str(py.path.local())
+REPORT_FILE = os.path.join(DIR_CUR, '.coverage')
+DIR_ANNOTATE_OUTPUT = os.path.join(DIR_CUR, '.coverage_annotate')
+COVERAGE_MODULES = set()
+# coverage output parsing
+REG_COVERAGE_SUMMARY = re.compile('([a-z_\.]+) +([0-9]+) +([0-9]+) +([0-9]+%)')
+REG_COVERAGE_SUMMARY_TOTAL = re.compile('(TOTAL) +([0-9]+) +([0-9]+) +([0-9]+%)')
+DEFAULT_COVERAGE_OUTPUT = '.coverage_annotation'
+# HTML output specific
+DIR_CSS_RESOURCE = os.path.dirname(__import__('pytest_coverage').__file__)
+CSS_RESOURCE_FILES = ['header_bg.jpg', 'links.gif']
+
+COVERAGE_TERM_HEADER = "\nCOVERAGE INFORMATION\n" \
+ "====================\n"
+HTML_INDEX_HEADER = '''
+
+
+ py.test - Coverage Index
+
+
+
+
+
+ Module Coverage
+
+
+
+ Module |
+ Statements |
+ Executed |
+ Coverage |
+
+ '''
+HTML_INDEX_FOOTER = '''
+
+
+ '''
+
+
+class CoverageHtmlFormatter(HtmlFormatter):
+ """XXX: doc"""
+
+ def __init__(self, *args, **kwargs):
+ HtmlFormatter.__init__(self,*args, **kwargs)
+ self.annotation_infos = kwargs.get('annotation_infos')
+
+ def _highlight_lines(self, tokensource):
+ """
+ XXX: doc
+ """
+
+ hls = self.hl_lines
+ self.annotation_infos = [None] + self.annotation_infos
+ hls = [l for l, i in enumerate(self.annotation_infos) if i]
+ for i, (t, value) in enumerate(tokensource):
+ if t != 1:
+ yield t, value
+ if i + 1 in hls: # i + 1 because Python indexes start at 0
+ if self.annotation_infos[i+1] == "!":
+ yield 1, '%s' \
+ % value
+ elif self.annotation_infos[i+1] == ">":
+ yield 1, '%s' \
+ % value
+ else:
+ raise ValueError("HHAHA: %s" % self.annotation_infos[i+1])
+ else:
+ yield 1, value
+
+
+def _rename_annotation_files(module_list, dir_annotate_output):
+ for m in module_list:
+ mod_fpath = os.path.basename(m.__file__)
+ if mod_fpath.endswith('pyc'):
+ mod_fpath = mod_fpath[:-1]
+ old = os.path.join(dir_annotate_output, '%s,cover'% mod_fpath)
+ new = os.path.join(dir_annotate_output, '%s,cover'% m.__name__)
+ if os.path.isfile(old):
+ shutil.move(old, new)
+ yield new
+
+def _generate_module_coverage(mc_path, anotation_infos, src_lines):
+ #XXX: doc
+
+ code = "".join(src_lines)
+ mc_path = "%s.html" % mc_path
+ lexer = get_lexer_by_name("python", stripall=True)
+ formatter = CoverageHtmlFormatter(linenos=True, noclasses=True,
+ hl_lines=[1], annotation_infos=anotation_infos)
+ result = highlight(code, lexer, formatter)
+ fp = open(mc_path, 'w')
+ fp.write(result)
+ fp.close()
+
+def _parse_modulecoverage(mc_fpath):
+ #XXX: doc
+
+ fd = open(mc_fpath, 'r')
+ anotate_infos = []
+ src_lines = []
+ for line in fd.readlines():
+ anotate_info = line[0:2].strip()
+ if not anotate_info:
+ anotate_info = None
+ src_line = line[2:]
+ anotate_infos.append(anotate_info)
+ src_lines.append(src_line)
+ return mc_fpath, anotate_infos, src_lines
+
+def _parse_coverage_summary(fd):
+ """Parses coverage summary output."""
+
+ if hasattr(fd, 'readlines'):
+ fd.seek(0)
+ for l in fd.readlines():
+ m = REG_COVERAGE_SUMMARY.match(l)
+ if m:
+ # yield name, stmts, execs, cover
+ yield m.group(1), m.group(2), m.group(3), m.group(4)
+ else:
+ m = REG_COVERAGE_SUMMARY_TOTAL.match(l)
+ if m:
+ # yield name, stmts, execs, cover
+ yield m.group(1), m.group(2), m.group(3), m.group(4)
+
+
+def _get_coverage_index(mod_name, stmts, execs, cover, annotation_dir):
+ """
+ Generates the index page where are all modulare coverage reports are
+ linked.
+ """
+
+ if mod_name == 'TOTAL':
+ return '%s | %s | %s | %s |
\n' % (mod_name, stmts, execs, cover)
+ covrep_fpath = os.path.join(annotation_dir, '%s,cover.html' % mod_name)
+ assert os.path.isfile(covrep_fpath) == True
+ fname = os.path.basename(covrep_fpath)
+ modlink = '%s' % (fname, mod_name)
+ return '%s | %s | %s | %s |
\n' % (modlink, stmts, execs, cover)
+
+
+class CoveragePlugin:
+ def pytest_addoption(self, parser):
+ group = parser.addgroup('coverage options')
+ group.addoption('-C', action='store_true', default=False,
+ dest = 'coverage',
+ help=('displays coverage information.'))
+ group.addoption('--coverage-html', action='store', default=False,
+ dest='coverage_annotation',
+ help='path to the coverage HTML output dir.')
+ group.addoption('--coverage-css-resourcesdir', action='store',
+ default=DIR_CSS_RESOURCE,
+ dest='coverage_css_ressourcedir',
+ help='path to dir with css-resources (%s) for '
+ 'being copied to the HTML output dir.' % \
+ ", ".join(CSS_RESOURCE_FILES))
+
+ def pytest_configure(self, config):
+ if config.getvalue('coverage'):
+ try:
+ import coverage
+ except ImportError:
+ raise config.Error("To run use the coverage option you have to install " \
+ "Ned Batchelder's coverage: "\
+ "http://nedbatchelder.com/code/modules/coverage.html")
+ self.coverage = coverage
+ self.summary = None
+
+ def pytest_terminal_summary(self, terminalreporter):
+ if hasattr(self, 'coverage'):
+ self.coverage.stop()
+ module_list = [sys.modules[mod] for mod in COVERAGE_MODULES]
+ module_list.sort()
+ summary_fd = StringIO()
+ # get coverage reports by module list
+ self.coverage.report(module_list, file=summary_fd)
+ summary = COVERAGE_TERM_HEADER + summary_fd.getvalue()
+ terminalreporter._tw.write(summary)
+
+ config = terminalreporter.config
+ dir_annotate_output = config.getvalue('coverage_annotation')
+ if dir_annotate_output:
+ if dir_annotate_output == "":
+ dir_annotate_output = DIR_ANNOTATE_OUTPUT
+ # create dir
+ if os.path.isdir(dir_annotate_output):
+ shutil.rmtree(dir_annotate_output)
+ os.mkdir(dir_annotate_output)
+ # generate annotation text files for later parsing
+ self.coverage.annotate(module_list, dir_annotate_output)
+ # generate the separate module coverage reports
+ for mc_fpath in _rename_annotation_files(module_list, \
+ dir_annotate_output):
+ # mc_fpath, anotate_infos, src_lines from _parse_do
+ _generate_module_coverage(*_parse_modulecoverage(mc_fpath))
+ # creating contents for the index pagee for coverage report
+ idxpage_html = StringIO()
+ idxpage_html.write(HTML_INDEX_HEADER)
+ total_sum = None
+ for args in _parse_coverage_summary(summary_fd):
+ # mod_name, stmts, execs, cover = args
+ idxpage_html.write(_get_coverage_index(*args, \
+ **dict(annotation_dir=dir_annotate_output)))
+ idxpage_html.write(HTML_INDEX_FOOTER)
+ idx_fpath = os.path.join(dir_annotate_output, 'index.html')
+ idx_fd = open(idx_fpath, 'w')
+ idx_fd.write(idxpage_html.getvalue())
+ idx_fd.close()
+
+ dir_css_resource_dir = config.getvalue('coverage_css_ressourcedir')
+ if dir_annotate_output and dir_css_resource_dir != "":
+ if not os.path.isdir(dir_css_resource_dir):
+ raise config.Error("CSS resource dir not found: '%s'" % \
+ dir_css_resource_dir)
+ for r in CSS_RESOURCE_FILES:
+ src = os.path.join(dir_css_resource_dir, r)
+ if os.path.isfile(src):
+ dest = os.path.join(dir_annotate_output, r)
+ shutil.copy(src, dest)
+
+ def pyevent__collectionstart(self, collector):
+ if isinstance(collector, py.__.test.pycollect.Module):
+ COVERAGE_MODULES.update(getattr(collector.obj,
+ 'COVERAGE_MODULES', []))
+
+ def pyevent__testrunstart(self):
+ if hasattr(self, 'coverage'):
+ self.coverage.erase()
+ self.coverage.start()
+
+
+# ===============================================================================
+# plugin tests
+# ===============================================================================
+# XXX
+'''
+def test_generic(plugintester):
+ plugintester.apicheck(EventlogPlugin)
+
+ testdir = plugintester.testdir()
+ testdir.makepyfile("""
+ def test_pass():
+ pass
+ """)
+ testdir.runpytest("--eventlog=event.log")
+ s = testdir.tmpdir.join("event.log").read()
+ assert s.find("TestrunStart") != -1
+ assert s.find("ItemTestReport") != -1
+ assert s.find("TestrunFinish") != -1
+'''
diff --git a/contrib/pytest_coverage/header_bg.jpg b/contrib/pytest_coverage/header_bg.jpg
new file mode 100644
index 000000000..ba3513468
Binary files /dev/null and b/contrib/pytest_coverage/header_bg.jpg differ
diff --git a/contrib/pytest_coverage/links.gif b/contrib/pytest_coverage/links.gif
new file mode 100644
index 000000000..e160f23b6
Binary files /dev/null and b/contrib/pytest_coverage/links.gif differ