diff options
-rw-r--r-- | .pylintrc | 318 | ||||
-rw-r--r-- | CHANGES | 175 | ||||
-rw-r--r-- | MANIFEST.in | 7 | ||||
-rw-r--r-- | Makefile | 58 | ||||
-rw-r--r-- | README.txt | 7 | ||||
-rw-r--r-- | TODO.txt | 117 | ||||
-rw-r--r-- | allkits.cmd | 12 | ||||
-rw-r--r-- | alltests.cmd | 12 | ||||
-rw-r--r-- | checkeol.py | 24 | ||||
-rw-r--r-- | coverage/__init__.py | 76 | ||||
-rw-r--r-- | coverage/analyzer.py | 232 | ||||
-rw-r--r-- | coverage/cmdline.py | 149 | ||||
-rw-r--r-- | coverage/collector.py | 110 | ||||
-rw-r--r-- | coverage/control.py | 410 | ||||
-rw-r--r-- | coverage/data.py | 122 | ||||
-rw-r--r-- | coverage/misc.py | 18 | ||||
-rw-r--r-- | coverage/tracer.c | 211 | ||||
-rw-r--r-- | coverage_coverage.py | 27 | ||||
-rw-r--r-- | doc/coverage.px | 250 | ||||
-rw-r--r-- | ez_setup.py | 276 | ||||
-rw-r--r-- | setup.py | 60 | ||||
-rw-r--r-- | test/black.py | 15 | ||||
-rw-r--r-- | test/covmodzip1.py | 3 | ||||
-rw-r--r-- | test/modules/covmod1.py | 3 | ||||
-rw-r--r-- | test/white.py | 15 | ||||
-rw-r--r-- | test/white.py,cover | 15 | ||||
-rw-r--r-- | test_coverage.py | 1954 | ||||
-rw-r--r-- | test_files.py | 53 |
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&r1=28703&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') |