From 76273c367483b5acd0078edd3abf9bf40625eb65 Mon Sep 17 00:00:00 2001 From: mzeidler Date: Tue, 7 Apr 2009 15:58:12 +0200 Subject: [PATCH] [svn r63795] Initial import. --HG-- branch : trunk --- contrib/pytest_coverage/__init__.py | 346 ++++++++++++++++++++++++++ contrib/pytest_coverage/header_bg.jpg | Bin 0 -> 10857 bytes contrib/pytest_coverage/links.gif | Bin 0 -> 75 bytes 3 files changed, 346 insertions(+) create mode 100644 contrib/pytest_coverage/__init__.py create mode 100644 contrib/pytest_coverage/header_bg.jpg create mode 100644 contrib/pytest_coverage/links.gif 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 0000000000000000000000000000000000000000..ba3513468ada9070b7c609677b63e467152a068a GIT binary patch literal 10857 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 R)&TAhPhrd3XWM)4{U5