From 0cc8fd18714cb214327a0a58b704401a9efdf8dc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 5 Mar 2009 21:21:43 -0500 Subject: Initial coverage.py 3.0 beta 1 --- .pylintrc | 318 ++++++++ CHANGES | 175 +++++ MANIFEST.in | 7 + Makefile | 58 ++ README.txt | 7 + TODO.txt | 117 +++ allkits.cmd | 12 + alltests.cmd | 12 + checkeol.py | 24 + coverage/__init__.py | 76 ++ coverage/analyzer.py | 232 ++++++ coverage/cmdline.py | 149 ++++ coverage/collector.py | 110 +++ coverage/control.py | 410 ++++++++++ coverage/data.py | 122 +++ coverage/misc.py | 18 + coverage/tracer.c | 211 +++++ coverage_coverage.py | 27 + doc/coverage.px | 250 ++++++ ez_setup.py | 276 +++++++ setup.py | 60 ++ test/black.py | 15 + test/covmodzip1.py | 3 + test/modules/covmod1.py | 3 + test/white.py | 15 + test/white.py,cover | 15 + test_coverage.py | 1954 +++++++++++++++++++++++++++++++++++++++++++++++ test_files.py | 53 ++ 28 files changed, 4729 insertions(+) create mode 100644 .pylintrc create mode 100644 CHANGES create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.txt create mode 100644 TODO.txt create mode 100644 allkits.cmd create mode 100644 alltests.cmd create mode 100644 checkeol.py create mode 100644 coverage/__init__.py create mode 100644 coverage/analyzer.py create mode 100644 coverage/cmdline.py create mode 100644 coverage/collector.py create mode 100644 coverage/control.py create mode 100644 coverage/data.py create mode 100644 coverage/misc.py create mode 100644 coverage/tracer.c create mode 100644 coverage_coverage.py create mode 100644 doc/coverage.px create mode 100644 ez_setup.py create mode 100644 setup.py create mode 100644 test/black.py create mode 100644 test/covmodzip1.py create mode 100644 test/modules/covmod1.py create mode 100644 test/white.py create mode 100644 test/white.py,cover create mode 100644 test_coverage.py create mode 100644 test_files.py 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 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 "" + 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 == '': + # 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 @@ + + + +Created. +Updated to 2.5. +Updated to 2.6. +Added a problem description for doctest users. +Updated to 2.7. +Updated to 2.75. +Updated to 2.76. +Updated to 2.77. +Updated to 2.78. +Updated to 2.8. +Updated to 2.80. +Updated to 2.85. + + +

Coverage.py is a Python module that measures code coverage during Python execution. +It uses the code analysis tools and tracing hooks provided in the Python standard +library to determine which lines are executable, and which have been executed. +The original version was written by +Gareth Rees. +I've updated it to determine executable statements more accurately. +

+ +

Installation

+ +

To install coverage, unpack the tar file, and run "setup.py install", +or use "easy_install coverage".

+ + + +

You will have a coverage module for importing, and a coverage command +for command line execution.

+ +

There is also a set of tests for coverage: +test_coverage.py and +coverage_coverage.py. +These will only be of interest if you want to modify coverage.py. +

+ +

Command-line usage

+ +

The command line interface hasn't changed from the original version. +All of the +original instructions +still hold. +Read that document if you haven't used coverage.py before. +Here I'll describe the changes I've introduced.

+ +

The identification of executable statements is now more accurate. +Docstrings are not flagged as missing statements, nor are "global" statements. +Complex multi-line if and elif conditions are handled properly. +

+ +

Statements can now be excluded from consideration. This is useful if you +have lines of code that you know will not be executed, and you don't want the +coverage report to be filled with their noise. For example, you may have +interactive test clauses at the ends of your modules that your test +suite will not execute:

+ + + +

This suite of code can be excluded from the coverage report by adding +a specially-formed comment:

+ + + +

The #pragma line can be placed on any line of code. If the line contains the +colon that introduces a suite of statements, the entire suite is excluded. +Annotated files (created with "coverage -a") will prefix excluded lines with "-".

+ + +

Programmatic usage

+ +

