![]() | Ned Batchelder : Blog | Code | Text | Site coverage » Home : Code : Python Modules |
Created 12 December 2004, last updated 25 May 2008 Coverage.py is a Python module that measures code coverage during Python execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. The original version was written by Gareth Rees. I've updated it to determine executable statements more accurately. InstallationTo install coverage, unpack the tar file, and run "setup.py install", or use "easy_install coverage". Download: coverage-2.80.tar.gzYou will have a coverage module for importing, and a coverage command for command line execution. There is also a set of tests for coverage: test_coverage.py and coverage_coverage.py. These will only be of interest if you want to modify coverage.py. Command-line usageThe command line interface hasn't changed from the original version. All of the original instructions still hold. Read that document if you haven't used coverage.py before. Here I'll describe the changes I've introduced. The identification of executable statements is now more accurate. Docstrings are not flagged as missing statements, nor are "global" statements. Complex multi-line if and elif conditions are handled properly. Statements can now be excluded from consideration. This is useful if you have lines of code that you know will not be executed, and you don't want the coverage report to be filled with their noise. For example, you may have interactive test clauses at the ends of your modules that your test suite will not execute: #.. all the real code .. This suite of code can be excluded from the coverage report by adding a specially-formed comment: #.. all the real code .. The #pragma line can be placed on any line of code. If the line contains the colon that introduces a suite of statements, the entire suite is excluded. Annotated files (created with "coverage -a") will prefix excluded lines with "-". Programmatic usageAgain, the original instructions still hold, but I've made a few additions. The form of the exclusion marker can be changed using the exclude() method. It takes a regular expression, and excludes any line that contains a match: # Don't count branches that will never execute As the example shows, the marker pattern doesn't have to be a comment. Any number of regular expressions can be added by calling exclude a number of times. The use of a file to hold coverage data can be suppressed by calling use_cache(0) before calling start(). The analysis(module) function still returns a 4-tuple (filename, statement list, missing list, missing string). A new analysis2(module) function extends the return to a 5-tuple (filename, statement list, excluded list, missing list, missing string). The annotate(modules) function is available to annotate a list of modules, and the annotate_file(filename, statements, excluded, missing) function provides the bulk of annotation in a directly callable form. Known ProblemsOlder versions of doctest interfere with coverage's tracing of statements, and you may get reports that none of your code is executing. Use this patch to doctest.py if you are experiencing problems. HistoryChanges I've made over time: Version 2.80, May 25 2008
Version 2.78, September 30 2007
Version 2.77, July 29 2007
Version 2.76, July 23 2007
Version 2.75, July 22 2007
Version 2.7, July 21 2007
Version 2.6, August 22 2006
Version 2.5, December 4 2005
Version 2.2, December 31 2004
Version 2.1, December 14 2004
Version 2, December 12 2004
ProblemsCoverage.py has been tested successfully on Pythons 2.2.3, 2.3.5, 2.4.3, 2.5.1 and 2.6a3. If you have code that it doesn't handle properly, send it to me! Be sure to mention the version of Python you are using. See also
| |
Comments
Nice work, Ned, thanks a lot. That was something I had wanted to do for myself but never managed to.
There is minor incompatibility with original API which IMO should be clearly noted/fixed.
1. analysis() now returns tuples of length 5 (was 4) with the extra parameter being explicitly-excluded lines I guess.
2. There is no more coverage.annotate global (this is probably just an oversight?).
Overall, it works fine. When compared with original version, it correctly identified about 150 lines that were previously marked as executable, giving a nice raise of 4% in total coverage. ;-)
Btw, while we are on API issues, can I ask for a small change. Could I suggest to extract body of annotate(morfs) into annotate1(filename, statements, excluded, missing) so I could call the latter directly?
Your wishes are my commands. I've updated the source and page above.
The original coverage.py that I have didn't include a global for annotate, but now it's there.
Thanks a lot!
Hi,
Your new improved coverage looks great.
One problem I had was with multithreaded code - only calls from the main thread are traced.
Looking at the python documentation for sys.settrace() explained why but this may not be obvious to everyone using coverage.
Calling threading.settrace(coverage.t) fixes this but maybe coverage should do this itself?
Another little suggestion, how about passing a file like object to report() rather than using print statements - handy if you want to put the results in a file.
Martin
There seems to be a bug with keyword handling in the module functions. I think they all need a **kw argument. like:
# Module functions call methods in the singleton object.
def use_cache(*args,**kw): return the_coverage.use_cache(*args,**kw)
def start(*args,**kw): return the_coverage.start(*args,**kw)
def stop(*args,**kw): return the_coverage.stop(*args,**kw)
def erase(*args,**kw): return the_coverage.erase(*args,**kw)
def exclude(*args,**kw): return the_coverage.exclude(*args,**kw)
def analysis(*args,**kw): return the_coverage.analysis(*args,**kw)
def analysis2(*args,**kw): return the_coverage.analysis2(*args,**kw)
def report(*args,**kw): return the_coverage.report(*args,**kw)
def annotate(*args,**kw): return the_coverage.annotate(*args,**kw)
def annotate_file(*args,**kw): return the_coverage.annotate_file(*args,**kw)
Thanks, now they all have them.
I've added three features:
1) include the relative or full path of each file in the report output.
2) sort the output by filename
3) -omit dir,dir2,...
* omits from report and annonotation files that have an path beginning with dir,dir2, ...
Here's the svn diff output:
Index: coverage.py
===================================================================
--- coverage.py (revision 4383)
+++ coverage.py (working copy)
@@ -41,6 +41,11 @@
the -d option, make the copies in that directory. Without the -d
option, make each copy in the same directory as the original.
+-o dir,dir2,...
+ Omit reporting or annotating files when their filename path starts with
+ a directory listed in the omit list.
+ e.g. python coverage.py -i -r -o c:\python23,lib\enthought\traits,lib\enthought\logging,lib\enthought\util
+
Coverage data is saved in the file .coverage by default. Set the
COVERAGE_FILE environment variable to save it somewhere else."""
@@ -289,6 +294,7 @@
'-m': 'show-missing',
'-r': 'report',
'-x': 'execute',
+ '-o:': 'omit=',
}
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
long_opts = optmap.values()
@@ -339,10 +345,20 @@
ignore_errors = settings.get('ignore-errors')
show_missing = settings.get('show-missing')
directory = settings.get('directory=')
+
+ omit = settings.get('omit=')
+ if omit is not None :
+ omit = omit.split(',')
+ else:
+ omit = []
+
+ self.relative_dir = os.path.normcase(os.path.abspath(os.curdir)+"\\")
+ self.relative_dir = self.relative_dir.replace("\\\\", "\\")
+
if settings.get('report'):
- self.report(args, show_missing, ignore_errors)
+ self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
if settings.get('annotate'):
- self.annotate(args, directory, ignore_errors)
+ self.annotate(args, directory, ignore_errors, omit_prefixes=omit)
def use_cache(self, usecache):
self.usecache = usecache
@@ -588,15 +604,43 @@
return (filename, statements, excluded, missing,
self.format_lines(statements, missing))
+ def relative_filename( self, filename ) :
+ """ convert filename to relative filename from self.relative_dir """
+ relfilename = filename.replace( self.relative_dir, "")
+ return relfilename
+
def morf_name(self, morf):
+ """ name of morf as used in report """
if isinstance(morf, types.ModuleType):
return morf.__name__
else:
- return os.path.splitext(os.path.basename(morf))[0]
+ return self.relative_filename( os.path.normcase(morf) )
- def report(self, morfs, show_missing=1, ignore_errors=0):
+ def filter_by_prefix( self, morfs, omit_prefixes ) :
+ """ return list of morfs where the morf name does not begin
+ with any one of the omit_prefixes
+ """
+ filtered_morfs = []
+ for morf in morfs :
+ for prefix in omit_prefixes :
+ if self.morf_name(morf).startswith(prefix) :
+ break
+ else :
+ filtered_morfs.append(morf)
+
+ return filtered_morfs
+
+ def morf_name_compare( self, x, y ) :
+ return cmp( self.morf_name(x), self.morf_name(y) )
+
+ def report(self, morfs, show_missing=1, ignore_errors=0, omit_prefixes=[]):
if not isinstance(morfs, types.ListType):
morfs = [morfs]
+
+ morfs = self.filter_by_prefix( morfs, omit_prefixes)
+
+ morfs.sort( self.morf_name_compare )
+
max_name = max([5,] + map(len, map(self.morf_name, morfs)))
fmt_name = "%%- %ds " % max_name
fmt_err = fmt_name + "%s: %s"
@@ -612,7 +656,7 @@
for morf in morfs:
name = self.morf_name(morf)
try:
- _, statements, _, missing, readable = self.analysis2(morf)
+ filename, statements, _, missing, readable = self.analysis2(morf)
n = len(statements)
m = n - len(missing)
if n > 0:
@@ -647,7 +691,8 @@
blank_re = re.compile("\\s*(#|$)")
else_re = re.compile("\\s*else\\s*:\\s*(#|$)")
- def annotate(self, morfs, directory=None, ignore_errors=0):
+ def annotate(self, morfs, directory=None, ignore_errors=0, omit_prefixes=[]):
+ morfs = self.filter_by_prefix( morfs, omit_prefixes)
for morf in morfs:
try:
filename, statements, excluded, missing, _ = self.analysis2(morf)
Has anyone had any luck running this with doctest? I use some reasonably full featured doctest tests with my current project (with both doctest.testfile and doctest.testmod), but coverage.py doesn't seem to "see" any of the tests running.
It always reports that none of the code in my various modules was actually run.
I don't use doctest, but I'd like to make coverage.py as useful as possible. Can you send me a runnable sample of what you are seeing?
Alright, I emailed you a quick example of what I'm seeing. I didn't post it here because I never trust blogs and whitespace.
Turns out the Zope svn repository already has a patch to doctest.py for this problem:
http://svn.zope.org/Zope3/trunk/src/zope/testing/doctest.py?view=diff&r1=28703&r2=28705
Thanks for the improvements, Ned!
I found I wanted full file names in my reports, so I modified morf_name a bit:
def morf_name(self, morf):
if isinstance(morf, types.ModuleType):
return morf.__file__
else:
return morf
Maybe that could be controlled via another flag (command-line arg)?
The original site seems gone - do you have copies of the original usage notes available?
Hugo: thanks for noticing the broken links. I snarfed the pages from archive.org and have made local copies. The links above now point to those.
I'm using "coverage" to start a test
suite script which in turn runs a bunch
of pyunit tests designed to exercise "pexpect.py". When I use the "-r" option I get a report like this:
Name Stmts Exec Cover
-------------------------------------
...
pexpect 511 367 71%
...
This doesn't look too bad. The problem
is when I try to use the "-a" option
I get a file "pexpect.py,cover" where
almost every line starts with "!". Only
the global doc string and imports show ">". Any idea what is going on?
This could be a great tool to use
with pyUnit testing!
Yours,
Noah
I fixed a small bug in the 'omit' commandline option. It needed a ':' behind the 'o' for the getopt parameters.
The original patch had it, so it probably went missing somewhere.
regards,
Mark
*** coverage.py.original 2006-02-13 20:48:30.000000000 +0100
--- coverage.py 2006-02-13 20:45:29.000000000 +0100
***************
*** 299,305 ****
'-m': 'show-missing',
'-r': 'report',
'-x': 'execute',
! '-o': 'omit=',
}
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
long_opts = optmap.values()
--- 299,305 ----
'-m': 'show-missing',
'-r': 'report',
'-x': 'execute',
! '-o:': 'omit=',
}
short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '')
long_opts = optmap.values()
Thanks Ned. Python really needs a set of tools to help convince the fuddy-wuds that test-driven development is more than possible in Python. IMO, coverage is an invaluable metric in verifying the efficacy of unit tests.
Just thought I'd pass this along, but I've created a simple module to play with annotations generated by coverage (basically generate XML and thus HTML throught XSLT). This is in the xix.utils.cover module of xix-utils:
$ svn co http://svn.xix.python-hosting.com/trunk XixUtils
The API is pretty simple:
>>> from xix.utils.cover import AnnotationParser, annotationToXML
>>> ann = AnnotationParser().parse('blah.py,cover')
>>> html = annotationToHTML(ann)
Hi Ned,
I had a go at generating coverage for my PyGTK GUI using this, but no statements were marked covered if they were covered from the GUI (i.e. via the user doing something). This made the coverage rather unusable.
I guess this is because GTK is C code underneath so control passes out of Python when the GUI main loop is entered.
Do you/anyone else have any bright ideas as to what to do about this?
Sorry, ignore the previous comment. I was confused. The problem was actually caused by running tests simultaneously.
Hi, Ned,
It seems there are two errors with handling of --omit / -o options. One of the fixes is described above, the other is new, I think.
C:\Python24\Lib\site-packages>"c:\Program Files\GnuWin32\bin\diff.exe" coverage.
py.org coverage.py
302c302
< '-o': 'omit=',
---
> '-o:': 'omit=',
315c315
< settings[o[2:]] = a
---
> settings[o[2:] + "="] = a
352a353
>
This is great work -- thanks so much!
I did find a decorator case it's still having trouble with:
> class Decorator(object):
> def __init__(self, func):
> self.func = func
> def __call__(self, *args, **kwargs):
> return self.func(*args, **kwargs)
> class C(object):
> @Decorator
! def f(self):
> return 42
> if __name__ == '__main__':
> C.f(C())
I've added the feature of being able to handle collecting information from multiple processes at once. (My system has multiple components in Python and besides this I like to run my tests in parallel on a grid. Everything writing to the same file was rather a limitation - it meant I could only collect one component at once and had to run everything in series).
To achieve this, there are two new options:
1) "-p" in conjunction with -x will append the machine name and process ID to the file to write to, so that every process is guaranteed a unique file.
2) "-c" will read from all such files and merge their information into the parent coverage file.
Patch (from diff -c) follows:
*** coverage.py 2006-05-16 16:20:36.013313000 +0200
--- /usr/local/bin/coverage.py 2006-07-10 18:46:12.622698000 +0200
***************
*** 24,36 ****
"""Usage:
! coverage.py -x MODULE.py [ARG1 ARG2 ...]
Execute module, passing the given command-line arguments, collecting
! coverage data.
coverage.py -e
Erase collected coverage data.
coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
Report on the statement coverage for the given files. With the -m
option, show line numbers of the statements that weren't executed.
--- 24,41 ----
"""Usage:
! coverage.py -x MODULE.py [-p] [ARG1 ARG2 ...]
Execute module, passing the given command-line arguments, collecting
! coverage data. With the -p option, write to a temporary file containing
! the machine name and process ID.
coverage.py -e
Erase collected coverage data.
+ coverage.py -c
+ Collect data from multiple coverage files (as created by -p option above)
+ and store it into a single file representing the union of the coverage.
+
coverage.py -r [-m] [-o dir1,dir2,...] FILE1 FILE2 ...
Report on the statement coverage for the given files. With the -m
option, show line numbers of the statements that weren't executed.
***************
*** 59,64 ****
--- 64,70 ----
import sys
import threading
import types
+ from socket import gethostname
# 2. IMPLEMENTATION
#
***************
*** 292,302 ****
--- 298,310 ----
settings = {}
optmap = {
'-a': 'annotate',
+ '-c': 'collect',
'-d:': 'directory=',
'-e': 'erase',
'-h': 'help',
'-i': 'ignore-errors',
'-m': 'show-missing',
+ '-p': 'parallel-mode',
'-r': 'report',
'-x': 'execute',
'-o': 'omit=',
***************
*** 318,337 ****
if settings.get('help'):
self.help()
for i in ['erase', 'execute']:
! for j in ['annotate', 'report']:
if settings.get(i) and settings.get(j):
self.help("You can't specify the '%s' and '%s' "
"options at the same time." % (i, j))
args_needed = (settings.get('execute')
or settings.get('annotate')
! or settings.get('report'))
action = settings.get('erase') or args_needed
if not action:
! self.help("You must specify at least one of -e, -x, -r, or -a.")
if not args_needed and args:
self.help("Unexpected arguments %s." % args)
! self.get_ready()
self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
if settings.get('erase'):
--- 326,346 ----
if settings.get('help'):
self.help()
for i in ['erase', 'execute']:
! for j in ['annotate', 'report', 'collect']:
if settings.get(i) and settings.get(j):
self.help("You can't specify the '%s' and '%s' "
"options at the same time." % (i, j))
args_needed = (settings.get('execute')
or settings.get('annotate')
! or settings.get('report')
! or settings.get('collect'))
action = settings.get('erase') or args_needed
if not action:
! self.help("You must specify at least one of -c, -e, -x, -r, or -a.")
if not args_needed and args:
self.help("Unexpected arguments %s." % args)
! self.get_ready(settings.get('parallel-mode'))
self.exclude('#pragma[: ]+[nN][oO] [cC][oO][vV][eE][rR]')
if settings.get('erase'):
***************
*** 344,349 ****
--- 353,360 ----
import __main__
sys.path[0] = os.path.dirname(sys.argv[0])
execfile(sys.argv[0], __main__.__dict__)
+ if settings.get('collect'):
+ self.collect()
if not args:
args = self.cexecuted.keys()
ignore_errors = settings.get('ignore-errors')
***************
*** 363,371 ****
def use_cache(self, usecache):
self.usecache = usecache
! def get_ready(self):
if self.usecache and not self.cache:
self.cache = os.environ.get(self.cache_env, self.cache_default)
self.restore()
self.analysis_cache = {}
--- 374,384 ----
def use_cache(self, usecache):
self.usecache = usecache
! def get_ready(self, parallel_mode=False):
if self.usecache and not self.cache:
self.cache = os.environ.get(self.cache_env, self.cache_default)
+ if parallel_mode:
+ self.cache += "." + gethostname() + "." + str(os.getpid())
self.restore()
self.analysis_cache = {}
***************
*** 421,437 ****
self.c = {}
self.cexecuted = {}
assert self.usecache
! if not os.path.exists(self.cache):
! return
try:
! cache = open(self.cache, 'rb')
import marshal
cexecuted = marshal.load(cache)
cache.close()
if isinstance(cexecuted, types.DictType):
! self.cexecuted = cexecuted
except:
! pass
# canonical_filename(filename). Return a canonical filename for the
# file (that is, an absolute path with no redundant components and
--- 434,478 ----
self.c = {}
self.cexecuted = {}
assert self.usecache
! if os.path.exists(self.cache):
! self.cexecuted = self.restore_file(self.cache)
!
! def restore_file(self, file_name):
try:
! cache = open(file_name, 'rb')
import marshal
cexecuted = marshal.load(cache)
cache.close()
if isinstance(cexecuted, types.DictType):
! return cexecuted
! else:
! return {}
except:
! return {}
!
! # collect(). Collect data in multiple files produced by parallel mode
!
! def collect(self):
! cache_dir, local = os.path.split(self.cache)
! for file in os.listdir(cache_dir):
! if not file.startswith(local):
! continue
!
! full_path = os.path.join(cache_dir, file)
! cexecuted = self.restore_file(full_path)
! self.merge_data(cexecuted)
!
! def merge_data(self, new_data):
! for file_name, file_data in new_data.items():
! if self.cexecuted.has_key(file_name):
! self.merge_file_data(self.cexecuted[file_name], file_data)
! else:
! self.cexecuted[file_name] = file_data
!
Hi There,
If I have code along the lines of:
x = 37
""" this is a docstring for the value x """
the docstring line will not be executed, so it does not get marked as covered, but when the parser goes to find "executable lines" for reporting, this comes up in the parser and gets treated as a normal, executable line of code.
I fixed this by changing visitDiscard to do nothing. I don't know if such a change would be considered a bug fix, but it certainly makes things play nice for me.
Cheers,
Tim
Tim, that's a very unusual usage. Why not simply use a comment for the value x? Docstrings have a special place in Python because they are available as __doc__. Your "docstring" is inaccessible except to read in the source code. You may as well use a comment and save some space in your object files.
But I'll look into the change you suggest. It may help for a different case I've been looking into...
Please help a very stupid person. I downloaded coverage.py, made it exectuable, added it to my site-packages and my path and tried the following on a piece of my code:
coverage.py -x dbauth.py
there seems to be no output anywhere. I get my programs prints to the console, but nothing from coverage and there are no files created in the current dir or in tmp.
What am I doing wrong?
Ok, never mind.. I reread and found that it is stored in .coverage in the current dir by default, so it didn't show up in my ls -l. Sorry for the dumb question
I have a testsuite module that I run coverage.py on. The testsuite recursively searches a source directory and loads all the modules dynamically through the __import__ function. These modules are then added to a unitttest.testsuite. However, when a located module A imports B, and A is dynamically loaded first, B does not get proper coverage reported when it comes time to load and exercise it.
However, when I run the testsuite module on module B only with coverage, then coverage is reported properly.
What seems to be going wrong??
Found It!!!
psyco is the culprit!!
Not sure exactly what psyco is doing to break coverage.py. But, as soon as I removed it from my python path, coverage.py started to report proper results.
Can anyone explain why psyco causes this? or know how to fix this?
Just a little note to let you know that your script does not work on 2.2.3 anymore due to the use of os.path.sep which was introduced sometime later.
I simply changed it to os.sep, and it works just fine now.
Hi,
Your release notes for v.2.6 says 'Functional Decorator' are now handled correctly. But I still see the problem reported by Steven Bethard in his post.
Is that issue fixed?
Thanks,
Julie
Now that doctest is part of Python, how would one go about patching it? Also, is the version of doctest in 2.4.4 one of the older versions you mention as being incompatible?
Terrific tool!!!!
(And it's nice to see a name I recognize from the Django lists)
Todd
I like the '#pragma: no cover' feature but it appears to only work when coverage.py is invoked from the command line.
Might it be possible to have the pragma feature work consistently across both the command line and programmatic invocations?
The windows shell doesn't expand wildcards so scripts have to do it themselves with glob. Here's a patch against 2.6:
--- coverage_orig.py 2007-02-07 20:17:25.781250000 +0000
+++ coverage.py 2007-02-07 19:04:34.781250000 +0000
@@ -63,6 +63,7 @@
import string
import sys
import threading
+import glob
import types
from socket import gethostname
@@ -715,6 +716,10 @@
print >>file, "-" * len(header)
total_statements = 0
total_executed = 0
+ newmorfs = []
+ for morf in morfs:
+ newmorfs.extend(glob.glob(morf))
+ morfs = newmorfs
for morf in morfs:
name = self.morf_name(morf)
try:
Hi Ned,
Thank you very much for this neat tool. And, btw here is a fix for the -c option. By default the cache path is '.coverage', with no '/' so os.path.split() puts an empty dirname and listdir chokes on it. At least on Linux with python 2.4.4.
--- coverage.py
+++ coverage.py
@@ -464,6 +464,8 @@
def collect(self):
cache_dir, local = os.path.split(self.cache)
+ if not cache_dir:
+ cache_dir = "."
for file in os.listdir(cache_dir):
if not file.startswith(local):
continue
Cheers.
Very nice tool,
I was about to handle the problem, googleing just if someone... and one hour after it works on my computer in my real situation.
Not so frequent at this level of need/result fit.
Thanks.
I use nose and coverage and bumped into the issue described here:
http://nose.python-hosting.com/ticket/119
I didn't see anyone else mentioning the problem here, so I didn't know if you knew about it or not.
At the bottom of that bug link is a couple of attachments that might fix the issue. I haven't looked at them, though, so that's just a cursory guess.
If you want me to look into it further, let me know.
Thanks and rock on!
Hi,
I have found bug in find_executable_statements method. Problem is when text contains a Windows endofline chars. Try following in Python interpreter:
import parser
tree = parser.suite('from os import path\r\n\n').totuple(1)
It will raise a SyntaxError exception.
If you will remove a '\r' end of line char everything will be ok.
Solution for this is remove all '\r' from text variable in the beginning of method find_executable_statements:
text = string.replace(text, '\r', '')
Hello,
I have a setup where the modules being traced and coverage.py startup directory are in /tmp, which happens to be symlinked to /private/tmp under OSX. Two things happen:
1- relative_dir is computed from os.curdir and resolves to the real path. But modules canonalized paths still start with /tmp. I think they should be normalized to realpath()
2- If [1] is done, obviously the supplied omitted paths should be normalized as well with realpath()
Does it make any sense ?
diff a/tests/coverage.py b/tests/coverage.py
--- a/tests/coverage.py
+++ b/tests/coverage.py
@@ -412,6 +412,9 @@ class coverage:
else:
omit = []
+ omit = [os.path.normcase(os.path.abspath(os.path.realpath(p)))
+ for p in omit]
+
if settings.get('report'):
self.report(args, show_missing, ignore_errors, omit_prefixes=omit)
if settings.get('annotate'):
@@ -537,7 +540,7 @@ class coverage:
if os.path.exists(g):
f = g
break
- cf = os.path.normcase(os.path.abspath(f))
+ cf = os.path.normcase(os.path.abspath(os.path.realpath(f)))
self.canonical_filename_cache[filename] = cf
return self.canonical_filename_cache[filename]
The python-nose test runner points out a possible bug in coverage.py: http://code.google.com/p/python-nose/issues/detail?id=90
The problem is that at run-time coverage receives an email.LazyImporter object that acts as a Module but doesn't subclass it, so it gets treated as a string, then throws an error since it doesn't have an "rfind" method.
This patch fixes it:
C:\projects\coverage-2.78>diff -w coverage.py C:\Python25\lib\site-packages\coverage-2.78-py2.5.egg\coverage.py
61d60
< import email
562c561
< if isinstance(morf, (types.ModuleType, email.LazyImporter)):
---
> if isinstance(morf, types.ModuleType):
782c781
< if isinstance(morf, (types.ModuleType, email.LazyImporter)):
---
> if isinstance(morf, types.ModuleType):
The doctest.py from Python 2.5 still has the problem (most code is reported as not executed), and the Zope patch linked does not apply cleanly to it. I applied the patch changes manually and it fixed the problem. Here's the unified diff:
--- doctest.py.orig 2008-01-30 13:21:17.000000000 -0500
+++ doctest.py 2008-01-30 13:26:16.000000000 -0500
@@ -317,8 +317,19 @@
"""
def __init__(self, out):
self.__out = out
+ self.__debugger_used = False
pdb.Pdb.__init__(self, stdout=out)
+ def set_trace(self):
+ self.__debugger_used = True
+ pdb.Pdb.set_trace(self)
+
+ def set_continue(self):
+ # Calling set_continue unconditionally would break unit test coverage
+ # reporting, as Bdb.set_continue calls sys.settrace(None).
+ if self.__debugger_used:
+ pdb.Pdb.set_continue(self)
+
def trace_dispatch(self, *args):
# Redirect stdout to the given stream.
save_stdout = sys.stdout
I have installed Pydev plugin with Eclipse. I can get the coverage details within eclipse.
1. Is there a way to view the coverage results outside of eclipse.
2. Is is possible to hook the coverage start/stop with Ant.
I'd recommend changing the line that reads the source code to use universal newlines; I encountered at least one file where coverage.py failed because the newline conventions were not the same as the local os:
- source = open(filename, 'rU')
+ source = open(filename, 'r')
I couldn't find anything on this webpage or in the downloaded tarball that says what license (if any) this package is released under. Could you please let me know? Thanks.
I found the license -- it's at the bottom of the 'coverage.py' source file. Thanks.
The nosetests --with-coverage bug will reported earlier ( http://nose.python-hosting.com/ticket/119 ) is due to the fact that coverage's command line script runner is named coverage.py, which clashes with the coverage module itself.
This problem only creeps up when using nose because nose is ran with its 'nosetests' command line script, which resides in the same directory of the coverage.py command line script. Python adds any script it runs' working directory to sys.path -- so when running nosetests its 'import coverage' actually incorrectly imports the coverage script runner.
The only solution is for Ned to rename coverage's command line script to something else -- like 'coverage' as opposed to 'coverage.py' would work (like 'easy_install', 'paster' and 'nosetests')
Here's a quick hack to get coverage 2.78 working with nosetests.
Edit the coverage.py script inside your python2x/scripts directory. Basically, you point it at the "coverage.py" two directories above the one it currently points to (EGGINFO/scripts/coverage.py). This worked on Windows for me.
#!c:\python25\python.exe
# EASY-INSTALL-SCRIPT: 'coverage==2.78','coverage.py'
__requires__ = 'coverage==2.78'
import pkg_resources
pkg_resources.run_script('coverage==2.78', '..\..\coverage.py')
I might be missing something pretty basic here, but when you have the .coverage file how do you analyse the results. When I open it up in notepad I get unreadable information,.
@Ben: you need to run coverage -r to interpret and report on the .coverage file.
Thanks for your help.
One more thing. Is there anyway I can produce the annotated files (displaying the ! against lines of code that have not been exectuted). I know how to do this through a batch file but can not do it through python directly. Can this be done?
Dont worry. I have figured it out!
Note that there is a package for this module for ubuntu since gutsy:
http://packages.ubuntu.com/search?suite=default§ion=all&arch=any&searchon=names&keywords=python-coverage
There seems to be a problem with #pragma: no cover and empty suites (containing only pass)
$ more bug.py
def f():
if False: # pragma: no cover
pass # This line still reported as missing
if False: # pragma: no cover
x = 1 # Now it is skipped.
def test():
f()
test()
$ coverage -e
$ coverage -x bug.py
$ coverage -rm bug.py
Name Stmts Exec Cover Missing
-------------------------------------
bug 5 4 80% 3
$ python
Python 2.5.2 (r252:60911, Mar 21 2008, 23:32:43)
[GCC 4.0.1 (Apple Computer, Inc. build 5363)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import coverage
>>> coverage.__version__
'2.80.20080525'
>>>
Hi,
Here is an additional enhancement for the -c option. The basic issue is what to do when trying to get unified coverage from tests run on UNIX and tests run on Windows. You need to be able to recognise that /home/geoff/some/path/file.py and h:\some\path\file.py are the same file and unify coverage information for them.
So the changes will now try to canonicalize the filenames on reading in when running with -c (instead of just when writing as with the other options), and I also added some hacks so that Windows paths can be recognised when running coverage.py on UNIX.
Regards,
Geoff Bache
*** /users/geoff/work/downloads/coverage-2.80/coverage.py 2008-05-25 23:32:12.000000000 +0200
--- /users/geoff/bin/coverage.py 2008-08-26 17:25:33.898334000 +0200
***************
*** 507,513 ****
full_path = os.path.join(cache_dir, f)
cexecuted = self.restore_file(full_path)
! self.merge_data(cexecuted)
def merge_data(self, new_data):
for file_name, file_data in new_data.items():
--- 507,524 ----
full_path = os.path.join(cache_dir, f)
cexecuted = self.restore_file(full_path)
! # When collecting, we expect many different sources
! # which may be different platforms and refer to the same path names
! # in different ways. We standardise around our current platform
! canonical_cexecuted = self.canonicalize_input(cexecuted)
! self.merge_data(canonical_cexecuted)
!
! def canonicalize_input(self, cexecuted):
! result = {}
! for key, value in cexecuted.items():
! canonical_key = self.canonical_filename(key)
! result[canonical_key] = value
! return result
def merge_data(self, new_data):
for file_name, file_data in new_data.items():
***************
*** 528,535 ****
def canonical_filename(self, filename):
if not self.canonical_filename_cache.has_key(filename):
f = filename
! if os.path.isabs(f) and not os.path.exists(f):
! f = os.path.basename(f)
if not os.path.isabs(f):
for path in [os.curdir] + sys.path:
g = os.path.join(path, f)
--- 539,552 ----
def canonical_filename(self, filename):
if not self.canonical_filename_cache.has_key(filename):
f = filename
! if not os.path.exists(f):
! if os.path.isabs(f):
! f = os.path.basename(f)
! # If we're on UNIX, we can't spot an absolute Windows path with os.path.isabs.
! # (The converse isn't true)
! # So check if it contains the Windows file separator
! elif os.name == "posix" and f.find("\\") != -1:
! f = os.path.basename(f.replace("\\", "/"))
if not os.path.isabs(f):
for path in [os.curdir] + sys.path:
g = os.path.join(path, f)
Ned,
I just installed coverage 2.80 from PyPI to use the
nosetests --with-coverage option. At the end it complains
about a file in an egg:
magnitude coverage.CoverageException: No source for compiled code '/home/tuba/skipm/src/tl-filters/python/modules/snake/magnitude.pyc'.
The magnitude module is installed as an egg:
% python
iPython 2.4.5 (#4, Apr 12 2008, 09:09:16)
[GCC 3.4.1] on sunos5
Type "help", "copyright", "credits" or "license" for more information.
>>> import magnitude
>>> magnitude.__file__
% unzip -t /opt/app/g++lib6/python-2.4/lib/python2.4/site-packages/magnitude-0.9.3-py2.4.egg
Archive: /opt/app/g++lib6/python-2.4/lib/python2.4/site-packages/magnitude-0.9.3-py2.4.egg
testing: magnitude.py OK
testing: magnitude.pyc OK
testing: EGG-INFO/PKG-INFO OK
testing: EGG-INFO/SOURCES.txt OK
testing: EGG-INFO/dependency_links.txt OK
testing: EGG-INFO/top_level.txt OK
testing: EGG-INFO/zip-safe OK
testing: EGG-INFO/scripts/mgload.py OK
No errors detected in compressed data of /opt/app/g++lib6/python-2.4/lib/python2.4/site-packages/magnitude-0.9.3-py2.4.egg.
Is this an issue with coverage or nose? Is there a workaround?
Thanks,
Skip Montanaro
Hi,
In my *,cover file every 'class' or 'def' line is marked with a '!'; is that a normal thing ?
Yann, that probably means that you imported your module before starting the coverage module. The class and def lines are executed when the module is imported, not when they are called, so you have to make sure to begin the coverage analysis before importing your module.
Thanks Ned,
Maybe I should go to bed ^^.
Thanks for this module, it helps a lot.
Thanks for a great tool, Ned.
I've come across a code case that seems to cause trouble for coverage though: http://paste.turbogears.org/paste/7081 This is with Python 2.5.2.
It's distilled, with some help from the SQLAlchemy list (http://groups.google.com/group/sqlalchemy/browse_thread/thread/66eea947fdb79b81), from an issue that was raised re: coverage applied to TurboGears 2 projects: http://groups.google.com/group/turbogears/browse_thread/thread/7fd3639a5a4d4b8c
Do you have any suggestions?
Add a comment: