"""Code-coverage tools for CherryPy. To use this module, or the coverage tools in the test suite, you need to download 'coverage.py', either Gareth Rees' `original implementation `_ or Ned Batchelder's `enhanced version: `_ To turn on coverage tracing, use the following code:: cherrypy.engine.subscribe('start', covercp.start) DO NOT subscribe anything on the 'start_thread' channel, as previously recommended. Calling start once in the main thread should be sufficient to start coverage on all threads. Calling start again in each thread effectively clears any coverage data gathered up to that point. Run your code, then use the ``covercp.serve()`` function to browse the results in a web browser. If you run this module from the command line, it will call ``serve()`` for you. """ import re import sys import cgi from cherrypy._cpcompat import quote_plus import os import os.path localFile = os.path.join(os.path.dirname(__file__), "coverage.cache") the_coverage = None try: from coverage import coverage the_coverage = coverage(data_file=localFile) def start(): the_coverage.start() except ImportError: # Setting the_coverage to None will raise errors # that need to be trapped downstream. the_coverage = None import warnings warnings.warn( "No code coverage will be performed; " "coverage.py could not be imported.") def start(): pass start.priority = 20 TEMPLATE_MENU = """ CherryPy Coverage Menu

CherryPy Coverage

""" TEMPLATE_FORM = """
Show percentages
Hide files over %%
Exclude files matching

""" TEMPLATE_FRAMESET = """ CherryPy coverage data """ TEMPLATE_COVERAGE = """ Coverage for %(name)s

%(name)s

%(fullpath)s

Coverage: %(pc)s%%

""" TEMPLATE_LOC_COVERED = """ %s  %s \n""" TEMPLATE_LOC_NOT_COVERED = """ %s  %s \n""" TEMPLATE_LOC_EXCLUDED = """ %s  %s \n""" TEMPLATE_ITEM = ( "%s%s%s\n" ) def _percent(statements, missing): s = len(statements) e = s - len(missing) if s > 0: return int(round(100.0 * e / s)) return 0 def _show_branch(root, base, path, pct=0, showpct=False, exclude="", coverage=the_coverage): # Show the directory name and any of our children dirs = [k for k, v in root.items() if v] dirs.sort() for name in dirs: newpath = os.path.join(path, name) if newpath.lower().startswith(base): relpath = newpath[len(base):] yield "| " * relpath.count(os.sep) yield ( "%s\n" % (newpath, quote_plus(exclude), name) ) for chunk in _show_branch( root[name], base, newpath, pct, showpct, exclude, coverage=coverage ): yield chunk # Now list the files if path.lower().startswith(base): relpath = path[len(base):] files = [k for k, v in root.items() if not v] files.sort() for name in files: newpath = os.path.join(path, name) pc_str = "" if showpct: try: _, statements, _, missing, _ = coverage.analysis2(newpath) except: # Yes, we really want to pass on all errors. pass else: pc = _percent(statements, missing) pc_str = ("%3d%% " % pc).replace(' ', ' ') if pc < float(pct) or pc == -1: pc_str = "%s" % pc_str else: pc_str = "%s" % pc_str yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), pc_str, newpath, name) def _skip_file(path, exclude): if exclude: return bool(re.search(exclude, path)) def _graft(path, tree): d = tree p = path atoms = [] while True: p, tail = os.path.split(p) if not tail: break atoms.append(tail) atoms.append(p) if p != "/": atoms.append("/") atoms.reverse() for node in atoms: if node: d = d.setdefault(node, {}) def get_tree(base, exclude, coverage=the_coverage): """Return covered module names as a nested dict.""" tree = {} runs = coverage.data.executed_files() for path in runs: if not _skip_file(path, exclude) and not os.path.isdir(path): _graft(path, tree) return tree class CoverStats(object): def __init__(self, coverage, root=None): self.coverage = coverage if root is None: # Guess initial depth. Files outside this path will not be # reachable from the web interface. import cherrypy root = os.path.dirname(cherrypy.__file__) self.root = root def index(self): return TEMPLATE_FRAMESET % self.root.lower() index.exposed = True def menu(self, base="/", pct="50", showpct="", exclude=r'python\d\.\d|test|tut\d|tutorial'): # The coverage module uses all-lower-case names. base = base.lower().rstrip(os.sep) yield TEMPLATE_MENU yield TEMPLATE_FORM % locals() # Start by showing links for parent paths yield "
" path = "" atoms = base.split(os.sep) atoms.pop() for atom in atoms: path += atom + os.sep yield ("%s %s" % (path, quote_plus(exclude), atom, os.sep)) yield "
" yield "
" # Then display the tree tree = get_tree(base, exclude, self.coverage) if not tree: yield "

No modules covered.

" else: for chunk in _show_branch(tree, base, "/", pct, showpct == 'checked', exclude, coverage=self.coverage): yield chunk yield "
" yield "" menu.exposed = True def annotated_file(self, filename, statements, excluded, missing): source = open(filename, 'r') buffer = [] for lineno, line in enumerate(source.readlines()): lineno += 1 line = line.strip("\n\r") empty_the_buffer = True if lineno in excluded: template = TEMPLATE_LOC_EXCLUDED elif lineno in missing: template = TEMPLATE_LOC_NOT_COVERED elif lineno in statements: template = TEMPLATE_LOC_COVERED else: empty_the_buffer = False buffer.append((lineno, line)) if empty_the_buffer: for lno, pastline in buffer: yield template % (lno, cgi.escape(pastline)) buffer = [] yield template % (lineno, cgi.escape(line)) def report(self, name): filename, statements, excluded, missing, _ = self.coverage.analysis2( name) pc = _percent(statements, missing) yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), fullpath=name, pc=pc) yield '\n' for line in self.annotated_file(filename, statements, excluded, missing): yield line yield '
' yield '' yield '' report.exposed = True def serve(path=localFile, port=8080, root=None): if coverage is None: raise ImportError("The coverage module could not be imported.") from coverage import coverage cov = coverage(data_file=path) cov.load() import cherrypy cherrypy.config.update({'server.socket_port': int(port), 'server.thread_pool': 10, 'environment': "production", }) cherrypy.quickstart(CoverStats(cov, root)) if __name__ == "__main__": serve(*tuple(sys.argv[1:]))