""" 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 pytest_collectstart(self, collector): if isinstance(collector, py.__.test.pycollect.Module): COVERAGE_MODULES.update(getattr(collector.obj, 'COVERAGE_MODULES', [])) def pytest_testrunstart(self): print "self.coverage", self.coverage if hasattr(self, 'coverage'): print "START 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 '''