changeset 169:722215d17899

For more accurate code coverage reporting, include the statistics for modules that haven't been run at all during the tests. To do this, we need to count the lines of code in those modules. This is done by the `bitten.util.loc` module, which is based on [http://starship.python.net/crew/gherman/playground/pycount/ pycount.py] (but heavily modified).
author cmlenz
date Tue, 30 Aug 2005 09:50:20 +0000
parents 565f8b5126f8
children 46b9c9124cee
files bitten/build/pythontools.py bitten/util/loc.py
diffstat 2 files changed, 169 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/build/pythontools.py
+++ b/bitten/build/pythontools.py
@@ -12,7 +12,7 @@
 import re
 
 from bitten.build import CommandLine, FileSet
-from bitten.util import xmlio
+from bitten.util import loc, xmlio
 
 log = logging.getLogger('bitten.build.pythontools')
 
@@ -101,8 +101,8 @@
 
     fileset = FileSet(ctxt.basedir, include, exclude)
     missing_files = []
-    for file in fileset:
-        missing_files.append(file)
+    for filename in fileset:
+        missing_files.append(filename)
 
     try:
         summary_file = open(ctxt.resolve(summary), 'r')
@@ -142,10 +142,18 @@
                         coverage.append(module)
 
             for filename in missing_files:
-                module = xmlio.Element('coverage', file=filename, percentage=0)
-                # FIXME: Determine module name
-                # FIXME: For every non-comment, non-empty line in the file,
-                #        add a <line hits="0"> element
+                modulename = os.path.splitext(filename.replace(os.sep, '.'))[0]
+                module = xmlio.Element('coverage', module=modulename,
+                                       file=filename, percentage=0)
+                filepath = ctxt.resolve(filename)
+                fileobj = file(filepath, 'r')
+                try:
+                    for lineno, linetype, line in loc.count(fileobj):
+                        if linetype == loc.CODE:
+                            line = xmlio.Element('line', line=lineno, hits=0)
+                            module.append(line)
+                finally:
+                    fileobj.close()
                 coverage.append(module)
 
             ctxt.report(coverage)
new file mode 100644
--- /dev/null
+++ b/bitten/util/loc.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+# Copyright (C) 1998 Dinu C. Gherman <gherman@europemail.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# 
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.cmlenz.net/wiki/License.
+# 
+# This module is based on the pycount.py script written by Dinu C.
+# Gherman, and is used here under the following license:
+# 
+#     Permission to use, copy, modify, and distribute this software
+#     and its documentation without fee and for any purpose, except
+#     direct commerial advantage, is hereby granted, provided that
+#     the above copyright notice appear in all copies and that both
+#     that copyright notice and this  permission notice appear in
+#     supporting documentation.
+
+import os
+import re
+
+# Reg. exps. to find the end of a triple quote, given that
+# we know we're in one; use the "match" method; .span()[1]
+# will be the index of the character following the final
+# quote.
+_squote3_finder = re.compile(
+    r"([^\']|"
+    r"\.|"
+    r"'[^\']|"
+    r"'\.|"
+    r"''[^\']|"
+    r"''\.)*'''")
+
+_dquote3_finder = re.compile(
+    r'([^\"]|'
+    r'\.|'
+    r'"[^\"]|'
+    r'"\.|'
+    r'""[^\"]|'
+    r'""\.)*"""')
+
+# Reg. exps. to find the leftmost one-quoted string; use the
+# "search" method; .span()[0] bounds the string found.
+_dquote1_finder = re.compile(r'"([^"]|\.)*"')
+_squote1_finder = re.compile(r"'([^']|\.)*'")
+
+# _is_comment matches pure comment line.
+_is_comment = re.compile(r"^[ \t]*#").match
+
+# _is_blank matches empty line.
+_is_blank = re.compile(r"^[ \t]*$").match
+
+# find leftmost splat or quote.
+_has_nightmare = re.compile(r"""[\"'#]""").search
+
+# _is_doc_candidate matches lines that start with a triple quote.
+_is_doc_candidate = re.compile(r"^[ \t]*('''|\"\"\")")
+
+BLANK, CODE, COMMENT, DOC  = 0, 1, 2, 3
+
+def count(source):
+    """Parse the given file-like object.
+    
+    For every line, returns a `(lineno, type, line)` tuple, where `lineno`
+    is the line number starting at 1, `type` is one of `BLANK`, `CODE, `COMMENT`
+    or `DOC`, and `line` is the actual content of the line."""
+
+    quote3_finder = {'"': _dquote3_finder, "'": _squote3_finder}
+    quote1_finder = {'"': _dquote1_finder, "'": _squote1_finder }
+
+    in_doc = False
+    in_triple_quote = None
+
+    for lineno, line in enumerate(source):
+        classified = False
+
+        if in_triple_quote:
+            if in_doc:
+                yield lineno + 1, DOC, line
+            else:
+                yield lineno + 1, CODE, line
+            classified = True
+            m = in_triple_quote.match(line)
+            if m == None:
+                continue
+            # Get rid of everything through the end of the triple.
+            end = m.span()[1]
+            line = line[end:]
+            in_doc = in_triple_quote = False
+
+        if _is_blank(line):
+            if not classified:
+                yield lineno + 1, BLANK, line
+            continue
+
+        if _is_comment(line):
+            if not classified:
+                yield lineno + 1, COMMENT, line
+            continue
+
+        # Now we have a code line, a doc start line, or crap left
+        # over following the close of a multi-line triple quote; in
+        # (& only in) the last case, classified==1.
+        if not classified:
+            if _is_doc_candidate.match(line):
+                yield lineno + 1, DOC, line
+                in_doc = True
+            else:
+                yield lineno + 1, CODE, line
+
+        # The only reason to continue parsing is to make sure the
+        # start of a multi-line triple quote isn't missed.
+        while True:
+            m = _has_nightmare(line)
+            if not m:
+                break
+            else:
+                i = m.span()[0]
+
+            ch = line[i]    # splat or quote
+            if ch == '#':
+                # Chop off comment; and there are no quotes
+                # remaining because splat was leftmost.
+                break
+            # A quote is leftmost.
+            elif ch * 3 == line[i:i + 3]:
+                # at the start of a triple quote
+                in_triple_quote = quote3_finder[ch]
+                m = in_triple_quote.match(line, i + 3)
+                if m:
+                    # Remove the string & continue.
+                    end = m.span()[1]
+                    line = line[:i] + line[end:]
+                    in_doc = in_triple_quote = False
+                else:
+                    # Triple quote doesn't end on this line.
+                    break
+            else:
+                # At a single quote; remove the string & continue.
+                prev_line = line[:]
+                line = re.sub(quote1_finder[ch], ' ', line, 1)
+                # No more change detected, so be quiet or give up.
+                if prev_line == line:
+                    # Let's be quiet and hope only one line is affected.
+                    line = ''
+
+
+if __name__ == '__main__':
+    import sys
+    source = file(sys.argv[1], 'r')
+    try:
+        print crunch(source)
+    finally:
+        source.close()
Copyright (C) 2012-2017 Edgewall Software