summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.treerc3
-rw-r--r--CHANGES.txt49
-rw-r--r--Makefile3
-rw-r--r--coverage/__init__.py28
-rw-r--r--coverage/__main__.py2
-rw-r--r--coverage/backward.py16
-rw-r--r--coverage/bytecode.py1
-rw-r--r--coverage/cmdline.py334
-rw-r--r--coverage/config.py184
-rw-r--r--coverage/control.py97
-rw-r--r--coverage/data.py33
-rw-r--r--coverage/execfile.py2
-rw-r--r--coverage/files.py83
-rw-r--r--coverage/fullcoverage/encodings.py8
-rw-r--r--coverage/html.py14
-rw-r--r--coverage/htmlfiles/coverage_html.js1
-rw-r--r--coverage/htmlfiles/index.html4
-rw-r--r--coverage/misc.py14
-rw-r--r--coverage/phystokens.py2
-rw-r--r--coverage/report.py5
-rw-r--r--coverage/results.py6
-rw-r--r--coverage/summary.py2
-rw-r--r--coverage/tracer.c5
-rw-r--r--coverage/version.py9
-rw-r--r--coverage/xmlreport.py3
-rw-r--r--doc/_ext/px_xlator.py1
-rw-r--r--doc/api.rst5
-rw-r--r--doc/branch.rst1
-rw-r--r--doc/cmd.rst24
-rw-r--r--doc/config.rst3
-rw-r--r--doc/contributing.rst156
-rw-r--r--doc/faq.rst1
-rw-r--r--doc/index.rst1
-rw-r--r--doc/install.rst1
-rw-r--r--doc/source.rst1
-rw-r--r--howto.txt3
-rw-r--r--igor.py9
-rw-r--r--requirements.txt4
-rw-r--r--setup.py74
-rw-r--r--test/backtest.py2
-rw-r--r--test/backunittest.py32
-rw-r--r--test/farm/html/src/partial.py1
-rw-r--r--test/farm/html/src/tabbed.py1
-rw-r--r--test/farm/run/src/chdir.py1
-rw-r--r--test/test_api.py197
-rw-r--r--test/test_arcs.py25
-rw-r--r--test/test_cmdline.py40
-rw-r--r--test/test_config.py19
-rw-r--r--test/test_files.py9
-rw-r--r--test/test_html.py59
-rw-r--r--test/test_misc.py50
-rw-r--r--test/test_oddball.py1
-rw-r--r--test/test_phystokens.py4
-rw-r--r--test/test_process.py81
-rw-r--r--test/test_results.py1
-rw-r--r--test/test_summary.py112
-rw-r--r--test/test_testing.py43
-rw-r--r--test/test_xml.py36
-rw-r--r--tox.ini6
59 files changed, 1337 insertions, 575 deletions
diff --git a/.treerc b/.treerc
index a17ee3cb..4d980fe7 100644
--- a/.treerc
+++ b/.treerc
@@ -2,12 +2,13 @@
[default]
ignore =
.treerc
+ .hgtags
build
htmlcov
.tox*
distribute_setup.py ez_setup.py mock.py
*.min.js
- sample_html
+ sample_html sample_html_beta
*.so *.pyd
*.zip
_build
diff --git a/CHANGES.txt b/CHANGES.txt
index 57a7df29..7f77a131 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -5,19 +5,68 @@ Change history for Coverage.py
Version 3.5.4b1
---------------
+- Wildcards in ``include=`` and ``omit=`` arguments were not handled properly
+ in reporting functions, though they were when running. Now they are handled
+ uniformly, closing `issue 143` and `issue 163`. **NOTE**: it is possible
+ that your configurations may now be incorrect. If you use ``include`` or
+ ``omit`` during reporting, whether on the command line, through the API, or
+ in a configuration file, please check carefully that you were not relying on
+ the old broken behavior.
+
+- The **report**, **html**, and **xml** commands now accept a ``--fail-under``
+ switch that indicates in the exit status whether the coverage percentage was
+ less than a particular value. Closes `issue 139`_.
+
+- The reporting functions coverage.report(), coverage.html_report(), and
+ coverage.xml_report() now all return a float, the total percentage covered
+ measurement.
+
+- The HTML report's title can now be set in the configuration file, with the
+ ``--title`` switch on the command line, or via the API.
+
+- Embarrassingly, the `[xml] output=' setting in the .coveragerc file simply
+ didn't work. Now it does.
+
+- When installing, now in addition to creating a "coverage" command, a
+ "coverage2" or "coverage3" command will be created, depending on whether you
+ are installing in Python 2.x or 3.x. Thanks, Julian Berman.
+
+- On Windows, files are now reported in their correct case, fixing `issue 89`_
+ and `issue 203`_.
+
- Running an HTML report in Python 3 in the same directory as an old Python 2
HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_)
is now fixed.
+- If `coverage xml` fails because there is no data to report, it used to
+ create a zero-length XML file. Now it doesn't, fixing `issue 210`_.
+
- Running coverage under a debugger is unlikely to work, but it shouldn't fail
with "TypeError: 'NoneType' object is not iterable". Fixes `issue 201`_.
- Docstrings for the legacy singleton methods are more helpful. Thanks Marius
Gedminas. Closes `issue 205`_.
+- The pydoc tool can now show docmentation for the class `coverage.coverage`.
+ Closes `issue 206`_.
+
+- Added a page to the docs about contributing to coverage.py, closing
+ `issue 171`_.
+
+- Other minor bugs fixed: `issue 153`_.
+
+.. _issue 89: https://bitbucket.org/ned/coveragepy/issue/89/on-windows-all-packages-are-reported-in
+.. _issue 139: https://bitbucket.org/ned/coveragepy/issue/139/easy-check-for-a-certain-coverage-in-tests
+.. _issue 143: https://bitbucket.org/ned/coveragepy/issue/143/omit-doesnt-seem-to-work-in-coverage
+.. _issue 153: https://bitbucket.org/ned/coveragepy/issue/153/non-existent-filename-triggers
+.. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename
+.. _issue 171: https://bitbucket.org/ned/coveragepy/issue/171/how-to-contribute-and-run-tests
.. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy
.. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on
+.. _issue 203: https://bitbucket.org/ned/coveragepy/issue/203/duplicate-filenames-reported-when-filename
.. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly
+.. _issue 206: https://bitbucket.org/ned/coveragepy/issue/206/pydoc-coveragecoverage-fails-with-an-error
+.. _issue 210: https://bitbucket.org/ned/coveragepy/issue/210/if-theres-no-coverage-data-coverage-xml
Version 3.5.3 --- 29 September 2012
diff --git a/Makefile b/Makefile
index cb66b469..3e921862 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,9 @@ clean:
-rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz
-rm -rf doc/_build/*
+sterile: clean
+ -rm -rf .tox .tox_kits
+
LINTABLE = coverage setup.py test
lint:
diff --git a/coverage/__init__.py b/coverage/__init__.py
index e2db3a56..0ccc699f 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -5,19 +5,13 @@ http://nedbatchelder.com/code/coverage
"""
-__version__ = "3.5.4b1" # see detailed history in CHANGES.txt
-
-__url__ = "http://nedbatchelder.com/code/coverage"
-if max(__version__).isalpha():
- # For pre-releases, use a version-specific URL.
- __url__ += "/" + __version__
+from coverage.version import __version__, __url__
from coverage.control import coverage, process_startup
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. That design hampered programmability, so the current api uses
@@ -36,6 +30,10 @@ def _singleton_method(name):
called.
"""
+ # Disable pylint msg W0612, because a bunch of variables look unused, but
+ # they're accessed via locals().
+ # pylint: disable=W0612
+
def wrapper(*args, **kwargs):
"""Singleton wrapper around a coverage method."""
global _the_coverage
@@ -75,6 +73,22 @@ report = _singleton_method('report')
annotate = _singleton_method('annotate')
+# On Windows, we encode and decode deep enough that something goes wrong and
+# the encodings.utf_8 module is loaded and then unloaded, I don't know why.
+# Adding a reference here prevents it from being unloaded. Yuk.
+import encodings.utf_8
+
+# Because of the "from coverage.control import fooey" lines at the top of the
+# file, there's an entry for coverage.coverage in sys.modules, mapped to None.
+# This makes some inspection tools (like pydoc) unable to find the class
+# coverage.coverage. So remove that entry.
+import sys
+try:
+ del sys.modules['coverage.coverage']
+except KeyError:
+ pass
+
+
# COPYRIGHT AND LICENSE
#
# Copyright 2001 Gareth Rees. All rights reserved.
diff --git a/coverage/__main__.py b/coverage/__main__.py
index 111ca2e0..55e0d259 100644
--- a/coverage/__main__.py
+++ b/coverage/__main__.py
@@ -1,4 +1,4 @@
-"""Coverage.py's main entrypoint."""
+"""Coverage.py's main entry point."""
import sys
from coverage.cmdline import main
sys.exit(main())
diff --git a/coverage/backward.py b/coverage/backward.py
index 637a5976..6347501a 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -49,6 +49,16 @@ try:
except NameError:
range = range
+# A function to iterate listlessly over a dict's items.
+if "iteritems" in dir({}):
+ def iitems(d):
+ """Produce the items from dict `d`."""
+ return d.iteritems()
+else:
+ def iitems(d):
+ """Produce the items from dict `d`."""
+ return d.items()
+
# Exec is a statement in Py2, a function in Py3
if sys.version_info >= (3, 0):
def exec_code_object(code, global_map):
@@ -66,12 +76,6 @@ else:
)
)
-# ConfigParser was renamed to the more-standard configparser
-try:
- import configparser
-except ImportError:
- import ConfigParser as configparser
-
# Reading Python source and interpreting the coding comment is a big deal.
if sys.version_info >= (3, 0):
# Python 3.2 provides `tokenize.open`, the best way to open source files.
diff --git a/coverage/bytecode.py b/coverage/bytecode.py
index 61c311eb..fd5c7da2 100644
--- a/coverage/bytecode.py
+++ b/coverage/bytecode.py
@@ -27,6 +27,7 @@ class ByteCodes(object):
Returns `ByteCode` objects.
"""
+ # pylint: disable=R0924
def __init__(self, code):
self.code = code
self.offset = 0
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 1ce5e0f5..5217a313 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,6 +1,6 @@
"""Command-line support for Coverage."""
-import optparse, re, sys, traceback
+import optparse, sys, traceback
from coverage.backward import sorted # pylint: disable=W0622
from coverage.execfile import run_python_file, run_python_module
@@ -20,10 +20,13 @@ class Opts(object):
help="Measure branch coverage in addition to statement coverage."
)
directory = optparse.make_option(
- '-d', '--directory', action='store',
- metavar="DIR",
+ '-d', '--directory', action='store', metavar="DIR",
help="Write the output files to DIR."
)
+ fail_under = optparse.make_option(
+ '', '--fail-under', action='store', metavar="MIN", type="int",
+ help="Exit with a status of 2 if the total coverage is less than MIN."
+ )
help = optparse.make_option(
'-h', '--help', action='store_true',
help="Get help on this command."
@@ -89,6 +92,10 @@ class Opts(object):
help="Use a simpler but slower trace method. Try this if you get "
"seemingly impossible results!"
)
+ title = optparse.make_option(
+ '', '--title', action='store', metavar="TITLE",
+ help="A text string to use as the title on the HTML."
+ )
version = optparse.make_option(
'', '--version', action='store_true',
help="Display version information and exit."
@@ -111,6 +118,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
actions=[],
branch=None,
directory=None,
+ fail_under=None,
help=None,
ignore_errors=None,
include=None,
@@ -122,6 +130,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
show_missing=None,
source=None,
timid=None,
+ title=None,
erase_first=None,
version=None,
)
@@ -273,9 +282,11 @@ CMDS = {
'html': CmdOptionParser("html",
[
Opts.directory,
+ Opts.fail_under,
Opts.ignore_errors,
Opts.omit,
Opts.include,
+ Opts.title,
] + GLOBAL_ARGS,
usage = "[options] [modules]",
description = "Create an HTML report of the coverage of the files. "
@@ -285,6 +296,7 @@ CMDS = {
'report': CmdOptionParser("report",
[
+ Opts.fail_under,
Opts.ignore_errors,
Opts.omit,
Opts.include,
@@ -314,20 +326,20 @@ CMDS = {
'xml': CmdOptionParser("xml",
[
+ Opts.fail_under,
Opts.ignore_errors,
Opts.omit,
Opts.include,
Opts.output_xml,
] + GLOBAL_ARGS,
cmd = "xml",
- defaults = {'outfile': 'coverage.xml'},
usage = "[options] [modules]",
description = "Generate an XML report of coverage results."
),
}
-OK, ERR = 0, 1
+OK, ERR, FAIL_UNDER = 0, 1, 2
class CoverageScript(object):
@@ -346,27 +358,10 @@ class CoverageScript(object):
self.run_python_file = _run_python_file or run_python_file
self.run_python_module = _run_python_module or run_python_module
self.help_fn = _help_fn or self.help
+ self.classic = False
self.coverage = None
- def help(self, error=None, topic=None, parser=None):
- """Display an error message, or the named topic."""
- assert error or topic or parser
- if error:
- print(error)
- print("Use 'coverage help' for help.")
- elif parser:
- print(parser.format_help().strip())
- else:
- # Parse out the topic we want from HELP_TOPICS
- topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS)
- topics = dict(zip(topic_list[1::2], topic_list[2::2]))
- help_msg = topics.get(topic, '').strip()
- if help_msg:
- print(help_msg % self.covpkg.__dict__)
- else:
- print("Don't know topic %r" % topic)
-
def command_line(self, argv):
"""The bulk of the command line interface to Coverage.
@@ -376,15 +371,14 @@ class CoverageScript(object):
"""
# Collect the command-line options.
-
if not argv:
self.help_fn(topic='minimum_help')
return OK
# The command syntax we parse depends on the first argument. Classic
# syntax always starts with an option.
- classic = argv[0].startswith('-')
- if classic:
+ self.classic = argv[0].startswith('-')
+ if self.classic:
parser = ClassicOptionParser()
else:
parser = CMDS.get(argv[0])
@@ -398,58 +392,12 @@ class CoverageScript(object):
if not ok:
return ERR
- # Handle help.
- if options.help:
- if classic:
- self.help_fn(topic='help')
- else:
- self.help_fn(parser=parser)
- return OK
-
- if "help" in options.actions:
- if args:
- for a in args:
- parser = CMDS.get(a)
- if parser:
- self.help_fn(parser=parser)
- else:
- self.help_fn(topic=a)
- else:
- self.help_fn(topic='help')
- return OK
-
- # Handle version.
- if options.version:
- self.help_fn(topic='version')
+ # Handle help and version.
+ if self.do_help(options, args, parser):
return OK
# Check for conflicts and problems in the options.
- for i in ['erase', 'execute']:
- for j in ['annotate', 'html', 'report', 'combine']:
- if (i in options.actions) and (j in options.actions):
- self.help_fn("You can't specify the '%s' and '%s' "
- "options at the same time." % (i, j))
- return ERR
-
- if not options.actions:
- self.help_fn(
- "You must specify at least one of -e, -x, -c, -r, -a, or -b."
- )
- return ERR
- args_allowed = (
- 'execute' in options.actions or
- 'annotate' in options.actions or
- 'html' in options.actions or
- 'debug' in options.actions or
- 'report' in options.actions or
- 'xml' in options.actions
- )
- if not args_allowed and args:
- self.help_fn("Unexpected arguments: %s" % " ".join(args))
- return ERR
-
- if 'execute' in options.actions and not args:
- self.help_fn("Nothing to do.")
+ if not self.args_ok(options, args):
return ERR
# Listify the list options.
@@ -470,38 +418,7 @@ class CoverageScript(object):
)
if 'debug' in options.actions:
- if not args:
- self.help_fn("What information would you like: data, sys?")
- return ERR
- for info in args:
- if info == 'sys':
- print("-- sys ----------------------------------------")
- for label, info in self.coverage.sysinfo():
- if info == []:
- info = "-none-"
- if isinstance(info, list):
- print("%15s:" % label)
- for e in info:
- print("%15s %s" % ("", e))
- else:
- print("%15s: %s" % (label, info))
- elif info == 'data':
- print("-- data ---------------------------------------")
- self.coverage.load()
- print("path: %s" % self.coverage.data.filename)
- print("has_arcs: %r" % self.coverage.data.has_arcs())
- summary = self.coverage.data.summary(fullpath=True)
- if summary:
- filenames = sorted(summary.keys())
- print("\n%d files:" % len(filenames))
- for f in filenames:
- print("%s: %d lines" % (f, summary[f]))
- else:
- print("No data collected")
- else:
- self.help_fn("Don't know what you mean by %r" % info)
- return ERR
- return OK
+ return self.do_debug(args)
if 'erase' in options.actions or options.erase_first:
self.coverage.erase()
@@ -509,22 +426,7 @@ class CoverageScript(object):
self.coverage.load()
if 'execute' in options.actions:
- # Run the script.
- self.coverage.start()
- code_ran = True
- try:
- try:
- if options.module:
- self.run_python_module(args[0], args)
- else:
- self.run_python_file(args[0], args)
- except NoSource:
- code_ran = False
- raise
- finally:
- if code_ran:
- self.coverage.stop()
- self.coverage.save()
+ self.do_execute(options, args)
if 'combine' in options.actions:
self.coverage.combine()
@@ -539,18 +441,165 @@ class CoverageScript(object):
)
if 'report' in options.actions:
- self.coverage.report(
+ total = self.coverage.report(
show_missing=options.show_missing, **report_args)
if 'annotate' in options.actions:
self.coverage.annotate(
directory=options.directory, **report_args)
if 'html' in options.actions:
- self.coverage.html_report(
- directory=options.directory, **report_args)
+ total = self.coverage.html_report(
+ directory=options.directory, title=options.title,
+ **report_args)
if 'xml' in options.actions:
outfile = options.outfile
- self.coverage.xml_report(outfile=outfile, **report_args)
+ total = self.coverage.xml_report(outfile=outfile, **report_args)
+
+ if options.fail_under is not None:
+ if total >= options.fail_under:
+ return OK
+ else:
+ return FAIL_UNDER
+ else:
+ return OK
+
+ def help(self, error=None, topic=None, parser=None):
+ """Display an error message, or the named topic."""
+ assert error or topic or parser
+ if error:
+ print(error)
+ print("Use 'coverage help' for help.")
+ elif parser:
+ print(parser.format_help().strip())
+ else:
+ help_msg = HELP_TOPICS.get(topic, '').strip()
+ if help_msg:
+ print(help_msg % self.covpkg.__dict__)
+ else:
+ print("Don't know topic %r" % topic)
+
+ def do_help(self, options, args, parser):
+ """Deal with help requests.
+
+ Return True if it handled the request, False if not.
+
+ """
+ # Handle help.
+ if options.help:
+ if self.classic:
+ self.help_fn(topic='help')
+ else:
+ self.help_fn(parser=parser)
+ return True
+
+ if "help" in options.actions:
+ if args:
+ for a in args:
+ parser = CMDS.get(a)
+ if parser:
+ self.help_fn(parser=parser)
+ else:
+ self.help_fn(topic=a)
+ else:
+ self.help_fn(topic='help')
+ return True
+
+ # Handle version.
+ if options.version:
+ self.help_fn(topic='version')
+ return True
+
+ return False
+
+ def args_ok(self, options, args):
+ """Check for conflicts and problems in the options.
+
+ Returns True if everything is ok, or False if not.
+
+ """
+ for i in ['erase', 'execute']:
+ for j in ['annotate', 'html', 'report', 'combine']:
+ if (i in options.actions) and (j in options.actions):
+ self.help_fn("You can't specify the '%s' and '%s' "
+ "options at the same time." % (i, j))
+ return False
+ if not options.actions:
+ self.help_fn(
+ "You must specify at least one of -e, -x, -c, -r, -a, or -b."
+ )
+ return False
+ args_allowed = (
+ 'execute' in options.actions or
+ 'annotate' in options.actions or
+ 'html' in options.actions or
+ 'debug' in options.actions or
+ 'report' in options.actions or
+ 'xml' in options.actions
+ )
+ if not args_allowed and args:
+ self.help_fn("Unexpected arguments: %s" % " ".join(args))
+ return False
+
+ if 'execute' in options.actions and not args:
+ self.help_fn("Nothing to do.")
+ return False
+
+ return True
+
+ def do_execute(self, options, args):
+ """Implementation of 'coverage run'."""
+
+ # Run the script.
+ self.coverage.start()
+ code_ran = True
+ try:
+ try:
+ if options.module:
+ self.run_python_module(args[0], args)
+ else:
+ self.run_python_file(args[0], args)
+ except NoSource:
+ code_ran = False
+ raise
+ finally:
+ self.coverage.stop()
+ if code_ran:
+ self.coverage.save()
+
+ def do_debug(self, args):
+ """Implementation of 'coverage debug'."""
+
+ if not args:
+ self.help_fn("What information would you like: data, sys?")
+ return ERR
+ for info in args:
+ if info == 'sys':
+ print("-- sys ----------------------------------------")
+ for label, info in self.coverage.sysinfo():
+ if info == []:
+ info = "-none-"
+ if isinstance(info, list):
+ print("%15s:" % label)
+ for e in info:
+ print("%15s %s" % ("", e))
+ else:
+ print("%15s: %s" % (label, info))
+ elif info == 'data':
+ print("-- data ---------------------------------------")
+ self.coverage.load()
+ print("path: %s" % self.coverage.data.filename)
+ print("has_arcs: %r" % self.coverage.data.has_arcs())
+ summary = self.coverage.data.summary(fullpath=True)
+ if summary:
+ filenames = sorted(summary.keys())
+ print("\n%d files:" % len(filenames))
+ for f in filenames:
+ print("%s: %d lines" % (f, summary[f]))
+ else:
+ print("No data collected")
+ else:
+ self.help_fn("Don't know what you mean by %r" % info)
+ return ERR
return OK
@@ -568,10 +617,10 @@ def unshell_list(s):
return s.split(',')
-HELP_TOPICS = r"""
-
-== classic ====================================================================
-Coverage.py version %(__version__)s
+HELP_TOPICS = {
+# -------------------------
+'classic':
+r"""Coverage.py version %(__version__)s
Measure, collect, and report on code coverage in Python programs.
Usage:
@@ -615,8 +664,9 @@ coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...]
Coverage data is saved in the file .coverage by default. Set the
COVERAGE_FILE environment variable to save it somewhere else.
-
-== help =======================================================================
+""",
+# -------------------------
+'help': """\
Coverage.py, version %(__version__)s
Measure, collect, and report on code coverage in Python programs.
@@ -635,20 +685,22 @@ Commands:
Use "coverage help <command>" for detailed help on any command.
Use "coverage help classic" for help on older command syntax.
For more information, see %(__url__)s
-
-== minimum_help ===============================================================
+""",
+# -------------------------
+'minimum_help': """\
Code coverage for Python. Use 'coverage help' for help.
-
-== version ====================================================================
+""",
+# -------------------------
+'version': """\
Coverage.py, version %(__version__)s. %(__url__)s
-
-"""
+""",
+}
def main(argv=None):
- """The main entrypoint to Coverage.
+ """The main entry point to Coverage.
- This is installed as the script entrypoint.
+ This is installed as the script entry point.
"""
if argv is None:
diff --git a/coverage/config.py b/coverage/config.py
index 49d74e7a..8f1f6710 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -1,7 +1,55 @@
"""Config file for coverage.py"""
-import os
-from coverage.backward import configparser # pylint: disable=W0622
+import os, sys
+from coverage.backward import string_class, iitems
+
+# In py3, # ConfigParser was renamed to the more-standard configparser
+try:
+ import configparser # pylint: disable=F0401
+except ImportError:
+ import ConfigParser as configparser
+
+
+class HandyConfigParser(configparser.ConfigParser):
+ """Our specialization of ConfigParser."""
+
+ def read(self, filename):
+ """Read a filename as UTF-8 configuration data."""
+ kwargs = {}
+ if sys.version_info >= (3, 2):
+ kwargs['encoding'] = "utf-8"
+ configparser.ConfigParser.read(self, filename, **kwargs)
+
+ def getlist(self, section, option):
+ """Read a list of strings.
+
+ The value of `section` and `option` is treated as a comma- and newline-
+ separated list of strings. Each value is stripped of whitespace.
+
+ Returns the list of strings.
+
+ """
+ value_list = self.get(section, option)
+ values = []
+ for value_line in value_list.split('\n'):
+ for value in value_line.split(','):
+ value = value.strip()
+ if value:
+ values.append(value)
+ return values
+
+ def getlinelist(self, section, option):
+ """Read a list of full-line strings.
+
+ The value of `section` and `option` is treated as a newline-separated
+ list of strings. Each value is stripped of whitespace.
+
+ Returns the list of strings.
+
+ """
+ value_list = self.get(section, option)
+ return list(filter(None, value_list.split('\n')))
+
# The default line exclusion regexes
DEFAULT_EXCLUDE = [
@@ -29,7 +77,6 @@ class CoverageConfig(object):
operation of coverage.py.
"""
-
def __init__(self):
"""Initialize the configuration attributes to their defaults."""
# Defaults for [run]
@@ -53,6 +100,7 @@ class CoverageConfig(object):
# Defaults for [html]
self.html_dir = "htmlcov"
self.extra_css = None
+ self.html_title = "Coverage report"
# Defaults for [xml]
self.xml_output = "coverage.xml"
@@ -69,102 +117,66 @@ class CoverageConfig(object):
if env:
self.timid = ('--timid' in env)
+ MUST_BE_LIST = ["omit", "include"]
+
def from_args(self, **kwargs):
"""Read config values from `kwargs`."""
- for k, v in kwargs.items():
+ for k, v in iitems(kwargs):
if v is not None:
+ if k in self.MUST_BE_LIST and isinstance(v, string_class):
+ v = [v]
setattr(self, k, v)
- def from_file(self, *files):
- """Read configuration from .rc files.
+ def from_file(self, filename):
+ """Read configuration from a .rc file.
- Each argument in `files` is a file name to read.
+ `filename` is a file name to read.
"""
- cp = configparser.RawConfigParser()
- cp.read(files)
+ cp = HandyConfigParser()
+ cp.read(filename)
- # [run]
- if cp.has_option('run', 'branch'):
- self.branch = cp.getboolean('run', 'branch')
- if cp.has_option('run', 'cover_pylib'):
- self.cover_pylib = cp.getboolean('run', 'cover_pylib')
- if cp.has_option('run', 'data_file'):
- self.data_file = cp.get('run', 'data_file')
- if cp.has_option('run', 'include'):
- self.include = self.get_list(cp, 'run', 'include')
- if cp.has_option('run', 'omit'):
- self.omit = self.get_list(cp, 'run', 'omit')
- if cp.has_option('run', 'parallel'):
- self.parallel = cp.getboolean('run', 'parallel')
- if cp.has_option('run', 'source'):
- self.source = self.get_list(cp, 'run', 'source')
- if cp.has_option('run', 'timid'):
- self.timid = cp.getboolean('run', 'timid')
+ for option_spec in self.CONFIG_FILE_OPTIONS:
+ self.set_attr_from_config_option(cp, *option_spec)
- # [report]
- if cp.has_option('report', 'exclude_lines'):
- self.exclude_list = \
- self.get_line_list(cp, 'report', 'exclude_lines')
- if cp.has_option('report', 'ignore_errors'):
- self.ignore_errors = cp.getboolean('report', 'ignore_errors')
- if cp.has_option('report', 'include'):
- self.include = self.get_list(cp, 'report', 'include')
- if cp.has_option('report', 'omit'):
- self.omit = self.get_list(cp, 'report', 'omit')
- if cp.has_option('report', 'partial_branches'):
- self.partial_list = \
- self.get_line_list(cp, 'report', 'partial_branches')
- if cp.has_option('report', 'partial_branches_always'):
- self.partial_always_list = \
- self.get_line_list(cp, 'report', 'partial_branches_always')
- if cp.has_option('report', 'precision'):
- self.precision = cp.getint('report', 'precision')
- if cp.has_option('report', 'show_missing'):
- self.show_missing = cp.getboolean('report', 'show_missing')
-
- # [html]
- if cp.has_option('html', 'directory'):
- self.html_dir = cp.get('html', 'directory')
- if cp.has_option('html', 'extra_css'):
- self.extra_css = cp.get('html', 'extra_css')
-
- # [xml]
- if cp.has_option('xml', 'output'):
- self.xml_output = cp.get('xml', 'output')
-
- # [paths]
+ # [paths] is special
if cp.has_section('paths'):
for option in cp.options('paths'):
- self.paths[option] = self.get_list(cp, 'paths', option)
-
- def get_list(self, cp, section, option):
- """Read a list of strings from the ConfigParser `cp`.
-
- The value of `section` and `option` is treated as a comma- and newline-
- separated list of strings. Each value is stripped of whitespace.
-
- Returns the list of strings.
+ self.paths[option] = cp.getlist('paths', option)
- """
- value_list = cp.get(section, option)
- values = []
- for value_line in value_list.split('\n'):
- for value in value_line.split(','):
- value = value.strip()
- if value:
- values.append(value)
- return values
-
- def get_line_list(self, cp, section, option):
- """Read a list of full-line strings from the ConfigParser `cp`.
-
- The value of `section` and `option` is treated as a newline-separated
- list of strings. Each value is stripped of whitespace.
+ CONFIG_FILE_OPTIONS = [
+ # [run]
+ ('branch', 'run:branch', 'boolean'),
+ ('cover_pylib', 'run:cover_pylib', 'boolean'),
+ ('data_file', 'run:data_file'),
+ ('include', 'run:include', 'list'),
+ ('omit', 'run:omit', 'list'),
+ ('parallel', 'run:parallel', 'boolean'),
+ ('source', 'run:source', 'list'),
+ ('timid', 'run:timid', 'boolean'),
- Returns the list of strings.
+ # [report]
+ ('exclude_list', 'report:exclude_lines', 'linelist'),
+ ('ignore_errors', 'report:ignore_errors', 'boolean'),
+ ('include', 'report:include', 'list'),
+ ('omit', 'report:omit', 'list'),
+ ('partial_list', 'report:partial_branches', 'linelist'),
+ ('partial_always_list', 'report:partial_branches_always', 'linelist'),
+ ('precision', 'report:precision', 'int'),
+ ('show_missing', 'report:show_missing', 'boolean'),
- """
- value_list = cp.get(section, option)
- return list(filter(None, value_list.split('\n')))
+ # [html]
+ ('html_dir', 'html:directory'),
+ ('extra_css', 'html:extra_css'),
+ ('html_title', 'html:title'),
+ # [xml]
+ ('xml_output', 'xml:output'),
+ ]
+
+ def set_attr_from_config_option(self, cp, attr, where, type_=''):
+ """Set an attribute on self if it exists in the ConfigParser."""
+ section, option = where.split(":")
+ if cp.has_option(section, option):
+ method = getattr(cp, 'get'+type_)
+ setattr(self, attr, method(section, option))
diff --git a/coverage/control.py b/coverage/control.py
index c21d885e..f80e62b6 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -3,21 +3,22 @@
import atexit, os, random, socket, sys
from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class
+from coverage.backward import string_class, iitems
from coverage.codeunit import code_unit_factory, CodeUnit
from coverage.collector import Collector
from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
-from coverage.files import PathAliases, find_python_files
+from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.html import HtmlReporter
from coverage.misc import CoverageException, bool_or_none, join_regex
+from coverage.misc import file_be_gone
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
class coverage(object):
- """Programmatic access to Coverage.
+ """Programmatic access to coverage.py.
To use::
@@ -25,7 +26,7 @@ class coverage(object):
cov = coverage()
cov.start()
- #.. blah blah (run your code) blah blah ..
+ #.. call your code ..
cov.stop()
cov.html_report(directory='covhtml')
@@ -96,10 +97,6 @@ class coverage(object):
self.config.data_file = env_data_file
# 4: from constructor arguments:
- if isinstance(omit, string_class):
- omit = [omit]
- if isinstance(include, string_class):
- include = [include]
self.config.from_args(
data_file=data_file, cover_pylib=cover_pylib, timid=timid,
branch=branch, parallel=bool_or_none(data_suffix),
@@ -125,8 +122,8 @@ class coverage(object):
else:
self.source_pkgs.append(src)
- self.omit = self._prep_patterns(self.config.omit)
- self.include = self._prep_patterns(self.config.include)
+ self.omit = prep_patterns(self.config.omit)
+ self.include = prep_patterns(self.config.include)
self.collector = Collector(
self._should_trace, timid=self.config.timid,
@@ -183,14 +180,6 @@ class coverage(object):
# Set the reporting precision.
Numbers.set_precision(self.config.precision)
- # When tearing down the coverage object, modules can become None.
- # Saving the modules as object attributes avoids problems, but it is
- # quite ad-hoc which modules need to be saved and which references
- # need to use the object attributes.
- self.socket = socket
- self.os = os
- self.random = random
-
def _canonical_dir(self, f):
"""Return the canonical directory of the file `f`."""
return os.path.split(self.file_locator.canonical_filename(f))[0]
@@ -212,9 +201,6 @@ class coverage(object):
should not.
"""
- if os is None:
- return False
-
if filename.startswith('<'):
# Lots of non-file execution is represented with artificial
# filenames like "<string>", "<doctest readme.txt[0]>", or
@@ -281,25 +267,6 @@ class coverage(object):
self._warnings.append(msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
- def _prep_patterns(self, patterns):
- """Prepare the file patterns for use in a `FnmatchMatcher`.
-
- If a pattern starts with a wildcard, it is used as a pattern
- as-is. If it does not start with a wildcard, then it is made
- absolute with the current directory.
-
- If `patterns` is None, an empty list is returned.
-
- """
- patterns = patterns or []
- prepped = []
- for p in patterns or []:
- if p.startswith("*") or p.startswith("?"):
- prepped.append(p)
- else:
- prepped.append(self.file_locator.abs_file(p))
- return prepped
-
def _check_for_packages(self):
"""Update the source_match matcher with latest imported packages."""
# Our self.source_pkgs attribute is a list of package names we want to
@@ -354,7 +321,15 @@ class coverage(object):
self.data.read()
def start(self):
- """Start measuring code coverage."""
+ """Start measuring code coverage.
+
+ Coverage measurement actually occurs in functions called after `start`
+ is invoked. Statements in the same scope as `start` won't be measured.
+
+ Once you invoke `start`, you must also call `stop` eventually, or your
+ process might not shut down cleanly.
+
+ """
if self.run_suffix:
# Calling start() means we're running code, so use the run_suffix
# as the data_suffix when we eventually save the data.
@@ -385,7 +360,6 @@ class coverage(object):
def stop(self):
"""Stop measuring code coverage."""
self.collector.stop()
- self._harvest_data()
def erase(self):
"""Erase previously-collected coverage data.
@@ -450,8 +424,8 @@ class coverage(object):
# `save()` at the last minute so that the pid will be correct even
# if the process forks.
data_suffix = "%s.%s.%06d" % (
- self.socket.gethostname(), self.os.getpid(),
- self.random.randint(0, 99999)
+ socket.gethostname(), os.getpid(),
+ random.randint(0, 99999)
)
self._harvest_data()
@@ -498,6 +472,7 @@ class coverage(object):
# Find files that were never executed at all.
for src in self.source:
for py_file in find_python_files(src):
+ py_file = self.file_locator.canonical_filename(py_file)
self.data.touch_file(py_file)
self._harvested = True
@@ -537,6 +512,7 @@ class coverage(object):
Returns an `Analysis` object.
"""
+ self._harvest_data()
if not isinstance(it, CodeUnit):
it = code_unit_factory(it, self.file_locator)[0]
@@ -555,13 +531,16 @@ class coverage(object):
match those patterns will be included in the report. Modules matching
`omit` will not be included in the report.
+ Returns a float, the total percentage covered.
+
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
show_missing=show_missing,
)
reporter = SummaryReporter(self, self.config)
- reporter.report(morfs, outfile=file)
+ return reporter.report(morfs, outfile=file)
def annotate(self, morfs=None, directory=None, ignore_errors=None,
omit=None, include=None):
@@ -575,6 +554,7 @@ class coverage(object):
See `coverage.report()` for other arguments.
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include
)
@@ -582,7 +562,7 @@ class coverage(object):
reporter.report(morfs, directory=directory)
def html_report(self, morfs=None, directory=None, ignore_errors=None,
- omit=None, include=None, extra_css=None):
+ omit=None, include=None, extra_css=None, title=None):
"""Generate an HTML report.
The HTML is written to `directory`. The file "index.html" is the
@@ -592,15 +572,21 @@ class coverage(object):
`extra_css` is a path to a file of other CSS to apply on the page.
It will be copied into the HTML directory.
+ `title` is a text string (not HTML) to use as the title of the HTML
+ report.
+
See `coverage.report()` for other arguments.
+ Returns a float, the total percentage covered.
+
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
- html_dir=directory, extra_css=extra_css,
+ html_dir=directory, extra_css=extra_css, html_title=title,
)
reporter = HtmlReporter(self, self.config)
- reporter.report(morfs)
+ return reporter.report(morfs)
def xml_report(self, morfs=None, outfile=None, ignore_errors=None,
omit=None, include=None):
@@ -613,12 +599,16 @@ class coverage(object):
See `coverage.report()` for other arguments.
+ Returns a float, the total percentage covered.
+
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
xml_output=outfile,
)
file_to_close = None
+ delete_file = False
if self.config.xml_output:
if self.config.xml_output == '-':
outfile = sys.stdout
@@ -627,10 +617,15 @@ class coverage(object):
file_to_close = outfile
try:
reporter = XmlReporter(self, self.config)
- reporter.report(morfs, outfile=outfile)
+ return reporter.report(morfs, outfile=outfile)
+ except CoverageException:
+ delete_file = True
+ raise
finally:
if file_to_close:
file_to_close.close()
+ if delete_file:
+ file_be_gone(self.config.xml_output)
def sysinfo(self):
"""Return a list of (key, value) pairs showing internal information."""
@@ -656,8 +651,8 @@ class coverage(object):
('cwd', os.getcwd()),
('path', sys.path),
('environment', [
- ("%s = %s" % (k, v)) for k, v in os.environ.items()
- if re.search("^COV|^PY", k)
+ ("%s = %s" % (k, v)) for k, v in iitems(os.environ)
+ if re.search(r"^COV|^PY", k)
]),
]
return info
diff --git a/coverage/data.py b/coverage/data.py
index 7a8d656f..c86a77f2 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -2,8 +2,9 @@
import os
-from coverage.backward import pickle, sorted # pylint: disable=W0622
+from coverage.backward import iitems, pickle, sorted # pylint: disable=W0622
from coverage.files import PathAliases
+from coverage.misc import file_be_gone
class CoverageData(object):
@@ -60,10 +61,6 @@ class CoverageData(object):
#
self.arcs = {}
- self.os = os
- self.sorted = sorted
- self.pickle = pickle
-
def usefile(self, use_file=True):
"""Set whether or not to use a disk file for data."""
self.use_file = use_file
@@ -93,21 +90,21 @@ class CoverageData(object):
def erase(self):
"""Erase the data, both in this object, and from its file storage."""
if self.use_file:
- if self.filename and os.path.exists(self.filename):
- os.remove(self.filename)
+ if self.filename:
+ file_be_gone(self.filename)
self.lines = {}
self.arcs = {}
def line_data(self):
"""Return the map from filenames to lists of line numbers executed."""
return dict(
- [(f, self.sorted(lmap.keys())) for f, lmap in self.lines.items()]
+ [(f, sorted(lmap.keys())) for f, lmap in iitems(self.lines)]
)
def arc_data(self):
"""Return the map from filenames to lists of line number pairs."""
return dict(
- [(f, self.sorted(amap.keys())) for f, amap in self.arcs.items()]
+ [(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)]
)
def write_file(self, filename):
@@ -127,7 +124,7 @@ class CoverageData(object):
# Write the pickle to the file.
fdata = open(filename, 'wb')
try:
- self.pickle.dump(data, fdata, 2)
+ pickle.dump(data, fdata, 2)
finally:
fdata.close()
@@ -159,12 +156,12 @@ class CoverageData(object):
# Unpack the 'lines' item.
lines = dict([
(f, dict.fromkeys(linenos, None))
- for f, linenos in data.get('lines', {}).items()
+ for f, linenos in iitems(data.get('lines', {}))
])
# Unpack the 'arcs' item.
arcs = dict([
(f, dict.fromkeys(arcpairs, None))
- for f, arcpairs in data.get('arcs', {}).items()
+ for f, arcpairs in iitems(data.get('arcs', {}))
])
except Exception:
pass
@@ -187,10 +184,10 @@ class CoverageData(object):
if f.startswith(localdot):
full_path = os.path.join(data_dir, f)
new_lines, new_arcs = self._read_file(full_path)
- for filename, file_data in new_lines.items():
+ for filename, file_data in iitems(new_lines):
filename = aliases.map(filename)
self.lines.setdefault(filename, {}).update(file_data)
- for filename, file_data in new_arcs.items():
+ for filename, file_data in iitems(new_arcs):
filename = aliases.map(filename)
self.arcs.setdefault(filename, {}).update(file_data)
if f != local:
@@ -202,7 +199,7 @@ class CoverageData(object):
`line_data` is { filename: { lineno: None, ... }, ...}
"""
- for filename, linenos in line_data.items():
+ for filename, linenos in iitems(line_data):
self.lines.setdefault(filename, {}).update(linenos)
def add_arc_data(self, arc_data):
@@ -211,7 +208,7 @@ class CoverageData(object):
`arc_data` is { filename: { (l1,l2): None, ... }, ...}
"""
- for filename, arcs in arc_data.items():
+ for filename, arcs in iitems(arc_data):
self.arcs.setdefault(filename, {}).update(arcs)
def touch_file(self, filename):
@@ -252,8 +249,8 @@ class CoverageData(object):
if fullpath:
filename_fn = lambda f: f
else:
- filename_fn = self.os.path.basename
- for filename, lines in self.lines.items():
+ filename_fn = os.path.basename
+ for filename, lines in iitems(self.lines):
summ[filename_fn(filename)] = len(lines)
return summ
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 3283a3f7..587c2d3c 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -110,7 +110,7 @@ def run_python_file(filename, args, package=None):
# We have the source. `compile` still needs the last line to be clean,
# so make sure it is, then compile a code object from it.
- if source[-1] != '\n':
+ if not source or source[-1] != '\n':
source += '\n'
code = compile(source, filename, "exec")
diff --git a/coverage/files.py b/coverage/files.py
index 13f43930..40af7bf7 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -2,23 +2,19 @@
from coverage.backward import to_string
from coverage.misc import CoverageException
-import fnmatch, os, re, sys
+import fnmatch, os, os.path, re, sys
class FileLocator(object):
"""Understand how filenames work."""
def __init__(self):
# The absolute path to our current directory.
- self.relative_dir = self.abs_file(os.curdir) + os.sep
+ self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep)
# Cache of results of calling the canonical_filename() method, to
# avoid duplicating work.
self.canonical_filename_cache = {}
- def abs_file(self, filename):
- """Return the absolute normalized form of `filename`."""
- return os.path.normcase(os.path.abspath(os.path.realpath(filename)))
-
def relative_filename(self, filename):
"""Return the relative form of `filename`.
@@ -26,8 +22,9 @@ class FileLocator(object):
`FileLocator` was constructed.
"""
- if filename.startswith(self.relative_dir):
- filename = filename.replace(self.relative_dir, "", 1)
+ fnorm = os.path.normcase(filename)
+ if fnorm.startswith(self.relative_dir):
+ filename = filename[len(self.relative_dir):]
return filename
def canonical_filename(self, filename):
@@ -49,7 +46,7 @@ class FileLocator(object):
if os.path.exists(g):
f = g
break
- cf = self.abs_file(f)
+ cf = abs_file(f)
self.canonical_filename_cache[filename] = cf
return self.canonical_filename_cache[filename]
@@ -78,6 +75,72 @@ class FileLocator(object):
return None
+if sys.platform == 'win32':
+
+ def actual_path(path):
+ """Get the actual path of `path`, including the correct case."""
+ if path in actual_path.cache:
+ return actual_path.cache[path]
+
+ head, tail = os.path.split(path)
+ if not tail:
+ actpath = head
+ elif not head:
+ actpath = tail
+ else:
+ head = actual_path(head)
+ if head in actual_path.list_cache:
+ files = actual_path.list_cache[head]
+ else:
+ try:
+ files = os.listdir(head)
+ except OSError:
+ files = []
+ actual_path.list_cache[head] = files
+ normtail = os.path.normcase(tail)
+ for f in files:
+ if os.path.normcase(f) == normtail:
+ tail = f
+ break
+ actpath = os.path.join(head, tail)
+ actual_path.cache[path] = actpath
+ return actpath
+
+ actual_path.cache = {}
+ actual_path.list_cache = {}
+
+else:
+ def actual_path(filename):
+ """The actual path for non-Windows platforms."""
+ return filename
+
+def abs_file(filename):
+ """Return the absolute normalized form of `filename`."""
+ path = os.path.abspath(os.path.realpath(filename))
+ path = actual_path(path)
+ return path
+
+
+def prep_patterns(patterns):
+ """Prepare the file patterns for use in a `FnmatchMatcher`.
+
+ If a pattern starts with a wildcard, it is used as a pattern
+ as-is. If it does not start with a wildcard, then it is made
+ absolute with the current directory.
+
+ If `patterns` is None, an empty list is returned.
+
+ """
+ patterns = patterns or []
+ prepped = []
+ for p in patterns or []:
+ if p.startswith("*") or p.startswith("?"):
+ prepped.append(p)
+ else:
+ prepped.append(abs_file(p))
+ return prepped
+
+
class TreeMatcher(object):
"""A matcher for files in a tree."""
def __init__(self, directories):
@@ -175,7 +238,7 @@ class PathAliases(object):
# either separator.
regex_pat = regex_pat.replace(r"\/", r"[\\/]")
# We want case-insensitive matching, so add that flag.
- regex = re.compile("(?i)" + regex_pat)
+ regex = re.compile(r"(?i)" + regex_pat)
# Normalize the result: it must end with a path separator.
result_sep = sep(result)
diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py
index ad350bc0..6a258d67 100644
--- a/coverage/fullcoverage/encodings.py
+++ b/coverage/fullcoverage/encodings.py
@@ -37,6 +37,14 @@ class FullCoverageTracer(object):
sys.settrace(FullCoverageTracer().fullcoverage_trace)
+# In coverage/files.py is actual_filename(), which uses glob.glob. I don't
+# understand why, but that use of glob borks everything if fullcoverage is in
+# effect. So here we make an ugly hail-mary pass to switch off glob.glob over
+# there. This means when using fullcoverage, Windows path names will not be
+# their actual case.
+
+#sys.fullcoverage = True
+
# Finally, remove our own directory from sys.path; remove ourselves from
# sys.modules; and re-import "encodings", which will be the real package
# this time. Note that the delete from sys.modules dictionary has to
diff --git a/coverage/html.py b/coverage/html.py
index 34bf6a61..6a6c648e 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -7,6 +7,7 @@ from coverage.backward import pickle
from coverage.misc import CoverageException, Hasher
from coverage.phystokens import source_token_lines, source_encoding
from coverage.report import Reporter
+from coverage.results import Numbers
from coverage.templite import Templite
# Disable pylint msg W0612, because a bunch of variables look unused, but
@@ -46,6 +47,7 @@ class HtmlReporter(Reporter):
self.directory = None
self.template_globals = {
'escape': escape,
+ 'title': self.config.html_title,
'__url__': coverage.__url__,
'__version__': coverage.__version__,
}
@@ -59,6 +61,7 @@ class HtmlReporter(Reporter):
self.arcs = self.coverage.data.has_arcs()
self.status = HtmlStatus()
self.extra_css = None
+ self.totals = Numbers()
def report(self, morfs):
"""Generate an HTML report for `morfs`.
@@ -94,6 +97,8 @@ class HtmlReporter(Reporter):
self.make_local_static_report_files()
+ return self.totals.pc_covered
+
def make_local_static_report_files(self):
"""Make local instances of static files for HTML report."""
# The files we provide must always be copied.
@@ -245,12 +250,15 @@ class HtmlReporter(Reporter):
files = self.files
arcs = self.arcs
- totals = sum([f['nums'] for f in files])
+ self.totals = totals = sum([f['nums'] for f in files])
extra_css = self.extra_css
+ html = index_tmpl.render(locals())
+ if sys.version_info < (3, 0):
+ html = html.decode("utf-8")
self.write_html(
os.path.join(self.directory, "index.html"),
- index_tmpl.render(locals())
+ html
)
# Write the latest hashes for next time.
@@ -358,5 +366,5 @@ def spaceless(html):
Get rid of some.
"""
- html = re.sub(">\s+<p ", ">\n<p ", html)
+ html = re.sub(r">\s+<p ", ">\n<p ", html)
return html
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
index 5906e653..b24006d2 100644
--- a/coverage/htmlfiles/coverage_html.js
+++ b/coverage/htmlfiles/coverage_html.js
@@ -374,4 +374,3 @@ coverage.scroll_window = function (to_pos) {
coverage.finish_scrolling = function () {
$("html,body").stop(true, true);
};
-
diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html
index c6d9eec0..5a7c8c2e 100644
--- a/coverage/htmlfiles/index.html
+++ b/coverage/htmlfiles/index.html
@@ -2,7 +2,7 @@
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
- <title>Coverage report</title>
+ <title>{{ title|escape }}</title>
<link rel='stylesheet' href='style.css' type='text/css'>
{% if extra_css %}
<link rel='stylesheet' href='{{ extra_css }}' type='text/css'>
@@ -19,7 +19,7 @@
<div id='header'>
<div class='content'>
- <h1>Coverage report:
+ <h1>{{ title|escape }}:
<span class='pc_cov'>{{totals.pc_covered_str}}%</span>
</h1>
<img id='keyboard_icon' src='keybd_closed.png'>
diff --git a/coverage/misc.py b/coverage/misc.py
index fd9be857..3ed854a7 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -1,6 +1,10 @@
"""Miscellaneous stuff for Coverage."""
+import errno
import inspect
+import os
+import sys
+
from coverage.backward import md5, sorted # pylint: disable=W0622
from coverage.backward import string_class, to_bytes
@@ -83,6 +87,16 @@ def join_regex(regexes):
return ""
+def file_be_gone(path):
+ """Remove a file, and don't get annoyed if it doesn't exist."""
+ try:
+ os.remove(path)
+ except OSError:
+ _, e, _ = sys.exc_info()
+ if e.errno != errno.ENOENT:
+ raise
+
+
class Hasher(object):
"""Hashes Python data into md5."""
def __init__(self):
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index 3beebab1..166020e1 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -119,7 +119,7 @@ def source_encoding(source):
# This is mostly code adapted from Py3.2's tokenize module.
- cookie_re = re.compile("coding[:=]\s*([-\w.]+)")
+ cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)")
# Do this so the detect_encode code we copied will work.
readline = iter(source.splitlines()).next
diff --git a/coverage/report.py b/coverage/report.py
index e351340f..34f44422 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -2,6 +2,7 @@
import fnmatch, os
from coverage.codeunit import code_unit_factory
+from coverage.files import prep_patterns
from coverage.misc import CoverageException, NoSource, NotPython
class Reporter(object):
@@ -35,7 +36,7 @@ class Reporter(object):
self.code_units = code_unit_factory(morfs, file_locator)
if self.config.include:
- patterns = [file_locator.abs_file(p) for p in self.config.include]
+ patterns = prep_patterns(self.config.include)
filtered = []
for cu in self.code_units:
for pattern in patterns:
@@ -45,7 +46,7 @@ class Reporter(object):
self.code_units = filtered
if self.config.omit:
- patterns = [file_locator.abs_file(p) for p in self.config.omit]
+ patterns = prep_patterns(self.config.omit)
filtered = []
for cu in self.code_units:
for pattern in patterns:
diff --git a/coverage/results.py b/coverage/results.py
index d7e2a9d1..b39966ca 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -2,7 +2,7 @@
import os
-from coverage.backward import set, sorted # pylint: disable=W0622
+from coverage.backward import iitems, set, sorted # pylint: disable=W0622
from coverage.misc import format_lines, join_regex, NoSource
from coverage.parser import CodeParser
@@ -42,7 +42,7 @@ class Analysis(object):
n_branches = self.total_branches()
mba = self.missing_branch_arcs()
n_missing_branches = sum(
- [len(v) for k,v in mba.items() if k not in self.missing]
+ [len(v) for k,v in iitems(mba) if k not in self.missing]
)
else:
n_branches = n_missing_branches = 0
@@ -109,7 +109,7 @@ class Analysis(object):
def branch_lines(self):
"""Returns a list of line numbers that have more than one exit."""
exit_counts = self.parser.exit_counts()
- return [l1 for l1,count in exit_counts.items() if count > 1]
+ return [l1 for l1,count in iitems(exit_counts) if count > 1]
def total_branches(self):
"""How many total branches are there?"""
diff --git a/coverage/summary.py b/coverage/summary.py
index c8fa5be4..03648e5f 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -82,3 +82,5 @@ class SummaryReporter(Reporter):
if self.config.show_missing:
args += ("",)
outfile.write(fmt_coverage % args)
+
+ return total.pc_covered
diff --git a/coverage/tracer.c b/coverage/tracer.c
index c17dc03c..97dd113b 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -235,10 +235,8 @@ CTracer_record_pair(CTracer *self, int l1, int l2)
{
int ret = RET_OK;
- PyObject * t = PyTuple_New(2);
+ PyObject * t = Py_BuildValue("(ii)", l1, l2);
if (t != NULL) {
- PyTuple_SET_ITEM(t, 0, MyInt_FromLong(l1));
- PyTuple_SET_ITEM(t, 1, MyInt_FromLong(l2));
if (PyDict_SetItem(self->cur_file_data, t, Py_None) < 0) {
STATS( self->stats.errors++; )
ret = RET_ERROR;
@@ -730,4 +728,3 @@ inittracer(void)
}
#endif /* Py3k */
-
diff --git a/coverage/version.py b/coverage/version.py
new file mode 100644
index 00000000..5a8a2d6e
--- /dev/null
+++ b/coverage/version.py
@@ -0,0 +1,9 @@
+"""The version and URL for coverage.py"""
+# This file is exec'ed in setup.py, don't import anything!
+
+__version__ = "3.5.4b1" # see detailed history in CHANGES.txt
+
+__url__ = "http://nedbatchelder.com/code/coverage"
+if max(__version__).isalpha():
+ # For pre-releases, use a version-specific URL.
+ __url__ += "/" + __version__
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 03f910c8..e062ceee 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -84,6 +84,9 @@ class XmlReporter(Reporter):
# Use the DOM to write the output file.
outfile.write(self.xml_out.toprettyxml())
+ # Return the total percentage.
+ return 100.0 * (lhits_tot + bhits_tot) / (lnum_tot + bnum_tot)
+
def xml_file(self, cu, analysis):
"""Add to the XML report for a single file."""
diff --git a/doc/_ext/px_xlator.py b/doc/_ext/px_xlator.py
index 98929e9c..6ad1063e 100644
--- a/doc/_ext/px_xlator.py
+++ b/doc/_ext/px_xlator.py
@@ -78,6 +78,7 @@ class PxTranslator(BaseHtmlXlator):
def visit_desc_parameterlist(self, node):
self.body.append('(')
self.first_param = 1
+ self.param_separator = node.child_text_separator
def depart_desc_parameterlist(self, node):
self.body.append(')')
diff --git a/doc/api.rst b/doc/api.rst
index 09b94af4..9ce1ee1e 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -8,6 +8,7 @@ Coverage API
:history: 20090613T164000, final touches for 3.0
:history: 20100221T151500, docs for 3.3 (on the plane back from PyCon)
:history: 20100725T211700, updated for 3.4.
+:history: 20121111T235800, added a bit of clarification.
The API to coverage.py is very simple, contained in a single module called
@@ -20,11 +21,13 @@ in the command line interface. For example, a simple use would be::
cov = coverage.coverage()
cov.start()
- # .. run your code ..
+ # .. call your code ..
cov.stop()
cov.save()
+ cov.html_report()
+
The coverage module
-------------------
diff --git a/doc/branch.rst b/doc/branch.rst
index 3ee09aa3..13b9dc6d 100644
--- a/doc/branch.rst
+++ b/doc/branch.rst
@@ -118,4 +118,3 @@ Here the while loop will never complete because the break will always be taken
at some point. Coverage.py can't work that out on its own, but the
"no branch" pragma indicates that the branch is known to be partial, and
the line is not flagged.
-
diff --git a/doc/cmd.rst b/doc/cmd.rst
index 45552089..dd11f92f 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -21,8 +21,11 @@ Coverage command line usage
When you install coverage.py, a command-line script simply called ``coverage``
-is placed in your Python scripts directory. Coverage has a number of commands
-which determine the action performed:
+is placed in your Python scripts directory. To help with multi-version
+installs, it will also create either a ``coverage2`` or ``coverage3`` alias,
+depending on the version of Python you're using.
+
+Coverage has a number of commands which determine the action performed:
* **run** -- Run a Python program and collect execution data.
@@ -204,6 +207,11 @@ encountered trying to find source files to report on. This can be useful if
some files are missing, or if your Python execution is tricky enough that file
names are synthesized without real source files.
+If you provide a ``--fail-under`` value, the total percentage covered will be
+compared to that value. If it is less, the command will exit with a status
+code of 2, indicating that the total coverage was less than your target. This
+can be used as part of a pass/fail condition, for example in a continuous
+integration server. This option isn't available for **annotate**.
.. _cmd_summary:
@@ -269,13 +277,17 @@ Lines are highlighted green for executed, red for missing, and gray for
excluded. The counts at the top of the file are buttons to turn on and off
the highlighting.
-If you prefer a different style for your HTML report, you can provide your
-own CSS file to apply, by specifying a CSS file in the [html] section of the
-configuration file. See :ref:`config_html` for details.
-
A number of keyboard shortcuts are available for navigating the report.
Click the keyboard icon in the upper right to see the complete list.
+The title of the report can be set with the ``title`` setting in the
+``[html]`` section of the configuration file, or the ``--title`` switch on
+the command line.
+
+If you prefer a different style for your HTML report, you can provide your
+own CSS file to apply, by specifying a CSS file in the ``[html]`` section of
+the configuration file. See :ref:`config_html` for details.
+
The ``-d`` argument specifies an output directory, defaulting to "htmlcov"::
$ coverage html -d coverage_html
diff --git a/doc/config.rst b/doc/config.rst
index 74cab4fd..159a42f5 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -180,6 +180,9 @@ The file will be copied into the HTML output directory. Don't name it
"style.css". This CSS is in addition to the CSS normally used, though you can
overwrite as many of the rules as you like.
+``title`` (string, default "Coverage report"): the title to use for the report.
+Note this is text, not HTML.
+
[xml]
-----
diff --git a/doc/contributing.rst b/doc/contributing.rst
new file mode 100644
index 00000000..d0967f88
--- /dev/null
+++ b/doc/contributing.rst
@@ -0,0 +1,156 @@
+.. _contributing:
+
+===========================
+Contributing to coverage.py
+===========================
+
+:history: 20121112T154100, brand new docs.
+
+.. highlight:: console
+
+I welcome contributions to coverage.py. Over the years, dozens of people have
+provided patches of various sizes to add features or fix bugs. This page
+should have all the information you need to make a contribution.
+
+One source of history or ideas are the `bug reports`_ against coverage.py.
+There you can find ideas for requested features, or the remains of rejected
+ideas.
+
+.. _bug reports: https://bitbucket.org/ned/coveragepy/issues?status=new&status=open
+
+
+Before you begin
+----------------
+
+If you have an idea for coverage.py, run it by me before you begin writing
+code. This way, I can get you going in the right direction, or point you to
+previous work in the area. Things are not always as straightforward as they
+seem, and having the benefit of lessons learned by those before you can save
+you frustration.
+
+
+Getting the code
+----------------
+
+The coverage.py code is hosted on a `Mercurial`_ repository at
+https://bitbucket.org/ned/coveragepy. To get a working environment, follow
+these steps:
+
+#. (Optional, but recommended) Create a virtualenv to work in, and activate
+ it.
+
+#. Clone the repo::
+
+ $ hg clone https://bitbucket.org/ned/coveragepy
+
+#. Install the requirements::
+
+ $ pip install -r requirements.txt
+
+#. Install a number of versions of Python. Coverage.py supports a wide range
+ of Python versions. The more you can test with, the more easily your code
+ can be used as-is. If you only have one version, that's OK too, but may
+ mean more work integrating your contribution.
+
+
+Running the tests
+-----------------
+
+The tests are written as standard unittest-style tests, and are run with
+`tox`_::
+
+ $ tox
+ GLOB sdist-make: /home/ned/coverage/setup.py
+ py25 sdist-reinst: /home/ned/coverage/tox/dist/coverage-3.5.4b1.zip
+ py25 runtests: commands[0]
+ py25 runtests: commands[1]
+ py25 runtests: commands[2]
+ py25 runtests: commands[3]
+ py25 runtests: commands[4]
+ === Python 2.5.5 with Python tracer (/home/ned/coverage/tox/py25/bin/python) ===
+ ...........................................................................................(etc)
+ ----------------------------------------------------------------------
+ Ran 360 tests in 10.836s
+
+ OK
+ py25 runtests: commands[5]
+ py25 runtests: commands[6]
+ === Python 2.5.5 with C tracer (/home/ned/coverage/tox/py25/bin/python) ===
+ ...........................................................................................(etc)
+ ----------------------------------------------------------------------
+ Ran 360 tests in 10.044s
+
+ OK
+ py26 sdist-reinst: /home/ned/coverage/trunk/.tox/dist/coverage-3.5.4b1.zip
+ py26 runtests: commands[0]
+ py26 runtests: commands[1]
+ py26 runtests: commands[2]
+ py26 runtests: commands[3]
+ py26 runtests: commands[4]
+ === CPython 2.6.6 with Python tracer (/home/ned/coverage/tox/py26/bin/python) ===
+ ...........................................................................................(etc)
+ ----------------------------------------------------------------------
+ Ran 364 tests in 12.572s
+
+ OK
+ py26 runtests: commands[5]
+ py26 runtests: commands[6]
+ === CPython 2.6.6 with C tracer (/home/ned/coverage/tox/py26/bin/python) ===
+ ...........................................................................................(etc)
+ ----------------------------------------------------------------------
+ Ran 364 tests in 11.458s
+
+ OK
+ (and so on...)
+
+Tox runs the complete test suite twice for each version of Python you have
+installed. The first run uses the Python implementation of the trace
+function, the second uses the C implementation.
+
+To limit tox to just a few versions of Python, use the ``-e`` switch::
+
+ $ tox -e py27,py33
+
+To run just a few tests, you can use nose test selector syntax::
+
+ $ tox test.test_misc:SetupPyTest.test_metadata
+
+This looks in `test/test_misc.py` to find the `SetupPyTest` class, and runs the
+`test_metadata` test method.
+
+Of course, run all the tests on every version of Python you have, before
+submitting a change.
+
+
+Lint, etc
+---------
+
+I try to keep the coverage.py as clean as possible. I use pylint to alert me
+to possible problems::
+
+ $ make lint
+ pylint --rcfile=.pylintrc coverage setup.py test
+ ************* Module coverage.collector
+ F0401: 7,4: Unable to import 'coverage.tracer'
+ E0611: 7,4: No name 'tracer' in module 'coverage'
+ ************* Module coverage.parser
+ W0631:532,32:ByteParser._split_into_chunks: Using possibly undefined loop variable 'bc'
+ W0631:579,40:ByteParser._arcs: Using possibly undefined loop variable 'ch'
+ make: [lint] Error 7 (ignored)
+ python -m tabnanny coverage setup.py test
+ python igor.py check_eol
+
+As you can see, a few warnings persist, don't worry about them. But clean up
+any ones you may be responsible for.
+
+
+Contributing
+------------
+
+When you are ready to contribute a change, any way you can get it to me is
+probably fine. A pull request on Bitbucket is great, but a simple diff or
+patch is great too.
+
+
+.. _Mercurial: http://mercurial.selenic.com/
+.. _tox: http://tox.testrun.org/
diff --git a/doc/faq.rst b/doc/faq.rst
index ef27c291..0eff28b6 100644
--- a/doc/faq.rst
+++ b/doc/faq.rst
@@ -113,4 +113,3 @@ Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of
.. _Gareth Rees: http://garethrees.org/
.. _Ned Batchelder: http://nedbatchelder.com
.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/AUTHORS.txt
-
diff --git a/doc/index.rst b/doc/index.rst
index 9e8ed502..6fc7a057 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -154,6 +154,7 @@ More information
branch
subprocess
api
+ contributing
faq
changes
diff --git a/doc/install.rst b/doc/install.rst
index 5692b69b..ffce72fa 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -66,4 +66,3 @@ coverage installed properly::
$ coverage --version
Coverage.py, version 3.5.3. http://nedbatchelder.com/code/coverage
-
diff --git a/doc/source.rst b/doc/source.rst
index 8700bcb7..aafb976c 100644
--- a/doc/source.rst
+++ b/doc/source.rst
@@ -72,4 +72,3 @@ reporting.
Note that these are ways of specifying files to measure. You can also exclude
individual source lines. See :ref:`excluding` for details.
-
diff --git a/howto.txt b/howto.txt
index fd5c2ff3..2cde1a17 100644
--- a/howto.txt
+++ b/howto.txt
@@ -5,7 +5,7 @@
- Ubuntu
- Mac
- Pythons 2.3, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3
-- Version number in coverage/__init__.py
+- Version number in coverage/version.py
- 3.1a1, 3.1b1, 3.1c1, 3.1
- Update CHANGES.txt, including release date.
- Update docstring in setup.py, including "New in x.y:"
@@ -81,4 +81,3 @@
- To run the Javascript tests:
open test/js/index.html in variety of browsers.
-
diff --git a/igor.py b/igor.py
index e596f873..0a1552b2 100644
--- a/igor.py
+++ b/igor.py
@@ -55,7 +55,10 @@ def do_zip_mods(args):
def do_check_eol(args):
"""Check files for incorrect newlines and trailing whitespace."""
- ignore_dirs = ['.svn', '.hg', '.tox', '.tox_kits']
+ ignore_dirs = [
+ '.svn', '.hg', '.tox', '.tox_kits', 'coverage.egg-info',
+ '_build',
+ ]
checked = set([])
def check_file(fname, crlf=True, trail_white=True):
@@ -64,6 +67,7 @@ def do_check_eol(args):
return
checked.add(fname)
+ line = None
for n, line in enumerate(open(fname, "rb")):
if crlf:
if "\r" in line:
@@ -77,6 +81,9 @@ def do_check_eol(args):
print("%s@%d: trailing whitespace found" % (fname, n+1))
return
+ if line is not None and not line.strip():
+ print("%s: final blank line" % (fname,))
+
def check_files(root, patterns, **kwargs):
for root, dirs, files in os.walk(root):
for f in files:
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..bdeb80c9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+nose
+mock
+tox
+pylint
diff --git a/setup.py b/setup.py
index 67745515..c8527819 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-# setup.py for coverage.
+# setup.py for coverage.py
"""Code coverage measurement for Python
@@ -19,12 +19,11 @@ can be reported.
New in 3.3: .coveragerc files.
New in 3.2: Branch coverage!
-
"""
# This file is used unchanged under all versions of Python, 2.x and 3.x.
-classifiers = """
+classifiers = """\
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: BSD License
@@ -36,7 +35,7 @@ Topic :: Software Development :: Testing
"""
# Pull in the tools we need.
-import sys
+import os, sys
# Distribute is a new fork of setuptools. It's supported on Py3.x, so we use
# it there, but stick with classic setuptools on Py2.x until Distribute becomes
@@ -51,17 +50,21 @@ use_setuptools()
from setuptools import setup
from distutils.core import Extension # pylint: disable=E0611,F0401
-# Get or massage our metadata.
-
-from coverage import __url__, __version__
+# Get or massage our metadata. We exec coverage/version.py so we can avoid
+# importing the product code into setup.py.
-scripts = ['coverage = coverage:main']
-if sys.version_info >= (3, 0):
- scripts.append('coverage3 = coverage:main')
+doc = __doc__ # __doc__ will be overwritten by version.py.
+__version__ = __url__ = "" # keep pylint happy.
-doclines = (__doc__ % __url__).split('\n')
+cov_ver_py = os.path.join(os.path.split(__file__)[0], "coverage/version.py")
+version_file = open(cov_ver_py)
+try:
+ exec(compile(version_file.read(), cov_ver_py, 'exec'))
+finally:
+ version_file.close()
-classifier_list = [c for c in classifiers.split("\n") if c]
+doclines = (doc % __url__).splitlines()
+classifier_list = classifiers.splitlines()
if 'a' in __version__:
devstat = "3 - Alpha"
@@ -71,7 +74,13 @@ else:
devstat = "5 - Production/Stable"
classifier_list.append("Development Status :: " + devstat)
-# Set it up!
+# Install a script as "coverage", and as "coverage[23]"
+scripts = [
+ 'coverage = coverage:main',
+ 'coverage%d = coverage:main' % sys.version_info[0],
+ ]
+
+# Create the keyword arguments for setup()
setup_args = dict(
name = 'coverage',
@@ -126,20 +135,25 @@ if sys.version_info >= (3, 0):
use_2to3=False,
))
-# For a variety of reasons, it might not be possible to install the C
-# extension. Try it with, and if it fails, try it without.
-try:
- setup(**setup_args)
-except: # pylint: disable=W0702
- # When setup() can't compile, it tries to exit. We'll catch SystemExit
- # here :-(, and try again.
- if 'install' not in sys.argv or 'ext_modules' not in setup_args:
- # We weren't trying to install an extension, so forget it.
- raise
- msg = "Couldn't install with extension module, trying without it..."
- exc = sys.exc_info()[1]
- exc_msg = "%s: %s" % (exc.__class__.__name__, exc)
- print("**\n** %s\n** %s\n**" % (msg, exc_msg))
-
- del setup_args['ext_modules']
- setup(**setup_args)
+def main():
+ """Actually invoke setup() with the arguments we built above."""
+ # For a variety of reasons, it might not be possible to install the C
+ # extension. Try it with, and if it fails, try it without.
+ try:
+ setup(**setup_args)
+ except: # pylint: disable=W0702
+ # When setup() can't compile, it tries to exit. We'll catch SystemExit
+ # here :-(, and try again.
+ if 'install' not in sys.argv or 'ext_modules' not in setup_args:
+ # We weren't trying to install an extension, so forget it.
+ raise
+ msg = "Couldn't install with extension module, trying without it..."
+ exc = sys.exc_info()[1]
+ exc_msg = "%s: %s" % (exc.__class__.__name__, exc)
+ print("**\n** %s\n** %s\n**" % (msg, exc_msg))
+
+ del setup_args['ext_modules']
+ setup(**setup_args)
+
+if __name__ == '__main__':
+ main()
diff --git a/test/backtest.py b/test/backtest.py
index c54171d3..b17aa242 100644
--- a/test/backtest.py
+++ b/test/backtest.py
@@ -31,7 +31,7 @@ else:
stderr=subprocess.STDOUT
)
output, _ = proc.communicate()
- status = proc.returncode
+ status = proc.returncode # pylint: disable=E1101
# Get the output, and canonicalize it to strings with newlines.
if not isinstance(output, str):
diff --git a/test/backunittest.py b/test/backunittest.py
index c1685e0c..30da78eb 100644
--- a/test/backunittest.py
+++ b/test/backunittest.py
@@ -29,6 +29,27 @@ class TestCase(unittest.TestCase):
if exp:
self.fail(msg)
+ if _need('assertIn'):
+ def assertIn(self, member, container, msg=None):
+ """Assert that `member` is in `container`."""
+ if member not in container:
+ msg = msg or ('%r not found in %r' % (member, container))
+ self.fail(msg)
+
+ if _need('assertNotIn'):
+ def assertNotIn(self, member, container, msg=None):
+ """Assert that `member` is not in `container`."""
+ if member in container:
+ msg = msg or ('%r found in %r' % (member, container))
+ self.fail(msg)
+
+ if _need('assertGreater'):
+ def assertGreater(self, a, b, msg=None):
+ """Assert that `a` is greater than `b`."""
+ if not a > b:
+ msg = msg or ('%r not greater than %r' % (a, b))
+ self.fail(msg)
+
if _need('assertRaisesRegexp'):
def assertRaisesRegexp(self, excClass, regexp, callobj, *args, **kw):
""" Just like unittest.TestCase.assertRaises,
@@ -46,7 +67,7 @@ class TestCase(unittest.TestCase):
# Message provided, and it didn't match: fail!
raise self.failureException(
"Right exception, wrong message: "
- "'%s' doesn't match '%s'" % (excMsg, regexp)
+ "%r doesn't match %r" % (excMsg, regexp)
)
# No need to catch other exceptions: They'll fail the test all by
# themselves!
@@ -66,11 +87,12 @@ class TestCase(unittest.TestCase):
self.assertEqual(set(s1), set(s2))
if _need('assertRegexpMatches'):
- def assertRegexpMatches(self, s, regex):
- """Assert that `s` matches `regex`."""
- m = re.search(regex, s)
+ def assertRegexpMatches(self, text, regex, msg=None):
+ """Assert that `text` matches `regex`."""
+ m = re.search(regex, text)
if not m:
- raise self.failureException("%r doesn't match %r" % (s, regex))
+ msg = msg or ("%r doesn't match %r" % (text, regex))
+ raise self.failureException(msg)
if _need('assertMultiLineEqual'):
def assertMultiLineEqual(self, first, second, msg=None):
diff --git a/test/farm/html/src/partial.py b/test/farm/html/src/partial.py
index 9126844f..8d62f5c5 100644
--- a/test/farm/html/src/partial.py
+++ b/test/farm/html/src/partial.py
@@ -16,4 +16,3 @@ if 0:
if 1:
a = 13
-
diff --git a/test/farm/html/src/tabbed.py b/test/farm/html/src/tabbed.py
index 4c39cafe..2035852f 100644
--- a/test/farm/html/src/tabbed.py
+++ b/test/farm/html/src/tabbed.py
@@ -5,4 +5,3 @@ if x:
if x: # look nice
b = "No spaces" # when they
c = "Done" # line up.
-
diff --git a/test/farm/run/src/chdir.py b/test/farm/run/src/chdir.py
index d8287ed7..6d834924 100644
--- a/test/farm/run/src/chdir.py
+++ b/test/farm/run/src/chdir.py
@@ -2,4 +2,3 @@ import os
print("Line One")
os.chdir("subdir")
print("Line Two")
-
diff --git a/test/test_api.py b/test/test_api.py
index 8f270098..e2ebc656 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -84,7 +84,7 @@ class SingletonApiTest(CoverageTest):
self.do_report_work("mycode4")
coverage.report()
rpt = re.sub(r"\s+", " ", self.stdout())
- self.assertTrue("mycode4 7 3 57% 4-6" in rpt)
+ self.assertIn("mycode4 7 3 57% 4-6", rpt)
class ApiTest(CoverageTest):
@@ -360,130 +360,125 @@ class UsingModulesMixin(object):
super(UsingModulesMixin, self).tearDown()
-class SourceOmitIncludeTest(UsingModulesMixin, CoverageTest):
- """Test using `source`, `omit` and `include` when measuring code."""
-
- def coverage_usepkgs_summary(self, **kwargs):
- """Run coverage on usepkgs and return the line summary.
+class OmitIncludeTestsMixin(UsingModulesMixin):
+ """Test methods for coverage methods taking include and omit."""
- Arguments are passed to the `coverage.coverage` constructor.
-
- """
- cov = coverage.coverage(**kwargs)
- cov.start()
- import usepkgs # pylint: disable=F0401,W0612
- cov.stop()
- return cov.data.summary()
-
- def filenames_in_summary(self, summary, filenames):
+ def filenames_in(self, summary, filenames):
"""Assert the `filenames` are in the keys of `summary`."""
for filename in filenames.split():
- self.assert_(filename in summary,
- "%s should be in %r" % (filename, summary)
- )
+ self.assertIn(filename, summary)
- def filenames_not_in_summary(self, summary, filenames):
+ def filenames_not_in(self, summary, filenames):
"""Assert the `filenames` are not in the keys of `summary`."""
for filename in filenames.split():
- self.assert_(filename not in summary,
- "%s should not be in %r" % (filename, summary)
- )
+ self.assertNotIn(filename, summary)
def test_nothing_specified(self):
- lines = self.coverage_usepkgs_summary()
- self.filenames_in_summary(lines,
- "p1a.py p1b.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
- self.filenames_not_in_summary(lines,
- "p1c.py"
- )
+ result = self.coverage_usepkgs()
+ self.filenames_in(result, "p1a p1b p2a p2b othera otherb osa osb")
+ self.filenames_not_in(result, "p1c")
# Because there was no source= specified, we don't search for
# unexecuted files.
- def test_source_package(self):
- lines = self.coverage_usepkgs_summary(source=["pkg1"])
- self.filenames_in_summary(lines,
- "p1a.py p1b.py"
- )
- self.filenames_not_in_summary(lines,
- "p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
- # Because source= was specified, we do search for unexecuted files.
- self.assertEqual(lines['p1c.py'], 0)
-
- def test_source_package_dotted(self):
- lines = self.coverage_usepkgs_summary(source=["pkg1.p1b"])
- self.filenames_in_summary(lines,
- "p1b.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
-
def test_include(self):
- lines = self.coverage_usepkgs_summary(include=["*/p1a.py"])
- self.filenames_in_summary(lines,
- "p1a.py"
- )
- self.filenames_not_in_summary(lines,
- "p1b.py p1c.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
+ result = self.coverage_usepkgs(include=["*/p1a.py"])
+ self.filenames_in(result, "p1a")
+ self.filenames_not_in(result, "p1b p1c p2a p2b othera otherb osa osb")
def test_include_2(self):
- lines = self.coverage_usepkgs_summary(include=["*a.py"])
- self.filenames_in_summary(lines,
- "p1a.py p2a.py othera.py osa.py"
- )
- self.filenames_not_in_summary(lines,
- "p1b.py p1c.py p2b.py otherb.py osb.py"
- )
+ result = self.coverage_usepkgs(include=["*a.py"])
+ self.filenames_in(result, "p1a p2a othera osa")
+ self.filenames_not_in(result, "p1b p1c p2b otherb osb")
def test_include_as_string(self):
- lines = self.coverage_usepkgs_summary(include="*a.py")
- self.filenames_in_summary(lines,
- "p1a.py p2a.py othera.py osa.py"
- )
- self.filenames_not_in_summary(lines,
- "p1b.py p1c.py p2b.py otherb.py osb.py"
- )
+ result = self.coverage_usepkgs(include="*a.py")
+ self.filenames_in(result, "p1a p2a othera osa")
+ self.filenames_not_in(result, "p1b p1c p2b otherb osb")
def test_omit(self):
- lines = self.coverage_usepkgs_summary(omit=["*/p1a.py"])
- self.filenames_in_summary(lines,
- "p1b.py p2a.py p2b.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py"
- )
+ result = self.coverage_usepkgs(omit=["*/p1a.py"])
+ self.filenames_in(result, "p1b p2a p2b")
+ self.filenames_not_in(result, "p1a p1c")
def test_omit_2(self):
- lines = self.coverage_usepkgs_summary(omit=["*a.py"])
- self.filenames_in_summary(lines,
- "p1b.py p2b.py otherb.py osb.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py othera.py osa.py"
- )
+ result = self.coverage_usepkgs(omit=["*a.py"])
+ self.filenames_in(result, "p1b p2b otherb osb")
+ self.filenames_not_in(result, "p1a p1c p2a othera osa")
def test_omit_as_string(self):
- lines = self.coverage_usepkgs_summary(omit="*a.py")
- self.filenames_in_summary(lines,
- "p1b.py p2b.py otherb.py osb.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py othera.py osa.py"
- )
+ result = self.coverage_usepkgs(omit="*a.py")
+ self.filenames_in(result, "p1b p2b otherb osb")
+ self.filenames_not_in(result, "p1a p1c p2a othera osa")
def test_omit_and_include(self):
- lines = self.coverage_usepkgs_summary(
- include=["*/p1*"], omit=["*/p1a.py"]
- )
- self.filenames_in_summary(lines,
- "p1b.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py p2b.py"
- )
+ result = self.coverage_usepkgs( include=["*/p1*"], omit=["*/p1a.py"])
+ self.filenames_in(result, "p1b")
+ self.filenames_not_in(result, "p1a p1c p2a p2b")
+
+
+class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest):
+ """Test using `source`, `omit` and `include` when measuring code."""
+
+ def coverage_usepkgs(self, **kwargs):
+ """Run coverage on usepkgs and return the line summary.
+
+ Arguments are passed to the `coverage.coverage` constructor.
+
+ """
+ cov = coverage.coverage(**kwargs)
+ cov.start()
+ import usepkgs # pylint: disable=F0401,W0612
+ cov.stop()
+ cov._harvest_data() # private! sshhh...
+ summary = cov.data.summary()
+ for k, v in list(summary.items()):
+ assert k.endswith(".py")
+ summary[k[:-3]] = v
+ return summary
+
+ def test_source_package(self):
+ lines = self.coverage_usepkgs(source=["pkg1"])
+ self.filenames_in(lines, "p1a p1b")
+ self.filenames_not_in(lines, "p2a p2b othera otherb osa osb")
+ # Because source= was specified, we do search for unexecuted files.
+ self.assertEqual(lines['p1c'], 0)
+
+ def test_source_package_dotted(self):
+ lines = self.coverage_usepkgs(source=["pkg1.p1b"])
+ self.filenames_in(lines, "p1b")
+ self.filenames_not_in(lines, "p1a p1c p2a p2b othera otherb osa osb")
+
+
+class ReportIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest):
+ """Tests of the report include/omit functionality."""
+
+ def coverage_usepkgs(self, **kwargs):
+ """Try coverage.report()."""
+ cov = coverage.coverage()
+ cov.start()
+ import usepkgs # pylint: disable=F0401,W0612
+ cov.stop()
+ report = StringIO()
+ cov.report(file=report, **kwargs)
+ return report.getvalue()
+
+
+class XmlIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest):
+ """Tests of the xml include/omit functionality.
+
+ This also takes care of the HTML and annotate include/omit, by virtue
+ of the structure of the code.
+
+ """
+
+ def coverage_usepkgs(self, **kwargs):
+ """Try coverage.xml_report()."""
+ cov = coverage.coverage()
+ cov.start()
+ import usepkgs # pylint: disable=F0401,W0612
+ cov.stop()
+ cov.xml_report(outfile="-", **kwargs)
+ return self.stdout()
class AnalysisTest(CoverageTest):
diff --git a/test/test_arcs.py b/test/test_arcs.py
index ce550042..a9f7470b 100644
--- a/test/test_arcs.py
+++ b/test/test_arcs.py
@@ -450,18 +450,19 @@ class ExceptionArcTest(CoverageTest):
arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB AD BC CD D.",
arcz_missing="3D AB BC CD", arcz_unpredicted="")
- def xxx_xest_finally_in_loop_2(self):
- self.check_coverage("""\
- for i in range(5):
- try:
- j = 3
- finally:
- f = 5
- g = 6
- h = 7
- """,
- arcz=".1 12 23 35 56 61 17 7.",
- arcz_missing="", arcz_unpredicted="")
+ if 0:
+ def test_finally_in_loop_2(self):
+ self.check_coverage("""\
+ for i in range(5):
+ try:
+ j = 3
+ finally:
+ f = 5
+ g = 6
+ h = 7
+ """,
+ arcz=".1 12 23 35 56 61 17 7.",
+ arcz_missing="", arcz_unpredicted="")
if sys.version_info >= (2, 5):
# Try-except-finally was new in 2.5
diff --git a/test/test_cmdline.py b/test/test_cmdline.py
index d4cc763d..eb7fe0f5 100644
--- a/test/test_cmdline.py
+++ b/test/test_cmdline.py
@@ -251,35 +251,35 @@ class ClassicCmdLineTest(CmdLineTest):
def test_html_report(self):
# coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("-b", self.INIT_LOAD + """\
- .html_report(directory=None, ignore_errors=None,
+ .html_report(directory=None, ignore_errors=None, title=None,
omit=None, include=None, morfs=[])
""")
self.cmd_executes("-b -d dir1", self.INIT_LOAD + """\
- .html_report(directory="dir1", ignore_errors=None,
+ .html_report(directory="dir1", ignore_errors=None, title=None,
omit=None, include=None, morfs=[])
""")
self.cmd_executes("-b -i", self.INIT_LOAD + """\
- .html_report(directory=None, ignore_errors=True,
+ .html_report(directory=None, ignore_errors=True, title=None,
omit=None, include=None, morfs=[])
""")
self.cmd_executes("-b -o fooey", """\
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"])
.load()
- .html_report(directory=None, ignore_errors=None,
+ .html_report(directory=None, ignore_errors=None, title=None,
omit=["fooey"], include=None, morfs=[])
""")
self.cmd_executes("-b -o fooey,booey", """\
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"])
.load()
- .html_report(directory=None, ignore_errors=None,
+ .html_report(directory=None, ignore_errors=None, title=None,
omit=["fooey", "booey"], include=None, morfs=[])
""")
self.cmd_executes("-b mod1", self.INIT_LOAD + """\
- .html_report(directory=None, ignore_errors=None,
+ .html_report(directory=None, ignore_errors=None, title=None,
omit=None, include=None, morfs=["mod1"])
""")
self.cmd_executes("-b mod1 mod2 mod3", self.INIT_LOAD + """\
- .html_report(directory=None, ignore_errors=None,
+ .html_report(directory=None, ignore_errors=None, title=None,
omit=None, include=None, morfs=["mod1", "mod2", "mod3"])
""")
@@ -447,6 +447,14 @@ class NewCmdLineTest(CmdLineTest):
self.cmd_executes_same("html --omit f,b", "-b --omit f,b")
self.cmd_executes_same("html m1", "-b m1")
self.cmd_executes_same("html m1 m2 m3", "-b m1 m2 m3")
+ self.cmd_executes("html", self.INIT_LOAD + """\
+ .html_report(ignore_errors=None, omit=None, include=None, morfs=[],
+ directory=None, title=None)
+ """)
+ self.cmd_executes("html --title=Hello_there", self.INIT_LOAD + """\
+ .html_report(ignore_errors=None, omit=None, include=None, morfs=[],
+ directory=None, title='Hello_there')
+ """)
def test_report(self):
self.cmd_executes_same("report", "-r")
@@ -553,11 +561,11 @@ class NewCmdLineTest(CmdLineTest):
# coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("xml", self.INIT_LOAD + """\
.xml_report(ignore_errors=None, omit=None, include=None, morfs=[],
- outfile="coverage.xml")
+ outfile=None)
""")
self.cmd_executes("xml -i", self.INIT_LOAD + """\
.xml_report(ignore_errors=True, omit=None, include=None, morfs=[],
- outfile="coverage.xml")
+ outfile=None)
""")
self.cmd_executes("xml -o myxml.foo", self.INIT_LOAD + """\
.xml_report(ignore_errors=None, omit=None, include=None, morfs=[],
@@ -571,21 +579,21 @@ class NewCmdLineTest(CmdLineTest):
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"])
.load()
.xml_report(ignore_errors=None, omit=["fooey"], include=None, morfs=[],
- outfile="coverage.xml")
+ outfile=None)
""")
self.cmd_executes("xml --omit fooey,booey", """\
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"])
.load()
.xml_report(ignore_errors=None, omit=["fooey", "booey"], include=None,
- morfs=[], outfile="coverage.xml")
+ morfs=[], outfile=None)
""")
self.cmd_executes("xml mod1", self.INIT_LOAD + """\
.xml_report(ignore_errors=None, omit=None, include=None, morfs=["mod1"],
- outfile="coverage.xml")
+ outfile=None)
""")
self.cmd_executes("xml mod1 mod2 mod3", self.INIT_LOAD + """\
.xml_report(ignore_errors=None, omit=None, include=None,
- morfs=["mod1", "mod2", "mod3"], outfile="coverage.xml")
+ morfs=["mod1", "mod2", "mod3"], outfile=None)
""")
def test_no_arguments_at_all(self):
@@ -604,6 +612,12 @@ class CmdLineStdoutTest(CmdLineTest):
assert "Code coverage for Python." in out
assert out.count("\n") < 4
+ def test_version(self):
+ self.command_line("--version")
+ out = self.stdout()
+ assert "ersion " in out
+ assert out.count("\n") < 4
+
def test_help(self):
self.command_line("help")
out = self.stdout()
diff --git a/test/test_config.py b/test/test_config.py
index bf42bf76..19e37ab9 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
"""Test the config file handling for coverage.py"""
import os, sys
@@ -139,6 +140,7 @@ class ConfigFileTest(CoverageTest):
directory = c:\\tricky\\dir.somewhere
extra_css=something/extra.css
+ title = Title & nums # nums!
[xml]
output=mycov.xml
@@ -159,7 +161,7 @@ class ConfigFileTest(CoverageTest):
self.assertTrue(cov.config.parallel)
self.assertEqual(cov.get_exclude_list(),
- ["if 0:", "pragma:?\s+no cover", "another_tab"]
+ ["if 0:", r"pragma:?\s+no cover", "another_tab"]
)
self.assertTrue(cov.config.ignore_errors)
self.assertEqual(cov.config.include, ["a/", "b/"])
@@ -169,7 +171,7 @@ class ConfigFileTest(CoverageTest):
self.assertEqual(cov.config.precision, 3)
self.assertEqual(cov.config.partial_list,
- ["pragma:?\s+no branch"]
+ [r"pragma:?\s+no branch"]
)
self.assertEqual(cov.config.partial_always_list,
["if 0:", "while True:"]
@@ -177,6 +179,7 @@ class ConfigFileTest(CoverageTest):
self.assertTrue(cov.config.show_missing)
self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere")
self.assertEqual(cov.config.extra_css, "something/extra.css")
+ self.assertEqual(cov.config.html_title, "Title & nums # nums!")
self.assertEqual(cov.config.xml_output, "mycov.xml")
@@ -185,3 +188,15 @@ class ConfigFileTest(CoverageTest):
'other': ['other', '/home/ned/other', 'c:\\Ned\\etc']
})
+ if sys.version_info[:2] != (3,1):
+ def test_one(self):
+ # This sample file tries to use lots of variation of syntax...
+ self.make_file(".coveragerc", """\
+ [html]
+ title = tabblo & «ταБЬℓσ» # numbers
+ """)
+ cov = coverage.coverage()
+
+ self.assertEqual(cov.config.html_title,
+ "tabblo & «ταБЬℓσ» # numbers"
+ )
diff --git a/test/test_files.py b/test/test_files.py
index f2f3581e..5692699c 100644
--- a/test/test_files.py
+++ b/test/test_files.py
@@ -3,7 +3,7 @@
import os, sys
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
-from coverage.files import PathAliases, find_python_files
+from coverage.files import PathAliases, find_python_files, abs_file
from coverage.backward import set # pylint: disable=W0622
from coverage.misc import CoverageException
@@ -38,15 +38,15 @@ class FileLocatorTest(CoverageTest):
self.assertEqual(fl.relative_filename(a2), a2)
def test_filepath_contains_absolute_prefix_twice(self):
- # https://bitbucket.org/ned/coveragepy/issue/194/filelocatorrelative_filename-could-mangle
+ # https://bitbucket.org/ned/coveragepy/issue/194
# Build a path that has two pieces matching the absolute path prefix.
# Technically, this test doesn't do that on Windows, but drive
# letters make that impractical to acheive.
fl = FileLocator()
- d = fl.abs_file(os.curdir)
+ d = abs_file(os.curdir)
trick = os.path.splitdrive(d)[1].lstrip(os.path.sep)
rel = os.path.join('sub', trick, 'file1.py')
- self.assertEqual(fl.relative_filename(fl.abs_file(rel)), rel)
+ self.assertEqual(fl.relative_filename(abs_file(rel)), rel)
class MatcherTest(CoverageTest):
@@ -168,4 +168,3 @@ class FindPythonFilesTest(CoverageTest):
"sub/a.py", "sub/b.py",
"sub/ssub/__init__.py", "sub/ssub/s.py",
])
-
diff --git a/test/test_html.py b/test/test_html.py
index 1877a30d..5e5f7ce3 100644
--- a/test/test_html.py
+++ b/test/test_html.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
"""Tests that HTML generation is awesome."""
import os.path, sys
@@ -17,6 +18,8 @@ class HtmlTest(CoverageTest):
# so grab it here to restore it later.
self.real_coverage_version = coverage.__version__
+ self.maxDiff = None
+
def tearDown(self):
coverage.__version__ = self.real_coverage_version
super(HtmlTest, self).tearDown()
@@ -38,14 +41,14 @@ class HtmlTest(CoverageTest):
print("x is %d" % x)
""")
- def run_coverage(self, **kwargs):
+ def run_coverage(self, covargs=None, htmlargs=None):
"""Run coverage on main_file.py, and create an HTML report."""
self.clean_local_file_imports()
- cov = coverage.coverage(**kwargs)
+ cov = coverage.coverage(**(covargs or {}))
cov.start()
self.import_local_file("main_file")
cov.stop()
- cov.html_report()
+ cov.html_report(**(htmlargs or {}))
def remove_html_files(self):
"""Remove the HTML files created as part of the HTML report."""
@@ -118,11 +121,11 @@ class HtmlTest(CoverageTest):
# In this case, everything changes because the coverage settings have
# changed.
self.create_initial_files()
- self.run_coverage(timid=False)
+ self.run_coverage(covargs=dict(timid=False))
index1 = open("htmlcov/index.html").read()
self.remove_html_files()
- self.run_coverage(timid=True)
+ self.run_coverage(covargs=dict(timid=True))
# All the files have been reported again.
self.assert_exists("htmlcov/index.html")
@@ -155,6 +158,52 @@ class HtmlTest(CoverageTest):
fixed_index2 = index2.replace("XYZZY", self.real_coverage_version)
self.assertMultiLineEqual(index1, fixed_index2)
+ def test_default_title(self):
+ self.create_initial_files()
+ self.run_coverage()
+ index = open("htmlcov/index.html").read()
+ self.assertIn("<title>Coverage report</title>", index)
+ self.assertIn("<h1>Coverage report:", index)
+
+ def test_title_set_in_config_file(self):
+ self.create_initial_files()
+ self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n")
+ self.run_coverage()
+ index = open("htmlcov/index.html").read()
+ self.assertIn("<title>Metrics &amp; stuff!</title>", index)
+ self.assertIn("<h1>Metrics &amp; stuff!:", index)
+
+ if sys.version_info[:2] != (3,1):
+ def test_non_ascii_title_set_in_config_file(self):
+ self.create_initial_files()
+ self.make_file(".coveragerc",
+ "[html]\ntitle = «ταБЬℓσ» numbers"
+ )
+ self.run_coverage()
+ index = open("htmlcov/index.html").read()
+ self.assertIn(
+ "<title>&#171;&#964;&#945;&#1041;&#1068;&#8467;&#963;&#187;"
+ " numbers", index
+ )
+ self.assertIn(
+ "<h1>&#171;&#964;&#945;&#1041;&#1068;&#8467;&#963;&#187;"
+ " numbers", index
+ )
+
+ def test_title_set_in_args(self):
+ self.create_initial_files()
+ self.make_file(".coveragerc", "[html]\ntitle = Good title\n")
+ self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!"))
+ index = open("htmlcov/index.html").read()
+ self.assertIn(
+ "<title>&#171;&#964;&#945;&#1041;&#1068;&#8467;&#963;&#187;"
+ " &amp; st&#252;ff!</title>", index
+ )
+ self.assertIn(
+ "<h1>&#171;&#964;&#945;&#1041;&#1068;&#8467;&#963;&#187;"
+ " &amp; st&#252;ff!:", index
+ )
+
class HtmlWithUnparsableFilesTest(CoverageTest):
"""Test the behavior when measuring unparsable files."""
diff --git a/test/test_misc.py b/test/test_misc.py
index 72f5caac..ac53cddb 100644
--- a/test/test_misc.py
+++ b/test/test_misc.py
@@ -2,7 +2,8 @@
import os, sys
-from coverage.misc import Hasher
+from coverage.misc import Hasher, file_be_gone
+from coverage import __version__, __url__
sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
from coveragetest import CoverageTest
@@ -26,3 +27,50 @@ class HasherTest(CoverageTest):
h2.update({'b': 23, 'a': 17})
self.assertEqual(h1.digest(), h2.digest())
+
+class RemoveFileTest(CoverageTest):
+ """Tests of misc.file_be_gone."""
+
+ def test_remove_nonexistent_file(self):
+ # it's ok to try to remove a file that doesn't exist.
+ file_be_gone("not_here.txt")
+
+ def test_remove_actual_file(self):
+ # it really does remove a file that does exist.
+ self.make_file("here.txt", "We are here, we are here, we are here!")
+ file_be_gone("here.txt")
+ self.assert_doesnt_exist("here.txt")
+
+ def test_actual_errors(self):
+ # Errors can still happen.
+ # ". is a directory" on Unix, or "Access denied" on Windows
+ self.assertRaises(OSError, file_be_gone, ".")
+
+
+class SetupPyTest(CoverageTest):
+ """Tests of setup.py"""
+
+ run_in_temp_dir = False
+
+ def test_metadata(self):
+ status, output = self.run_command_status(
+ "python setup.py --description --version --url --author"
+ )
+ self.assertEqual(status, 0)
+ out = output.splitlines()
+ self.assertIn("measurement", out[0])
+ self.assertEqual(out[1], __version__)
+ self.assertEqual(out[2], __url__)
+ self.assertIn("Ned Batchelder", out[3])
+
+ def test_more_metadata(self):
+ from setup import setup_args
+
+ classifiers = setup_args['classifiers']
+ self.assertGreater(len(classifiers), 7)
+ self.assertTrue(classifiers[-1].startswith("Development Status ::"))
+
+ long_description = setup_args['long_description'].splitlines()
+ self.assertGreater(len(long_description), 7)
+ self.assertNotEqual(long_description[0].strip(), "")
+ self.assertNotEqual(long_description[-1].strip(), "")
diff --git a/test/test_oddball.py b/test/test_oddball.py
index 1a3bd22f..a8c243de 100644
--- a/test/test_oddball.py
+++ b/test/test_oddball.py
@@ -307,6 +307,7 @@ class ExceptionTest(CoverageTest):
# Clean the line data and compare to expected results.
# The filenames are absolute, so keep just the base.
+ cov._harvest_data() # private! sshhh...
lines = cov.data.line_data()
clean_lines = {}
for f, llist in lines.items():
diff --git a/test/test_phystokens.py b/test/test_phystokens.py
index 0e778510..d4e417e8 100644
--- a/test/test_phystokens.py
+++ b/test/test_phystokens.py
@@ -37,8 +37,8 @@ class PhysTokensTest(CoverageTest):
# source_token_lines doesn't preserve trailing spaces, so trim all that
# before comparing.
source = source.replace('\r\n', '\n')
- source = re.sub("(?m)[ \t]+$", "", source)
- tokenized = re.sub("(?m)[ \t]+$", "", tokenized)
+ source = re.sub(r"(?m)[ \t]+$", "", source)
+ tokenized = re.sub(r"(?m)[ \t]+$", "", tokenized)
self.assertMultiLineEqual(source, tokenized)
def check_file_tokenization(self, fname):
diff --git a/test/test_process.py b/test/test_process.py
index f8d1b8d1..4df8860f 100644
--- a/test/test_process.py
+++ b/test/test_process.py
@@ -175,8 +175,8 @@ class ProcessTest(CoverageTest):
data.read_file(".coverage")
summary = data.summary(fullpath=True)
self.assertEqual(len(summary), 1)
- actual = os.path.abspath(list(summary.keys())[0])
- expected = os.path.abspath('src/x.py')
+ actual = os.path.normcase(os.path.abspath(list(summary.keys())[0]))
+ expected = os.path.normcase(os.path.abspath('src/x.py'))
self.assertEqual(actual, expected)
self.assertEqual(list(summary.values())[0], 6)
@@ -190,7 +190,7 @@ class ProcessTest(CoverageTest):
os.remove("fleeting.py")
out = self.run_command("coverage html -d htmlcov")
self.assertRegexpMatches(out, "No source for code: '.*fleeting.py'")
- self.assertFalse("Traceback" in out)
+ self.assertNotIn("Traceback", out)
# It happens that the code paths are different for *.py and other
# files, so try again with no extension.
@@ -202,13 +202,13 @@ class ProcessTest(CoverageTest):
os.remove("fleeting")
status, out = self.run_command_status("coverage html -d htmlcov", 1)
self.assertRegexpMatches(out, "No source for code: '.*fleeting'")
- self.assertFalse("Traceback" in out)
+ self.assertNotIn("Traceback", out)
self.assertEqual(status, 1)
def test_running_missing_file(self):
status, out = self.run_command_status("coverage run xyzzy.py", 1)
self.assertRegexpMatches(out, "No file to run: .*xyzzy.py")
- self.assertFalse("Traceback" in out)
+ self.assertNotIn("Traceback", out)
self.assertEqual(status, 1)
def test_code_throws(self):
@@ -233,9 +233,9 @@ class ProcessTest(CoverageTest):
self.assertMultiLineEqual(out, out2)
# But also make sure that the output is what we expect.
- self.assertTrue('File "throw.py", line 5, in f2' in out)
- self.assertTrue('raise Exception("hey!")' in out)
- self.assertFalse('coverage' in out)
+ self.assertIn('File "throw.py", line 5, in f2', out)
+ self.assertIn('raise Exception("hey!")', out)
+ self.assertNotIn('coverage', out)
self.assertEqual(status, 1)
def test_code_exits(self):
@@ -290,6 +290,15 @@ class ProcessTest(CoverageTest):
out2 = self.run_command("python -m test.try_execfile")
self.assertMultiLineEqual(out, out2)
+ if 0:
+ # For https://bitbucket.org/ned/coveragepy/issue/207
+ def test_coverage_run_dashm_is_like_python_dashm_with__main__(self):
+ self.make_file("package/__init__.py") # empty
+ self.make_file("package/__main__.py", "#\n") # empty
+ out = self.run_command("coverage run -m package")
+ out2 = self.run_command("python -m package")
+ self.assertMultiLineEqual(out, out2)
+
if hasattr(os, 'fork'):
def test_fork(self):
self.make_file("fork.py", """\
@@ -337,25 +346,27 @@ class ProcessTest(CoverageTest):
""")
out = self.run_command("coverage run --source=sys,xyzzy,quux hello.py")
- self.assertTrue("Hello\n" in out)
- self.assertTrue(textwrap.dedent("""\
+ self.assertIn("Hello\n", out)
+ self.assertIn(textwrap.dedent("""\
Coverage.py warning: Module sys has no Python source.
Coverage.py warning: Module xyzzy was never imported.
Coverage.py warning: Module quux was never imported.
Coverage.py warning: No data was collected.
- """) in out)
+ """), out)
def test_warnings_if_never_run(self):
out = self.run_command("coverage run i_dont_exist.py")
- self.assertTrue("No file to run: 'i_dont_exist.py'" in out)
- self.assertTrue("warning" not in out)
+ self.assertIn("No file to run: 'i_dont_exist.py'", out)
+ self.assertNotIn("warning", out)
+ self.assertNotIn("Exception", out)
out = self.run_command("coverage run -m no_such_module")
self.assertTrue(
("No module named no_such_module" in out) or
("No module named 'no_such_module'" in out)
)
- self.assertTrue("warning" not in out)
+ self.assertNotIn("warning", out)
+ self.assertNotIn("Exception", out)
if sys.version_info >= (3, 0): # This only works on 3.x for now.
# It only works with the C tracer.
@@ -382,3 +393,45 @@ class ProcessTest(CoverageTest):
# imported is 120 or so. Just running os.getenv executes
# about 5.
self.assertGreater(data.summary()['os.py'], 50)
+
+ def test_version_aliases(self):
+ cmd = "coverage%d" % sys.version_info[0]
+ out = self.run_command(cmd)
+ self.assertIn("Code coverage for Python", out)
+ badcmd = "coverage%d" % (5 - sys.version_info[0])
+ out = self.run_command(badcmd)
+ self.assertNotIn("Code coverage for Python", out)
+
+
+class FailUnderTest(CoverageTest):
+ """Tests of the --fail-under switch."""
+
+ def setUp(self):
+ super(FailUnderTest, self).setUp()
+ self.make_file("fifty.py", """\
+ # I have 50% coverage!
+ a = 1
+ if a > 2:
+ b = 3
+ c = 4
+ """)
+ st, _ = self.run_command_status("coverage run fifty.py", 0)
+ self.assertEqual(st, 0)
+
+ def test_report(self):
+ st, _ = self.run_command_status("coverage report --fail-under=50", 0)
+ self.assertEqual(st, 0)
+ st, _ = self.run_command_status("coverage report --fail-under=51", 2)
+ self.assertEqual(st, 2)
+
+ def test_html_report(self):
+ st, _ = self.run_command_status("coverage html --fail-under=50", 0)
+ self.assertEqual(st, 0)
+ st, _ = self.run_command_status("coverage html --fail-under=51", 2)
+ self.assertEqual(st, 2)
+
+ def test_xml_report(self):
+ st, _ = self.run_command_status("coverage xml --fail-under=50", 0)
+ self.assertEqual(st, 0)
+ st, _ = self.run_command_status("coverage xml --fail-under=51", 2)
+ self.assertEqual(st, 2)
diff --git a/test/test_results.py b/test/test_results.py
index d6919fa2..3da92e4c 100644
--- a/test/test_results.py
+++ b/test/test_results.py
@@ -62,4 +62,3 @@ class NumbersTest(CoverageTest):
self.assertEqual(n9999.pc_covered_str, "0.1")
self.assertEqual(n10000.pc_covered_str, "0.0")
Numbers.set_precision(0)
-
diff --git a/test/test_summary.py b/test/test_summary.py
index 71fbb1a6..644aa9dd 100644
--- a/test/test_summary.py
+++ b/test/test_summary.py
@@ -25,7 +25,7 @@ class SummaryTest(CoverageTest):
def report_from_command(self, cmd):
"""Return the report from the `cmd`, with some convenience added."""
report = self.run_command(cmd).replace('\\', '/')
- self.assertFalse("error" in report.lower())
+ self.assertNotIn("error", report.lower())
return report
def line_count(self, report):
@@ -51,10 +51,10 @@ class SummaryTest(CoverageTest):
# ---------------------------------------------------------------------
# TOTAL 8 0 100%
- self.assertFalse("/coverage/__init__/" in report)
- self.assertTrue("/test/modules/covmod1 " in report)
- self.assertTrue("/test/zipmods.zip/covmodzip1 " in report)
- self.assertTrue("mycode " in report)
+ self.assertNotIn("/coverage/__init__/", report)
+ self.assertIn("/test/modules/covmod1 ", report)
+ self.assertIn("/test/zipmods.zip/covmodzip1 ", report)
+ self.assertIn("mycode ", report)
self.assertEqual(self.last_line_squeezed(report), "TOTAL 8 0 100%")
def test_report_just_one(self):
@@ -67,10 +67,10 @@ class SummaryTest(CoverageTest):
# mycode 4 0 100%
self.assertEqual(self.line_count(report), 3)
- self.assertFalse("/coverage/" in report)
- self.assertFalse("/test/modules/covmod1 " in report)
- self.assertFalse("/test/zipmods.zip/covmodzip1 " in report)
- self.assertTrue("mycode " in report)
+ self.assertNotIn("/coverage/", report)
+ self.assertNotIn("/test/modules/covmod1 ", report)
+ self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report)
+ self.assertIn("mycode ", report)
self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%")
def test_report_omitting(self):
@@ -84,10 +84,10 @@ class SummaryTest(CoverageTest):
# mycode 4 0 100%
self.assertEqual(self.line_count(report), 3)
- self.assertFalse("/coverage/" in report)
- self.assertFalse("/test/modules/covmod1 " in report)
- self.assertFalse("/test/zipmods.zip/covmodzip1 " in report)
- self.assertTrue("mycode " in report)
+ self.assertNotIn("/coverage/", report)
+ self.assertNotIn("/test/modules/covmod1 ", report)
+ self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report)
+ self.assertIn("mycode ", report)
self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%")
def test_report_including(self):
@@ -100,10 +100,10 @@ class SummaryTest(CoverageTest):
# mycode 4 0 100%
self.assertEqual(self.line_count(report), 3)
- self.assertFalse("/coverage/" in report)
- self.assertFalse("/test/modules/covmod1 " in report)
- self.assertFalse("/test/zipmods.zip/covmodzip1 " in report)
- self.assertTrue("mycode " in report)
+ self.assertNotIn("/coverage/", report)
+ self.assertNotIn("/test/modules/covmod1 ", report)
+ self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report)
+ self.assertIn("mycode ", report)
self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%")
def test_report_branches(self):
@@ -123,7 +123,7 @@ class SummaryTest(CoverageTest):
# mybranch 5 0 2 1 85%
self.assertEqual(self.line_count(report), 3)
- self.assertTrue("mybranch " in report)
+ self.assertIn("mybranch ", report)
self.assertEqual(self.last_line_squeezed(report),
"mybranch 5 0 2 1 86%")
@@ -135,15 +135,16 @@ class SummaryTest(CoverageTest):
self.make_file("mycode.py", "This isn't python at all!")
report = self.report_from_command("coverage -r mycode.py")
+ # pylint: disable=C0301
# Name Stmts Miss Cover
# ----------------------------
# mycode NotPython: Couldn't parse '/tmp/test_cover/63354509363/mycode.py' as Python source: 'invalid syntax' at line 1
last = self.last_line_squeezed(report)
# The actual file name varies run to run.
- last = re.sub("parse '.*mycode.py", "parse 'mycode.py", last)
+ last = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", last)
# The actual error message varies version to version
- last = re.sub(": '.*' at", ": 'error' at", last)
+ last = re.sub(r": '.*' at", ": 'error' at", last)
self.assertEqual(last,
"mycode NotPython: "
"Couldn't parse 'mycode.py' as Python source: "
@@ -178,6 +179,36 @@ class SummaryTest(CoverageTest):
self.assertEqual(self.line_count(report), 2)
+ def run_TheCode_and_report_it(self):
+ """A helper for the next few tests."""
+ cov = coverage.coverage()
+ cov.start()
+ import TheCode # pylint: disable=F0401,W0612
+ cov.stop()
+
+ repout = StringIO()
+ cov.report(file=repout, show_missing=False)
+ report = repout.getvalue().replace('\\', '/')
+ report = re.sub(r"\s+", " ", report)
+ return report
+
+ def test_bug_203_mixed_case_listed_twice_with_rc(self):
+ self.make_file("TheCode.py", "a = 1\n")
+ self.make_file(".coveragerc", "[run]\nsource = .\n")
+
+ report = self.run_TheCode_and_report_it()
+
+ self.assertIn("TheCode", report)
+ self.assertNotIn("thecode", report)
+
+ def test_bug_203_mixed_case_listed_twice(self):
+ self.make_file("TheCode.py", "a = 1\n")
+
+ report = self.run_TheCode_and_report_it()
+
+ self.assertIn("TheCode", report)
+ self.assertNotIn("thecode", report)
+
class SummaryTest2(CoverageTest):
"""Another bunch of summary tests."""
@@ -207,5 +238,42 @@ class SummaryTest2(CoverageTest):
report = repout.getvalue().replace('\\', '/')
report = re.sub(r"\s+", " ", report)
- self.assert_("test/modules/pkg1/__init__ 1 0 100%" in report)
- self.assert_("test/modules/pkg2/__init__ 0 0 100%" in report)
+ self.assertIn("test/modules/pkg1/__init__ 1 0 100%", report)
+ self.assertIn("test/modules/pkg2/__init__ 0 0 100%", report)
+
+
+class ReportingReturnValue(CoverageTest):
+ """Tests of reporting functions returning values."""
+
+ def run_coverage(self):
+ """Run coverage on doit.py and return the coverage object."""
+ self.make_file("doit.py", """\
+ a = 1
+ b = 2
+ c = 3
+ d = 4
+ if a > 10:
+ f = 6
+ g = 7
+ """)
+
+ cov = coverage.coverage()
+ cov.start()
+ self.import_local_file("doit")
+ cov.stop()
+ return cov
+
+ def test_report(self):
+ cov = self.run_coverage()
+ val = cov.report(include="*/doit.py")
+ self.assertAlmostEqual(val, 85.7, 1)
+
+ def test_html(self):
+ cov = self.run_coverage()
+ val = cov.html_report(include="*/doit.py")
+ self.assertAlmostEqual(val, 85.7, 1)
+
+ def test_xml(self):
+ cov = self.run_coverage()
+ val = cov.xml_report(include="*/doit.py")
+ self.assertAlmostEqual(val, 85.7, 1)
diff --git a/test/test_testing.py b/test/test_testing.py
index bcf7d281..9943b65c 100644
--- a/test/test_testing.py
+++ b/test/test_testing.py
@@ -1,7 +1,9 @@
+# -*- coding: utf-8 -*-
"""Tests that our test infrastructure is really working!"""
import os, sys
sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
+from coverage.backward import to_bytes
from backunittest import TestCase
from coveragetest import CoverageTest
@@ -92,6 +94,34 @@ class TestingTest(TestCase):
self.assertFalse(False)
self.assertRaises(AssertionError, self.assertFalse, True)
+ def test_assert_in(self):
+ self.assertIn("abc", "hello abc")
+ self.assertIn("abc", ["xyz", "abc", "foo"])
+ self.assertIn("abc", {'abc': 1, 'xyz': 2})
+ self.assertRaises(AssertionError, self.assertIn, "abc", "xyz")
+ self.assertRaises(AssertionError, self.assertIn, "abc", ["x", "xabc"])
+ self.assertRaises(AssertionError, self.assertIn, "abc", {'x':'abc'})
+
+ def test_assert_not_in(self):
+ self.assertRaises(AssertionError, self.assertNotIn, "abc", "hello abc")
+ self.assertRaises(AssertionError,
+ self.assertNotIn, "abc", ["xyz", "abc", "foo"]
+ )
+ self.assertRaises(AssertionError,
+ self.assertNotIn, "abc", {'abc': 1, 'xyz': 2}
+ )
+ self.assertNotIn("abc", "xyz")
+ self.assertNotIn("abc", ["x", "xabc"])
+ self.assertNotIn("abc", {'x':'abc'})
+
+ def test_assert_greater(self):
+ self.assertGreater(10, 9)
+ self.assertGreater("xyz", "abc")
+ self.assertRaises(AssertionError, self.assertGreater, 9, 10)
+ self.assertRaises(AssertionError, self.assertGreater, 10, 10)
+ self.assertRaises(AssertionError, self.assertGreater, "abc", "xyz")
+ self.assertRaises(AssertionError, self.assertGreater, "xyz", "xyz")
+
class CoverageTestTest(CoverageTest):
"""Test the methods in `CoverageTest`."""
@@ -122,11 +152,18 @@ class CoverageTestTest(CoverageTest):
self.make_file("mac.txt", "Hello\n", newline="\r")
self.assertEqual(self.file_text("mac.txt"), "Hello\r")
+ def test_make_file_non_ascii(self):
+ self.make_file("unicode.txt", "tabblo: «ταБЬℓσ»")
+ self.assertEqual(
+ open("unicode.txt", "rb").read(),
+ to_bytes("tabblo: «ταБЬℓσ»")
+ )
+
def test_file_exists(self):
self.make_file("whoville.txt", "We are here!")
self.assert_exists("whoville.txt")
self.assert_doesnt_exist("shadow.txt")
- self.assertRaises(AssertionError, self.assert_doesnt_exist,
- "whoville.txt")
+ self.assertRaises(
+ AssertionError, self.assert_doesnt_exist, "whoville.txt"
+ )
self.assertRaises(AssertionError, self.assert_exists, "shadow.txt")
-
diff --git a/test/test_xml.py b/test/test_xml.py
new file mode 100644
index 00000000..c7205a59
--- /dev/null
+++ b/test/test_xml.py
@@ -0,0 +1,36 @@
+"""Tests for XML reports from coverage.py."""
+
+import os, sys
+
+sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
+from coveragetest import CoverageTest
+
+class XmlReportTest(CoverageTest):
+ """Tests of the XML reports from coverage.py."""
+
+ def run_mycode(self):
+ """Run mycode.py, so we can report on it."""
+ self.make_file("mycode.py", "print('hello')\n")
+ self.run_command("coverage run mycode.py")
+
+ def test_default_file_placement(self):
+ self.run_mycode()
+ self.run_command("coverage xml")
+ self.assert_exists("coverage.xml")
+
+ def test_argument_affects_xml_placement(self):
+ self.run_mycode()
+ self.run_command("coverage xml -o put_it_there.xml")
+ self.assert_doesnt_exist("coverage.xml")
+ self.assert_exists("put_it_there.xml")
+
+ def test_config_affects_xml_placement(self):
+ self.run_mycode()
+ self.make_file(".coveragerc", "[xml]\noutput = xml.out\n")
+ self.run_command("coverage xml")
+ self.assert_doesnt_exist("coverage.xml")
+ self.assert_exists("xml.out")
+
+ def test_no_data(self):
+ self.run_command("coverage xml")
+ self.assert_doesnt_exist("coverage.xml")
diff --git a/tox.ini b/tox.ini
index 6fbb2084..e20c8f77 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,8 +11,8 @@ setenv =
PYTHONPATH=test/eggsrc
commands =
- {envpython} setup.py clean
- {envpython} setup.py develop
+ {envpython} setup.py --quiet clean
+ {envpython} setup.py --quiet develop
# Create test/zipmods.zip
{envpython} igor.py zip_mods
@@ -24,7 +24,7 @@ commands =
{envpython} igor.py test_with_tracer py {posargs}
# Build the C extension and test with the CTracer
- {envpython} setup.py build_ext --inplace
+ {envpython} setup.py --quiet build_ext --inplace
{envpython} igor.py test_with_tracer c {posargs}
deps =