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