summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.pylintrc318
-rw-r--r--CHANGES175
-rw-r--r--MANIFEST.in7
-rw-r--r--Makefile58
-rw-r--r--README.txt7
-rw-r--r--TODO.txt117
-rw-r--r--allkits.cmd12
-rw-r--r--alltests.cmd12
-rw-r--r--checkeol.py24
-rw-r--r--coverage/__init__.py76
-rw-r--r--coverage/analyzer.py232
-rw-r--r--coverage/cmdline.py149
-rw-r--r--coverage/collector.py110
-rw-r--r--coverage/control.py410
-rw-r--r--coverage/data.py122
-rw-r--r--coverage/misc.py18
-rw-r--r--coverage/tracer.c211
-rw-r--r--coverage_coverage.py27
-rw-r--r--doc/coverage.px250
-rw-r--r--ez_setup.py276
-rw-r--r--setup.py60
-rw-r--r--test/black.py15
-rw-r--r--test/covmodzip1.py3
-rw-r--r--test/modules/covmod1.py3
-rw-r--r--test/white.py15
-rw-r--r--test/white.py,cover15
-rw-r--r--test_coverage.py1954
-rw-r--r--test_files.py53
28 files changed, 4729 insertions, 0 deletions
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..c145d001
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,318 @@
+# lint Python modules using external checkers.
+#
+# This is the main checker controling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+#
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories.
+#enable-msg-cat=
+
+# Disable all messages in the listed categories.
+#disable-msg-cat=
+
+# Enable the message(s) with the given id(s).
+#enable-msg=
+
+# Disable the message(s) with the given id(s).
+# Messages that are just silly:
+# I0011:106: Locally disabling E1101
+# W0603: 28:call_singleton_method: Using the global statement
+# W0142: 31:call_singleton_method: Used * or ** magic
+# C0323:311:coverage.report: Operator not followed by a space
+# Messages that may be silly:
+# R0201: 42:Tracer.stop: Method could be a function
+# Messages that are noisy for now, eventually maybe we'll turn them on:
+# C0111:169:coverage.analyze_morf: Missing docstring
+# C0103:256:coverage.morf_filename: Invalid name "f" (should match [a-z_][a-z0-9_]{2,30}$)
+disable-msg=I0011,W0603,W0142,C0323, R0201, C0111,C0103
+
+[REPORTS]
+
+# set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells wether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note).You have access to the variables errors warning, statement which
+# respectivly contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# Enable the report(s) with the given id(s).
+#enable-report=
+
+# Disable the report(s) with the given id(s).
+#disable-report=
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branchs, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+#
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=
+
+
+# try to find bugs in the code using type inference
+#
+[TYPECHECK]
+
+# Tells wether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamicaly set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, consider the acquired-members option to ignore
+# access to some undefined attributes.
+zope=no
+
+# List of members which are usually get through zope's acquisition mecanism and
+# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
+acquired-members=REQUEST,acl_users,aq_parent
+
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assigment
+#
+[VARIABLES]
+
+# Tells wether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy|unused|.*_unused
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existant members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+#
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+#
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=15
+
+# Maximum number of locals for function / method body
+max-locals=50
+
+# Maximum number of return / yield for function / method body
+max-returns=20
+
+# Maximum number of branch for function / method body
+max-branchs=50
+
+# Maximum number of statements in function / method body
+max-statements=150
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=40
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=1
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=500
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+#
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+#
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+#
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+#
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 00000000..71063d7e
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,175 @@
+------------------------------
+CHANGE HISTORY for coverage.py
+------------------------------
+
+Version 3.0b1
+-------------
+
+Major overhaul.
+
+- Coverage.py is now a package rather than a module, so the name is a bit of a
+ misnomer, since there is no longer a file named coverage.py. Functionality
+ has been split into classes.
+
+- The trace function is implemented in C for speed.
+
+- Executable lines are identified by reading the line number tables in the
+ compiled code, removing a great deal of complicated analysis code.
+
+- The singleton coverage object is only created if the module-level functions
+ are used. This maintains the old interface while allowing better
+ programmatic use of coverage.py.
+
+- Minimum supported Python version is 2.3.
+
+
+Version 2.85, 14 September 2008
+-------------------------------
+
+- Add support for finding source files in eggs. Don't check for
+ morf's being instances of ModuleType, instead use duck typing so that
+ pseudo-modules can participate. Thanks, Imri Goldberg.
+
+- Use os.realpath as part of the fixing of filenames so that symlinks won't
+ confuse things. Thanks, Patrick Mezard.
+
+
+Version 2.80, 25 May 2008
+-------------------------
+
+- Open files in rU mode to avoid line ending craziness. Thanks, Edward Loper.
+
+
+Version 2.78, 30 September 2007
+-------------------------------
+
+- Don't try to predict whether a file is Python source based on the extension.
+ Extensionless files are often Pythons scripts. Instead, simply parse the file
+ and catch the syntax errors. Hat tip to Ben Finney.
+
+
+Version 2.77, 29 July 2007
+--------------------------
+
+- Better packaging.
+
+
+Version 2.76, 23 July 2007
+--------------------------
+
+- Now Python 2.5 is *really* fully supported: the body of the new with
+ statement is counted as executable.
+
+
+Version 2.75, 22 July 2007
+--------------------------
+
+- Python 2.5 now fully supported. The method of dealing with multi-line
+ statements is now less sensitive to the exact line that Python reports during
+ execution. Pass statements are handled specially so that their disappearance
+ during execution won't throw off the measurement.
+
+
+Version 2.7, 21 July 2007
+-------------------------
+
+- "#pragma: nocover" is excluded by default.
+
+- Properly ignore docstrings and other constant expressions that appear in the
+ middle of a function, a problem reported by Tim Leslie.
+
+- coverage.erase() shouldn't clobber the exclude regex. Change how parallel
+ mode is invoked, and fix erase() so that it erases the cache when called
+ programmatically.
+
+- In reports, ignore code executed from strings, since we can't do anything
+ useful with it anyway.
+
+- Better file handling on Linux, thanks Guillaume Chazarain.
+
+- Better shell support on Windows, thanks Noel O'Boyle.
+
+- Python 2.2 support maintained, thanks Catherine Proulx.
+
+- Minor changes to avoid lint warnings.
+
+
+Version 2.6, 23 August 2006
+---------------------------
+
+- Applied Joseph Tate's patch for function decorators.
+
+- Applied Sigve Tjora and Mark van der Wal's fixes for argument handling.
+
+- Applied Geoff Bache's parallel mode patch.
+
+- Refactorings to improve testability. Fixes to command-line logic for parallel
+ mode and collect.
+
+
+Version 2.5, 4 December 2005
+----------------------------
+
+- Call threading.settrace so that all threads are measured. Thanks Martin
+ Fuzzey.
+
+- Add a file argument to report so that reports can be captured to a different
+ destination.
+
+- coverage.py can now measure itself.
+
+- Adapted Greg Rogers' patch for using relative filenames, and sorting and
+ omitting files to report on.
+
+
+Version 2.2, 31 December 2004
+-----------------------------
+
+- Allow for keyword arguments in the module global functions. Thanks, Allen.
+
+
+Version 2.1, 14 December 2004
+-----------------------------
+
+- Return 'analysis' to its original behavior and add 'analysis2'. Add a global
+ for 'annotate', and factor it, adding 'annotate_file'.
+
+
+Version 2.0, 12 December 2004
+-----------------------------
+
+Significant code changes.
+
+- Finding executable statements has been rewritten so that docstrings and
+ other quirks of Python execution aren't mistakenly identified as missing
+ lines.
+
+- Lines can be excluded from consideration, even entire suites of lines.
+
+- The filesystem cache of covered lines can be disabled programmatically.
+
+- Modernized the code.
+
+
+Earlier History
+---------------
+
+2001-12-04 GDR Created.
+
+2001-12-06 GDR Added command-line interface and source code annotation.
+
+2001-12-09 GDR Moved design and interface to separate documents.
+
+2001-12-10 GDR Open cache file as binary on Windows. Allow simultaneous -e and
+-x, or -a and -r.
+
+2001-12-12 GDR Added command-line help. Cache analysis so that it only needs to
+be done once when you specify -a and -r.
+
+2001-12-13 GDR Improved speed while recording. Portable between Python 1.5.2
+and 2.1.1.
+
+2002-01-03 GDR Module-level functions work correctly.
+
+2002-01-07 GDR Update sys.path when running a file with the -x option, so that
+it matches the value the program would get if it were run on its own.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..b55dbd5d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,7 @@
+# For some reason, building the egg includes everything!
+exclude *.* *
+include coverage.egg-info/*.*
+include coverage/*.py
+include ez_setup.py
+include setup.py
+include README.txt
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..a0f74c20
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,58 @@
+# Makefile for utility work on coverage.py
+
+default:
+ @echo "* No default action *"
+
+TEST_ZIP = test/zipmods.zip
+
+clean:
+ -rm -rf build
+ -rm -rf dist
+ -rm -rf coverage.egg-info
+ -rm -f *.pyd */*.pyd
+ -rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc
+ -rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo
+ -rm -f *.bak */*.bak */*/*.bak */*/*/*.bak
+ -rm -f MANIFEST
+ -rm -f .coverage .coverage.*
+ -rm -f $(TEST_ZIP)
+
+lint: clean
+ python -x /Python25/Scripts/pylint.bat --rcfile=.pylintrc coverage
+ python /Python25/Lib/tabnanny.py coverage
+ python checkeol.py
+
+tests: $(TEST_ZIP)
+ python test_coverage.py
+
+$(TEST_ZIP): test/covmodzip1.py
+ zip -j $@ $+
+
+coverage:
+ python coverage_coverage.py
+
+WEBHOME = c:/ned/web/stellated/pages/code/modules
+
+publish: kit
+ cp coverage.py $(WEBHOME)
+ cp test_coverage.py $(WEBHOME)
+ cp coverage_coverage.py $(WEBHOME)
+ cp doc/coverage.px $(WEBHOME)
+ cp dist/coverage*.tar.gz $(WEBHOME)
+
+kit:
+ python setup.py sdist --formats=gztar
+ python setup.py bdist_wininst
+
+pypi:
+ python setup.py register
+
+install:
+ python setup.py install
+
+devinst:
+ python setup.py develop
+
+uninstall:
+ -rm -rf $(PYHOME)/lib/site-packages/coverage*
+ -rm -rf $(PYHOME)/scripts/coverage*
diff --git a/README.txt b/README.txt
new file mode 100644
index 00000000..b6bc7589
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,7 @@
+Coverage: code coverage testing for Python
+
+Coverage.py is a Python module that measures code coverage during test 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.
+
+For more information, see http://nedbatchelder.com/code/modules/coverage.html
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 00000000..77386e62
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,117 @@
+Coverage TODO
+
+* v3.0 beta
+
+- Windows kit.
+ - Why doesn't setup.py install work? procmon to the rescue?
+- Try installation on Ubuntu.
+- Proper project layout.
+- Code moved to Google code.
+- Investigate package over module installation.
+
+
+* BUGS
+
++ Threading is broken: C and Python trace fns called differently?
+
+
+* Speed
+
++ C extension collector
+- Ignore certain modules
+- Tricky swapping of collector like figleaf, pycov, et al.
+- Seems like there should be a faster way to manage all the line number sets in
+ CodeAnalyzer.raw_analyze.
+- If tracing, canonical_filename_cache overlaps with should_trace_cache. Skip
+ canonical_filename_cache. Maybe it isn't even worth it...
+
+* Accuracy
+
+- Record magic number of module to ensure code hasn't changed
+- Record version of coverage data file, so we can update what's stored there.
+- Record options in coverage data file, so multiple runs are certain to make
+ sense together.
+- Do I still need the lines in annotate_file that deal specially with "else"?
+
+* Power
+
+- API for getting coverage data.
+- Instruction tracing instead of line tracing.
+- Path tracing (how does this even work?)
+- Branch coverage
+- Count execution of lines
+- Track callers of functions (ala std module trace)
+- Method/Class/Module coverage reporting.
+
+* Convenience
+
+- Why can't you specify execute (-x) and report (-r) in the same invocation?
+ Maybe just because -x needs the rest of the command line?
+- How will coverage.py package install over coverage.py module?
+- Support 2.3 - 3.0?
+ http://pythonology.blogspot.com/2009/02/making-code-run-on-python-20-through-30.html
+
+* Beauty
+
+- HTML report
+- Syntax coloring in HTML report
+- Dynamic effects in HTML report
+- Footer in reports pointing to coverage home page.
+
+* Community
+
+- New docs, rather than pointing to Gareth's
+ - Min version is 2.3.
+ - Distinction between ignore (files not to trace), exclude (lines not to trace),
+ and omit (files not to report)
+ - Changes from coverage 2.x:
+ - Bare "except:" lines now count as executable code.
+ - Double function decorators: all decorator lines count as executable code.
++ Be sure --help text is complete (-i is missing).
+- Host the project somewhere with a real bug tracker, google code I guess.
+- Point discussion to TIP
+- PEP 8 compliance?
+
+* Modernization
+
++ Decide on minimum supported version
+ + 2.3
+ + Get rid of the basestring protection
+ + Use enumerate
+ + Use sets instead of dicts
+- Get rid of the recursive nonsense.
+- Docstrings.
+- Remove huge document-style comments.
++ Remove singleton
+ + Initialization of instance variables in the class.
+- Better names:
+ + self.cache -> self.cache_filename -> CoverageData.filename
+ + self.usecache -> CoverageData.use_file
+- More classes:
+ - Module munging
+ + Coverage data files
+- Why are some imports at the top of the file, and some in functions?
++ Get rid of sys.exitfunc use.
++ True and False (with no backward adaptation: the constants are new in 2.2.1)
++ Get rid of compiler module
+ + In analyzing code
+ + In test_coverage.py
+- Style:
+ + lineno
+ + filename
+
+* Correctness
+
+- What does -p (parallel mode) mean with -e (erase data)?
+
+* Tests
+
+- Tests about the .coverage file.
+- Tests about the --long-form of arguments.
+- Tests about overriding the .coverage filename.
+- Tests about parallel mode.
++ Tests about assigning a multi-line string.
+- Tests about tricky docstrings.
+- Coverage test coverage.py!
+- Tests that tracing stops after calling stop()
+- More intensive thread testing.
diff --git a/allkits.cmd b/allkits.cmd
new file mode 100644
index 00000000..ecbe74f0
--- /dev/null
+++ b/allkits.cmd
@@ -0,0 +1,12 @@
+call \ned\bin\switchpy 23
+python setup.py sdist --formats=gztar
+python setup.py bdist_wininst
+call \ned\bin\switchpy 24
+python setup.py sdist --formats=gztar
+python setup.py bdist_wininst
+call \ned\bin\switchpy 25
+python setup.py sdist --formats=gztar
+python setup.py bdist_wininst
+call \ned\bin\switchpy 26
+python setup.py sdist --formats=gztar
+python setup.py bdist_wininst
diff --git a/alltests.cmd b/alltests.cmd
new file mode 100644
index 00000000..bef277f8
--- /dev/null
+++ b/alltests.cmd
@@ -0,0 +1,12 @@
+call \ned\bin\switchpy 23
+python setup.py develop
+python test_coverage.py
+call \ned\bin\switchpy 24
+python setup.py develop
+python test_coverage.py
+call \ned\bin\switchpy 25
+python setup.py develop
+python test_coverage.py
+call \ned\bin\switchpy 26
+python setup.py develop
+python test_coverage.py
diff --git a/checkeol.py b/checkeol.py
new file mode 100644
index 00000000..65843eec
--- /dev/null
+++ b/checkeol.py
@@ -0,0 +1,24 @@
+# Check files for incorrect newlines
+
+import fnmatch, os
+
+def check_file(fname):
+ for n, line in enumerate(open(fname, "rb")):
+ if "\r" in line:
+ print "%s@%d: CR found" % (fname, n)
+ return
+
+def check_files(root, patterns):
+ for root, dirs, files in os.walk(root):
+ for f in files:
+ fname = os.path.join(root, f)
+ for p in patterns:
+ if fnmatch.fnmatch(fname, p):
+ check_file(fname)
+ break
+ if '.svn' in dirs:
+ dirs.remove('.svn')
+
+check_files("coverage", ["*.py"])
+check_files("test", ["*.py"])
+check_file("setup.py")
diff --git a/coverage/__init__.py b/coverage/__init__.py
new file mode 100644
index 00000000..8086877c
--- /dev/null
+++ b/coverage/__init__.py
@@ -0,0 +1,76 @@
+"""Code coverage measurement for Python.
+
+Ned Batchelder
+http://nedbatchelder.com/code/modules/coverage.html
+
+"""
+
+__version__ = "3.0b1" # see detailed history in CHANGES
+
+import sys
+
+from coverage.control import coverage
+from coverage.data import CoverageData
+from coverage.cmdline import main, CoverageScript
+from coverage.misc import CoverageException
+
+
+# Module-level functions. The original API to this module was based on
+# functions defined directly in the module, with a singleton of the coverage()
+# class. This design hampered programmability. Here we define the top-level
+# functions to create the singleton when they are first called.
+
+# Singleton object for use with module-level functions. The singleton is
+# created as needed when one of the module-level functions is called.
+the_coverage = None
+
+def call_singleton_method(name, args, kwargs):
+ global the_coverage
+ if not the_coverage:
+ the_coverage = coverage()
+ return getattr(the_coverage, name)(*args, **kwargs)
+
+mod_funcs = """
+ use_cache start stop erase begin_recursive end_recursive exclude
+ analysis analysis2 report annotate annotate_file
+ """
+
+coverage_module = sys.modules[__name__]
+
+for func_name in mod_funcs.split():
+ # Have to define a function here to make a closure so the function name
+ # is locked in.
+ def func(name):
+ return lambda *a, **kw: call_singleton_method(name, a, kw)
+ setattr(coverage_module, func_name, func(func_name))
+
+
+# COPYRIGHT AND LICENSE
+#
+# Copyright 2001 Gareth Rees. All rights reserved.
+# Copyright 2004-2009 Ned Batchelder. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
diff --git a/coverage/analyzer.py b/coverage/analyzer.py
new file mode 100644
index 00000000..55dae7f7
--- /dev/null
+++ b/coverage/analyzer.py
@@ -0,0 +1,232 @@
+"""Code analysis for coverage.py"""
+
+import re, token, tokenize, types
+import cStringIO as StringIO
+
+from coverage.misc import nice_pair, CoverageException
+
+
+# Python version compatibility
+try:
+ set() # new in 2.4
+except NameError:
+ import sets
+ set = sets.Set # pylint: disable-msg=W0622
+
+
+class CodeAnalyzer:
+ """Analyze code to find executable lines, excluded lines, etc."""
+
+ def __init__(self, show_tokens=False):
+ self.show_tokens = show_tokens
+
+ # The text lines of the analyzed code.
+ self.lines = None
+
+ # The line numbers of excluded lines of code.
+ self.excluded = set()
+
+ # The line numbers of docstring lines.
+ self.docstrings = set()
+
+ # A dict mapping line numbers to (lo,hi) for multi-line statements.
+ self.multiline = {}
+
+ # The line numbers that start statements.
+ self.statement_starts = set()
+
+ def find_statement_starts(self, code):
+ """Find the starts of statements in compiled code.
+
+ Uses co_lnotab described in Python/compile.c to find line numbers that
+ start statements, adding them to `self.statement_starts`.
+
+ """
+ # Adapted from dis.py in the standard library.
+ byte_increments = [ord(c) for c in code.co_lnotab[0::2]]
+ line_increments = [ord(c) for c in code.co_lnotab[1::2]]
+
+ last_line_num = None
+ line_num = code.co_firstlineno
+ for byte_incr, line_incr in zip(byte_increments, line_increments):
+ if byte_incr:
+ if line_num != last_line_num:
+ self.statement_starts.add(line_num)
+ last_line_num = line_num
+ line_num += line_incr
+ if line_num != last_line_num:
+ self.statement_starts.add(line_num)
+
+ def find_statements(self, code):
+ """Find the statements in `code`.
+
+ Update `self.statement_starts`, a set of line numbers that start
+ statements. Recurses into all code objects reachable from `code`.
+
+ """
+ # Adapted from trace.py in the standard library.
+
+ # Get all of the lineno information from this code.
+ self.find_statement_starts(code)
+
+ # Check the constants for references to other code objects.
+ for c in code.co_consts:
+ if isinstance(c, types.CodeType):
+ # Found another code object, so recurse into it.
+ self.find_statements(c)
+
+ def raw_analyze(self, text=None, filename=None, exclude=None):
+ """Analyze `text` to find the interesting facts about its lines.
+
+ A handful of member fields are updated.
+
+ """
+ if not text:
+ sourcef = open(filename, 'rU')
+ text = sourcef.read()
+ sourcef.close()
+ text = text.replace('\r\n', '\n')
+ self.lines = text.split('\n')
+
+ # Find lines which match an exclusion pattern.
+ if exclude:
+ re_exclude = re.compile(exclude)
+ for i, ltext in enumerate(self.lines):
+ if re_exclude.search(ltext):
+ self.excluded.add(i+1)
+
+ # Tokenize, to find excluded suites, to find docstrings, and to find
+ # multi-line statements.
+ indent = 0
+ exclude_indent = 0
+ excluding = False
+ prev_toktype = token.INDENT
+ first_line = None
+
+ tokgen = tokenize.generate_tokens(StringIO.StringIO(text).readline)
+ for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
+ if self.show_tokens:
+ print "%10s %5s %-20r %r" % (
+ tokenize.tok_name.get(toktype, toktype),
+ nice_pair((slineno, elineno)), ttext, ltext
+ )
+ if toktype == token.INDENT:
+ indent += 1
+ elif toktype == token.DEDENT:
+ indent -= 1
+ elif toktype == token.OP and ttext == ':':
+ if not excluding and elineno in self.excluded:
+ # Start excluding a suite. We trigger off of the colon
+ # token so that the #pragma comment will be recognized on
+ # the same line as the colon.
+ exclude_indent = indent
+ excluding = True
+ elif toktype == token.STRING and prev_toktype == token.INDENT:
+ # Strings that are first on an indented line are docstrings.
+ # (a trick from trace.py in the stdlib.)
+ for i in xrange(slineno, elineno+1):
+ self.docstrings.add(i)
+ elif toktype == token.NEWLINE:
+ if first_line is not None and elineno != first_line:
+ # We're at the end of a line, and we've ended on a
+ # different line than the first line of the statement,
+ # so record a multi-line range.
+ rng = (first_line, elineno)
+ for l in xrange(first_line, elineno+1):
+ self.multiline[l] = rng
+ first_line = None
+
+ if ttext.strip() and toktype != tokenize.COMMENT:
+ # A non-whitespace token.
+ if first_line is None:
+ # The token is not whitespace, and is the first in a
+ # statement.
+ first_line = slineno
+ # Check whether to end an excluded suite.
+ if excluding and indent <= exclude_indent:
+ excluding = False
+ if excluding:
+ self.excluded.add(elineno)
+
+ prev_toktype = toktype
+
+ # Find the starts of the executable statements.
+ filename = filename or "<code>"
+ try:
+ # Python 2.3 and 2.4 don't like partial last lines, so be sure the
+ # text ends nicely for them.
+ text += '\n'
+ code = compile(text, filename, "exec")
+ except SyntaxError, synerr:
+ raise CoverageException(
+ "Couldn't parse '%s' as Python source: '%s' at line %d" %
+ (filename, synerr.msg, synerr.lineno)
+ )
+
+ self.find_statements(code)
+
+ def map_to_first_line(self, lines, ignore=None):
+ """Map the line numbers in `lines` to the correct first line of the
+ statement.
+
+ Skip any line mentioned in `ignore`.
+
+ Returns a sorted list of the first lines.
+
+ """
+ ignore = ignore or []
+ lset = set()
+ for l in lines:
+ if l in ignore:
+ continue
+ rng = self.multiline.get(l)
+ if rng:
+ new_l = rng[0]
+ else:
+ new_l = l
+ if new_l not in ignore:
+ lset.add(new_l)
+ lines = list(lset)
+ lines.sort()
+ return lines
+
+ def analyze_source(self, text=None, filename=None, exclude=None):
+ """Analyze source text to find executable lines, excluded lines, etc.
+
+ Source can be provided as `text`, the text itself, or `filename`, from
+ which text will be read. Excluded lines are those that match `exclude`,
+ a regex.
+
+ Return values are 1) a sorted list of executable line numbers,
+ 2) a sorted list of excluded line numbers, and 3) a dict mapping line
+ numbers to pairs (lo,hi) for multi-line statements.
+
+ """
+ self.raw_analyze(text, filename, exclude)
+
+ excluded_lines = self.map_to_first_line(self.excluded)
+ ignore = excluded_lines + list(self.docstrings)
+ lines = self.map_to_first_line(self.statement_starts, ignore)
+
+ return lines, excluded_lines, self.multiline
+
+ def print_analysis(self):
+ """Print the results of the analysis."""
+ for i, ltext in enumerate(self.lines):
+ lineno = i+1
+ m0 = m1 = m2 = ' '
+ if lineno in self.statement_starts:
+ m0 = '-'
+ if lineno in self.docstrings:
+ m1 = '"'
+ if lineno in self.excluded:
+ m2 = 'x'
+ print "%4d %s%s%s %s" % (lineno, m0, m1, m2, ltext)
+
+
+if __name__ == '__main__':
+ import sys
+
+ analyzer = CodeAnalyzer(show_tokens=True)
+ analyzer.raw_analyze(filename=sys.argv[1], exclude=r"no\s*cover")
+ analyzer.print_analysis()
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
new file mode 100644
index 00000000..469338e7
--- /dev/null
+++ b/coverage/cmdline.py
@@ -0,0 +1,149 @@
+"""Command-line support for coverage.py"""
+
+import getopt, os, sys
+
+USAGE = r"""
+Coverage version %(__version__)s
+
+Usage:
+
+coverage -x [-p] MODULE.py [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 -e
+ Erase collected coverage data.
+
+coverage -c
+ Combine 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 -r [-m] [-i] [-o DIR,...] [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.
+
+coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...]
+ Make annotated copies of the given files, marking statements that
+ are executed with > and statements that are missed with !. With
+ the -d option, make the copies in that directory. Without the -d
+ option, make each copy in the same directory as the original.
+
+-h Print this help.
+
+-i Ignore errors while reporting or annotating.
+
+-o DIR,...
+ Omit reporting or annotating files when their filename path starts with
+ a directory listed in the omit list.
+ e.g. coverage -i -r -o c:\python25,lib\enthought\traits
+
+Coverage data is saved in the file .coverage by default. Set the
+COVERAGE_FILE environment variable to save it somewhere else.
+""".strip()
+
+class CoverageScript:
+ def __init__(self):
+ import coverage
+ self.covpkg = coverage
+ self.coverage = coverage.coverage()
+
+ def help(self, error=None): #pragma: no cover
+ if error:
+ print error
+ print
+ print USAGE % self.covpkg.__dict__
+ sys.exit(1)
+
+ def command_line(self, argv, help_fn=None):
+ # Collect the command-line options.
+ help_fn = help_fn or self.help
+ settings = {}
+ optmap = {
+ '-a': 'annotate',
+ '-c': 'combine',
+ '-d:': 'directory=',
+ '-e': 'erase',
+ '-h': 'help',
+ '-i': 'ignore-errors',
+ '-m': 'show-missing',
+ '-p': 'parallel-mode',
+ '-r': 'report',
+ '-x': 'execute',
+ '-o:': 'omit=',
+ }
+ short_opts = ''.join(map(lambda o: o[1:], optmap.keys()))
+ long_opts = optmap.values()
+ options, args = getopt.getopt(argv, short_opts, long_opts)
+ for o, a in options:
+ if optmap.has_key(o):
+ settings[optmap[o]] = True
+ elif optmap.has_key(o + ':'):
+ settings[optmap[o + ':']] = a
+ elif o[2:] in long_opts:
+ settings[o[2:]] = True
+ elif o[2:] + '=' in long_opts:
+ settings[o[2:]+'='] = a
+
+ if settings.get('help'):
+ help_fn()
+
+ # Check for conflicts and problems in the options.
+ for i in ['erase', 'execute']:
+ for j in ['annotate', 'report', 'combine']:
+ if settings.get(i) and settings.get(j):
+ help_fn("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 settings.get('combine')
+ or args_needed)
+ if not action:
+ help_fn("You must specify at least one of -e, -x, -c, -r, or -a.")
+ if not args_needed and args:
+ help_fn("Unexpected arguments: %s" % " ".join(args))
+
+ # Do something.
+ self.coverage.parallel_mode = settings.get('parallel-mode')
+ self.coverage.get_ready()
+
+ if settings.get('erase'):
+ self.coverage.erase()
+ if settings.get('execute'):
+ if not args:
+ help_fn("Nothing to do.")
+ sys.argv = args
+ self.coverage.start()
+ import __main__
+ sys.path[0] = os.path.dirname(sys.argv[0])
+ execfile(sys.argv[0], __main__.__dict__)
+ if settings.get('combine'):
+ self.coverage.combine()
+ if not args:
+ # For report and annotate, if no files are given on the command
+ # line, then report or annotate everything that was executed.
+ args = self.coverage.data.executed.keys() # TODO: Yikes!
+
+ 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 = [self.coverage.abs_file(p) for p in omit.split(',')]
+ else:
+ omit = []
+
+ if settings.get('report'):
+ self.coverage.report(args, show_missing, ignore_errors, omit_prefixes=omit)
+ if settings.get('annotate'):
+ self.coverage.annotate(args, directory, ignore_errors, omit_prefixes=omit)
+
+
+# Main entrypoint. This is installed as the script entrypoint, so don't
+# refactor it away...
+def main():
+ CoverageScript().command_line(sys.argv[1:])
diff --git a/coverage/collector.py b/coverage/collector.py
new file mode 100644
index 00000000..dfcfeb6d
--- /dev/null
+++ b/coverage/collector.py
@@ -0,0 +1,110 @@
+"""Raw data collector for coverage.py."""
+
+import sys, threading
+
+try:
+ # Use the C extension code when we can, for speed.
+ from coverage.tracer import Tracer
+except ImportError:
+ # If we don't have the C tracer, use this Python one.
+ class Tracer:
+ """Python implementation of the raw data tracer."""
+ def __init__(self):
+ self.cur_filename = None
+ self.filename_stack = []
+
+ def _global_trace(self, frame, event, arg_unused):
+ """The trace function passed to sys.settrace."""
+ if event == 'call':
+ filename = frame.f_code.co_filename
+ tracename = self.should_trace_cache.get(filename)
+ if tracename is None:
+ tracename = self.should_trace(filename)
+ self.should_trace_cache[filename] = tracename
+ if tracename:
+ self.filename_stack.append(self.cur_filename)
+ self.cur_filename = tracename
+ return self._local_trace
+ else:
+ return None
+ return self.trace
+
+ def _local_trace(self, frame, event, arg_unused):
+ if event == 'line':
+ self.data[(self.cur_filename, frame.f_lineno)] = True
+ elif event == 'return':
+ self.cur_filename = self.filename_stack.pop()
+ return self._local_trace
+
+ def start(self):
+ sys.settrace(self._global_trace)
+
+ def stop(self):
+ sys.settrace(None)
+
+
+class Collector:
+ """Collects trace data.
+
+ Creates a Tracer object for each thread, since they track stack information.
+ Each Tracer points to the same shared data, contributing traced data points.
+
+ """
+
+ def __init__(self, should_trace):
+ """Create a collector.
+
+ `should_trace` is a function, taking a filename, and returns a
+ canonicalized filename, or False depending on whether the file should be
+ traced or not.
+
+ """
+ self.should_trace = should_trace
+ self.reset()
+
+ def reset(self):
+ # A dictionary with an entry for (Python source file name, line number
+ # in that file) if that line has been executed.
+ self.data = {}
+
+ # A cache of the decision about whether to trace execution in a file.
+ # A dict of filename to boolean.
+ self.should_trace_cache = {}
+
+ def _start_tracer(self):
+ tracer = Tracer()
+ tracer.data = self.data
+ tracer.should_trace = self.should_trace
+ tracer.should_trace_cache = self.should_trace_cache
+ tracer.start()
+ return tracer
+
+ # The trace function has to be set individually on each thread before
+ # execution begins. Ironically, the only support the threading module has
+ # for running code before the thread main is the tracing function. So we
+ # install this as a trace function, and the first time it's called, it does
+ # the real trace installation.
+
+ def _installation_trace(self, frame_unused, event_unused, arg_unused):
+ """Called on new threads, installs the real tracer."""
+ # Remove ourselves as the trace function
+ sys.settrace(None)
+ # Install the real tracer
+ self._start_tracer()
+ # Return None to reiterate that we shouldn't be used for tracing.
+ return None
+
+ def start(self):
+ # Install the tracer on this thread.
+ self.tracer = self._start_tracer()
+ # Install our installation tracer in threading, to jump start other
+ # threads.
+ threading.settrace(self._installation_trace)
+
+ def stop(self):
+ self.tracer.stop()
+ threading.settrace(None)
+
+ def data_points(self):
+ """Return the (filename, lineno) pairs collected."""
+ return self.data.keys()
diff --git a/coverage/control.py b/coverage/control.py
new file mode 100644
index 00000000..78a65a2e
--- /dev/null
+++ b/coverage/control.py
@@ -0,0 +1,410 @@
+"""Core control stuff for coverage.py"""
+
+import glob, os, re, sys, types
+
+from coverage.data import CoverageData
+from coverage.misc import nice_pair, CoverageException
+
+
+class coverage:
+ def __init__(self):
+ from coverage.collector import Collector
+
+ self.parallel_mode = False
+ self.exclude_re = ''
+ self.nesting = 0
+ self.cstack = []
+ self.xstack = []
+ self.relative_dir = self.abs_file(os.curdir)+os.sep
+
+ self.collector = Collector(self.should_trace)
+
+ self.data = CoverageData()
+
+ # Cache of results of calling the analysis2() method, so that you can
+ # specify both -r and -a without doing double work.
+ self.analysis_cache = {}
+
+ # Cache of results of calling the canonical_filename() method, to
+ # avoid duplicating work.
+ self.canonical_filename_cache = {}
+
+ # The default exclude pattern.
+ self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
+
+ # Save coverage data when Python exits.
+ import atexit
+ atexit.register(self.save)
+
+ def should_trace(self, filename):
+ """Decide whether to trace execution in `filename`
+
+ Returns a canonicalized filename if it should be traced, False if it
+ should not.
+ """
+ if filename == '<string>':
+ # There's no point in ever tracing string executions, we can't do
+ # anything with the data later anyway.
+ return False
+ # TODO: flag: ignore std lib?
+ # TODO: ignore by module as well as file?
+ return self.canonical_filename(filename)
+
+ def use_cache(self, usecache, cache_file=None):
+ self.data.usefile(usecache, cache_file)
+
+ def get_ready(self):
+ self.collector.reset()
+ self.data.read(parallel=self.parallel_mode)
+ self.analysis_cache = {}
+
+ def start(self):
+ self.get_ready()
+ if self.nesting == 0: #pragma: no cover
+ self.collector.start()
+ self.nesting += 1
+
+ def stop(self):
+ self.nesting -= 1
+ if self.nesting == 0: #pragma: no cover
+ self.collector.stop()
+
+ def erase(self):
+ self.get_ready()
+ self.collector.reset()
+ self.analysis_cache = {}
+ self.data.erase()
+
+ def exclude(self, regex):
+ if self.exclude_re:
+ self.exclude_re += "|"
+ self.exclude_re += "(" + regex + ")"
+
+ def begin_recursive(self):
+ #self.cstack.append(self.c)
+ self.xstack.append(self.exclude_re)
+
+ def end_recursive(self):
+ #self.c = self.cstack.pop()
+ self.exclude_re = self.xstack.pop()
+
+ def save(self):
+ self.group_collected_data()
+ self.data.write()
+
+ def combine(self):
+ """Entry point for combining together parallel-mode coverage data."""
+ self.data.combine_parallel_data()
+
+ def get_zip_data(self, filename):
+ """ Get data from `filename` if it is a zip file path, or return None
+ if it is not.
+ """
+ import zipimport
+ markers = ['.zip'+os.sep, '.egg'+os.sep]
+ for marker in markers:
+ if marker in filename:
+ parts = filename.split(marker)
+ try:
+ zi = zipimport.zipimporter(parts[0]+marker[:-1])
+ except zipimport.ZipImportError:
+ continue
+ try:
+ data = zi.get_data(parts[1])
+ except IOError:
+ continue
+ return data
+ return None
+
+ def abs_file(self, filename):
+ """ Helper function to turn a filename into an absolute normalized
+ filename.
+ """
+ return os.path.normcase(os.path.abspath(os.path.realpath(filename)))
+
+ def relative_filename(self, filename):
+ """ Convert filename to relative filename from self.relative_dir.
+ """
+ return filename.replace(self.relative_dir, "")
+
+ def canonical_filename(self, filename):
+ """Return a canonical filename for `filename`.
+
+ An absolute path with no redundant components and normalized case.
+
+ """
+ if not self.canonical_filename_cache.has_key(filename):
+ f = filename
+ if os.path.isabs(f) and not os.path.exists(f):
+ if not self.get_zip_data(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)
+ if os.path.exists(g):
+ f = g
+ break
+ cf = self.abs_file(f)
+ self.canonical_filename_cache[filename] = cf
+ return self.canonical_filename_cache[filename]
+
+ def group_collected_data(self):
+ """Group the collected data by filename and reset the collector."""
+ self.data.add_raw_data(self.collector.data_points())
+ self.collector.reset()
+
+ # analyze_morf(morf). Analyze the module or filename passed as
+ # the argument. If the source code can't be found, raise an error.
+ # Otherwise, return a tuple of (1) the canonical filename of the
+ # source code for the module, (2) a list of lines of statements
+ # in the source code, (3) a list of lines of excluded statements,
+ # and (4), a map of line numbers to multi-line line number ranges, for
+ # statements that cross lines.
+
+ # The word "morf" means a module object (from which the source file can
+ # be deduced by suitable manipulation of the __file__ attribute) or a
+ # filename.
+
+ def analyze_morf(self, morf):
+ from coverage.analyzer import CodeAnalyzer
+
+ if self.analysis_cache.has_key(morf):
+ return self.analysis_cache[morf]
+ orig_filename = filename = self.morf_filename(morf)
+ ext = os.path.splitext(filename)[1]
+ source = None
+ if ext == '.pyc':
+ filename = filename[:-1]
+ ext = '.py'
+ if ext == '.py':
+ if not os.path.exists(filename):
+ source = self.get_zip_data(filename)
+ if not source:
+ raise CoverageException(
+ "No source for code '%s'." % orig_filename
+ )
+
+ analyzer = CodeAnalyzer()
+ lines, excluded_lines, line_map = analyzer.analyze_source(
+ text=source, filename=filename, exclude=self.exclude_re
+ )
+
+ result = filename, lines, excluded_lines, line_map
+ self.analysis_cache[morf] = result
+ return result
+
+ # format_lines(statements, lines). Format a list of line numbers
+ # for printing by coalescing groups of lines as long as the lines
+ # represent consecutive statements. This will coalesce even if
+ # there are gaps between statements, so if statements =
+ # [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then
+ # format_lines will return "1-2, 5-11, 13-14".
+
+ def format_lines(self, statements, lines):
+ pairs = []
+ i = 0
+ j = 0
+ start = None
+ pairs = []
+ while i < len(statements) and j < len(lines):
+ if statements[i] == lines[j]:
+ if start == None:
+ start = lines[j]
+ end = lines[j]
+ j = j + 1
+ elif start:
+ pairs.append((start, end))
+ start = None
+ i = i + 1
+ if start:
+ pairs.append((start, end))
+ ret = ', '.join(map(nice_pair, pairs))
+ return ret
+
+ # Backward compatibility with version 1.
+ def analysis(self, morf):
+ f, s, _, m, mf = self.analysis2(morf)
+ return f, s, m, mf
+
+ def analysis2(self, morf):
+ filename, statements, excluded, line_map = self.analyze_morf(morf)
+ self.group_collected_data()
+
+ # Identify missing statements.
+ missing = []
+ execed = self.data.executed_lines(filename)
+ for line in statements:
+ lines = line_map.get(line)
+ if lines:
+ for l in range(lines[0], lines[1]+1):
+ if l in execed:
+ break
+ else:
+ missing.append(line)
+ else:
+ if line not in execed:
+ missing.append(line)
+
+ return (filename, statements, excluded, missing,
+ self.format_lines(statements, missing))
+
+ # morf_filename(morf). Return the filename for a module or file.
+
+ def morf_filename(self, morf):
+ if hasattr(morf, '__file__'):
+ f = morf.__file__
+ else:
+ f = morf
+ return self.canonical_filename(f)
+
+ def morf_name(self, morf):
+ """ Return the name of morf as used in report.
+ """
+ if hasattr(morf, '__name__'):
+ return morf.__name__
+ else:
+ return self.relative_filename(os.path.splitext(morf)[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=True, ignore_errors=False, file=None, omit_prefixes=None):
+ if not isinstance(morfs, types.ListType):
+ morfs = [morfs]
+ # On windows, the shell doesn't expand wildcards. Do it here.
+ globbed = []
+ for morf in morfs:
+ if isinstance(morf, basestring) and ('?' in morf or '*' in morf):
+ globbed.extend(glob.glob(morf))
+ else:
+ globbed.append(morf)
+ morfs = globbed
+
+ if omit_prefixes:
+ morfs = self.filter_by_prefix(morfs, omit_prefixes)
+ morfs.sort(self.morf_name_compare)
+
+ max_name = max(5, max(map(len, map(self.morf_name, morfs))))
+ fmt_name = "%%- %ds " % max_name
+ fmt_err = fmt_name + "%s: %s"
+ header = fmt_name % "Name" + " Stmts Exec Cover"
+ fmt_coverage = fmt_name + "% 6d % 6d % 5d%%"
+ if show_missing:
+ header = header + " Missing"
+ fmt_coverage = fmt_coverage + " %s"
+ if not file:
+ file = sys.stdout
+ print >>file, header
+ print >>file, "-" * len(header)
+ total_statements = 0
+ total_executed = 0
+ for morf in morfs:
+ name = self.morf_name(morf)
+ try:
+ _, statements, _, missing, readable = self.analysis2(morf)
+ n = len(statements)
+ m = n - len(missing)
+ if n > 0:
+ pc = 100.0 * m / n
+ else:
+ pc = 100.0
+ args = (name, n, m, pc)
+ if show_missing:
+ args = args + (readable,)
+ print >>file, fmt_coverage % args
+ total_statements = total_statements + n
+ total_executed = total_executed + m
+ except KeyboardInterrupt: #pragma: no cover
+ raise
+ except:
+ if not ignore_errors:
+ typ, msg = sys.exc_info()[:2]
+ print >>file, fmt_err % (name, typ, msg)
+ if len(morfs) > 1:
+ print >>file, "-" * len(header)
+ if total_statements > 0:
+ pc = 100.0 * total_executed / total_statements
+ else:
+ pc = 100.0
+ args = ("TOTAL", total_statements, total_executed, pc)
+ if show_missing:
+ args = args + ("",)
+ print >>file, fmt_coverage % args
+
+ # annotate(morfs, ignore_errors).
+
+ blank_re = re.compile(r"\s*(#|$)")
+ else_re = re.compile(r"\s*else\s*:\s*(#|$)")
+
+ def annotate(self, morfs, directory=None, ignore_errors=False, omit_prefixes=None):
+ if omit_prefixes:
+ morfs = self.filter_by_prefix(morfs, omit_prefixes)
+ for morf in morfs:
+ try:
+ filename, statements, excluded, missing, _ = self.analysis2(morf)
+ self.annotate_file(filename, statements, excluded, missing, directory)
+ except KeyboardInterrupt:
+ raise
+ except:
+ if not ignore_errors:
+ raise
+
+ def annotate_file(self, filename, statements, excluded, missing, directory=None):
+ source = open(filename, 'r')
+ if directory:
+ dest_file = os.path.join(directory,
+ os.path.basename(filename)
+ + ',cover')
+ else:
+ dest_file = filename + ',cover'
+ dest = open(dest_file, 'w')
+ lineno = 0
+ i = 0
+ j = 0
+ covered = True
+ while True:
+ line = source.readline()
+ if line == '':
+ break
+ lineno = lineno + 1
+ while i < len(statements) and statements[i] < lineno:
+ i = i + 1
+ while j < len(missing) and missing[j] < lineno:
+ j = j + 1
+ if i < len(statements) and statements[i] == lineno:
+ covered = j >= len(missing) or missing[j] > lineno
+ if self.blank_re.match(line):
+ dest.write(' ')
+ elif self.else_re.match(line):
+ # Special logic for lines containing only 'else:'.
+ if i >= len(statements) and j >= len(missing):
+ dest.write('! ')
+ elif i >= len(statements) or j >= len(missing):
+ dest.write('> ')
+ elif statements[i] == missing[j]:
+ dest.write('! ')
+ else:
+ dest.write('> ')
+ elif lineno in excluded:
+ dest.write('- ')
+ elif covered:
+ dest.write('> ')
+ else:
+ dest.write('! ')
+ dest.write(line)
+ source.close()
+ dest.close()
diff --git a/coverage/data.py b/coverage/data.py
new file mode 100644
index 00000000..5d14a337
--- /dev/null
+++ b/coverage/data.py
@@ -0,0 +1,122 @@
+"""Coverage data for coverage.py"""
+
+import os, marshal, socket, types
+
+class CoverageData:
+ """Manages collected coverage data."""
+ # Name of the data file (unless environment variable is set).
+ filename_default = ".coverage"
+
+ # Environment variable naming the data file.
+ filename_env = "COVERAGE_FILE"
+
+ def __init__(self):
+ self.filename = None
+ self.use_file = True
+
+ # A map from canonical Python source file name to a dictionary in
+ # which there's an entry for each line number that has been
+ # executed:
+ #
+ # {
+ # 'filename1.py': { 12: True, 47: True, ... },
+ # ...
+ # }
+ #
+ self.executed = {}
+
+ def usefile(self, use_file=True, filename_default=None):
+ self.use_file = use_file
+ if filename_default and not self.filename:
+ self.filename_default = filename_default
+
+ def read(self, parallel=False):
+ """Read coverage data from the coverage data file (if it exists)."""
+ data = {}
+ if self.use_file and not self.filename:
+ self.filename = os.environ.get(
+ self.filename_env, self.filename_default)
+ if parallel:
+ self.filename += "." + socket.gethostname()
+ self.filename += "." + str(os.getpid())
+ if os.path.exists(self.filename):
+ data = self._read_file(self.filename)
+ self.executed = data
+
+ def write(self):
+ """Write the collected coverage data to a file."""
+ if self.use_file and self.filename:
+ self.write_file(self.filename)
+
+ def erase(self):
+ if self.filename and os.path.exists(self.filename):
+ os.remove(self.filename)
+
+ def write_file(self, filename):
+ """Write the coverage data to `filename`."""
+ f = open(filename, 'wb')
+ try:
+ marshal.dump(self.executed, f)
+ finally:
+ f.close()
+
+ def read_file(self, filename):
+ self.executed = self._read_file(filename)
+
+ def _read_file(self, filename):
+ """ Return the stored coverage data from the given file.
+ """
+ try:
+ fdata = open(filename, 'rb')
+ executed = marshal.load(fdata)
+ fdata.close()
+ if isinstance(executed, types.DictType):
+ return executed
+ else:
+ return {}
+ except:
+ return {}
+
+ def combine_parallel_data(self):
+ """ Treat self.filename as a file prefix, and combine the data from all
+ of the files starting with that prefix.
+ """
+ data_dir, local = os.path.split(self.filename)
+ for f in os.listdir(data_dir or '.'):
+ if f.startswith(local):
+ full_path = os.path.join(data_dir, f)
+ file_data = self._read_file(full_path)
+ self._combine_data(file_data)
+
+ def _combine_data(self, new_data):
+ """Combine the `new_data` into `executed`."""
+ for filename, file_data in new_data.items():
+ self.executed.setdefault(filename, {}).update(file_data)
+
+ def add_raw_data(self, data_points):
+ """Add raw data.
+
+ `data_points` is (filename, lineno) pairs.
+
+ """
+ for filename, lineno in data_points:
+ self.executed.setdefault(filename, {})[lineno] = True
+
+ def executed_lines(self, filename):
+ """Return a mapping object such that "lineno in obj" is true if that
+ line number had been executed in `filename`.
+ """
+ # TODO: Write a better description.
+ return self.executed[filename]
+
+ def summary(self):
+ """Return a dict summarizing the coverage data.
+
+ Keys are the basename of the filenames, and values are the number of
+ executed lines. This is useful in the unit tests.
+
+ """
+ summ = {}
+ for filename, lines in self.executed.items():
+ summ[os.path.basename(filename)] = len(lines)
+ return summ
diff --git a/coverage/misc.py b/coverage/misc.py
new file mode 100644
index 00000000..15ddad08
--- /dev/null
+++ b/coverage/misc.py
@@ -0,0 +1,18 @@
+"""Miscellaneous stuff for coverage.py"""
+
+def nice_pair(pair):
+ """Make a nice string representation of a pair of numbers.
+
+ If the numbers are equal, just return the number, otherwise return the pair
+ with a dash between them, indicating the range.
+
+ """
+ start, end = pair
+ if start == end:
+ return "%d" % start
+ else:
+ return "%d-%d" % (start, end)
+
+
+class CoverageException(Exception):
+ pass
diff --git a/coverage/tracer.c b/coverage/tracer.c
new file mode 100644
index 00000000..cd07ded2
--- /dev/null
+++ b/coverage/tracer.c
@@ -0,0 +1,211 @@
+// C-based Tracer for coverage.py
+
+#include "Python.h"
+#include "compile.h" // in 2.3, this wasn't part of Python.h
+#include "eval.h" // or this.
+#include "structmember.h"
+#include "frameobject.h"
+
+// The Tracer type.
+
+typedef struct {
+ PyObject_HEAD
+ PyObject * should_trace;
+ PyObject * data;
+ PyObject * should_trace_cache;
+ int started;
+ // The index of the last-used entry in tracenames.
+ int depth;
+ // Filenames to record at each level, or NULL if not recording.
+ PyObject * tracenames[300];
+} Tracer;
+
+static int
+Tracer_init(Tracer *self, PyObject *args, PyObject *kwds)
+{
+ self->should_trace = NULL;
+ self->data = NULL;
+ self->should_trace_cache = NULL;
+ self->started = 0;
+ self->depth = -1;
+ return 0;
+}
+
+static void
+Tracer_dealloc(Tracer *self)
+{
+ if (self->started) {
+ PyEval_SetTrace(NULL, NULL);
+ }
+
+ Py_XDECREF(self->should_trace);
+ Py_XDECREF(self->data);
+ Py_XDECREF(self->should_trace_cache);
+
+ while (self->depth >= 0) {
+ Py_XDECREF(self->tracenames[self->depth]);
+ self->depth--;
+ }
+
+ self->ob_type->tp_free((PyObject*)self);
+}
+
+static int
+Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
+{
+ PyObject * filename = NULL;
+ PyObject * tracename = NULL;
+
+ // printf("trace: %d @ %d\n", what, frame->f_lineno);
+
+ switch (what) {
+ case PyTrace_CALL: // 0
+ self->depth++;
+ if (self->depth > sizeof(self->tracenames)/sizeof(self->tracenames[0])) {
+ PyErr_SetString(PyExc_RuntimeError, "Tracer stack overflow");
+ return -1;
+ }
+ // Check if we should trace this line.
+ filename = frame->f_code->co_filename;
+ tracename = PyDict_GetItem(self->should_trace_cache, filename);
+ if (tracename == NULL) {
+ // We've never considered this file before. Ask should_trace about it.
+ PyObject * args = Py_BuildValue("(O)", filename);
+ tracename = PyObject_Call(self->should_trace, args, NULL);
+ Py_DECREF(args);
+ if (tracename == NULL) {
+ // An error occurred inside should_trace.
+ return -1;
+ }
+ PyDict_SetItem(self->should_trace_cache, filename, tracename);
+ }
+ else {
+ Py_INCREF(tracename);
+ }
+
+ // If tracename is a string, then we're supposed to trace.
+ self->tracenames[self->depth] = PyString_Check(tracename) ? tracename : NULL;
+ break;
+
+ case PyTrace_RETURN: // 3
+ if (self->depth >= 0) {
+ Py_XDECREF(self->tracenames[self->depth]);
+ self->depth--;
+ }
+ break;
+
+ case PyTrace_LINE: // 2
+ if (self->depth >= 0) {
+ if (self->tracenames[self->depth]) {
+ PyObject * t = PyTuple_New(2);
+ tracename = self->tracenames[self->depth];
+ Py_INCREF(tracename);
+ PyTuple_SetItem(t, 0, tracename);
+ PyTuple_SetItem(t, 1, PyInt_FromLong(frame->f_lineno));
+ Py_INCREF(Py_None);
+ PyDict_SetItem(self->data, t, Py_None);
+ Py_DECREF(t);
+ }
+ }
+ break;
+ }
+
+ return 0;
+}
+
+static PyObject *
+Tracer_start(Tracer *self, PyObject *args)
+{
+ PyEval_SetTrace((Py_tracefunc)Tracer_trace, (PyObject*)self);
+ self->started = 1;
+ return Py_BuildValue("");
+}
+
+static PyObject *
+Tracer_stop(Tracer *self, PyObject *args)
+{
+ if (self->started) {
+ PyEval_SetTrace(NULL, NULL);
+ self->started = 0;
+ }
+ return Py_BuildValue("");
+}
+
+static PyMemberDef
+Tracer_members[] = {
+ { "should_trace", T_OBJECT, offsetof(Tracer, should_trace), 0, "Function indicating whether to trace a file." },
+ { "data", T_OBJECT, offsetof(Tracer, data), 0, "The raw dictionary of trace data." },
+ { "should_trace_cache", T_OBJECT, offsetof(Tracer, should_trace_cache), 0, "Dictionary caching should_trace results." },
+ { NULL }
+};
+
+static PyMethodDef
+Tracer_methods[] = {
+ { "start", (PyCFunction) Tracer_start, METH_VARARGS, "Start the tracer" },
+ { "stop", (PyCFunction) Tracer_stop, METH_VARARGS, "Stop the tracer" },
+ { NULL }
+};
+
+static PyTypeObject
+TracerType = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /*ob_size*/
+ "coverage.Tracer", /*tp_name*/
+ sizeof(Tracer), /*tp_basicsize*/
+ 0, /*tp_itemsize*/
+ (destructor)Tracer_dealloc, /*tp_dealloc*/
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ 0, /*tp_repr*/
+ 0, /*tp_as_number*/
+ 0, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash */
+ 0, /*tp_call*/
+ 0, /*tp_str*/
+ 0, /*tp_getattro*/
+ 0, /*tp_setattro*/
+ 0, /*tp_as_buffer*/
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
+ "Tracer objects", /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ Tracer_methods, /* tp_methods */
+ Tracer_members, /* tp_members */
+ 0, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ (initproc)Tracer_init, /* tp_init */
+ 0, /* tp_alloc */
+ 0, /* tp_new */
+};
+
+// Module definition
+
+void
+inittracer(void)
+{
+ PyObject* mod;
+
+ mod = Py_InitModule3("coverage.tracer", NULL, "Fast coverage tracer.");
+ if (mod == NULL) {
+ return;
+ }
+
+ TracerType.tp_new = PyType_GenericNew;
+ if (PyType_Ready(&TracerType) < 0) {
+ return;
+ }
+
+ Py_INCREF(&TracerType);
+ PyModule_AddObject(mod, "Tracer", (PyObject *)&TracerType);
+}
diff --git a/coverage_coverage.py b/coverage_coverage.py
new file mode 100644
index 00000000..aaa1a936
--- /dev/null
+++ b/coverage_coverage.py
@@ -0,0 +1,27 @@
+# Coverage-test coverage.py!
+
+import coverage
+import test_coverage
+import unittest
+import sys
+
+print "Testing under Python version:\n", sys.version
+
+coverage.erase()
+coverage.start()
+coverage.exclude("#pragma: no cover")
+
+# Re-import coverage to get it coverage tested!
+covmod = sys.modules['coverage']
+del sys.modules['coverage']
+import coverage
+sys.modules['coverage'] = coverage = covmod
+
+suite = unittest.TestSuite()
+suite.addTest(unittest.defaultTestLoader.loadTestsFromNames(["test_coverage"]))
+
+testrunner = unittest.TextTestRunner()
+testrunner.run(suite)
+
+coverage.stop()
+coverage.report("coverage.py")
diff --git a/doc/coverage.px b/doc/coverage.px
new file mode 100644
index 00000000..ffc8b353
--- /dev/null
+++ b/doc/coverage.px
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<page title='coverage'>
+<history>
+<what when='20041212T183900'>Created.</what>
+<what when='20051204T131100'>Updated to 2.5.</what>
+<what when='20060822T210600'>Updated to 2.6.</what>
+<what when='20061001T164600'>Added a problem description for doctest users.</what>
+<what when='20070721T211900'>Updated to 2.7.</what>
+<what when='20070722T154900'>Updated to 2.75.</what>
+<what when='20070723T201400'>Updated to 2.76.</what>
+<what when='20070729T201400'>Updated to 2.77.</what>
+<what when='20080107T071400'>Updated to 2.78.</what>
+<what when='20080525T135029'>Updated to 2.8.</what>
+<what when='20080525T172606'>Updated to 2.80.</what>
+<what when='20081012T080912'>Updated to 2.85.</what>
+</history>
+
+<p>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
+<a href='code/modules/rees-coverage.html'>Gareth Rees</a>.
+I've updated it to determine executable statements more accurately.
+</p>
+
+<h1>Installation</h1>
+
+<p>To install coverage, unpack the tar file, and run "setup.py install",
+or use "easy_install coverage".</p>
+
+<download file='coverage-2.85.tar.gz' path='code/modules/coverage-2.85.tar.gz' />
+
+<p>You will have a coverage module for importing, and a coverage command
+for command line execution.</p>
+
+<p>There is also a set of tests for coverage:
+<a href='code/modules/test_coverage.py'>test_coverage.py</a> and
+<a href='code/modules/coverage_coverage.py'>coverage_coverage.py</a>.
+These will only be of interest if you want to modify coverage.py.
+</p>
+
+<h1>Command-line usage</h1>
+
+<p>The command line interface hasn't changed from the original version.
+All of the
+<a href='code/modules/rees-coverage.html'>original instructions</a>
+still hold.
+Read that document if you haven't used coverage.py before.
+Here I'll describe the changes I've introduced.</p>
+
+<p>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.
+</p>
+
+<p>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:</p>
+
+<code lang='python'><![CDATA[
+#.. all the real code ..
+
+if __name__ == '__main__':
+ # Run the test code from the command-line, for convenience
+ blah.run('sample')
+]]></code>
+
+<p>This suite of code can be excluded from the coverage report by adding
+a specially-formed comment:</p>
+
+<code lang='python'><![CDATA[
+#.. all the real code ..
+
+if __name__ == '__main__': #pragma: no cover
+ # Run the test code from the command-line, for convenience
+ blah.run('sample')
+]]></code>
+
+<p>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 "-".</p>
+
+
+<h1>Programmatic usage</h1>
+
+<p>Again, the
+<a href='code/modules/rees-coverage.html'>original instructions</a>
+still hold, but I've made a few additions.</p>
+
+<p>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:</p>
+
+<code lang='python'><![CDATA[
+# Don't count branches that will never execute
+coverage.exclude('if super_debug:')
+]]></code>
+
+<p>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.</p>
+
+<p>The use of a file to hold coverage data can be suppressed by calling use_cache(0)
+before calling start().</p>
+
+<p>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).</p>
+
+<p>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.</p>
+
+
+<h1>Known Problems</h1>
+
+<p>Older versions of doctest interfere with coverage's tracing of statements, and you
+may get reports that none of your code is executing.
+Use <a href='http://svn.zope.org/Zope3/trunk/src/zope/testing/doctest.py?rev=28679&amp;r1=28703&amp;r2=28705'>this patch to doctest.py</a>
+if you are experiencing problems.</p>
+
+
+<h1>History</h1>
+
+<p>Changes I've made over time:</p>
+
+<h2>Version 2.85, September 14 2008</h2>
+
+<ul>
+ <li>Add support for finding source files in eggs.</li>
+ <li>Don't check for morf's being instances of ModuleType, instead use duck typing
+ so that pseudo-modules can participate. Thanks, Imri Goldberg.</li>
+ <li>Use os.realpath as part of the fixing of filenames so that symlinks won't
+ confuse things. Thanks, Patrick Mezard.</li>
+</ul>
+
+<h2>Version 2.80, May 25 2008</h2>
+
+<ul>
+ <li>Coverage.py is now installed as an egg, making integration with
+ <a href='http://www.somethingaboutorange.com/mrl/projects/nose/'>nose</a> smoother.
+ If you had an older version of coverage, remove the old coverage.py in the
+ command directory (for example, /usr/bin or \Python25\Scripts).</li>
+ <li>Source files are opened in rU mode, preventing problems with errant line endings.</li>
+</ul>
+
+<h2>Version 2.78, September 30 2007</h2>
+
+<ul>
+ <li>Better handling of Python source in files that don't end with .py.
+ Thanks, Ben Finney.</li>
+</ul>
+
+<h2>Version 2.77, July 29 2007</h2>
+
+<ul>
+<li>Better packaging, including Cheeseshop goodness.</li>
+</ul>
+
+<h2>Version 2.76, July 23 2007</h2>
+
+<ul>
+<li>Added support for the overlooked "with" statement in Python 2.5.</li>
+</ul>
+
+<h2>Version 2.75, July 22 2007</h2>
+
+<ul>
+<li>The way multi-line statements are handled has been revamped, allowing
+coverage.py to support Python 2.5.</li>
+<li>Functions with just a docstring and a pass statement no longer report the
+pass as uncovered.</li>
+</ul>
+
+<h2>Version 2.7, July 21 2007</h2>
+
+<ul>
+<li>The #pragma:nocover comment syntax is ignored by default, so programmatic
+invocations of coverage also attend to those declarations.</li>
+<li>Constants in the middle of functions are properly ignored, since they won't be executed.</li>
+<li>Code exec'ed from strings no longer clutters reports with exceptions.
+That code will be ignored by coverage.py, since we can't get the source code to
+analyze it anyway.</li>
+<li>Minor fixes: Linux current directory handling (thanks Guillaume Chazarain),
+globbing for Windows (thanks Noel O'Boyle), and Python 2.2 compatibility (thanks Catherine Proulx).</li>
+</ul>
+
+<h2>Version 2.6, August 22 2006</h2>
+
+<ul>
+<li>Function decorators are now handled properly (thanks Joseph Tate).</li>
+<li>Fixed a few bugs with the --omit option (thanks Mark van der Wal and Sigve Tjora)</li>
+<li>Coverage data files can be written from several processes at once with the -p and -c options (thanks Geoff Bache).</li>
+</ul>
+
+<h2>Version 2.5, December 4 2005</h2>
+
+<ul>
+<li>Multi-threaded programs now have all threads properly measured (thanks Martin Fuzzey).</li>
+<li>The report() method now takes an optional file argument which defaults to stdout.</li>
+<li>Adapted Greg Rogers' patch to allow omitting files by directory from the report and annotation,
+sorting files, and reporting files relatively.</li>
+<li>coverage.py can now recursively measure itself under test!</li>
+</ul>
+
+<h2>Version 2.2, December 31 2004</h2>
+
+<ul>
+<li>Made it possible to use keyword arguments with the module global functions (thanks Allen).</li>
+</ul>
+
+<h2>Version 2.1, December 14 2004</h2>
+
+<ul>
+<li>Fix some backward-compatibility problems with the analysis function.</li>
+<li>Refactor annotate to provide annotate_file.</li>
+</ul>
+
+<h2>Version 2, December 12 2004</h2>
+
+<ul>
+<li>My first version.</li>
+</ul>
+
+
+<h1>Problems</h1>
+
+<p>Coverage.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.
+</p>
+
+
+
+<h1>See also</h1>
+
+<ul>
+<li>Gareth Rees's <a href='code/modules/rees-design.html'>original page</a> about the design of coverage.py</li>
+<li><a href='http://www.mems-exchange.org/software/sancho/'>Sancho</a> is a unit testing framework that includes code coverage measurement.</li>
+<li><a href='http://www.geocities.com/drew_csillag/pycover.html'>pycover</a>, another Python implementation of code coverage.</li>
+<li>The <a href='http://docs.python.org/library/trace.html'>trace module</a> in the Python standard library.</li>
+<li><a href='blog'>My blog</a>, where topics often include those of interest to both casual and serious Python users.</li>
+</ul>
+
+<googleads/>
+<pagecomments/>
+
+</page>
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 00000000..d24e845e
--- /dev/null
+++ b/ez_setup.py
@@ -0,0 +1,276 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c9"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+ 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
+ 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
+ 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
+ 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
+}
+
+import sys, os
+try: from hashlib import md5
+except ImportError: from md5 import md5
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..2f0aeed5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,60 @@
+# setup.py for coverage.
+
+"""\
+Code coverage testing for Python
+
+Coverage.py is a Python package that measures code coverage during test 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.
+"""
+
+classifiers = """\
+Development Status :: 5 - Production/Stable
+Environment :: Console
+Intended Audience :: Developers
+License :: OSI Approved :: BSD License
+Operating System :: OS Independent
+Programming Language :: Python
+Topic :: Software Development :: Quality Assurance
+Topic :: Software Development :: Testing
+"""
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+from distutils.core import Extension
+
+from coverage import __version__
+
+doclines = __doc__.split("\n")
+
+setup(
+ name = 'coverage',
+ version = __version__,
+
+ packages = [
+ 'coverage',
+ ],
+
+ entry_points={
+ 'console_scripts': [
+ 'coverage = coverage:main',
+ ]
+ },
+ zip_safe = True, # __file__ appears in the source, but doesn't break zippy-ness.
+
+ ext_modules = [
+ Extension("coverage.tracer", sources=["coverage/tracer.c"])
+ ],
+
+ author = 'Ned Batchelder',
+ author_email = 'ned@nedbatchelder.com',
+ description = doclines[0],
+ long_description = "\n".join(doclines[2:]),
+ keywords = 'code coverage testing',
+ license = 'BSD',
+ classifiers = filter(None, classifiers.split("\n")),
+ url = 'http://nedbatchelder.com/code/modules/coverage.html',
+ download_url = 'http://nedbatchelder.com/code/modules/coverage-%s.tar.gz' % __version__,
+)
diff --git a/test/black.py b/test/black.py
new file mode 100644
index 00000000..cec90a9d
--- /dev/null
+++ b/test/black.py
@@ -0,0 +1,15 @@
+# A test case sent to me by Steve White
+
+def f(self):
+ if self==1:
+ pass
+ elif self.m('fred'):
+ pass
+ elif (g==1) and (b==2):
+ pass
+ elif self.m('fred')==True:
+ pass
+ elif ((g==1) and (b==2))==True:
+ pass
+ else:
+ pass
diff --git a/test/covmodzip1.py b/test/covmodzip1.py
new file mode 100644
index 00000000..c35688a8
--- /dev/null
+++ b/test/covmodzip1.py
@@ -0,0 +1,3 @@
+# covmodzip.py: for putting into a zip file.
+j = 1
+j += 1
diff --git a/test/modules/covmod1.py b/test/modules/covmod1.py
new file mode 100644
index 00000000..b3f5e5f2
--- /dev/null
+++ b/test/modules/covmod1.py
@@ -0,0 +1,3 @@
+# covmod1.py: Simplest module for testing.
+i = 1
+i += 1
diff --git a/test/white.py b/test/white.py
new file mode 100644
index 00000000..cec90a9d
--- /dev/null
+++ b/test/white.py
@@ -0,0 +1,15 @@
+# A test case sent to me by Steve White
+
+def f(self):
+ if self==1:
+ pass
+ elif self.m('fred'):
+ pass
+ elif (g==1) and (b==2):
+ pass
+ elif self.m('fred')==True:
+ pass
+ elif ((g==1) and (b==2))==True:
+ pass
+ else:
+ pass
diff --git a/test/white.py,cover b/test/white.py,cover
new file mode 100644
index 00000000..e1ad40e4
--- /dev/null
+++ b/test/white.py,cover
@@ -0,0 +1,15 @@
+ # A test case sent to me by Steve White
+
+> def f(self):
+! if self==1:
+! pass
+! elif self.m('fred'):
+! pass
+! elif (g==1) and (b==2):
+! pass
+! elif self.m('fred')==True:
+! pass
+! elif ((g==1) and (b==2))==True:
+! pass
+! else:
+! pass
diff --git a/test_coverage.py b/test_coverage.py
new file mode 100644
index 00000000..ec86c8d6
--- /dev/null
+++ b/test_coverage.py
@@ -0,0 +1,1954 @@
+# test coverage.py
+# Copyright 2004-2009, Ned Batchelder
+# http://nedbatchelder.com/code/modules/coverage.html
+
+# Change some of these 0's to 1's to get diagnostic output during testing.
+showstdout = 0
+
+import unittest
+import imp, os, pprint, random, sys, tempfile
+from cStringIO import StringIO
+
+import path # from http://www.jorendorff.com/articles/python/path/
+
+import coverage
+
+CovExc = coverage.CoverageException
+
+from textwrap import dedent
+
+
+coverage.use_cache(0)
+
+
+class CoverageTest(unittest.TestCase):
+ def setUp(self):
+ # Create a temporary directory.
+ self.noise = str(random.random())[2:]
+ self.temproot = path.path(tempfile.gettempdir()) / 'test_coverage'
+ self.tempdir = self.temproot / self.noise
+ self.tempdir.makedirs()
+ self.olddir = os.getcwd()
+ os.chdir(self.tempdir)
+ # Keep a counter to make every call to checkCoverage unique.
+ self.n = 0
+
+ # Capture stdout, so we can use print statements in the tests and not
+ # pollute the test output.
+ self.oldstdout = sys.stdout
+ self.capturedstdout = StringIO()
+ if not showstdout:
+ sys.stdout = self.capturedstdout
+ coverage.begin_recursive()
+
+ def tearDown(self):
+ coverage.end_recursive()
+ sys.stdout = self.oldstdout
+ # Get rid of the temporary directory.
+ os.chdir(self.olddir)
+ self.temproot.rmtree()
+
+ def getStdout(self):
+ return self.capturedstdout.getvalue()
+
+ def makeFile(self, modname, text):
+ """ Create a temp file with modname as the module name, and text as the
+ contents.
+ """
+ text = dedent(text)
+
+ # Create the python file.
+ f = open(modname + '.py', 'w')
+ f.write(text)
+ f.close()
+
+ def importModule(self, modname):
+ """ Import the module named modname, and return the module object.
+ """
+ modfile = modname + '.py'
+ f = open(modfile, 'r')
+
+ for suff in imp.get_suffixes():
+ if suff[0] == '.py':
+ break
+ try:
+ mod = imp.load_module(modname, f, modfile, suff)
+ finally:
+ f.close()
+ return mod
+
+ def getModuleName(self):
+ # We append self.n because otherwise two calls in one test will use the
+ # same filename and whether the test works or not depends on the
+ # timestamps in the .pyc file, so it becomes random whether the second
+ # call will use the compiled version of the first call's code or not!
+ modname = 'coverage_test_' + self.noise + str(self.n)
+ self.n += 1
+ return modname
+
+ def checkCoverage(self, text, lines, missing="", excludes=[], report=""):
+ self.checkEverything(text=text, lines=lines, missing=missing, excludes=excludes, report=report)
+
+ def checkEverything(self, text=None, file=None, lines=None, missing=None,
+ excludes=[], report="", annfile=None):
+ assert text or file
+ assert not (text and file)
+
+ # We write the code into a file so that we can import it.
+ # coverage.py wants to deal with things as modules with file names.
+ modname = self.getModuleName()
+
+ if text:
+ self.makeFile(modname, text)
+ elif file:
+ p = path.path(self.olddir) / file
+ p.copyfile(modname + '.py')
+
+ # Start up coverage.py
+ coverage.erase()
+ for exc in excludes:
+ coverage.exclude(exc)
+ coverage.start()
+
+ # Import the python file, executing it.
+ mod = self.importModule(modname)
+
+ # Stop coverage.py
+ coverage.stop()
+
+ # Clean up our side effects
+ del sys.modules[modname]
+
+ # Get the analysis results, and check that they are right.
+ _, clines, _, cmissing = coverage.analysis(mod)
+ if lines is not None:
+ if type(lines[0]) == type(1):
+ self.assertEqual(clines, lines)
+ else:
+ for line_list in lines:
+ if clines == line_list:
+ break
+ else:
+ self.fail("None of the lines choices matched %r" % clines)
+ if missing is not None:
+ if type(missing) == type(""):
+ self.assertEqual(cmissing, missing)
+ else:
+ for missing_list in missing:
+ if cmissing == missing_list:
+ break
+ else:
+ self.fail("None of the missing choices matched %r" % cmissing)
+
+ if report:
+ frep = StringIO()
+ coverage.report(mod, file=frep)
+ rep = " ".join(frep.getvalue().split("\n")[2].split()[1:])
+ self.assertEqual(report, rep)
+
+ if annfile:
+ coverage.annotate([modname+'.py'])
+ expect = (path.path(self.olddir) / annfile).text()
+ actual = path.path(modname + '.py,cover').text()
+ self.assertEqual(expect, actual)
+
+ def assertRaisesMsg(self, excClass, msg, callableObj, *args, **kwargs):
+ """ Just like unittest.TestCase.assertRaises,
+ but checks that the message is right too.
+ """
+ try:
+ callableObj(*args, **kwargs)
+ except excClass, exc:
+ excMsg = str(exc)
+ if not msg:
+ # No message provided: it passes.
+ return #pragma: no cover
+ elif excMsg == msg:
+ # Message provided, and we got the right message: it passes.
+ return
+ else: #pragma: no cover
+ # Message provided, and it didn't match: fail!
+ raise self.failureException("Right exception, wrong message: got '%s' expected '%s'" % (excMsg, msg))
+ # No need to catch other exceptions: They'll fail the test all by themselves!
+ else: #pragma: no cover
+ if hasattr(excClass,'__name__'):
+ excName = excClass.__name__
+ else:
+ excName = str(excClass)
+ raise self.failureException("Expected to raise %s, didn't get an exception at all" % excName)
+
+ def nice_file(self, *fparts):
+ return os.path.normcase(os.path.abspath(os.path.realpath(os.path.join(*fparts))))
+
+ def run_command(self, cmd):
+ """ Run the command-line `cmd`, print its output.
+ """
+ # Add our test modules directory to PYTHONPATH. I'm sure there's too
+ # much path munging here, but...
+ here = os.path.dirname(self.nice_file(coverage.__file__, ".."))
+ testmods = self.nice_file(here, 'test/modules')
+ zipfile = self.nice_file(here, 'test/zipmods.zip')
+ pypath = os.environ['PYTHONPATH']
+ if pypath:
+ pypath += os.pathsep
+ pypath += testmods + os.pathsep + zipfile
+ os.environ['PYTHONPATH'] = pypath
+
+ stdin, stdouterr = os.popen4(cmd)
+ output = stdouterr.read()
+ if showstdout:
+ print output
+ return output
+
+
+class BasicCoverageTests(CoverageTest):
+ def testSimple(self):
+ self.checkCoverage("""\
+ a = 1
+ b = 2
+
+ c = 4
+ # Nothing here
+ d = 6
+ """,
+ [1,2,4,6], report="4 4 100%")
+
+ def testIndentationWackiness(self):
+ # Partial final lines are OK.
+ self.checkCoverage("""\
+ import sys
+ if not sys.path:
+ a = 1
+ """,
+ [1,2,3], "3")
+
+ def testMultilineInitializer(self):
+ self.checkCoverage("""\
+ d = {
+ 'foo': 1+2,
+ 'bar': (lambda x: x+1)(1),
+ 'baz': str(1),
+ }
+
+ e = { 'foo': 1, 'bar': 2 }
+ """,
+ [1,7], "")
+
+ def testListComprehension(self):
+ self.checkCoverage("""\
+ l = [
+ 2*i for i in range(10)
+ if i > 5
+ ]
+ assert l == [12, 14, 16, 18]
+ """,
+ [1,5], "")
+
+
+class SimpleStatementTests(CoverageTest):
+ def testExpression(self):
+ self.checkCoverage("""\
+ 1 + 2
+ 1 + \\
+ 2
+ """,
+ [1,2], "")
+
+ def testAssert(self):
+ self.checkCoverage("""\
+ assert (1 + 2)
+ assert (1 +
+ 2)
+ assert (1 + 2), 'the universe is broken'
+ assert (1 +
+ 2), \\
+ 'something is amiss'
+ """,
+ [1,2,4,5], "")
+
+ def testAssignment(self):
+ # Simple variable assignment
+ self.checkCoverage("""\
+ a = (1 + 2)
+ b = (1 +
+ 2)
+ c = \\
+ 1
+ """,
+ [1,2,4], "")
+
+ def testAssignTuple(self):
+ self.checkCoverage("""\
+ a = 1
+ a,b,c = 7,8,9
+ assert a == 7 and b == 8 and c == 9
+ """,
+ [1,2,3], "")
+
+ def testAttributeAssignment(self):
+ # Attribute assignment
+ self.checkCoverage("""\
+ class obj: pass
+ o = obj()
+ o.foo = (1 + 2)
+ o.foo = (1 +
+ 2)
+ o.foo = \\
+ 1
+ """,
+ [1,2,3,4,6], "")
+
+ def testListofAttributeAssignment(self):
+ self.checkCoverage("""\
+ class obj: pass
+ o = obj()
+ o.a, o.b = (1 + 2), 3
+ o.a, o.b = (1 +
+ 2), (3 +
+ 4)
+ o.a, o.b = \\
+ 1, \\
+ 2
+ """,
+ [1,2,3,4,7], "")
+
+ def testAugmentedAssignment(self):
+ self.checkCoverage("""\
+ a = 1
+ a += 1
+ a += (1 +
+ 2)
+ a += \\
+ 1
+ """,
+ [1,2,3,5], "")
+
+ def testTripleStringStuff(self):
+ self.checkCoverage("""\
+ a = '''
+ a multiline
+ string.
+ '''
+ b = '''
+ long expression
+ ''' + '''
+ on many
+ lines.
+ '''
+ c = len('''
+ long expression
+ ''' +
+ '''
+ on many
+ lines.
+ ''')
+ """,
+ [1,5,11], "")
+
+ def testPass(self):
+ # pass is tricky: if it's the only statement in a block, then it is
+ # "executed". But if it is not the only statement, then it is not.
+ self.checkCoverage("""\
+ if 1==1:
+ pass
+ """,
+ [1,2], "")
+ self.checkCoverage("""\
+ def foo():
+ pass
+ foo()
+ """,
+ [1,2,3], "")
+ self.checkCoverage("""\
+ def foo():
+ "doc"
+ pass
+ foo()
+ """,
+ ([1,3,4], [1,4]), "")
+ self.checkCoverage("""\
+ class Foo:
+ def foo(self):
+ pass
+ Foo().foo()
+ """,
+ [1,2,3,4], "")
+ self.checkCoverage("""\
+ class Foo:
+ def foo(self):
+ "Huh?"
+ pass
+ Foo().foo()
+ """,
+ ([1,2,4,5], [1,2,5]), "")
+
+ def testDel(self):
+ self.checkCoverage("""\
+ d = { 'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1 }
+ del d['a']
+ del d[
+ 'b'
+ ]
+ del d['c'], \\
+ d['d'], \\
+ d['e']
+ assert(len(d.keys()) == 0)
+ """,
+ [1,2,3,6,9], "")
+
+ def testPrint(self):
+ self.checkCoverage("""\
+ print "hello, world!"
+ print ("hey: %d" %
+ 17)
+ print "goodbye"
+ print "hello, world!",
+ print ("hey: %d" %
+ 17),
+ print "goodbye",
+ """,
+ [1,2,4,5,6,8], "")
+
+ def testRaise(self):
+ self.checkCoverage("""\
+ try:
+ raise Exception(
+ "hello %d" %
+ 17)
+ except:
+ pass
+ """,
+ [1,2,5,6], "")
+
+ def testReturn(self):
+ self.checkCoverage("""\
+ def fn():
+ a = 1
+ return a
+
+ x = fn()
+ assert(x == 1)
+ """,
+ [1,2,3,5,6], "")
+ self.checkCoverage("""\
+ def fn():
+ a = 1
+ return (
+ a +
+ 1)
+
+ x = fn()
+ assert(x == 2)
+ """,
+ [1,2,3,7,8], "")
+ self.checkCoverage("""\
+ def fn():
+ a = 1
+ return (a,
+ a + 1,
+ a + 2)
+
+ x,y,z = fn()
+ assert x == 1 and y == 2 and z == 3
+ """,
+ [1,2,3,7,8], "")
+
+ def testYield(self):
+ self.checkCoverage("""\
+ from __future__ import generators
+ def gen():
+ yield 1
+ yield (2+
+ 3+
+ 4)
+ yield 1, \\
+ 2
+ a,b,c = gen()
+ assert a == 1 and b == 9 and c == (1,2)
+ """,
+ [1,2,3,4,7,9,10], "")
+
+ def testBreak(self):
+ self.checkCoverage("""\
+ for x in range(10):
+ print "Hello"
+ break
+ print "Not here"
+ """,
+ [1,2,3,4], "4")
+
+ def testContinue(self):
+ self.checkCoverage("""\
+ for x in range(10):
+ print "Hello"
+ continue
+ print "Not here"
+ """,
+ [1,2,3,4], "4")
+
+ if 0:
+ # Peephole optimization of jumps to jumps can mean that some statements
+ # never hit the line tracer. The behavior is different in different
+ # versions of Python, so don't run this test:
+ def testStrangeUnexecutedContinue(self):
+ self.checkCoverage("""\
+ a = b = c = 0
+ for n in range(100):
+ if n % 2:
+ if n % 4:
+ a += 1
+ continue # <-- This line may not be hit.
+ else:
+ b += 1
+ c += 1
+ assert a == 50 and b == 50 and c == 50
+
+ a = b = c = 0
+ for n in range(100):
+ if n % 2:
+ if n % 3:
+ a += 1
+ continue # <-- This line is always hit.
+ else:
+ b += 1
+ c += 1
+ assert a == 33 and b == 50 and c == 50
+ """,
+ [1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], "")
+
+ def testImport(self):
+ self.checkCoverage("""\
+ import string
+ from sys import path
+ a = 1
+ """,
+ [1,2,3], "")
+ self.checkCoverage("""\
+ import string
+ if 1 == 2:
+ from sys import path
+ a = 1
+ """,
+ [1,2,3,4], "3")
+ self.checkCoverage("""\
+ import string, \\
+ os, \\
+ re
+ from sys import path, \\
+ stdout
+ a = 1
+ """,
+ [1,4,6], "")
+ self.checkCoverage("""\
+ import sys, sys as s
+ assert s.path == sys.path
+ """,
+ [1,2], "")
+ self.checkCoverage("""\
+ import sys, \\
+ sys as s
+ assert s.path == sys.path
+ """,
+ [1,3], "")
+ self.checkCoverage("""\
+ from sys import path, \\
+ path as p
+ assert p == path
+ """,
+ [1,3], "")
+ self.checkCoverage("""\
+ from sys import \\
+ *
+ assert len(path) > 0
+ """,
+ [1,3], "")
+
+ def testGlobal(self):
+ self.checkCoverage("""\
+ g = h = i = 1
+ def fn():
+ global g
+ global h, \\
+ i
+ g = h = i = 2
+ fn()
+ assert g == 2 and h == 2 and i == 2
+ """,
+ [1,2,6,7,8], "")
+ self.checkCoverage("""\
+ g = h = i = 1
+ def fn():
+ global g; g = 2
+ fn()
+ assert g == 2 and h == 1 and i == 1
+ """,
+ [1,2,3,4,5], "")
+
+ def testExec(self):
+ self.checkCoverage("""\
+ a = b = c = 1
+ exec "a = 2"
+ exec ("b = " +
+ "c = " +
+ "2")
+ assert a == 2 and b == 2 and c == 2
+ """,
+ [1,2,3,6], "")
+ self.checkCoverage("""\
+ vars = {'a': 1, 'b': 1, 'c': 1}
+ exec "a = 2" in vars
+ exec ("b = " +
+ "c = " +
+ "2") in vars
+ assert vars['a'] == 2 and vars['b'] == 2 and vars['c'] == 2
+ """,
+ [1,2,3,6], "")
+ self.checkCoverage("""\
+ globs = {}
+ locs = {'a': 1, 'b': 1, 'c': 1}
+ exec "a = 2" in globs, locs
+ exec ("b = " +
+ "c = " +
+ "2") in globs, locs
+ assert locs['a'] == 2 and locs['b'] == 2 and locs['c'] == 2
+ """,
+ [1,2,3,4,7], "")
+
+ def testExtraDocString(self):
+ self.checkCoverage("""\
+ a = 1
+ "An extra docstring, should be a comment."
+ b = 3
+ assert (a,b) == (1,3)
+ """,
+ [1,3,4], "")
+ self.checkCoverage("""\
+ a = 1
+ "An extra docstring, should be a comment."
+ b = 3
+ 123 # A number for some reason: ignored
+ 1+1 # An expression: executed.
+ c = 6
+ assert (a,b,c) == (1,3,6)
+ """,
+ ([1,3,5,6,7], [1,3,4,5,6,7]), "")
+
+
+class CompoundStatementTests(CoverageTest):
+ def testStatementList(self):
+ self.checkCoverage("""\
+ a = 1;
+ b = 2; c = 3
+ d = 4; e = 5;
+
+ assert (a,b,c,d,e) == (1,2,3,4,5)
+ """,
+ [1,2,3,5], "")
+
+ def testIf(self):
+ self.checkCoverage("""\
+ a = 1
+ if a == 1:
+ x = 3
+ assert x == 3
+ if (a ==
+ 1):
+ x = 7
+ assert x == 7
+ """,
+ [1,2,3,4,5,7,8], "")
+ self.checkCoverage("""\
+ a = 1
+ if a == 1:
+ x = 3
+ else:
+ y = 5
+ assert x == 3
+ """,
+ [1,2,3,5,6], "5")
+ self.checkCoverage("""\
+ a = 1
+ if a != 1:
+ x = 3
+ else:
+ y = 5
+ assert y == 5
+ """,
+ [1,2,3,5,6], "3")
+ self.checkCoverage("""\
+ a = 1; b = 2
+ if a == 1:
+ if b == 2:
+ x = 4
+ else:
+ y = 6
+ else:
+ z = 8
+ assert x == 4
+ """,
+ [1,2,3,4,6,8,9], "6-8")
+
+ def testElif(self):
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a == 1:
+ x = 3
+ elif b == 2:
+ y = 5
+ else:
+ z = 7
+ assert x == 3
+ """,
+ [1,2,3,4,5,7,8], "4-7", report="7 4 57% 4-7")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a != 1:
+ x = 3
+ elif b == 2:
+ y = 5
+ else:
+ z = 7
+ assert y == 5
+ """,
+ [1,2,3,4,5,7,8], "3, 7", report="7 5 71% 3, 7")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a != 1:
+ x = 3
+ elif b != 2:
+ y = 5
+ else:
+ z = 7
+ assert z == 7
+ """,
+ [1,2,3,4,5,7,8], "3, 5", report="7 5 71% 3, 5")
+
+ def testElifNoElse(self):
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a == 1:
+ x = 3
+ elif b == 2:
+ y = 5
+ assert x == 3
+ """,
+ [1,2,3,4,5,6], "4-5", report="6 4 66% 4-5")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a != 1:
+ x = 3
+ elif b == 2:
+ y = 5
+ assert y == 5
+ """,
+ [1,2,3,4,5,6], "3", report="6 5 83% 3")
+
+ def testElifBizarre(self):
+ self.checkCoverage("""\
+ def f(self):
+ if self==1:
+ x = 3
+ elif self.m('fred'):
+ x = 5
+ elif (g==1) and (b==2):
+ x = 7
+ elif self.m('fred')==True:
+ x = 9
+ elif ((g==1) and (b==2))==True:
+ x = 11
+ else:
+ x = 13
+ """,
+ [1,2,3,4,5,6,7,8,9,10,11,13], "2-13")
+
+ def testSplitIf(self):
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if \\
+ a == 1:
+ x = 3
+ elif \\
+ b == 2:
+ y = 5
+ else:
+ z = 7
+ assert x == 3
+ """,
+ [1,2,4,5,7,9,10], "5-9")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if \\
+ a != 1:
+ x = 3
+ elif \\
+ b == 2:
+ y = 5
+ else:
+ z = 7
+ assert y == 5
+ """,
+ [1,2,4,5,7,9,10], "4, 9")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if \\
+ a != 1:
+ x = 3
+ elif \\
+ b != 2:
+ y = 5
+ else:
+ z = 7
+ assert z == 7
+ """,
+ [1,2,4,5,7,9,10], "4, 7")
+
+ def testPathologicalSplitIf(self):
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if (
+ a == 1
+ ):
+ x = 3
+ elif (
+ b == 2
+ ):
+ y = 5
+ else:
+ z = 7
+ assert x == 3
+ """,
+ [1,2,5,6,9,11,12], "6-11")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if (
+ a != 1
+ ):
+ x = 3
+ elif (
+ b == 2
+ ):
+ y = 5
+ else:
+ z = 7
+ assert y == 5
+ """,
+ [1,2,5,6,9,11,12], "5, 11")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if (
+ a != 1
+ ):
+ x = 3
+ elif (
+ b != 2
+ ):
+ y = 5
+ else:
+ z = 7
+ assert z == 7
+ """,
+ [1,2,5,6,9,11,12], "5, 9")
+
+ def testAbsurdSplitIf(self):
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a == 1 \\
+ :
+ x = 3
+ elif b == 2 \\
+ :
+ y = 5
+ else:
+ z = 7
+ assert x == 3
+ """,
+ [1,2,4,5,7,9,10], "5-9")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a != 1 \\
+ :
+ x = 3
+ elif b == 2 \\
+ :
+ y = 5
+ else:
+ z = 7
+ assert y == 5
+ """,
+ [1,2,4,5,7,9,10], "4, 9")
+ self.checkCoverage("""\
+ a = 1; b = 2; c = 3;
+ if a != 1 \\
+ :
+ x = 3
+ elif b != 2 \\
+ :
+ y = 5
+ else:
+ z = 7
+ assert z == 7
+ """,
+ [1,2,4,5,7,9,10], "4, 7")
+
+ def testWhile(self):
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while a:
+ b += 1
+ a -= 1
+ assert a == 0 and b == 3
+ """,
+ [1,2,3,4,5], "")
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while a:
+ b += 1
+ break
+ b = 99
+ assert a == 3 and b == 1
+ """,
+ [1,2,3,4,5,6], "5")
+
+ def testWhileElse(self):
+ # Take the else branch.
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while a:
+ b += 1
+ a -= 1
+ else:
+ b = 99
+ assert a == 0 and b == 99
+ """,
+ [1,2,3,4,6,7], "")
+ # Don't take the else branch.
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while a:
+ b += 1
+ a -= 1
+ break
+ b = 123
+ else:
+ b = 99
+ assert a == 2 and b == 1
+ """,
+ [1,2,3,4,5,6,8,9], "6-8")
+
+ def testSplitWhile(self):
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while \\
+ a:
+ b += 1
+ a -= 1
+ assert a == 0 and b == 3
+ """,
+ [1,2,4,5,6], "")
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while (
+ a
+ ):
+ b += 1
+ a -= 1
+ assert a == 0 and b == 3
+ """,
+ [1,2,5,6,7], "")
+
+ def testFor(self):
+ self.checkCoverage("""\
+ a = 0
+ for i in [1,2,3,4,5]:
+ a += i
+ assert a == 15
+ """,
+ [1,2,3,4], "")
+ self.checkCoverage("""\
+ a = 0
+ for i in [1,
+ 2,3,4,
+ 5]:
+ a += i
+ assert a == 15
+ """,
+ [1,2,5,6], "")
+ self.checkCoverage("""\
+ a = 0
+ for i in [1,2,3,4,5]:
+ a += i
+ break
+ a = 99
+ assert a == 1
+ """,
+ [1,2,3,4,5,6], "5")
+
+ def testForElse(self):
+ self.checkCoverage("""\
+ a = 0
+ for i in range(5):
+ a += i+1
+ else:
+ a = 99
+ assert a == 99
+ """,
+ [1,2,3,5,6], "")
+ self.checkCoverage("""\
+ a = 0
+ for i in range(5):
+ a += i+1
+ break
+ a = 99
+ else:
+ a = 123
+ assert a == 1
+ """,
+ [1,2,3,4,5,7,8], "5-7")
+
+ def testSplitFor(self):
+ self.checkCoverage("""\
+ a = 0
+ for \\
+ i in [1,2,3,4,5]:
+ a += i
+ assert a == 15
+ """,
+ [1,2,4,5], "")
+ self.checkCoverage("""\
+ a = 0
+ for \\
+ i in [1,
+ 2,3,4,
+ 5]:
+ a += i
+ assert a == 15
+ """,
+ [1,2,6,7], "")
+
+ def testTryExcept(self):
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ except:
+ a = 99
+ assert a == 1
+ """,
+ [1,2,3,4,5,6], "4-5")
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ assert a == 99
+ """,
+ [1,2,3,4,5,6,7], "")
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except ImportError:
+ a = 99
+ except:
+ a = 123
+ assert a == 123
+ """,
+ [1,2,3,4,5,6,7,8,9], "6")
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise IOError("foo")
+ except ImportError:
+ a = 99
+ except IOError:
+ a = 17
+ except:
+ a = 123
+ assert a == 17
+ """,
+ [1,2,3,4,5,6,7,8,9,10,11], "6, 9-10")
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ except:
+ a = 99
+ else:
+ a = 123
+ assert a == 123
+ """,
+ [1,2,3,4,5,7,8], "4-5")
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ else:
+ a = 123
+ assert a == 99
+ """,
+ [1,2,3,4,5,6,8,9], "8")
+
+ def testTryFinally(self):
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ finally:
+ a = 99
+ assert a == 99
+ """,
+ [1,2,3,5,6], "")
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ try:
+ raise Exception("foo")
+ finally:
+ b = 123
+ except:
+ a = 99
+ assert a == 99 and b == 123
+ """,
+ [1,2,3,4,5,7,8,9,10], "")
+
+ def testFunctionDef(self):
+ self.checkCoverage("""\
+ a = 99
+ def foo():
+ ''' docstring
+ '''
+ return 1
+
+ a = foo()
+ assert a == 1
+ """,
+ [1,2,5,7,8], "")
+ self.checkCoverage("""\
+ def foo(
+ a,
+ b
+ ):
+ ''' docstring
+ '''
+ return a+b
+
+ x = foo(17, 23)
+ assert x == 40
+ """,
+ [1,7,9,10], "")
+ self.checkCoverage("""\
+ def foo(
+ a = (lambda x: x*2)(10),
+ b = (
+ lambda x:
+ x+1
+ )(1)
+ ):
+ ''' docstring
+ '''
+ return a+b
+
+ x = foo()
+ assert x == 22
+ """,
+ [1,10,12,13], "")
+
+ def testClassDef(self):
+ self.checkCoverage("""\
+ # A comment.
+ class theClass:
+ ''' the docstring.
+ Don't be fooled.
+ '''
+ def __init__(self):
+ ''' Another docstring. '''
+ self.a = 1
+
+ def foo(self):
+ return self.a
+
+ x = theClass().foo()
+ assert x == 1
+ """,
+ [2,6,8,10,11,13,14], "")
+
+
+class ExcludeTests(CoverageTest):
+ def testSimple(self):
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if 0:
+ a = 4 # -cc
+ """,
+ [1,3], "", ['-cc'])
+
+ def testTwoExcludes(self):
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if a == 99:
+ a = 4 # -cc
+ b = 5
+ c = 6 # -xx
+ assert a == 1 and b == 2
+ """,
+ [1,3,5,7], "5", ['-cc', '-xx'])
+
+ def testExcludingIfSuite(self):
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if 0:
+ a = 4
+ b = 5
+ c = 6
+ assert a == 1 and b == 2
+ """,
+ [1,7], "", ['if 0:'])
+
+ def testExcludingIfButNotElseSuite(self):
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if 0:
+ a = 4
+ b = 5
+ c = 6
+ else:
+ a = 8
+ b = 9
+ assert a == 8 and b == 9
+ """,
+ [1,8,9,10], "", ['if 0:'])
+
+ def testExcludingElseSuite(self):
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if 1==1:
+ a = 4
+ b = 5
+ c = 6
+ else: #pragma: NO COVER
+ a = 8
+ b = 9
+ assert a == 4 and b == 5 and c == 6
+ """,
+ [1,3,4,5,6,10], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if 1==1:
+ a = 4
+ b = 5
+ c = 6
+
+ # Lots of comments to confuse the else handler.
+ # more.
+
+ else: #pragma: NO COVER
+
+ # Comments here too.
+
+ a = 8
+ b = 9
+ assert a == 4 and b == 5 and c == 6
+ """,
+ [1,3,4,5,6,17], "", ['#pragma: NO COVER'])
+
+ def testExcludingElifSuites(self):
+ self.checkCoverage("""\
+ a = 1; b = 2
+
+ if 1==1:
+ a = 4
+ b = 5
+ c = 6
+ elif 1==0: #pragma: NO COVER
+ a = 8
+ b = 9
+ else:
+ a = 11
+ b = 12
+ assert a == 4 and b == 5 and c == 6
+ """,
+ [1,3,4,5,6,11,12,13], "11-12", ['#pragma: NO COVER'])
+
+ def testExcludingOnelineIf(self):
+ self.checkCoverage("""\
+ def foo():
+ a = 2
+ if 0: x = 3 # no cover
+ b = 4
+
+ foo()
+ """,
+ [1,2,4,6], "", ["no cover"])
+
+ def testExcludingAColonNotASuite(self):
+ self.checkCoverage("""\
+ def foo():
+ l = range(10)
+ print l[:3] # no cover
+ b = 4
+
+ foo()
+ """,
+ [1,2,4,6], "", ["no cover"])
+
+ def testExcludingForSuite(self):
+ self.checkCoverage("""\
+ a = 0
+ for i in [1,2,3,4,5]: #pragma: NO COVER
+ a += i
+ assert a == 15
+ """,
+ [1,4], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ for i in [1,
+ 2,3,4,
+ 5]: #pragma: NO COVER
+ a += i
+ assert a == 15
+ """,
+ [1,6], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ for i in [1,2,3,4,5
+ ]: #pragma: NO COVER
+ a += i
+ break
+ a = 99
+ assert a == 1
+ """,
+ [1,7], "", ['#pragma: NO COVER'])
+
+ def testExcludingForElse(self):
+ self.checkCoverage("""\
+ a = 0
+ for i in range(5):
+ a += i+1
+ break
+ a = 99
+ else: #pragma: NO COVER
+ a = 123
+ assert a == 1
+ """,
+ [1,2,3,4,5,8], "5", ['#pragma: NO COVER'])
+
+ def testExcludingWhile(self):
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while a*b: #pragma: NO COVER
+ b += 1
+ break
+ b = 99
+ assert a == 3 and b == 0
+ """,
+ [1,6], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while (
+ a*b
+ ): #pragma: NO COVER
+ b += 1
+ break
+ b = 99
+ assert a == 3 and b == 0
+ """,
+ [1,8], "", ['#pragma: NO COVER'])
+
+ def testExcludingWhileElse(self):
+ self.checkCoverage("""\
+ a = 3; b = 0
+ while a:
+ b += 1
+ break
+ b = 99
+ else: #pragma: NO COVER
+ b = 123
+ assert a == 3 and b == 1
+ """,
+ [1,2,3,4,5,8], "5", ['#pragma: NO COVER'])
+
+ def testExcludingTryExcept(self):
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ except: #pragma: NO COVER
+ a = 99
+ assert a == 1
+ """,
+ [1,2,3,6], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ assert a == 99
+ """,
+ [1,2,3,4,5,6,7], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except ImportError: #pragma: NO COVER
+ a = 99
+ except:
+ a = 123
+ assert a == 123
+ """,
+ [1,2,3,4,7,8,9], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ except: #pragma: NO COVER
+ a = 99
+ else:
+ a = 123
+ assert a == 123
+ """,
+ [1,2,3,7,8], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ else: #pragma: NO COVER
+ a = 123
+ assert a == 99
+ """,
+ [1,2,3,4,5,6,9], "", ['#pragma: NO COVER'])
+
+ def testExcludingTryExceptPass(self):
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ except: #pragma: NO COVER
+ x = 2
+ assert a == 1
+ """,
+ [1,2,3,6], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except ImportError: #pragma: NO COVER
+ x = 2
+ except:
+ a = 123
+ assert a == 123
+ """,
+ [1,2,3,4,7,8,9], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ except: #pragma: NO COVER
+ x = 2
+ else:
+ a = 123
+ assert a == 123
+ """,
+ [1,2,3,7,8], "", ['#pragma: NO COVER'])
+ self.checkCoverage("""\
+ a = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ else: #pragma: NO COVER
+ x = 2
+ assert a == 99
+ """,
+ [1,2,3,4,5,6,9], "", ['#pragma: NO COVER'])
+
+ def testExcludingIfPass(self):
+ # From a comment on the coverage page by Michael McNeil Forbes:
+ self.checkCoverage("""\
+ 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.
+
+ f()
+ """,
+ [1,7], "", ["no cover"])
+
+ def testExcludingFunction(self):
+ self.checkCoverage("""\
+ def fn(foo): #pragma: NO COVER
+ a = 1
+ b = 2
+ c = 3
+
+ x = 1
+ assert x == 1
+ """,
+ [6,7], "", ['#pragma: NO COVER'])
+
+ def testExcludingMethod(self):
+ self.checkCoverage("""\
+ class Fooey:
+ def __init__(self):
+ self.a = 1
+
+ def foo(self): #pragma: NO COVER
+ return self.a
+
+ x = Fooey()
+ assert x.a == 1
+ """,
+ [1,2,3,8,9], "", ['#pragma: NO COVER'])
+
+ def testExcludingClass(self):
+ self.checkCoverage("""\
+ class Fooey: #pragma: NO COVER
+ def __init__(self):
+ self.a = 1
+
+ def foo(self):
+ return self.a
+
+ x = 1
+ assert x == 1
+ """,
+ [8,9], "", ['#pragma: NO COVER'])
+
+
+if sys.hexversion >= 0x020300f0:
+ # threading support was new in 2.3, only test there.
+ class ThreadingTests(CoverageTest):
+ def testThreading(self):
+ self.checkCoverage("""\
+ import time, threading
+
+ def fromMainThread():
+ return "called from main thread"
+
+ def fromOtherThread():
+ return "called from other thread"
+
+ def neverCalled():
+ return "no one calls me"
+
+ threading.Thread(target=fromOtherThread).start()
+ fromMainThread()
+ time.sleep(1)
+ """,
+ [1,3,4,6,7,9,10,12,13,14], "10")
+
+
+if sys.hexversion >= 0x020400f0:
+ class Py24Tests(CoverageTest):
+ def testFunctionDecorators(self):
+ self.checkCoverage("""\
+ def require_int(func):
+ def wrapper(arg):
+ assert isinstance(arg, int)
+ return func(arg)
+
+ return wrapper
+
+ @require_int
+ def p1(arg):
+ return arg*2
+
+ assert p1(10) == 20
+ """,
+ [1,2,3,4,6,8,10,12], "")
+
+ def testFunctionDecoratorsWithArgs(self):
+ self.checkCoverage("""\
+ def boost_by(extra):
+ def decorator(func):
+ def wrapper(arg):
+ return extra*func(arg)
+ return wrapper
+ return decorator
+
+ @boost_by(10)
+ def boosted(arg):
+ return arg*2
+
+ assert boosted(10) == 200
+ """,
+ [1,2,3,4,5,6,8,10,12], "")
+
+ def testDoubleFunctionDecorators(self):
+ self.checkCoverage("""\
+ def require_int(func):
+ def wrapper(arg):
+ assert isinstance(arg, int)
+ return func(arg)
+ return wrapper
+
+ def boost_by(extra):
+ def decorator(func):
+ def wrapper(arg):
+ return extra*func(arg)
+ return wrapper
+ return decorator
+
+ @require_int
+ @boost_by(10)
+ def boosted1(arg):
+ return arg*2
+
+ assert boosted1(10) == 200
+
+ @boost_by(10)
+ @require_int
+ def boosted2(arg):
+ return arg*2
+
+ assert boosted2(10) == 200
+ """,
+ ([1,2,3,4,5,7,8,9,10,11,12,14,15,17,19,21,22,24,26],
+ [1,2,3,4,5,7,8,9,10,11,12,14, 17,19,21, 24,26]), "")
+
+
+if sys.hexversion >= 0x020500f0:
+ class Py25Tests(CoverageTest):
+ def testWithStatement(self):
+ self.checkCoverage("""\
+ from __future__ import with_statement
+
+ class Managed:
+ def __enter__(self):
+ print "enter"
+
+ def __exit__(self, type, value, tb):
+ print "exit", type
+
+ m = Managed()
+ with m:
+ print "block1a"
+ print "block1b"
+
+ try:
+ with m:
+ print "block2"
+ raise Exception("Boo!")
+ except:
+ print "caught"
+ """,
+ [1,3,4,5,7,8,10,11,12,13,15,16,17,18,19,20], "")
+
+ def testTryExceptFinally(self):
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ except:
+ a = 99
+ finally:
+ b = 2
+ assert a == 1 and b == 2
+ """,
+ [1,2,3,4,5,7,8], "4-5")
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ finally:
+ b = 2
+ assert a == 99 and b == 2
+ """,
+ [1,2,3,4,5,6,8,9], "")
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except ImportError:
+ a = 99
+ except:
+ a = 123
+ finally:
+ b = 2
+ assert a == 123 and b == 2
+ """,
+ [1,2,3,4,5,6,7,8,10,11], "6")
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ raise IOError("foo")
+ except ImportError:
+ a = 99
+ except IOError:
+ a = 17
+ except:
+ a = 123
+ finally:
+ b = 2
+ assert a == 17 and b == 2
+ """,
+ [1,2,3,4,5,6,7,8,9,10,12,13], "6, 9-10")
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ except:
+ a = 99
+ else:
+ a = 123
+ finally:
+ b = 2
+ assert a == 123 and b == 2
+ """,
+ [1,2,3,4,5,7,9,10], "4-5")
+ self.checkCoverage("""\
+ a = 0; b = 0
+ try:
+ a = 1
+ raise Exception("foo")
+ except:
+ a = 99
+ else:
+ a = 123
+ finally:
+ b = 2
+ assert a == 99 and b == 2
+ """,
+ [1,2,3,4,5,6,8,10,11], "8")
+
+
+class ModuleTests(CoverageTest):
+ def testNotSingleton(self):
+ """ You *can* create another coverage object.
+ """
+ coverage.coverage()
+ coverage.coverage()
+
+
+class ApiTests(CoverageTest):
+ def testSimple(self):
+ coverage.erase()
+
+ self.makeFile("mycode", """\
+ a = 1
+ b = 2
+ if b == 3:
+ c = 4
+ d = 5
+ """)
+
+ # Import the python file, executing it.
+ coverage.start()
+ self.importModule("mycode")
+ coverage.stop()
+
+ filename, statements, missing, readablemissing = coverage.analysis("mycode.py")
+ self.assertEqual(statements, [1,2,3,4,5])
+ self.assertEqual(missing, [4])
+ self.assertEqual(readablemissing, "4")
+
+ def doReportWork(self, modname):
+ coverage.erase()
+
+ self.makeFile(modname, """\
+ a = 1
+ b = 2
+ if b == 3:
+ c = 4
+ d = 5
+ e = 6
+ f = 7
+ """)
+
+ # Import the python file, executing it.
+ coverage.start()
+ self.importModule(modname)
+ coverage.stop()
+ coverage.analysis(modname + ".py")
+
+ def testReport(self):
+ self.doReportWork("mycode2")
+ coverage.report(["mycode2.py"])
+ self.assertEqual(self.getStdout(), dedent("""\
+ Name Stmts Exec Cover Missing
+ ---------------------------------------
+ mycode2 7 4 57% 4-6
+ """))
+
+ def testReportFile(self):
+ self.doReportWork("mycode3")
+ fout = StringIO()
+ coverage.report(["mycode3.py"], file=fout)
+ self.assertEqual(self.getStdout(), "")
+ self.assertEqual(fout.getvalue(), dedent("""\
+ Name Stmts Exec Cover Missing
+ ---------------------------------------
+ mycode3 7 4 57% 4-6
+ """))
+
+
+class AnnotationTests(CoverageTest):
+ def testWhite(self):
+ self.checkEverything(file='test/white.py', annfile='test/white.py,cover')
+
+
+class CmdLineTests(CoverageTest):
+ def help_fn(self, error=None):
+ raise Exception(error or "__doc__")
+
+ def command_line(self, argv):
+ return coverage.CoverageScript().command_line(argv, self.help_fn)
+
+ def testHelp(self):
+ self.assertRaisesMsg(Exception, "__doc__", self.command_line, ['-h'])
+ self.assertRaisesMsg(Exception, "__doc__", self.command_line, ['--help'])
+
+ def testUnknownOption(self):
+ self.assertRaisesMsg(Exception, "option -z not recognized", self.command_line, ['-z'])
+
+ def testBadActionCombinations(self):
+ self.assertRaisesMsg(Exception, "You can't specify the 'erase' and 'annotate' options at the same time.", self.command_line, ['-e', '-a'])
+ self.assertRaisesMsg(Exception, "You can't specify the 'erase' and 'report' options at the same time.", self.command_line, ['-e', '-r'])
+ self.assertRaisesMsg(Exception, "You can't specify the 'erase' and 'combine' options at the same time.", self.command_line, ['-e', '-c'])
+ self.assertRaisesMsg(Exception, "You can't specify the 'execute' and 'annotate' options at the same time.", self.command_line, ['-x', '-a'])
+ self.assertRaisesMsg(Exception, "You can't specify the 'execute' and 'report' options at the same time.", self.command_line, ['-x', '-r'])
+ self.assertRaisesMsg(Exception, "You can't specify the 'execute' and 'combine' options at the same time.", self.command_line, ['-x', '-c'])
+
+ def testNeedAction(self):
+ self.assertRaisesMsg(Exception, "You must specify at least one of -e, -x, -c, -r, or -a.", self.command_line, ['-p'])
+
+ def testArglessActions(self):
+ self.assertRaisesMsg(Exception, "Unexpected arguments: foo bar", self.command_line, ['-e', 'foo', 'bar'])
+ self.assertRaisesMsg(Exception, "Unexpected arguments: baz quux", self.command_line, ['-c', 'baz', 'quux'])
+
+
+class ProcessTests(CoverageTest):
+ def testSaveOnExit(self):
+ self.makeFile("mycode", """\
+ a = 1
+ b = 2
+ if b == 3:
+ c = 4
+ d = 5
+ """)
+
+ self.assert_(not os.path.exists(".coverage"))
+ self.run_command("coverage -x mycode.py")
+ self.assert_(os.path.exists(".coverage"))
+
+ def testEnvironment(self):
+ # Checks that we can import modules from the test directory at all!
+ self.makeFile("mycode", """\
+ import covmod1
+ import covmodzip1
+ a = 1
+ print 'done'
+ """)
+
+ self.assert_(not os.path.exists(".coverage"))
+ out = self.run_command("coverage -x mycode.py")
+ self.assert_(os.path.exists(".coverage"))
+ self.assertEqual(out, 'done\n')
+
+ def testReport(self):
+ self.makeFile("mycode", """\
+ import covmod1
+ import covmodzip1
+ a = 1
+ print 'done'
+ """)
+
+ out = self.run_command("coverage -x mycode.py")
+ self.assertEqual(out, 'done\n')
+ report = self.run_command("coverage -r").replace('\\', '/')
+
+ # Name Stmts Exec Cover
+ # -----------------------------------------------------------------------
+ # c:/ned/coverage/trunk/coverage/__init__ 616 3 0%
+ # c:/ned/coverage/trunk/test/modules/covmod1 2 2 100%
+ # c:/ned/coverage/trunk/test/zipmods.zip/covmodzip1 2 2 100%
+ # c:/python25/lib/atexit 33 5 15%
+ # c:/python25/lib/ntpath 250 12 4%
+ # c:/python25/lib/threading 562 1 0%
+ # mycode 4 4 100%
+ # -----------------------------------------------------------------------
+ # TOTAL 1467 27 1%
+
+ self.assert_("/coverage/" in report)
+ self.assert_("/test/modules/covmod1 " in report)
+ self.assert_("/test/zipmods.zip/covmodzip1 " in report)
+ self.assert_("mycode " in report)
+
+ report = self.run_command("coverage -r mycode.py").replace('\\', '/')
+ self.assert_("/coverage/" not in report)
+ self.assert_("/test/modules/covmod1 " not in report)
+ self.assert_("/test/zipmods.zip/covmodzip1 " not in report)
+ self.assert_("mycode " in report)
+
+ def testCombineParallelData(self):
+ self.makeFile("b_or_c", """\
+ import sys
+ a = 1
+ if sys.argv[1] == 'b':
+ b = 1
+ else:
+ c = 1
+ d = 1
+ print 'done'
+ """)
+
+ out = self.run_command("coverage -x -p b_or_c.py b")
+ self.assertEqual(out, 'done\n')
+ self.assert_(not os.path.exists(".coverage"))
+
+ out = self.run_command("coverage -x -p b_or_c.py c")
+ self.assertEqual(out, 'done\n')
+ self.assert_(not os.path.exists(".coverage"))
+
+ # After two -p runs, there should be two .coverage.machine.123 files.
+ self.assertEqual(len([f for f in os.listdir('.') if f.startswith('.coverage.')]), 2)
+
+ # Combine the parallel coverage data files into .coverage .
+ self.run_command("coverage -c")
+ self.assert_(os.path.exists(".coverage"))
+
+ # Read the coverage file and see that b_or_c.py has all 7 lines executed.
+ data = coverage.CoverageData()
+ data.read_file(".coverage")
+ self.assertEqual(data.summary()['b_or_c.py'], 7)
+
+
+if __name__ == '__main__':
+ print "Testing under Python version: %s" % sys.version
+ unittest.main()
+
+
+# TODO: split "and" conditions across lines, and detect not running lines.
+# (Can't be done: trace function doesn't get called for each line
+# in an expression!)
+# TODO: Generator comprehensions?
+# TODO: Constant if tests ("if 1:"). Py 2.4 doesn't execute them.
+
+# $Id$
diff --git a/test_files.py b/test_files.py
new file mode 100644
index 00000000..88d34a2a
--- /dev/null
+++ b/test_files.py
@@ -0,0 +1,53 @@
+# File-based unit tests for coverage.py
+
+import path, sys, unittest
+import coverage
+
+class OneFileTestCase(unittest.TestCase):
+ def __init__(self, filename):
+ unittest.TestCase.__init__(self)
+ self.filename = filename
+
+ def shortDescription(self):
+ return self.filename
+
+ def setUp(self):
+ # Create a temporary directory.
+ self.noise = str(random.random())[2:]
+ self.temproot = path.path(tempfile.gettempdir()) / 'test_coverage'
+ self.tempdir = self.temproot / self.noise
+ self.tempdir.makedirs()
+ self.olddir = os.getcwd()
+ os.chdir(self.tempdir)
+ # Keep a counter to make every call to checkCoverage unique.
+ self.n = 0
+
+ # Capture stdout, so we can use print statements in the tests and not
+ # pollute the test output.
+ self.oldstdout = sys.stdout
+ self.capturedstdout = StringIO()
+ sys.stdout = self.capturedstdout
+ coverage.begin_recursive()
+
+ def tearDown(self):
+ coverage.end_recursive()
+ sys.stdout = self.oldstdout
+ # Get rid of the temporary directory.
+ os.chdir(self.olddir)
+ self.temproot.rmtree()
+
+ def runTest(self):
+ # THIS ISN'T DONE YET!
+ pass
+
+class MyTestSuite(unittest.TestSuite):
+ def __init__(self):
+ unittest.TestSuite.__init__(self)
+ for f in path.path('test').walk('*.py'):
+ self.addFile(f)
+
+ def addFile(self, f):
+ self.addTest(OneFileTestCase(f))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='MyTestSuite')