Again, the +original instructions +still hold, but I've made a few additions.

+ +

The form of the exclusion marker can be changed using the exclude() method. +It takes a regular expression, and excludes any line that contains +a match:

+ + + +

As the example shows, the marker pattern doesn't have to be a comment. +Any number of regular expressions can be added by calling exclude a number of times.

+ +

The use of a file to hold coverage data can be suppressed by calling use_cache(0) +before calling start().

+ +

The analysis(module) function still returns a 4-tuple +(filename, statement list, missing list, missing string). +A new analysis2(module) function extends the return to a 5-tuple +(filename, statement list, excluded list, missing list, missing string).

+ +

The annotate(modules) function is available to annotate a list of modules, and +the annotate_file(filename, statements, excluded, missing) function provides +the bulk of annotation in a directly callable form.

+ + +

Known Problems

+ +

Older versions of doctest interfere with coverage's tracing of statements, and you +may get reports that none of your code is executing. +Use this patch to doctest.py +if you are experiencing problems.

+ + +

History

+ +

Changes I've made over time:

+ +

Version 2.85, September 14 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, May 25 2008

+ +
    +
  • Coverage.py is now installed as an egg, making integration with + nose 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).
  • +
  • Source files are opened in rU mode, preventing problems with errant line endings.
  • +
+ +

Version 2.78, September 30 2007

+ +
    +
  • Better handling of Python source in files that don't end with .py. + Thanks, Ben Finney.
  • +
+ +

Version 2.77, July 29 2007

+ +
    +
  • Better packaging, including Cheeseshop goodness.
  • +
+ +

Version 2.76, July 23 2007

+ +
    +
  • Added support for the overlooked "with" statement in Python 2.5.
  • +
+ +

Version 2.75, July 22 2007

+ +
    +
  • The way multi-line statements are handled has been revamped, allowing +coverage.py to support Python 2.5.
  • +
  • Functions with just a docstring and a pass statement no longer report the +pass as uncovered.
  • +
+ +

Version 2.7, July 21 2007

+ +
    +
  • The #pragma:nocover comment syntax is ignored by default, so programmatic +invocations of coverage also attend to those declarations.
  • +
  • Constants in the middle of functions are properly ignored, since they won't be executed.
  • +
  • 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.
  • +
  • Minor fixes: Linux current directory handling (thanks Guillaume Chazarain), +globbing for Windows (thanks Noel O'Boyle), and Python 2.2 compatibility (thanks Catherine Proulx).
  • +
+ +

Version 2.6, August 22 2006

+ +
    +
  • Function decorators are now handled properly (thanks Joseph Tate).
  • +
  • Fixed a few bugs with the --omit option (thanks Mark van der Wal and Sigve Tjora)
  • +
  • Coverage data files can be written from several processes at once with the -p and -c options (thanks Geoff Bache).
  • +
+ +

Version 2.5, December 4 2005

+ +
    +
  • Multi-threaded programs now have all threads properly measured (thanks Martin Fuzzey).
  • +
  • The report() method now takes an optional file argument which defaults to stdout.
  • +
  • Adapted Greg Rogers' patch to allow omitting files by directory from the report and annotation, +sorting files, and reporting files relatively.
  • +
  • coverage.py can now recursively measure itself under test!
  • +
+ +

Version 2.2, December 31 2004

+ +
    +
  • Made it possible to use keyword arguments with the module global functions (thanks Allen).
  • +
+ +

Version 2.1, December 14 2004

+ +
    +
  • Fix some backward-compatibility problems with the analysis function.
  • +
  • Refactor annotate to provide annotate_file.
  • +
+ +

Version 2, December 12 2004

+ +
    +
  • My first version.
  • +
+ + +

Problems

+ +

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. +

+ + + +

See also

+ +
    +
  • Gareth Rees's original page about the design of coverage.py
  • +
  • Sancho is a unit testing framework that includes code coverage measurement.
  • +
  • pycover, another Python implementation of code coverage.
  • +
  • The trace module in the Python standard library.
  • +
  • My blog, where topics often include those of interest to both casual and serious Python users.
  • +
+ + + + +
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') -- cgit v1.2.1