summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanny Allen <me@dannya.com>2014-08-11 16:13:06 +0100
committerDanny Allen <me@dannya.com>2014-08-11 16:13:06 +0100
commite38016c499921dd7bf5919a699a76305a1936129 (patch)
tree07a4125732561f2489dfb6b75a339cfef46d80d4
parentc81183f614ca982cd2ed93ac8e6e76610d162202 (diff)
parentee5ea987f8978d91c1ef189fe4f334511ddf6215 (diff)
downloadpython-coveragepy-git-e38016c499921dd7bf5919a699a76305a1936129.tar.gz
Merged ned/coveragepy into default
-rw-r--r--AUTHORS.txt2
-rw-r--r--CHANGES.txt28
-rw-r--r--coverage/annotate.py79
-rw-r--r--coverage/backward.py65
-rw-r--r--coverage/cmdline.py25
-rw-r--r--coverage/codeunit.py167
-rw-r--r--coverage/collector.py95
-rw-r--r--coverage/config.py57
-rw-r--r--coverage/control.py88
-rw-r--r--coverage/data.py19
-rw-r--r--coverage/django.py61
-rw-r--r--coverage/execfile.py124
-rw-r--r--coverage/extension.py20
-rw-r--r--coverage/files.py8
-rw-r--r--coverage/html.py8
-rw-r--r--coverage/misc.py2
-rw-r--r--coverage/parser.py40
-rw-r--r--coverage/phystokens.py2
-rw-r--r--coverage/report.py20
-rw-r--r--coverage/results.py27
-rw-r--r--coverage/summary.py9
-rw-r--r--coverage/templite.py45
-rw-r--r--coverage/tracer.c24
-rw-r--r--doc/config.rst2
-rw-r--r--doc/faq.rst13
-rw-r--r--igor.py4
-rw-r--r--tests/backtest.py50
-rw-r--r--tests/backunittest.py29
-rw-r--r--tests/coveragetest.py70
-rw-r--r--tests/farm/annotate/annotate_dir.py4
-rw-r--r--tests/farm/annotate/run.py4
-rw-r--r--tests/farm/annotate/run_multi.py4
-rw-r--r--tests/farm/run/run_chdir.py2
-rw-r--r--tests/farm/run/run_timid.py8
-rw-r--r--tests/farm/run/run_xxx.py4
-rw-r--r--tests/modules/pkg1/p1a.py2
-rw-r--r--tests/test_api.py8
-rw-r--r--tests/test_backward.py2
-rw-r--r--tests/test_cmdline.py2
-rw-r--r--tests/test_codeunit.py28
-rw-r--r--tests/test_config.py144
-rw-r--r--tests/test_coroutine.py61
-rw-r--r--tests/test_coverage.py14
-rw-r--r--tests/test_data.py14
-rw-r--r--tests/test_execfile.py9
-rw-r--r--tests/test_farm.py20
-rw-r--r--tests/test_files.py14
-rw-r--r--tests/test_html.py42
-rw-r--r--tests/test_parser.py4
-rw-r--r--tests/test_phystokens.py5
-rw-r--r--tests/test_process.py91
-rw-r--r--tests/test_summary.py148
-rw-r--r--tests/test_templite.py6
-rw-r--r--tests/test_testing.py10
54 files changed, 1255 insertions, 578 deletions
diff --git a/AUTHORS.txt b/AUTHORS.txt
index d7e2d276..3aa04adf 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -24,6 +24,7 @@ Roger Hu
Stan Hu
Devin Jeanpierre
Ross Lawley
+Steve Leonard
Edward Loper
Sandra Martocchia
Patrick Mezard
@@ -37,6 +38,7 @@ Adi Roiban
Greg Rogers
Chris Rose
George Song
+Anthony Sottile
David Stanek
Joseph Tate
Sigve Tjora
diff --git a/CHANGES.txt b/CHANGES.txt
index 26bd45cb..539037a5 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -8,14 +8,42 @@ Change history for Coverage.py
- Python versions supported are now CPython 2.6, 2.7, 3.2, 3.3, and 3.4, and
PyPy 2.2.
+- Options are now also read from a setup.cfg file, if any. Sections are
+ prefixed with "coverage:", so the ``[run]`` options will be read from the
+ ``[coverage:run]`` section of setup.cfg. Finishes `issue 304`_.
+
+- The ``report`` command can now show missing branches when reporting on branch
+ coverage. Thanks, Steve Leonard. Closes `issue 230`.
+
- The XML report now contains a <source> element, fixing `issue 94`_. Thanks
Stan Hu.
+- The ``fail-under`` value is now rounded the same as reported results,
+ preventing paradoxical results, fixing `issue 284`_.
+
- The XML report will now create the output directory if need be, fixing
`issue 285`_. Thanks Chris Rose.
+- HTML reports no longer raise UnicodeDecodeError if a Python file has
+ undecodable characters, fixing `issue 303`_.
+
+- The annotate command will now annotate all files, not just ones relative to
+ the current directory, fixing `issue 57`_.
+
+- The coverage module no longer causes deprecation warnings on Python 3.4 by
+ importing the imp module, fixing `issue 305`_.
+
+- Encoding declarations in source files are only considered if they are truly
+ comments. Thanks, Anthony Sottile.
+
+.. _issue 57: https://bitbucket.org/ned/coveragepy/issue/57/annotate-command-fails-to-annotate-many
.. _issue 94: https://bitbucket.org/ned/coveragepy/issue/94/coverage-xml-doesnt-produce-sources
+.. _issue 230: https://bitbucket.org/ned/coveragepy/issue/230/show-line-no-for-missing-branches-in
+.. _issue 284: https://bitbucket.org/ned/coveragepy/issue/284/fail-under-should-show-more-precision
.. _issue 285: https://bitbucket.org/ned/coveragepy/issue/285/xml-report-fails-if-output-file-directory
+.. _issue 303: https://bitbucket.org/ned/coveragepy/issue/303/unicodedecodeerror
+.. _issue 304: https://bitbucket.org/ned/coveragepy/issue/304/attempt-to-get-configuration-from-setupcfg
+.. _issue 305: https://bitbucket.org/ned/coveragepy/issue/305/pendingdeprecationwarning-the-imp-module
3.7.1 -- 13 December 2013
diff --git a/coverage/annotate.py b/coverage/annotate.py
index 19777eaf..5b96448a 100644
--- a/coverage/annotate.py
+++ b/coverage/annotate.py
@@ -47,55 +47,44 @@ class AnnotateReporter(Reporter):
`cu` is the CodeUnit for the file to annotate.
"""
- if not cu.relative:
- return
+ statements = sorted(analysis.statements)
+ missing = sorted(analysis.missing)
+ excluded = sorted(analysis.excluded)
- filename = cu.filename
- source = cu.source_file()
if self.directory:
dest_file = os.path.join(self.directory, cu.flat_rootname())
dest_file += ".py,cover"
else:
- dest_file = filename + ",cover"
- dest = open(dest_file, 'w')
-
- statements = sorted(analysis.statements)
- missing = sorted(analysis.missing)
- excluded = sorted(analysis.excluded)
-
- lineno = 0
- i = 0
- j = 0
- covered = True
- while True:
- line = source.readline()
- if line == '':
- break
- lineno += 1
- while i < len(statements) and statements[i] < lineno:
- i += 1
- while j < len(missing) and missing[j] < lineno:
- j += 1
- if i < len(statements) and statements[i] == lineno:
- covered = j >= len(missing) or missing[j] > lineno
- if self.blank_re.match(line):
- dest.write(' ')
- elif self.else_re.match(line):
- # Special logic for lines containing only 'else:'.
- if i >= len(statements) and j >= len(missing):
- dest.write('! ')
- elif i >= len(statements) or j >= len(missing):
+ dest_file = cu.filename + ",cover"
+
+ with open(dest_file, 'w') as dest:
+ i = 0
+ j = 0
+ covered = True
+ source = cu.source()
+ for lineno, line in enumerate(source.splitlines(True), start=1):
+ while i < len(statements) and statements[i] < lineno:
+ i += 1
+ while j < len(missing) and missing[j] < lineno:
+ j += 1
+ if i < len(statements) and statements[i] == lineno:
+ covered = j >= len(missing) or missing[j] > lineno
+ if self.blank_re.match(line):
+ dest.write(' ')
+ elif self.else_re.match(line):
+ # Special logic for lines containing only 'else:'.
+ if i >= len(statements) and j >= len(missing):
+ dest.write('! ')
+ elif i >= len(statements) or j >= len(missing):
+ dest.write('> ')
+ elif statements[i] == missing[j]:
+ dest.write('! ')
+ else:
+ dest.write('> ')
+ elif lineno in excluded:
+ dest.write('- ')
+ elif covered:
dest.write('> ')
- elif statements[i] == missing[j]:
- dest.write('! ')
else:
- dest.write('> ')
- elif lineno in excluded:
- dest.write('- ')
- elif covered:
- dest.write('> ')
- else:
- dest.write('! ')
- dest.write(line)
- source.close()
- dest.close()
+ dest.write('! ')
+ dest.write(line)
diff --git a/coverage/backward.py b/coverage/backward.py
index e81dd199..a7888a24 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -1,10 +1,11 @@
"""Add things to old Pythons so I can pretend they are newer."""
# This file does lots of tricky stuff, so disable a bunch of lintisms.
-# pylint: disable=F0401,W0611,W0622
-# F0401: Unable to import blah
-# W0611: Unused import blah
-# W0622: Redefining built-in blah
+# pylint: disable=redefined-builtin
+# pylint: disable=import-error
+# pylint: disable=no-member
+# pylint: disable=unused-import
+# pylint: disable=no-name-in-module
import os, re, sys
@@ -124,3 +125,59 @@ try:
except ImportError:
import md5
md5 = md5.new
+
+
+try:
+ # In Py 2.x, the builtins were in __builtin__
+ BUILTINS = sys.modules['__builtin__']
+except KeyError:
+ # In Py 3.x, they're in builtins
+ BUILTINS = sys.modules['builtins']
+
+
+# imp was deprecated in Python 3.3
+try:
+ import importlib, importlib.util
+ imp = None
+except ImportError:
+ importlib = None
+
+# we only want to use importlib if it has everything we need.
+try:
+ importlib_util_find_spec = importlib.util.find_spec
+except Exception:
+ import imp
+ importlib_util_find_spec = None
+
+try:
+ PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER
+except AttributeError:
+ PYC_MAGIC_NUMBER = imp.get_magic()
+
+
+def import_local_file(modname):
+ """Import a local file as a module.
+
+ Opens a file in the current directory named `modname`.py, imports it
+ as `modname`, and returns the module object.
+
+ """
+ try:
+ from importlib.machinery import SourceFileLoader
+ except ImportError:
+ SourceFileLoader = None
+
+ modfile = modname + '.py'
+ if SourceFileLoader:
+ mod = SourceFileLoader(modname, modfile).load_module()
+ else:
+ for suff in imp.get_suffixes():
+ if suff[0] == '.py':
+ break
+
+ with open(modfile, 'r') as f:
+ # pylint: disable=W0631
+ # (Using possibly undefined loop variable 'suff')
+ mod = imp.load_module(modname, f, modfile, suff)
+
+ return mod
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 19e0536e..bd10d5a8 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,6 +1,6 @@
"""Command-line support for Coverage."""
-import optparse, os, sys, time, traceback
+import glob, optparse, os, sys, time, traceback
from coverage.execfile import run_python_file, run_python_module
from coverage.misc import CoverageException, ExceptionDuringRun, NoSource
@@ -449,7 +449,7 @@ class CoverageScript(object):
# Remaining actions are reporting, with some common options.
report_args = dict(
- morfs = args,
+ morfs = unglob_args(args),
ignore_errors = options.ignore_errors,
omit = omit,
include = include,
@@ -470,6 +470,14 @@ class CoverageScript(object):
total = self.coverage.xml_report(outfile=outfile, **report_args)
if options.fail_under is not None:
+ # Total needs to be rounded, but be careful of 0 and 100.
+ if 0 < total < 1:
+ total = 1
+ elif 99 < total < 100:
+ total = 99
+ else:
+ total = round(total)
+
if total >= options.fail_under:
return OK
else:
@@ -633,6 +641,19 @@ def unshell_list(s):
return s.split(',')
+def unglob_args(args):
+ """Interpret shell wildcards for platforms that need it."""
+ if sys.platform == 'win32':
+ globbed = []
+ for arg in args:
+ if '?' in arg or '*' in arg:
+ globbed.extend(glob.glob(arg))
+ else:
+ globbed.append(arg)
+ args = globbed
+ return args
+
+
HELP_TOPICS = {
# -------------------------
'classic':
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index 88858801..35167a72 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -1,20 +1,24 @@
"""Code unit (module) handling for Coverage."""
-import glob, os, re
+import os
-from coverage.backward import open_python_source, string_class, StringIO
+from coverage.backward import open_python_source, string_class
from coverage.misc import CoverageException, NoSource
from coverage.parser import CodeParser, PythonParser
from coverage.phystokens import source_token_lines, source_encoding
+from coverage.django import DjangoTracer
-def code_unit_factory(morfs, file_locator):
+
+def code_unit_factory(morfs, file_locator, get_ext=None):
"""Construct a list of CodeUnits from polymorphic inputs.
`morfs` is a module or a filename, or a list of same.
`file_locator` is a FileLocator that can help resolve filenames.
+ `get_ext` TODO
+
Returns a list of CodeUnit objects.
"""
@@ -22,25 +26,28 @@ def code_unit_factory(morfs, file_locator):
if not isinstance(morfs, (list, tuple)):
morfs = [morfs]
- # On Windows, the shell doesn't expand wildcards. Do it here.
- globbed = []
- for morf in morfs:
- if isinstance(morf, string_class) and ('?' in morf or '*' in morf):
- globbed.extend(glob.glob(morf))
- else:
- globbed.append(morf)
- morfs = globbed
+ django_tracer = DjangoTracer()
code_units = []
for morf in morfs:
- # Hacked-in Mako support. Disabled for going onto trunk.
- if 0 and isinstance(morf, string_class) and "/mako/" in morf:
- # Super hack! Do mako both ways!
- if 0:
- cu = PythonCodeUnit(morf, file_locator)
- cu.name += '_fako'
- code_units.append(cu)
- klass = MakoCodeUnit
+ ext = None
+ if isinstance(morf, string_class) and get_ext:
+ ext = get_ext(morf)
+ if ext:
+ klass = DjangoTracer # NOT REALLY! TODO
+ # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of
+ # the path that indicates the Python file is actually a compiled Mako
+ # template. THIS IS TEMPORARY!
+ #MAKO_PATH = os.environ.get('COVERAGE_MAKO_PATH')
+ #if MAKO_PATH and isinstance(morf, string_class) and MAKO_PATH in morf:
+ # # Super hack! Do mako both ways!
+ # if 0:
+ # cu = PythonCodeUnit(morf, file_locator)
+ # cu.name += '_fako'
+ # code_units.append(cu)
+ # klass = MakoCodeUnit
+ #elif isinstance(morf, string_class) and morf.endswith(".html"):
+ # klass = DjangoCodeUnit
else:
klass = PythonCodeUnit
code_units.append(klass(morf, file_locator))
@@ -87,6 +94,10 @@ class CodeUnit(object):
def __repr__(self):
return "<CodeUnit name=%r filename=%r>" % (self.name, self.filename)
+ def _adjust_filename(self, f):
+ # TODO: This shouldn't be in the base class, right?
+ return f
+
# Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
# of them defined.
@@ -119,22 +130,29 @@ class CodeUnit(object):
root = os.path.splitdrive(self.name)[1]
return root.replace('\\', '_').replace('/', '_').replace('.', '_')
- def source_file(self):
- """Return an open file for reading the source of the code unit."""
+ def source(self):
+ """Return the source code, as a string."""
if os.path.exists(self.filename):
# A regular text file: open it.
- return open_python_source(self.filename)
+ with open_python_source(self.filename) as f:
+ return f.read()
# Maybe it's in a zip file?
source = self.file_locator.get_zip_data(self.filename)
if source is not None:
- return StringIO(source)
+ return source
# Couldn't find source.
raise CoverageException(
"No source for code '%s'." % self.filename
)
+ def source_token_lines(self, source):
+ """Return the 'tokenized' text for the code."""
+ # TODO: Taking source here is wrong, change it?
+ for line in source.splitlines():
+ yield [('txt', line)]
+
def should_be_python(self):
"""Does it seem like this file should contain Python?
@@ -148,8 +166,6 @@ class CodeUnit(object):
class PythonCodeUnit(CodeUnit):
"""Represents a Python file."""
- parser_class = PythonParser
-
def _adjust_filename(self, fname):
# .pyc files should always refer to a .py instead.
if fname.endswith(('.pyc', '.pyo')):
@@ -158,7 +174,13 @@ class PythonCodeUnit(CodeUnit):
fname = fname[:-9] + ".py"
return fname
- def find_source(self, filename):
+ def get_parser(self, exclude=None):
+ actual_filename, source = self._find_source(self.filename)
+ return PythonParser(
+ text=source, filename=actual_filename, exclude=exclude,
+ )
+
+ def _find_source(self, filename):
"""Find the source for `filename`.
Returns two values: the actual filename, and the source.
@@ -223,78 +245,61 @@ class PythonCodeUnit(CodeUnit):
return source_encoding(source)
-def mako_template_name(py_filename):
- with open(py_filename) as f:
- py_source = f.read()
-
- # Find the template filename. TODO: string escapes in the string.
- m = re.search(r"^_template_filename = u?'([^']+)'", py_source, flags=re.MULTILINE)
- if not m:
- raise Exception("Couldn't find template filename in Mako file %r" % py_filename)
- template_filename = m.group(1)
- return template_filename
-
-
class MakoParser(CodeParser):
- def __init__(self, cu, text, filename, exclude):
- self.cu = cu
- self.text = text
- self.filename = filename
- self.exclude = exclude
+ def __init__(self, metadata):
+ self.metadata = metadata
def parse_source(self):
"""Returns executable_line_numbers, excluded_line_numbers"""
- with open(self.cu.filename) as f:
- py_source = f.read()
-
- # Get the line numbers.
- self.py_to_html = {}
- html_linenum = None
- for linenum, line in enumerate(py_source.splitlines(), start=1):
- m_source_line = re.search(r"^\s*# SOURCE LINE (\d+)$", line)
- if m_source_line:
- html_linenum = int(m_source_line.group(1))
- else:
- m_boilerplate_line = re.search(r"^\s*# BOILERPLATE", line)
- if m_boilerplate_line:
- html_linenum = None
- elif html_linenum:
- self.py_to_html[linenum] = html_linenum
-
- return set(self.py_to_html.values()), set()
+ executable = set(self.metadata['line_map'].values())
+ return executable, set()
def translate_lines(self, lines):
- tlines = set(self.py_to_html.get(l, -1) for l in lines)
- tlines.remove(-1)
+ tlines = set()
+ for l in lines:
+ try:
+ tlines.add(self.metadata['full_line_map'][l])
+ except IndexError:
+ pass
return tlines
class MakoCodeUnit(CodeUnit):
- parser_class = MakoParser
-
def __init__(self, *args, **kwargs):
super(MakoCodeUnit, self).__init__(*args, **kwargs)
- self.mako_filename = mako_template_name(self.filename)
+ from mako.template import ModuleInfo
+ py_source = open(self.filename).read()
+ self.metadata = ModuleInfo.get_module_source_metadata(py_source, full_line_map=True)
- def source_file(self):
- return open(self.mako_filename)
+ def source(self):
+ return open(self.metadata['filename']).read()
- def find_source(self, filename):
- """Find the source for `filename`.
+ def get_parser(self, exclude=None):
+ return MakoParser(self.metadata)
- Returns two values: the actual filename, and the source.
+ def source_encoding(self, source):
+ # TODO: Taking source here is wrong, change it!
+ return self.metadata['source_encoding']
- """
- mako_filename = mako_template_name(filename)
- with open(mako_filename) as f:
- source = f.read()
- return mako_filename, source
+class DjangoCodeUnit(CodeUnit):
+ def source(self):
+ with open(self.filename) as f:
+ return f.read()
- def source_token_lines(self, source):
- """Return the 'tokenized' text for the code."""
- for line in source.splitlines():
- yield [('txt', line)]
+ def get_parser(self, exclude=None):
+ return DjangoParser(self.filename)
def source_encoding(self, source):
- return "utf-8"
+ return "utf8"
+
+
+class DjangoParser(CodeParser):
+ def __init__(self, filename):
+ self.filename = filename
+
+ def parse_source(self):
+ with open(self.filename) as f:
+ source = f.read()
+ executable = set(range(1, len(source.splitlines())+1))
+ return executable, set()
diff --git a/coverage/collector.py b/coverage/collector.py
index 94af5df5..546525d2 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -41,17 +41,22 @@ class PyTracer(object):
# used to force the use of this tracer.
def __init__(self):
+ # Attributes set from the collector:
self.data = None
+ self.arcs = False
self.should_trace = None
self.should_trace_cache = None
self.warn = None
+ self.extensions = None
+
+ self.extension = None
+ self.cur_tracename = None # TODO: This is only maintained for the if0 debugging output. Get rid of it eventually.
self.cur_file_data = None
self.last_line = 0
self.data_stack = []
self.data_stacks = collections.defaultdict(list)
self.last_exc_back = None
self.last_exc_firstlineno = 0
- self.arcs = False
self.thread = None
self.stopped = False
self.coroutine_id_func = None
@@ -64,9 +69,31 @@ class PyTracer(object):
return
if 0:
- sys.stderr.write("trace event: %s %r @%d\n" % (
- event, frame.f_code.co_filename, frame.f_lineno
+ # A lot of debugging to try to understand why gevent isn't right.
+ import os.path, pprint
+ def short_ident(ident):
+ return "{}:{:06X}".format(ident.__class__.__name__, id(ident) & 0xFFFFFF)
+
+ ident = None
+ if self.coroutine_id_func:
+ ident = short_ident(self.coroutine_id_func())
+ sys.stdout.write("trace event: %s %s %r @%d\n" % (
+ event, ident, frame.f_code.co_filename, frame.f_lineno
))
+ pprint.pprint(
+ dict(
+ (
+ short_ident(ident),
+ [
+ (os.path.basename(tn or ""), sorted((cfd or {}).keys()), ll)
+ for ex, tn, cfd, ll in data_stacks
+ ]
+ )
+ for ident, data_stacks in self.data_stacks.items()
+ )
+ , width=250)
+ pprint.pprint(sorted((self.cur_file_data or {}).keys()), width=250)
+ print("TRYING: {}".format(sorted(next((v for k,v in self.data.items() if k.endswith("try_it.py")), {}).keys())))
if self.last_exc_back:
if frame == self.last_exc_back:
@@ -76,7 +103,7 @@ class PyTracer(object):
self.cur_file_data[pair] = None
if self.coroutine_id_func:
self.data_stack = self.data_stacks[self.coroutine_id_func()]
- self.cur_file_data, self.last_line = self.data_stack.pop()
+ self.handler, _, self.cur_file_data, self.last_line = self.data_stack.pop()
self.last_exc_back = None
if event == 'call':
@@ -85,19 +112,25 @@ class PyTracer(object):
if self.coroutine_id_func:
self.data_stack = self.data_stacks[self.coroutine_id_func()]
self.last_coroutine = self.coroutine_id_func()
- self.data_stack.append((self.cur_file_data, self.last_line))
+ self.data_stack.append((self.extension, self.cur_tracename, self.cur_file_data, self.last_line))
filename = frame.f_code.co_filename
- if filename not in self.should_trace_cache:
- tracename = self.should_trace(filename, frame)
- self.should_trace_cache[filename] = tracename
- else:
- tracename = self.should_trace_cache[filename]
+ disp = self.should_trace_cache.get(filename)
+ if disp is None:
+ disp = self.should_trace(filename, frame)
+ self.should_trace_cache[filename] = disp
#print("called, stack is %d deep, tracename is %r" % (
# len(self.data_stack), tracename))
+ tracename = disp.filename
+ if tracename and disp.extension:
+ tracename = disp.extension.file_name(frame)
if tracename:
if tracename not in self.data:
self.data[tracename] = {}
+ if disp.extension:
+ self.extensions[tracename] = disp.extension.__name__
+ self.cur_tracename = tracename
self.cur_file_data = self.data[tracename]
+ self.extension = disp.extension
else:
self.cur_file_data = None
# Set the last_line to -1 because the next arc will be entering a
@@ -105,16 +138,24 @@ class PyTracer(object):
self.last_line = -1
elif event == 'line':
# Record an executed line.
- #if self.coroutine_id_func:
- # assert self.last_coroutine == self.coroutine_id_func()
- if self.cur_file_data is not None:
- if self.arcs:
- #print("lin", self.last_line, frame.f_lineno)
- self.cur_file_data[(self.last_line, frame.f_lineno)] = None
- else:
- #print("lin", frame.f_lineno)
- self.cur_file_data[frame.f_lineno] = None
- self.last_line = frame.f_lineno
+ if 0 and self.coroutine_id_func:
+ this_coroutine = self.coroutine_id_func()
+ if self.last_coroutine != this_coroutine:
+ print("mismatch: {0} != {1}".format(self.last_coroutine, this_coroutine))
+ if self.extension:
+ lineno_from, lineno_to = self.extension.line_number_range(frame)
+ else:
+ lineno_from, lineno_to = frame.f_lineno, frame.f_lineno
+ if lineno_from != -1:
+ if self.cur_file_data is not None:
+ if self.arcs:
+ #print("lin", self.last_line, frame.f_lineno)
+ self.cur_file_data[(self.last_line, lineno_from)] = None
+ else:
+ #print("lin", frame.f_lineno)
+ for lineno in range(lineno_from, lineno_to+1):
+ self.cur_file_data[lineno] = None
+ self.last_line = lineno_to
elif event == 'return':
if self.arcs and self.cur_file_data:
first = frame.f_code.co_firstlineno
@@ -123,7 +164,7 @@ class PyTracer(object):
if self.coroutine_id_func:
self.data_stack = self.data_stacks[self.coroutine_id_func()]
self.last_coroutine = self.coroutine_id_func()
- self.cur_file_data, self.last_line = self.data_stack.pop()
+ self.extension, _, self.cur_file_data, self.last_line = self.data_stack.pop()
#print("returned, stack is %d deep" % (len(self.data_stack)))
elif event == 'exception':
#print("exc", self.last_line, frame.f_lineno)
@@ -240,6 +281,8 @@ class Collector(object):
# or mapping filenames to dicts with linenumber pairs as keys.
self.data = {}
+ self.extensions = {}
+
# A cache of the results from should_trace, the decision about whether
# to trace execution in a file. A dict of filename to (filename or
# None).
@@ -258,6 +301,8 @@ class Collector(object):
tracer.warn = self.warn
if hasattr(tracer, 'coroutine_id_func'):
tracer.coroutine_id_func = self.coroutine_id_func
+ if hasattr(tracer, 'extensions'):
+ tracer.extensions = self.extensions
fn = tracer.start()
self.tracers.append(tracer)
return fn
@@ -356,10 +401,7 @@ class Collector(object):
# to show line data.
line_data = {}
for f, arcs in self.data.items():
- line_data[f] = ldf = {}
- for l1, _ in list(arcs.keys()):
- if l1:
- ldf[l1] = None
+ line_data[f] = dict((l1, None) for l1, _ in arcs.keys() if l1)
return line_data
else:
return self.data
@@ -377,3 +419,6 @@ class Collector(object):
return self.data
else:
return {}
+
+ def get_extension_data(self):
+ return self.extensions
diff --git a/coverage/config.py b/coverage/config.py
index 60ec3f41..064bc1ca 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -13,6 +13,11 @@ except ImportError:
class HandyConfigParser(configparser.RawConfigParser):
"""Our specialization of ConfigParser."""
+ def __init__(self, section_prefix):
+ # pylint: disable=super-init-not-called
+ configparser.RawConfigParser.__init__(self)
+ self.section_prefix = section_prefix
+
def read(self, filename):
"""Read a filename as UTF-8 configuration data."""
kwargs = {}
@@ -20,8 +25,30 @@ class HandyConfigParser(configparser.RawConfigParser):
kwargs['encoding'] = "utf-8"
return configparser.RawConfigParser.read(self, filename, **kwargs)
- def get(self, *args, **kwargs):
- v = configparser.RawConfigParser.get(self, *args, **kwargs)
+ def has_option(self, section, option):
+ section = self.section_prefix + section
+ return configparser.RawConfigParser.has_option(self, section, option)
+
+ def has_section(self, section):
+ section = self.section_prefix + section
+ return configparser.RawConfigParser.has_section(self, section)
+
+ def options(self, section):
+ section = self.section_prefix + section
+ return configparser.RawConfigParser.options(self, section)
+
+ def get(self, section, *args, **kwargs):
+ """Get a value, replacing environment variables also.
+
+ The arguments are the same as `RawConfigParser.get`, but in the found
+ value, ``$WORD`` or ``${WORD}`` are replaced by the value of the
+ environment variable ``WORD``.
+
+ Returns the finished value.
+
+ """
+ section = self.section_prefix + section
+ v = configparser.RawConfigParser.get(self, section, *args, **kwargs)
def dollar_replace(m):
"""Called for each $replacement."""
# Only one of the groups will have matched, just get its text.
@@ -113,6 +140,7 @@ class CoverageConfig(object):
self.timid = False
self.source = None
self.debug = []
+ self.extensions = []
# Defaults for [report]
self.exclude_list = DEFAULT_EXCLUDE[:]
@@ -144,7 +172,7 @@ class CoverageConfig(object):
if env:
self.timid = ('--timid' in env)
- MUST_BE_LIST = ["omit", "include", "debug"]
+ MUST_BE_LIST = ["omit", "include", "debug", "extensions"]
def from_args(self, **kwargs):
"""Read config values from `kwargs`."""
@@ -154,18 +182,22 @@ class CoverageConfig(object):
v = [v]
setattr(self, k, v)
- def from_file(self, filename):
+ def from_file(self, filename, section_prefix=""):
"""Read configuration from a .rc file.
`filename` is a file name to read.
+ Returns True or False, whether the file could be read.
+
"""
self.attempted_config_files.append(filename)
- cp = HandyConfigParser()
+ cp = HandyConfigParser(section_prefix)
files_read = cp.read(filename)
- if files_read is not None: # return value changed in 2.4
- self.config_files.extend(files_read)
+ if not files_read:
+ return False
+
+ self.config_files.extend(files_read)
for option_spec in self.CONFIG_FILE_OPTIONS:
self.set_attr_from_config_option(cp, *option_spec)
@@ -175,13 +207,24 @@ class CoverageConfig(object):
for option in cp.options('paths'):
self.paths[option] = cp.getlist('paths', option)
+ return True
+
CONFIG_FILE_OPTIONS = [
+ # These are *args for set_attr_from_config_option:
+ # (attr, where, type_="")
+ #
+ # attr is the attribute to set on the CoverageConfig object.
+ # where is the section:name to read from the configuration file.
+ # type_ is the optional type to apply, by using .getTYPE to read the
+ # configuration value from the file.
+
# [run]
('branch', 'run:branch', 'boolean'),
('coroutine', 'run:coroutine'),
('cover_pylib', 'run:cover_pylib', 'boolean'),
('data_file', 'run:data_file'),
('debug', 'run:debug', 'list'),
+ ('extensions', 'run:extensions', 'list'),
('include', 'run:include', 'list'),
('omit', 'run:omit', 'list'),
('parallel', 'run:parallel', 'boolean'),
diff --git a/coverage/control.py b/coverage/control.py
index 44a70bf0..cb917e52 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -9,6 +9,7 @@ from coverage.collector import Collector
from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.debug import DebugControl
+from coverage.extension import load_extensions
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.html import HtmlReporter
@@ -18,6 +19,7 @@ from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
+
# Pypy has some unusual stuff in the "stdlib". Consider those locations
# when deciding where the stdlib is.
try:
@@ -97,17 +99,22 @@ class coverage(object):
# 1: defaults:
self.config = CoverageConfig()
- # 2: from the coveragerc file:
+ # 2: from the .coveragerc or setup.cfg file:
if config_file:
+ did_read_rc = should_read_setupcfg = False
if config_file is True:
config_file = ".coveragerc"
+ should_read_setupcfg = True
try:
- self.config.from_file(config_file)
+ did_read_rc = self.config.from_file(config_file)
except ValueError as err:
raise CoverageException(
"Couldn't read config file %s: %s" % (config_file, err)
)
+ if not did_read_rc and should_read_setupcfg:
+ self.config.from_file("setup.cfg", section_prefix="coverage:")
+
# 3: from environment variables:
self.config.from_environment('COVERAGE_OPTIONS')
env_data_file = os.environ.get('COVERAGE_FILE')
@@ -125,6 +132,10 @@ class coverage(object):
# Create and configure the debugging controller.
self.debug = DebugControl(self.config.debug, debug_file or sys.stderr)
+ # Load extensions
+ tracer_classes = load_extensions(self.config.extensions, "tracer")
+ self.tracer_extensions = [cls() for cls in tracer_classes]
+
self.auto_data = auto_data
# _exclude_re is a dict mapping exclusion list names to compiled
@@ -232,22 +243,24 @@ class coverage(object):
This function is called from the trace function. As each new file name
is encountered, this function determines whether it is traced or not.
- Returns a pair of values: the first indicates whether the file should
- be traced: it's a canonicalized filename if it should be traced, None
- if it should not. The second value is a string, the resason for the
- decision.
+ Returns a FileDisposition object.
"""
+ disp = FileDisposition(filename)
+
if not filename:
# Empty string is pretty useless
- return None, "empty string isn't a filename"
+ return disp.nope("empty string isn't a filename")
+
+ if filename.startswith('memory:'):
+ return disp.nope("memory isn't traceable")
if filename.startswith('<'):
# Lots of non-file execution is represented with artificial
# filenames like "<string>", "<doctest readme.txt[0]>", or
# "<exec_function>". Don't ever trace these executions, since we
# can't do anything with the data later anyway.
- return None, "not a real filename"
+ return disp.nope("not a real filename")
self._check_for_packages()
@@ -267,47 +280,51 @@ class coverage(object):
canonical = self.file_locator.canonical_filename(filename)
+ # Try the extensions, see if they have an opinion about the file.
+ for tracer in self.tracer_extensions:
+ ext_disp = tracer.should_trace(canonical)
+ if ext_disp:
+ ext_disp.extension = tracer
+ return ext_disp
+
# If the user specified source or include, then that's authoritative
# about the outer bound of what to measure and we don't have to apply
# any canned exclusions. If they didn't, then we have to exclude the
# stdlib and coverage.py directories.
if self.source_match:
if not self.source_match.match(canonical):
- return None, "falls outside the --source trees"
+ return disp.nope("falls outside the --source trees")
elif self.include_match:
if not self.include_match.match(canonical):
- return None, "falls outside the --include trees"
+ return disp.nope("falls outside the --include trees")
else:
# If we aren't supposed to trace installed code, then check if this
# is near the Python standard library and skip it if so.
if self.pylib_match and self.pylib_match.match(canonical):
- return None, "is in the stdlib"
+ return disp.nope("is in the stdlib")
# We exclude the coverage code itself, since a little of it will be
# measured otherwise.
if self.cover_match and self.cover_match.match(canonical):
- return None, "is part of coverage.py"
+ return disp.nope("is part of coverage.py")
# Check the file against the omit pattern.
if self.omit_match and self.omit_match.match(canonical):
- return None, "is inside an --omit pattern"
+ return disp.nope("is inside an --omit pattern")
- return canonical, "because we love you"
+ disp.filename = canonical
+ return disp
def _should_trace(self, filename, frame):
"""Decide whether to trace execution in `filename`.
- Calls `_should_trace_with_reason`, and returns just the decision.
+ Calls `_should_trace_with_reason`, and returns the FileDisposition.
"""
- canonical, reason = self._should_trace_with_reason(filename, frame)
+ disp = self._should_trace_with_reason(filename, frame)
if self.debug.should('trace'):
- if not canonical:
- msg = "Not tracing %r: %s" % (filename, reason)
- else:
- msg = "Tracing %r" % (filename,)
- self.debug.write(msg)
- return canonical
+ self.debug.write(disp.debug_message())
+ return disp
def _warn(self, msg):
"""Use `msg` as a warning."""
@@ -525,8 +542,10 @@ class coverage(object):
if not self._measured:
return
+ # TODO: seems like this parallel structure is getting kinda old...
self.data.add_line_data(self.collector.get_line_data())
self.data.add_arc_data(self.collector.get_arc_data())
+ self.data.add_extension_data(self.collector.get_extension_data())
self.collector.reset()
# If there are still entries in the source_pkgs list, then we never
@@ -594,7 +613,8 @@ class coverage(object):
"""
self._harvest_data()
if not isinstance(it, CodeUnit):
- it = code_unit_factory(it, self.file_locator)[0]
+ get_ext = self.data.extension_data().get
+ it = code_unit_factory(it, self.file_locator, get_ext)[0]
return Analysis(self, it)
@@ -760,6 +780,28 @@ class coverage(object):
return info
+class FileDisposition(object):
+ """A simple object for noting a number of details of files to trace."""
+ def __init__(self, original_filename):
+ self.original_filename = original_filename
+ self.filename = None
+ self.reason = ""
+ self.extension = None
+
+ def nope(self, reason):
+ """A helper for returning a NO answer from should_trace."""
+ self.reason = reason
+ return self
+
+ def debug_message(self):
+ """Produce a debugging message explaining the outcome."""
+ if not self.filename:
+ msg = "Not tracing %r: %s" % (self.original_filename, self.reason)
+ else:
+ msg = "Tracing %r" % (self.original_filename,)
+ return msg
+
+
def process_startup():
"""Call this at Python startup to perhaps measure coverage.
diff --git a/coverage/data.py b/coverage/data.py
index 042b6405..b78c931d 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -21,6 +21,11 @@ class CoverageData(object):
* arcs: a dict mapping filenames to sorted lists of line number pairs:
{ 'file1': [(17,23), (17,25), (25,26)], ... }
+ * extensions: a dict mapping filenames to extension names:
+ { 'file1': "django.coverage", ... }
+ # TODO: how to handle the difference between a extension module
+ # name, and the class in the module?
+
"""
def __init__(self, basename=None, collector=None, debug=None):
@@ -64,6 +69,14 @@ class CoverageData(object):
#
self.arcs = {}
+ # A map from canonical source file name to an extension module name:
+ #
+ # {
+ # 'filename1.py': 'django.coverage',
+ # ...
+ # }
+ self.extensions = {}
+
def usefile(self, use_file=True):
"""Set whether or not to use a disk file for data."""
self.use_file = use_file
@@ -110,6 +123,9 @@ class CoverageData(object):
(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)
)
+ def extension_data(self):
+ return self.extensions
+
def write_file(self, filename):
"""Write the coverage data to `filename`."""
@@ -213,6 +229,9 @@ class CoverageData(object):
for filename, arcs in iitems(arc_data):
self.arcs.setdefault(filename, {}).update(arcs)
+ def add_extension_data(self, extension_data):
+ self.extensions.update(extension_data)
+
def touch_file(self, filename):
"""Ensure that `filename` appears in the data, empty if needed."""
self.lines.setdefault(filename, {})
diff --git a/coverage/django.py b/coverage/django.py
new file mode 100644
index 00000000..00f2ed54
--- /dev/null
+++ b/coverage/django.py
@@ -0,0 +1,61 @@
+import sys
+
+
+ALL_TEMPLATE_MAP = {}
+
+def get_line_map(filename):
+ if filename not in ALL_TEMPLATE_MAP:
+ with open(filename) as template_file:
+ template_source = template_file.read()
+ line_lengths = [len(l) for l in template_source.splitlines(True)]
+ ALL_TEMPLATE_MAP[filename] = list(running_sum(line_lengths))
+ return ALL_TEMPLATE_MAP[filename]
+
+def get_line_number(line_map, offset):
+ for lineno, line_offset in enumerate(line_map, start=1):
+ if line_offset >= offset:
+ return lineno
+ return -1
+
+class DjangoTracer(object):
+ def should_trace(self, canonical):
+ return "/django/template/" in canonical
+
+ def source(self, frame):
+ if frame.f_code.co_name != 'render':
+ return None
+ that = frame.f_locals['self']
+ return getattr(that, "source", None)
+
+ def file_name(self, frame):
+ source = self.source(frame)
+ if not source:
+ return None
+ return source[0].name.encode(sys.getfilesystemencoding())
+
+ def line_number_range(self, frame):
+ source = self.source(frame)
+ if not source:
+ return -1, -1
+ filename = source[0].name
+ line_map = get_line_map(filename)
+ start = get_line_number(line_map, source[1][0])
+ end = get_line_number(line_map, source[1][1])
+ if start < 0 or end < 0:
+ return -1, -1
+ return start, end
+
+def running_sum(seq):
+ total = 0
+ for num in seq:
+ total += num
+ yield total
+
+def ppp(obj):
+ ret = []
+ import inspect
+ for name, value in inspect.getmembers(obj):
+ if not callable(value):
+ ret.append("%s=%r" % (name, value))
+ attrs = ", ".join(ret)
+ return "%s: %s" % (obj.__class__, attrs)
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 10c7b917..b7877b6a 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -1,23 +1,83 @@
"""Execute files of Python code."""
-import imp, marshal, os, sys
+import marshal, os, sys, types
-from coverage.backward import open_python_source
+from coverage.backward import open_python_source, BUILTINS
+from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
from coverage.misc import ExceptionDuringRun, NoCode, NoSource
-try:
- # In Py 2.x, the builtins were in __builtin__
- BUILTINS = sys.modules['__builtin__']
-except KeyError:
- # In Py 3.x, they're in builtins
- BUILTINS = sys.modules['builtins']
+if importlib_util_find_spec:
+ def find_module(modulename):
+ """Find the module named `modulename`.
+ Returns the file path of the module, and the name of the enclosing
+ package.
+ """
+ # pylint: disable=no-member
+ try:
+ spec = importlib_util_find_spec(modulename)
+ except ImportError as err:
+ raise NoSource(str(err))
+ if not spec:
+ raise NoSource("No module named %r" % (modulename,))
+ pathname = spec.origin
+ packagename = spec.name
+ if pathname.endswith("__init__.py"):
+ mod_main = modulename + ".__main__"
+ spec = importlib_util_find_spec(mod_main)
+ if not spec:
+ raise NoSource(
+ "No module named %s; "
+ "%r is a package and cannot be directly executed"
+ % (mod_main, modulename)
+ )
+ pathname = spec.origin
+ packagename = spec.name
+ packagename = packagename.rpartition(".")[0]
+ return pathname, packagename
+else:
+ def find_module(modulename):
+ """Find the module named `modulename`.
+
+ Returns the file path of the module, and the name of the enclosing
+ package.
+ """
+ openfile = None
+ glo, loc = globals(), locals()
+ try:
+ # Search for the module - inside its parent package, if any - using
+ # standard import mechanics.
+ if '.' in modulename:
+ packagename, name = modulename.rsplit('.', 1)
+ package = __import__(packagename, glo, loc, ['__path__'])
+ searchpath = package.__path__
+ else:
+ packagename, name = None, modulename
+ searchpath = None # "top-level search" in imp.find_module()
+ openfile, pathname, _ = imp.find_module(name, searchpath)
-def rsplit1(s, sep):
- """The same as s.rsplit(sep, 1), but works in 2.3"""
- parts = s.split(sep)
- return sep.join(parts[:-1]), parts[-1]
+ # Complain if this is a magic non-file module.
+ if openfile is None and pathname is None:
+ raise NoSource(
+ "module does not live in a file: %r" % modulename
+ )
+
+ # If `modulename` is actually a package, not a mere module, then we
+ # pretend to be Python 2.7 and try running its __main__.py script.
+ if openfile is None:
+ packagename = modulename
+ name = '__main__'
+ package = __import__(packagename, glo, loc, ['__path__'])
+ searchpath = package.__path__
+ openfile, pathname, _ = imp.find_module(name, searchpath)
+ except ImportError as err:
+ raise NoSource(str(err))
+ finally:
+ if openfile:
+ openfile.close()
+
+ return pathname, packagename
def run_python_module(modulename, args):
@@ -28,41 +88,8 @@ def run_python_module(modulename, args):
element naming the module being executed.
"""
- openfile = None
- glo, loc = globals(), locals()
- try:
- # Search for the module - inside its parent package, if any - using
- # standard import mechanics.
- if '.' in modulename:
- packagename, name = rsplit1(modulename, '.')
- package = __import__(packagename, glo, loc, ['__path__'])
- searchpath = package.__path__
- else:
- packagename, name = None, modulename
- searchpath = None # "top-level search" in imp.find_module()
- openfile, pathname, _ = imp.find_module(name, searchpath)
-
- # Complain if this is a magic non-file module.
- if openfile is None and pathname is None:
- raise NoSource(
- "module does not live in a file: %r" % modulename
- )
+ pathname, packagename = find_module(modulename)
- # If `modulename` is actually a package, not a mere module, then we
- # pretend to be Python 2.7 and try running its __main__.py script.
- if openfile is None:
- packagename = modulename
- name = '__main__'
- package = __import__(packagename, glo, loc, ['__path__'])
- searchpath = package.__path__
- openfile, pathname, _ = imp.find_module(name, searchpath)
- except ImportError as err:
- raise NoSource(str(err))
- finally:
- if openfile:
- openfile.close()
-
- # Finally, hand the file off to run_python_file for execution.
pathname = os.path.abspath(pathname)
args[0] = pathname
run_python_file(pathname, args, package=packagename)
@@ -79,7 +106,7 @@ def run_python_file(filename, args, package=None):
"""
# Create a module to serve as __main__
old_main_mod = sys.modules['__main__']
- main_mod = imp.new_module('__main__')
+ main_mod = types.ModuleType('__main__')
sys.modules['__main__'] = main_mod
main_mod.__file__ = filename
if package:
@@ -119,6 +146,7 @@ def run_python_file(filename, args, package=None):
# Restore the old argv and path
sys.argv = old_argv
+
def make_code_from_py(filename):
"""Get source from `filename` and make a code object of it."""
# Open the source file.
@@ -150,7 +178,7 @@ def make_code_from_pyc(filename):
# First four bytes are a version-specific magic number. It has to
# match or we won't run the file.
magic = fpyc.read(4)
- if magic != imp.get_magic():
+ if magic != PYC_MAGIC_NUMBER:
raise NoCode("Bad magic number in .pyc file")
# Skip the junk in the header that we don't need.
diff --git a/coverage/extension.py b/coverage/extension.py
new file mode 100644
index 00000000..8c89b88e
--- /dev/null
+++ b/coverage/extension.py
@@ -0,0 +1,20 @@
+"""Extension management for coverage.py"""
+
+def load_extensions(modules, name):
+ """Load extensions from `modules`, finding them by `name`.
+
+ Yields the loaded extensions.
+
+ """
+
+ for module in modules:
+ try:
+ __import__(module)
+ mod = sys.modules[module]
+ except ImportError:
+ blah()
+ continue
+
+ entry = getattr(mod, name, None)
+ if entry:
+ yield entry
diff --git a/coverage/files.py b/coverage/files.py
index 94388f96..08ce1e84 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -1,7 +1,7 @@
"""File wrangling."""
from coverage.backward import to_string
-from coverage.misc import CoverageException
+from coverage.misc import CoverageException, join_regex
import fnmatch, os, os.path, re, sys
import ntpath, posixpath
@@ -177,6 +177,7 @@ class FnmatchMatcher(object):
"""A matcher for files by filename pattern."""
def __init__(self, pats):
self.pats = pats[:]
+ self.re = re.compile(join_regex([fnmatch.translate(p) for p in pats]))
def __repr__(self):
return "<FnmatchMatcher %r>" % self.pats
@@ -187,10 +188,7 @@ class FnmatchMatcher(object):
def match(self, fpath):
"""Does `fpath` match one of our filename patterns?"""
- for pat in self.pats:
- if fnmatch.fnmatch(fpath, pat):
- return True
- return False
+ return self.re.match(fpath) is not None
def sep(s):
diff --git a/coverage/html.py b/coverage/html.py
index 42da2972..6e21efaa 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -149,9 +149,7 @@ class HtmlReporter(Reporter):
def html_file(self, cu, analysis):
"""Generate an HTML file for one source file."""
- source_file = cu.source_file()
- with source_file:
- source = source_file.read()
+ source = cu.source()
# Find out if the file on disk is already correct.
flat_rootname = cu.flat_rootname()
@@ -241,7 +239,9 @@ class HtmlReporter(Reporter):
}))
if sys.version_info < (3, 0):
- html = html.decode(encoding)
+ # In theory, all the characters in the source can be decoded, but
+ # strange things happen, so use 'replace' to keep errors at bay.
+ html = html.decode(encoding, 'replace')
html_filename = flat_rootname + ".html"
html_path = os.path.join(self.directory, html_filename)
diff --git a/coverage/misc.py b/coverage/misc.py
index c88d4ecd..4b1dccb2 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -87,7 +87,7 @@ def bool_or_none(b):
def join_regex(regexes):
"""Combine a list of regexes into one that matches any of them."""
if len(regexes) > 1:
- return "|".join("(%s)" % r for r in regexes)
+ return "|".join("(?:%s)" % r for r in regexes)
elif regexes:
return regexes[0]
else:
diff --git a/coverage/parser.py b/coverage/parser.py
index cfaf02fa..c5e95baa 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -14,23 +14,11 @@ class CodeParser(object):
"""
Base class for any code parser.
"""
- def _adjust_filename(self, fname):
- return fname
-
- def first_lines(self, lines):
- """Map the line numbers in `lines` to the correct first line of the
- statement.
-
- Returns a set of the first lines.
-
- """
- return set(self.first_line(l) for l in lines)
-
- def first_line(self, line):
- return line
-
def translate_lines(self, lines):
- return lines
+ return set(lines)
+
+ def translate_arcs(self, arcs):
+ return arcs
def exit_counts(self):
return {}
@@ -42,7 +30,7 @@ class CodeParser(object):
class PythonParser(CodeParser):
"""Parse code to find executable lines, excluded lines, etc."""
- def __init__(self, cu, text=None, filename=None, exclude=None):
+ def __init__(self, text=None, filename=None, exclude=None):
"""
Source can be provided as `text`, the text itself, or `filename`, from
which the text will be read. Excluded lines are those that match
@@ -197,6 +185,24 @@ class PythonParser(CodeParser):
else:
return line
+ def first_lines(self, lines):
+ """Map the line numbers in `lines` to the correct first line of the
+ statement.
+
+ Returns a set of the first lines.
+
+ """
+ return set(self.first_line(l) for l in lines)
+
+ def translate_lines(self, lines):
+ return self.first_lines(lines)
+
+ def translate_arcs(self, arcs):
+ return [
+ (self.first_line(a), self.first_line(b))
+ for (a, b) in arcs
+ ]
+
def parse_source(self):
"""Parse source text to find executable lines, excluded lines, etc.
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index e79ce01f..867388f7 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -120,7 +120,7 @@ def source_encoding(source):
# This is mostly code adapted from Py3.2's tokenize module.
- cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)")
+ cookie_re = re.compile(r"^\s*#.*coding[:=]\s*([-\w.]+)")
# Do this so the detect_encode code we copied will work.
readline = iter(source.splitlines(True)).next
diff --git a/coverage/report.py b/coverage/report.py
index 34f44422..7627d1aa 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -1,8 +1,8 @@
"""Reporter foundation for Coverage."""
-import fnmatch, os
+import os
from coverage.codeunit import code_unit_factory
-from coverage.files import prep_patterns
+from coverage.files import prep_patterns, FnmatchMatcher
from coverage.misc import CoverageException, NoSource, NotPython
class Reporter(object):
@@ -33,26 +33,24 @@ class Reporter(object):
"""
morfs = morfs or self.coverage.data.measured_files()
file_locator = self.coverage.file_locator
- self.code_units = code_unit_factory(morfs, file_locator)
+ get_ext = self.coverage.data.extension_data().get
+ self.code_units = code_unit_factory(morfs, file_locator, get_ext)
if self.config.include:
patterns = prep_patterns(self.config.include)
+ matcher = FnmatchMatcher(patterns)
filtered = []
for cu in self.code_units:
- for pattern in patterns:
- if fnmatch.fnmatch(cu.filename, pattern):
- filtered.append(cu)
- break
+ if matcher.match(cu.filename):
+ filtered.append(cu)
self.code_units = filtered
if self.config.omit:
patterns = prep_patterns(self.config.omit)
+ matcher = FnmatchMatcher(patterns)
filtered = []
for cu in self.code_units:
- for pattern in patterns:
- if fnmatch.fnmatch(cu.filename, pattern):
- break
- else:
+ if not matcher.match(cu.filename):
filtered.append(cu)
self.code_units = filtered
diff --git a/coverage/results.py b/coverage/results.py
index 08329766..6cbcbfc8 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -14,11 +14,7 @@ class Analysis(object):
self.code_unit = code_unit
self.filename = self.code_unit.filename
- actual_filename, source = self.code_unit.find_source(self.filename)
-
- self.parser = code_unit.parser_class(
- code_unit,
- text=source, filename=actual_filename,
+ self.parser = code_unit.get_parser(
exclude=self.coverage._exclude_regex('exclude')
)
self.statements, self.excluded = self.parser.parse_source()
@@ -26,7 +22,6 @@ class Analysis(object):
# Identify missing statements.
executed = self.coverage.data.executed_lines(self.filename)
executed = self.parser.translate_lines(executed)
- executed = self.parser.first_lines(executed)
self.missing = self.statements - executed
if self.coverage.data.has_arcs():
@@ -74,8 +69,7 @@ class Analysis(object):
def arcs_executed(self):
"""Returns a sorted list of the arcs actually executed in the code."""
executed = self.coverage.data.executed_arcs(self.filename)
- m2fl = self.parser.first_line
- executed = ((m2fl(l1), m2fl(l2)) for (l1,l2) in executed)
+ executed = self.parser.translate_arcs(executed)
return sorted(executed)
def arcs_missing(self):
@@ -89,6 +83,23 @@ class Analysis(object):
)
return sorted(missing)
+ def arcs_missing_formatted(self):
+ """ The missing branch arcs, formatted nicely.
+
+ Returns a string like "1->2, 1->3, 16->20". Omits any mention of
+ missing lines, so if line 17 is missing, then 16->17 won't be included.
+
+ """
+ arcs = self.missing_branch_arcs()
+ missing = self.missing
+ line_exits = sorted(iitems(arcs))
+ pairs = []
+ for line, exits in line_exits:
+ for ex in sorted(exits):
+ if line not in missing and ex not in missing:
+ pairs.append('%d->%d' % (line, ex))
+ return ', '.join(pairs)
+
def arcs_unpredicted(self):
"""Returns a sorted list of the executed arcs missing from the code."""
possible = self.arc_possibilities()
diff --git a/coverage/summary.py b/coverage/summary.py
index c99c5303..a6768cf9 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -59,7 +59,14 @@ class SummaryReporter(Reporter):
args += (nums.n_branches, nums.n_missing_branches)
args += (nums.pc_covered_str,)
if self.config.show_missing:
- args += (analysis.missing_formatted(),)
+ missing_fmtd = analysis.missing_formatted()
+ if self.branches:
+ branches_fmtd = analysis.arcs_missing_formatted()
+ if branches_fmtd:
+ if missing_fmtd:
+ missing_fmtd += ", "
+ missing_fmtd += branches_fmtd
+ args += (missing_fmtd,)
outfile.write(fmt_coverage % args)
total += nums
except KeyboardInterrupt: # pragma: not covered
diff --git a/coverage/templite.py b/coverage/templite.py
index a71caf63..53824e08 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -15,7 +15,7 @@ class CodeBuilder(object):
def __init__(self, indent=0):
self.code = []
- self.ident_level = indent
+ self.indent_level = indent
def __str__(self):
return "".join(str(c) for c in self.code)
@@ -26,28 +26,28 @@ class CodeBuilder(object):
Indentation and newline will be added for you, don't provide them.
"""
- self.code.extend([" " * self.ident_level, line, "\n"])
+ self.code.extend([" " * self.indent_level, line, "\n"])
- def add_subbuilder(self):
+ def add_section(self):
"""Add a section, a sub-CodeBuilder."""
- sect = CodeBuilder(self.ident_level)
- self.code.append(sect)
- return sect
+ section = CodeBuilder(self.indent_level)
+ self.code.append(section)
+ return section
INDENT_STEP = 4 # PEP8 says so!
def indent(self):
"""Increase the current indent for following lines."""
- self.ident_level += self.INDENT_STEP
+ self.indent_level += self.INDENT_STEP
def dedent(self):
"""Decrease the current indent for following lines."""
- self.ident_level -= self.INDENT_STEP
+ self.indent_level -= self.INDENT_STEP
def get_globals(self):
- """Compile the code, and return a dict of globals it defines."""
+ """Execute the code, and return a dict of globals it defines."""
# A check that the caller really finished all the blocks they started.
- assert self.ident_level == 0
+ assert self.indent_level == 0
# Get the Python source as a single string.
python_source = str(self)
# Execute the source, defining globals, and return them.
@@ -110,21 +110,21 @@ class Templite(object):
# it, and execute it to render the template.
code = CodeBuilder()
- code.add_line("def render_function(ctx, do_dots):")
+ code.add_line("def render_function(context, do_dots):")
code.indent()
- vars_code = code.add_subbuilder()
+ vars_code = code.add_section()
code.add_line("result = []")
- code.add_line("a = result.append")
- code.add_line("e = result.extend")
- code.add_line("s = str")
+ code.add_line("append_result = result.append")
+ code.add_line("extend_result = result.extend")
+ code.add_line("to_str = str")
buffered = []
def flush_output():
"""Force `buffered` to the code builder."""
if len(buffered) == 1:
- code.add_line("a(%s)" % buffered[0])
+ code.add_line("append_result(%s)" % buffered[0])
elif len(buffered) > 1:
- code.add_line("e([%s])" % ", ".join(buffered))
+ code.add_line("extend_result([%s])" % ", ".join(buffered))
del buffered[:]
ops_stack = []
@@ -138,7 +138,8 @@ class Templite(object):
continue
elif token.startswith('{{'):
# An expression to evaluate.
- buffered.append("s(%s)" % self._expr_code(token[2:-2].strip()))
+ expr = self._expr_code(token[2:-2].strip())
+ buffered.append("to_str(%s)" % expr)
elif token.startswith('{%'):
# Action tag: split into words and parse further.
flush_output()
@@ -187,7 +188,7 @@ class Templite(object):
flush_output()
for var_name in self.all_vars - self.loop_vars:
- vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name))
+ vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
code.add_line("return ''.join(result)")
code.dedent()
@@ -234,10 +235,10 @@ class Templite(object):
"""
# Make the complete context we'll use.
- ctx = dict(self.context)
+ render_context = dict(self.context)
if context:
- ctx.update(context)
- return self._render_function(ctx, self._do_dots)
+ render_context.update(context)
+ return self._render_function(render_context, self._do_dots)
def _do_dots(self, value, *dots):
"""Evaluate dotted expressions at runtime."""
diff --git a/coverage/tracer.c b/coverage/tracer.c
index 97dd113b..ca8d61c1 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -259,6 +259,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
int ret = RET_OK;
PyObject * filename = NULL;
PyObject * tracename = NULL;
+ PyObject * disposition = NULL;
#if WHAT_LOG || TRACE_LOG
PyObject * ascii = NULL;
#endif
@@ -335,41 +336,51 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
/* Check if we should trace this line. */
filename = frame->f_code->co_filename;
- tracename = PyDict_GetItem(self->should_trace_cache, filename);
- if (tracename == NULL) {
+ disposition = PyDict_GetItem(self->should_trace_cache, filename);
+ if (disposition == NULL) {
STATS( self->stats.new_files++; )
/* We've never considered this file before. */
/* Ask should_trace about it. */
PyObject * args = Py_BuildValue("(OO)", filename, frame);
- tracename = PyObject_Call(self->should_trace, args, NULL);
+ disposition = PyObject_Call(self->should_trace, args, NULL);
Py_DECREF(args);
- if (tracename == NULL) {
+ if (disposition == NULL) {
/* An error occurred inside should_trace. */
STATS( self->stats.errors++; )
return RET_ERROR;
}
- if (PyDict_SetItem(self->should_trace_cache, filename, tracename) < 0) {
+ if (PyDict_SetItem(self->should_trace_cache, filename, disposition) < 0) {
STATS( self->stats.errors++; )
return RET_ERROR;
}
}
else {
- Py_INCREF(tracename);
+ Py_INCREF(disposition);
}
/* If tracename is a string, then we're supposed to trace. */
+ tracename = PyObject_GetAttrString(disposition, "filename");
+ if (tracename == NULL) {
+ STATS( self->stats.errors++; )
+ Py_DECREF(disposition);
+ return RET_ERROR;
+ }
if (MyText_Check(tracename)) {
PyObject * file_data = PyDict_GetItem(self->data, tracename);
if (file_data == NULL) {
file_data = PyDict_New();
if (file_data == NULL) {
STATS( self->stats.errors++; )
+ Py_DECREF(tracename);
+ Py_DECREF(disposition);
return RET_ERROR;
}
ret = PyDict_SetItem(self->data, tracename, file_data);
Py_DECREF(file_data);
if (ret < 0) {
STATS( self->stats.errors++; )
+ Py_DECREF(tracename);
+ Py_DECREF(disposition);
return RET_ERROR;
}
}
@@ -385,6 +396,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
}
Py_DECREF(tracename);
+ Py_DECREF(disposition);
self->last_line = -1;
break;
diff --git a/doc/config.rst b/doc/config.rst
index 7ff82021..882fc777 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -41,7 +41,7 @@ Boolean values can be specified as ``on``, ``off``, ``true``, ``false``, ``1``,
or ``0`` and are case-insensitive.
Environment variables can be substituted in by using dollar signs: ``$WORD``
-``${WORD}`` will be replaced with the value of ``WORD`` in the environment.
+or ``${WORD}`` will be replaced with the value of ``WORD`` in the environment.
A dollar sign can be inserted with ``$$``. Missing environment variables
will result in empty strings with no error.
diff --git a/doc/faq.rst b/doc/faq.rst
index 78db591f..d7ae3641 100644
--- a/doc/faq.rst
+++ b/doc/faq.rst
@@ -50,6 +50,19 @@ If you are using the :ref:`API <api>`, you need to call coverage.start() before
importing the modules that define your functions.
+**Q: Coverage is much slower than I remember, what's going on?**
+
+Make sure you are using the C trace function. Coverage.py provides two
+implementations of the trace function. The C implementation runs much faster.
+To see what you are running, use ``coverage debug sys``. The output contains
+details of the environment, including a line that says either ``tracer: CTracer``
+or ``tracer: PyTracer``. If it says ``PyTracer`` then you are using the
+slow Python implementation.
+
+Try re-installing coverage.py to see what happened and if you get the CTracer
+as you should.
+
+
**Q: Does coverage.py work on Python 3.x?**
Yes, Python 3 is fully supported.
diff --git a/igor.py b/igor.py
index f7879d7c..e0071bd8 100644
--- a/igor.py
+++ b/igor.py
@@ -12,9 +12,13 @@ import os
import platform
import socket
import sys
+import warnings
import zipfile
+warnings.simplefilter("default")
+
+
# Functions named do_* are executable from the command line: do_blah is run
# by "python igor.py blah".
diff --git a/tests/backtest.py b/tests/backtest.py
index 89a25536..439493d1 100644
--- a/tests/backtest.py
+++ b/tests/backtest.py
@@ -4,41 +4,31 @@
# (Redefining built-in blah)
# The whole point of this file is to redefine built-ins, so shut up about it.
-import os
+import subprocess
-# Py2 and Py3 don't agree on how to run commands in a subprocess.
-try:
- import subprocess
-except ImportError:
- def run_command(cmd, status=0):
- """Run a command in a subprocess.
-
- Returns the exit status code and the combined stdout and stderr.
- """
- _, stdouterr = os.popen4(cmd)
- return status, stdouterr.read()
+# This isn't really a backward compatibility thing, should be moved into a
+# helpers file or something.
+def run_command(cmd):
+ """Run a command in a subprocess.
-else:
- def run_command(cmd, status=0):
- """Run a command in a subprocess.
+ Returns the exit status code and the combined stdout and stderr.
- Returns the exit status code and the combined stdout and stderr.
+ """
+ proc = subprocess.Popen(cmd, shell=True,
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ output, _ = proc.communicate()
+ status = proc.returncode # pylint: disable=E1101
- """
- proc = subprocess.Popen(cmd, shell=True,
- stdin=subprocess.PIPE, stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT
- )
- output, _ = proc.communicate()
- status = proc.returncode # pylint: disable=E1101
+ # Get the output, and canonicalize it to strings with newlines.
+ if not isinstance(output, str):
+ output = output.decode('utf-8')
+ output = output.replace('\r', '')
- # Get the output, and canonicalize it to strings with newlines.
- if not isinstance(output, str):
- output = output.decode('utf-8')
- output = output.replace('\r', '')
+ return status, output
- return status, output
# No more execfile in Py3
try:
@@ -46,4 +36,6 @@ try:
except NameError:
def execfile(filename, globs):
"""A Python 3 implementation of execfile."""
- exec(compile(open(filename).read(), filename, 'exec'), globs)
+ with open(filename) as fobj:
+ code = fobj.read()
+ exec(compile(code, filename, 'exec'), globs)
diff --git a/tests/backunittest.py b/tests/backunittest.py
index ca741d37..6498397f 100644
--- a/tests/backunittest.py
+++ b/tests/backunittest.py
@@ -8,9 +8,9 @@ except ImportError:
import unittest
-def _need(method):
- """Do we need to define our own `method` method?"""
- return not hasattr(unittest.TestCase, method)
+def unittest_has(method):
+ """Does `unitttest.TestCase` have `method` defined?"""
+ return hasattr(unittest.TestCase, method)
class TestCase(unittest.TestCase):
@@ -20,7 +20,22 @@ class TestCase(unittest.TestCase):
`unittest` doesn't have them.
"""
- if _need('assertSameElements'):
- def assertSameElements(self, s1, s2):
- """Assert that the two arguments are equal as sets."""
- self.assertEqual(set(s1), set(s2))
+ # pylint: disable=missing-docstring
+
+ if not unittest_has('assertCountEqual'):
+ if unittest_has('assertSameElements'):
+ def assertCountEqual(self, *args, **kwargs):
+ # pylint: disable=no-member
+ return self.assertSameElements(*args, **kwargs)
+ else:
+ def assertCountEqual(self, s1, s2):
+ """Assert these have the same elements, regardless of order."""
+ self.assertEqual(set(s1), set(s2))
+
+ if not unittest_has('assertRaisesRegex'):
+ def assertRaisesRegex(self, *args, **kwargs):
+ return self.assertRaisesRegexp(*args, **kwargs)
+
+ if not unittest_has('assertRegex'):
+ def assertRegex(self, *args, **kwargs):
+ return self.assertRegexpMatches(*args, **kwargs)
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index a309f17d..1eedad39 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -1,10 +1,11 @@
"""Base test case class for coverage testing."""
-import glob, imp, os, random, shlex, shutil, sys, tempfile, textwrap
+import glob, os, random, re, shlex, shutil, sys, tempfile, textwrap
import atexit, collections
import coverage
-from coverage.backward import StringIO, to_bytes
+from coverage.backward import StringIO, to_bytes, import_local_file
+from coverage.backward import importlib # pylint: disable=unused-import
from coverage.control import _TEST_NAME_FILE
from tests.backtest import run_command
from tests.backunittest import TestCase
@@ -221,18 +222,7 @@ class CoverageTest(TestCase):
as `modname`, and returns the module object.
"""
- modfile = modname + '.py'
-
- for suff in imp.get_suffixes():
- if suff[0] == '.py':
- break
-
- with open(modfile, 'r') as f:
- # pylint: disable=W0631
- # (Using possibly undefined loop variable 'suff')
- mod = imp.load_module(modname, f, modfile, suff)
-
- return mod
+ return import_local_file(modname)
def start_import_stop(self, cov, modname):
"""Start coverage, import a file, then stop coverage.
@@ -365,19 +355,21 @@ class CoverageTest(TestCase):
if statements == line_list:
break
else:
- self.fail("None of the lines choices matched %r" %
- statements
+ self.fail(
+ "None of the lines choices matched %r" % statements
)
+ missing_formatted = analysis.missing_formatted()
if type(missing) == type(""):
- self.assertEqual(analysis.missing_formatted(), missing)
+ self.assertEqual(missing_formatted, missing)
else:
for missing_list in missing:
- if analysis.missing_formatted() == missing_list:
+ if missing_formatted == missing_list:
break
else:
- self.fail("None of the missing choices matched %r" %
- analysis.missing_formatted()
+ self.fail(
+ "None of the missing choices matched %r" %
+ missing_formatted
)
if arcs is not None:
@@ -412,17 +404,17 @@ class CoverageTest(TestCase):
"""Assert that `flist1` and `flist2` are the same set of file names."""
flist1_nice = [self.nice_file(f) for f in flist1]
flist2_nice = [self.nice_file(f) for f in flist2]
- self.assertSameElements(flist1_nice, flist2_nice)
+ self.assertCountEqual(flist1_nice, flist2_nice)
def assert_exists(self, fname):
"""Assert that `fname` is a file that exists."""
msg = "File %r should exist" % fname
- self.assert_(os.path.exists(fname), msg)
+ self.assertTrue(os.path.exists(fname), msg)
def assert_doesnt_exist(self, fname):
"""Assert that `fname` is a file that doesn't exist."""
msg = "File %r shouldn't exist" % fname
- self.assert_(not os.path.exists(fname), msg)
+ self.assertTrue(not os.path.exists(fname), msg)
def assert_starts_with(self, s, prefix, msg=None):
"""Assert that `s` starts with `prefix`."""
@@ -466,7 +458,7 @@ class CoverageTest(TestCase):
_, output = self.run_command_status(cmd)
return output
- def run_command_status(self, cmd, status=0):
+ def run_command_status(self, cmd):
"""Run the command-line `cmd` in a subprocess, and print its output.
Use this when you need to test the process behavior of coverage.
@@ -475,9 +467,6 @@ class CoverageTest(TestCase):
Returns a pair: the process' exit status and stdout text.
- The `status` argument is returned as the status on older Pythons where
- we can't get the actual exit status of the process.
-
"""
# Add our test modules directory to PYTHONPATH. I'm sure there's too
# much path munging here, but...
@@ -490,10 +479,35 @@ class CoverageTest(TestCase):
pypath += testmods + os.pathsep + zipfile
self.set_environ('PYTHONPATH', pypath)
- status, output = run_command(cmd, status=status)
+ status, output = run_command(cmd)
print(output)
return status, output
+ def report_from_command(self, cmd):
+ """Return the report from the `cmd`, with some convenience added."""
+ report = self.run_command(cmd).replace('\\', '/')
+ self.assertNotIn("error", report.lower())
+ return report
+
+ def report_lines(self, report):
+ """Return the lines of the report, as a list."""
+ lines = report.split('\n')
+ self.assertEqual(lines[-1], "")
+ return lines[:-1]
+
+ def line_count(self, report):
+ """How many lines are in `report`?"""
+ return len(self.report_lines(report))
+
+ def squeezed_lines(self, report):
+ """Return a list of the lines in report, with the spaces squeezed."""
+ lines = self.report_lines(report)
+ return [re.sub(r"\s+", " ", l.strip()) for l in lines]
+
+ def last_line_squeezed(self, report):
+ """Return the last line of `report` with the spaces squeezed down."""
+ return self.squeezed_lines(report)[-1]
+
# We run some tests in temporary directories, because they may need to make
# files for the tests. But this is expensive, so we can change per-class
# whether a temp dir is used or not. It's easy to forget to set that
diff --git a/tests/farm/annotate/annotate_dir.py b/tests/farm/annotate/annotate_dir.py
index 3e37f9ed..86c18cab 100644
--- a/tests/farm/annotate/annotate_dir.py
+++ b/tests/farm/annotate/annotate_dir.py
@@ -1,7 +1,7 @@
copy("src", "run")
run("""
- coverage -e -x multi.py
- coverage -a -d out_anno_dir
+ coverage run multi.py
+ coverage annotate -d out_anno_dir
""", rundir="run")
compare("run/out_anno_dir", "gold_anno_dir", "*,cover", left_extra=True)
clean("run")
diff --git a/tests/farm/annotate/run.py b/tests/farm/annotate/run.py
index c645f21c..236f401f 100644
--- a/tests/farm/annotate/run.py
+++ b/tests/farm/annotate/run.py
@@ -1,7 +1,7 @@
copy("src", "out")
run("""
- coverage -e -x white.py
- coverage -a white.py
+ coverage run white.py
+ coverage annotate white.py
""", rundir="out")
compare("out", "gold", "*,cover")
clean("out")
diff --git a/tests/farm/annotate/run_multi.py b/tests/farm/annotate/run_multi.py
index 4e8252ed..ef1e8238 100644
--- a/tests/farm/annotate/run_multi.py
+++ b/tests/farm/annotate/run_multi.py
@@ -1,7 +1,7 @@
copy("src", "out_multi")
run("""
- coverage -e -x multi.py
- coverage -a
+ coverage run multi.py
+ coverage annotate
""", rundir="out_multi")
compare("out_multi", "gold_multi", "*,cover")
clean("out_multi")
diff --git a/tests/farm/run/run_chdir.py b/tests/farm/run/run_chdir.py
index f459f500..367cd0ad 100644
--- a/tests/farm/run/run_chdir.py
+++ b/tests/farm/run/run_chdir.py
@@ -1,7 +1,7 @@
copy("src", "out")
run("""
coverage run chdir.py
- coverage -r
+ coverage report
""", rundir="out", outfile="stdout.txt")
contains("out/stdout.txt",
"Line One",
diff --git a/tests/farm/run/run_timid.py b/tests/farm/run/run_timid.py
index ce78fff1..d4e69a46 100644
--- a/tests/farm/run/run_timid.py
+++ b/tests/farm/run/run_timid.py
@@ -17,8 +17,8 @@ if os.environ.get('COVERAGE_COVERAGE', ''):
copy("src", "out")
run("""
python showtrace.py none
- coverage -e -x showtrace.py regular
- coverage -e -x --timid showtrace.py timid
+ coverage run showtrace.py regular
+ coverage run --timid showtrace.py timid
""", rundir="out", outfile="showtraceout.txt")
# When running without coverage, no trace function
@@ -42,8 +42,8 @@ old_opts = os.environ.get('COVERAGE_OPTIONS')
os.environ['COVERAGE_OPTIONS'] = '--timid'
run("""
- coverage -e -x showtrace.py regular
- coverage -e -x --timid showtrace.py timid
+ coverage run showtrace.py regular
+ coverage run --timid showtrace.py timid
""", rundir="out", outfile="showtraceout.txt")
contains("out/showtraceout.txt",
diff --git a/tests/farm/run/run_xxx.py b/tests/farm/run/run_xxx.py
index 19e94a42..6fedc934 100644
--- a/tests/farm/run/run_xxx.py
+++ b/tests/farm/run/run_xxx.py
@@ -1,7 +1,7 @@
copy("src", "out")
run("""
- coverage -e -x xxx
- coverage -r
+ coverage run xxx
+ coverage report
""", rundir="out", outfile="stdout.txt")
contains("out/stdout.txt",
"xxx: 3 4 0 7",
diff --git a/tests/modules/pkg1/p1a.py b/tests/modules/pkg1/p1a.py
index be5fcdd3..337add49 100644
--- a/tests/modules/pkg1/p1a.py
+++ b/tests/modules/pkg1/p1a.py
@@ -1,5 +1,5 @@
import os, sys
# Invoke functions in os and sys so we can see if we measure code there.
-x = sys.getcheckinterval()
+x = sys.getfilesystemencoding()
y = os.getcwd()
diff --git a/tests/test_api.py b/tests/test_api.py
index 097947d2..31bfc57f 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -100,7 +100,7 @@ class ApiTest(CoverageTest):
"""Assert that the files here are `files`, ignoring the usual junk."""
here = os.listdir(".")
here = self.clean_files(here, ["*.pyc", "__pycache__"])
- self.assertSameElements(here, files)
+ self.assertCountEqual(here, files)
def test_unexecuted_file(self):
cov = coverage.coverage()
@@ -221,7 +221,7 @@ class ApiTest(CoverageTest):
self.assertEqual(cov.get_exclude_list(), ["foo"])
cov.exclude("bar")
self.assertEqual(cov.get_exclude_list(), ["foo", "bar"])
- self.assertEqual(cov._exclude_regex('exclude'), "(foo)|(bar)")
+ self.assertEqual(cov._exclude_regex('exclude'), "(?:foo)|(?:bar)")
cov.clear_exclude()
self.assertEqual(cov.get_exclude_list(), [])
@@ -233,7 +233,9 @@ class ApiTest(CoverageTest):
self.assertEqual(cov.get_exclude_list(which='partial'), ["foo"])
cov.exclude("bar", which='partial')
self.assertEqual(cov.get_exclude_list(which='partial'), ["foo", "bar"])
- self.assertEqual(cov._exclude_regex(which='partial'), "(foo)|(bar)")
+ self.assertEqual(
+ cov._exclude_regex(which='partial'), "(?:foo)|(?:bar)"
+ )
cov.clear_exclude(which='partial')
self.assertEqual(cov.get_exclude_list(which='partial'), [])
diff --git a/tests/test_backward.py b/tests/test_backward.py
index e98017ae..2c688edd 100644
--- a/tests/test_backward.py
+++ b/tests/test_backward.py
@@ -12,7 +12,7 @@ class BackwardTest(TestCase):
def test_iitems(self):
d = {'a': 1, 'b': 2, 'c': 3}
items = [('a', 1), ('b', 2), ('c', 3)]
- self.assertSameElements(list(iitems(d)), items)
+ self.assertCountEqual(list(iitems(d)), items)
def test_binary_bytes(self):
byte_values = [0, 255, 17, 23, 42, 57]
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 99bae516..038e9214 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -754,7 +754,7 @@ class CmdMainTest(CoverageTest):
self.assertEqual(err[-2], 'Exception: oh noes!')
def test_internalraise(self):
- with self.assertRaisesRegexp(ValueError, "coverage is broken"):
+ with self.assertRaisesRegex(ValueError, "coverage is broken"):
coverage.cmdline.main(['internalraise'])
def test_exit(self):
diff --git a/tests/test_codeunit.py b/tests/test_codeunit.py
index e4912e11..fe82ea1c 100644
--- a/tests/test_codeunit.py
+++ b/tests/test_codeunit.py
@@ -31,9 +31,9 @@ class CodeUnitTest(CoverageTest):
self.assertEqual(acu[0].flat_rootname(), "aa_afile")
self.assertEqual(bcu[0].flat_rootname(), "aa_bb_bfile")
self.assertEqual(ccu[0].flat_rootname(), "aa_bb_cc_cfile")
- self.assertEqual(acu[0].source_file().read(), "# afile.py\n")
- self.assertEqual(bcu[0].source_file().read(), "# bfile.py\n")
- self.assertEqual(ccu[0].source_file().read(), "# cfile.py\n")
+ self.assertEqual(acu[0].source(), "# afile.py\n")
+ self.assertEqual(bcu[0].source(), "# bfile.py\n")
+ self.assertEqual(ccu[0].source(), "# cfile.py\n")
def test_odd_filenames(self):
acu = code_unit_factory("aa/afile.odd.py", FileLocator())
@@ -45,9 +45,9 @@ class CodeUnitTest(CoverageTest):
self.assertEqual(acu[0].flat_rootname(), "aa_afile_odd")
self.assertEqual(bcu[0].flat_rootname(), "aa_bb_bfile_odd")
self.assertEqual(b2cu[0].flat_rootname(), "aa_bb_odd_bfile")
- self.assertEqual(acu[0].source_file().read(), "# afile.odd.py\n")
- self.assertEqual(bcu[0].source_file().read(), "# bfile.odd.py\n")
- self.assertEqual(b2cu[0].source_file().read(), "# bfile.py\n")
+ self.assertEqual(acu[0].source(), "# afile.odd.py\n")
+ self.assertEqual(bcu[0].source(), "# bfile.odd.py\n")
+ self.assertEqual(b2cu[0].source(), "# bfile.py\n")
def test_modules(self):
import aa, aa.bb, aa.bb.cc
@@ -58,9 +58,9 @@ class CodeUnitTest(CoverageTest):
self.assertEqual(cu[0].flat_rootname(), "aa")
self.assertEqual(cu[1].flat_rootname(), "aa_bb")
self.assertEqual(cu[2].flat_rootname(), "aa_bb_cc")
- self.assertEqual(cu[0].source_file().read(), "# aa\n")
- self.assertEqual(cu[1].source_file().read(), "# bb\n")
- self.assertEqual(cu[2].source_file().read(), "") # yes, empty
+ self.assertEqual(cu[0].source(), "# aa\n")
+ self.assertEqual(cu[1].source(), "# bb\n")
+ self.assertEqual(cu[2].source(), "") # yes, empty
def test_module_files(self):
import aa.afile, aa.bb.bfile, aa.bb.cc.cfile
@@ -72,9 +72,9 @@ class CodeUnitTest(CoverageTest):
self.assertEqual(cu[0].flat_rootname(), "aa_afile")
self.assertEqual(cu[1].flat_rootname(), "aa_bb_bfile")
self.assertEqual(cu[2].flat_rootname(), "aa_bb_cc_cfile")
- self.assertEqual(cu[0].source_file().read(), "# afile.py\n")
- self.assertEqual(cu[1].source_file().read(), "# bfile.py\n")
- self.assertEqual(cu[2].source_file().read(), "# cfile.py\n")
+ self.assertEqual(cu[0].source(), "# afile.py\n")
+ self.assertEqual(cu[1].source(), "# bfile.py\n")
+ self.assertEqual(cu[2].source(), "# cfile.py\n")
def test_comparison(self):
acu = code_unit_factory("aa/afile.py", FileLocator())[0]
@@ -97,7 +97,7 @@ class CodeUnitTest(CoverageTest):
self.assert_doesnt_exist(egg1.__file__)
cu = code_unit_factory([egg1, egg1.egg1], FileLocator())
- self.assertEqual(cu[0].source_file().read(), "")
- self.assertEqual(cu[1].source_file().read().split("\n")[0],
+ self.assertEqual(cu[0].source(), "")
+ self.assertEqual(cu[1].source().split("\n")[0],
"# My egg file!"
)
diff --git a/tests/test_config.py b/tests/test_config.py
index 7fa31208..7409f4aa 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -125,58 +125,71 @@ class ConfigTest(CoverageTest):
class ConfigFileTest(CoverageTest):
"""Tests of the config file settings in particular."""
- def test_config_file_settings(self):
- # This sample file tries to use lots of variation of syntax...
- self.make_file(".coveragerc", """\
- # This is a settings file for coverage.py
- [run]
- timid = yes
- data_file = something_or_other.dat
- branch = 1
- cover_pylib = TRUE
- parallel = on
- include = a/ , b/
-
- [report]
- ; these settings affect reporting.
- exclude_lines =
- if 0:
-
- pragma:?\\s+no cover
- another_tab
-
- ignore_errors = TRUE
- omit =
- one, another, some_more,
- yet_more
- precision = 3
-
- partial_branches =
- pragma:?\\s+no branch
- partial_branches_always =
- if 0:
- while True:
-
- show_missing= TruE
-
- [html]
-
- directory = c:\\tricky\\dir.somewhere
- extra_css=something/extra.css
- title = Title & nums # nums!
- [xml]
- output=mycov.xml
-
- [paths]
- source =
- .
- /home/ned/src/
-
- other = other, /home/ned/other, c:\\Ned\\etc
-
- """)
- cov = coverage.coverage()
-
+ # This sample file tries to use lots of variation of syntax...
+ # The {section} placeholder lets us nest these settings in another file.
+ LOTSA_SETTINGS = """\
+ # This is a settings file for coverage.py
+ [{section}run]
+ timid = yes
+ data_file = something_or_other.dat
+ branch = 1
+ cover_pylib = TRUE
+ parallel = on
+ include = a/ , b/
+
+ [{section}report]
+ ; these settings affect reporting.
+ exclude_lines =
+ if 0:
+
+ pragma:?\\s+no cover
+ another_tab
+
+ ignore_errors = TRUE
+ omit =
+ one, another, some_more,
+ yet_more
+ precision = 3
+
+ partial_branches =
+ pragma:?\\s+no branch
+ partial_branches_always =
+ if 0:
+ while True:
+
+ show_missing= TruE
+
+ [{section}html]
+
+ directory = c:\\tricky\\dir.somewhere
+ extra_css=something/extra.css
+ title = Title & nums # nums!
+ [{section}xml]
+ output=mycov.xml
+
+ [{section}paths]
+ source =
+ .
+ /home/ned/src/
+
+ other = other, /home/ned/other, c:\\Ned\\etc
+
+ """
+
+ # Just some sample setup.cfg text from the docs.
+ SETUP_CFG = """\
+ [bdist_rpm]
+ release = 1
+ packager = Jane Packager <janep@pysoft.com>
+ doc_files = CHANGES.txt
+ README.txt
+ USAGE.txt
+ doc/
+ examples/
+ """
+
+ def assert_config_settings_are_correct(self, cov):
+ """Check that `cov` has all the settings from LOTSA_SETTINGS."""
self.assertTrue(cov.config.timid)
self.assertEqual(cov.config.data_file, "something_or_other.dat")
self.assertTrue(cov.config.branch)
@@ -211,8 +224,33 @@ class ConfigFileTest(CoverageTest):
'other': ['other', '/home/ned/other', 'c:\\Ned\\etc']
})
+ def test_config_file_settings(self):
+ self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section=""))
+ cov = coverage.coverage()
+ self.assert_config_settings_are_correct(cov)
+
+ def test_config_file_settings_in_setupcfg(self):
+ nested = self.LOTSA_SETTINGS.format(section="coverage:")
+ self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG)
+ cov = coverage.coverage()
+ self.assert_config_settings_are_correct(cov)
+
+ def test_setupcfg_only_if_not_coveragerc(self):
+ self.make_file(".coveragerc", """\
+ [run]
+ include = foo
+ """)
+ self.make_file("setup.cfg", """\
+ [run]
+ omit = bar
+ branch = true
+ """)
+ cov = coverage.coverage()
+ self.assertEqual(cov.config.include, ["foo"])
+ self.assertEqual(cov.config.omit, None)
+ self.assertEqual(cov.config.branch, False)
+
def test_one(self):
- # This sample file tries to use lots of variation of syntax...
self.make_file(".coveragerc", """\
[html]
title = tabblo & «ταБЬℓσ» # numbers
diff --git a/tests/test_coroutine.py b/tests/test_coroutine.py
index 28539801..fe6c8326 100644
--- a/tests/test_coroutine.py
+++ b/tests/test_coroutine.py
@@ -1,5 +1,7 @@
"""Tests for coroutining."""
+import os.path, sys
+
from nose.plugins.skip import SkipTest
import coverage
@@ -20,15 +22,19 @@ except ImportError:
def line_count(s):
- """How many non-blank lines are in `s`?"""
- return sum(1 for l in s.splitlines() if l.strip())
+ """How many non-blank non-comment lines are in `s`?"""
+ def code_line(l):
+ """Is this a code line? Not blank, and not a full-line comment."""
+ return l.strip() and not l.strip().startswith('#')
+ return sum(1 for l in s.splitlines() if code_line(l))
class CoroutineTest(CoverageTest):
"""Tests of the coroutine support in coverage.py."""
- # The code common to all the concurrency models. Don't use any comments,
- # we're counting non-blank lines to see they are all covered.
+ LIMIT = 1000
+
+ # The code common to all the concurrency models.
COMMON = """
class Producer(threading.Thread):
def __init__(self, q):
@@ -36,7 +42,7 @@ class CoroutineTest(CoverageTest):
self.q = q
def run(self):
- for i in range(1000):
+ for i in range({LIMIT}):
self.q.put(i)
self.q.put(None)
@@ -54,28 +60,32 @@ class CoroutineTest(CoverageTest):
break
sum += i
- q = Queue.Queue()
+ q = queue.Queue()
c = Consumer(q)
- c.start()
p = Producer(q)
+ c.start()
p.start()
+
p.join()
c.join()
- """
+ """.format(LIMIT=LIMIT)
# Import the things to use threads.
- THREAD = """\
+ if sys.version_info < (3, 0):
+ THREAD = """\
+ import threading
+ import Queue as queue
+ """ + COMMON
+ else:
+ THREAD = """\
import threading
- try:
- import Queue
- except ImportError: # Python 3 :)
- import queue as Queue
+ import queue
""" + COMMON
# Import the things to use eventlet.
EVENTLET = """\
import eventlet.green.threading as threading
- import eventlet.queue as Queue
+ import eventlet.queue as queue
""" + COMMON
# Import the things to use gevent.
@@ -83,7 +93,7 @@ class CoroutineTest(CoverageTest):
from gevent import monkey
monkey.patch_thread()
import threading
- import gevent.queue as Queue
+ import gevent.queue as queue
""" + COMMON
def try_some_code(self, code, args):
@@ -91,16 +101,21 @@ class CoroutineTest(CoverageTest):
self.make_file("try_it.py", code)
- raise SkipTest("Need to put this on a back burner for a while...")
-
- out = self.run_command("coverage run %s try_it.py" % args)
- expected_out = "%d\n" % (sum(range(1000)))
+ out = self.run_command("coverage run --timid %s try_it.py" % args)
+ expected_out = "%d\n" % (sum(range(self.LIMIT)))
self.assertEqual(out, expected_out)
# Read the coverage file and see that try_it.py has all its lines
# executed.
data = coverage.CoverageData()
data.read_file(".coverage")
+
+ # If the test fails, it's helpful to see this info:
+ fname = os.path.abspath("try_it.py")
+ linenos = data.executed_lines(fname).keys()
+ print("{0}: {1}".format(len(linenos), linenos))
+ print_simple_annotation(code, linenos)
+
lines = line_count(code)
self.assertEqual(data.summary()['try_it.py'], lines)
@@ -114,7 +129,15 @@ class CoroutineTest(CoverageTest):
self.try_some_code(self.EVENTLET, "--coroutine=eventlet")
def test_gevent(self):
+ raise SkipTest("Still not sure why gevent isn't working...")
+
if gevent is None:
raise SkipTest("No gevent available")
self.try_some_code(self.GEVENT, "--coroutine=gevent")
+
+
+def print_simple_annotation(code, linenos):
+ """Print the lines in `code` with X for each line number in `linenos`."""
+ for lineno, line in enumerate(code.splitlines(), start=1):
+ print(" {0:s} {1}".format("X" if lineno in linenos else " ", line))
diff --git a/tests/test_coverage.py b/tests/test_coverage.py
index 33f644fa..565fa4e1 100644
--- a/tests/test_coverage.py
+++ b/tests/test_coverage.py
@@ -46,7 +46,7 @@ class TestCoverageTest(CoverageTest):
def test_failed_coverage(self):
# If the lines are wrong, the message shows right and wrong.
- with self.assertRaisesRegexp(AssertionError, r"\[1, 2] != \[1]"):
+ with self.assertRaisesRegex(AssertionError, r"\[1, 2] != \[1]"):
self.check_coverage("""\
a = 1
b = 2
@@ -55,7 +55,7 @@ class TestCoverageTest(CoverageTest):
)
# If the list of lines possibilities is wrong, the msg shows right.
msg = r"None of the lines choices matched \[1, 2]"
- with self.assertRaisesRegexp(AssertionError, msg):
+ with self.assertRaisesRegex(AssertionError, msg):
self.check_coverage("""\
a = 1
b = 2
@@ -63,7 +63,7 @@ class TestCoverageTest(CoverageTest):
([1], [2])
)
# If the missing lines are wrong, the message shows right and wrong.
- with self.assertRaisesRegexp(AssertionError, r"'3' != '37'"):
+ with self.assertRaisesRegex(AssertionError, r"'3' != '37'"):
self.check_coverage("""\
a = 1
if a == 2:
@@ -74,7 +74,7 @@ class TestCoverageTest(CoverageTest):
)
# If the missing lines possibilities are wrong, the msg shows right.
msg = r"None of the missing choices matched '3'"
- with self.assertRaisesRegexp(AssertionError, msg):
+ with self.assertRaisesRegex(AssertionError, msg):
self.check_coverage("""\
a = 1
if a == 2:
@@ -1671,7 +1671,7 @@ class ReportingTest(CoverageTest):
def test_no_data_to_report_on_annotate(self):
# Reporting with no data produces a nice message and no output dir.
- with self.assertRaisesRegexp(CoverageException, "No data to report."):
+ with self.assertRaisesRegex(CoverageException, "No data to report."):
self.command_line("annotate -d ann")
self.assert_doesnt_exist("ann")
@@ -1681,12 +1681,12 @@ class ReportingTest(CoverageTest):
def test_no_data_to_report_on_html(self):
# Reporting with no data produces a nice message and no output dir.
- with self.assertRaisesRegexp(CoverageException, "No data to report."):
+ with self.assertRaisesRegex(CoverageException, "No data to report."):
self.command_line("html -d htmlcov")
self.assert_doesnt_exist("htmlcov")
def test_no_data_to_report_on_xml(self):
# Reporting with no data produces a nice message.
- with self.assertRaisesRegexp(CoverageException, "No data to report."):
+ with self.assertRaisesRegex(CoverageException, "No data to report."):
self.command_line("xml")
self.assert_doesnt_exist("coverage.xml")
diff --git a/tests/test_data.py b/tests/test_data.py
index 31578f26..b048fd18 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -33,7 +33,7 @@ class DataTest(CoverageTest):
def assert_measured_files(self, covdata, measured):
"""Check that `covdata`'s measured files are `measured`."""
- self.assertSameElements(covdata.measured_files(), measured)
+ self.assertCountEqual(covdata.measured_files(), measured)
def test_reading_empty(self):
covdata = CoverageData()
@@ -96,9 +96,9 @@ class DataTest(CoverageTest):
data = pickle.load(fdata)
lines = data['lines']
- self.assertSameElements(lines.keys(), MEASURED_FILES_1)
- self.assertSameElements(lines['a.py'], A_PY_LINES_1)
- self.assertSameElements(lines['b.py'], B_PY_LINES_1)
+ self.assertCountEqual(lines.keys(), MEASURED_FILES_1)
+ self.assertCountEqual(lines['a.py'], A_PY_LINES_1)
+ self.assertCountEqual(lines['b.py'], B_PY_LINES_1)
# If not measuring branches, there's no arcs entry.
self.assertEqual(data.get('arcs', 'not there'), 'not there')
@@ -111,10 +111,10 @@ class DataTest(CoverageTest):
with open(".coverage", 'rb') as fdata:
data = pickle.load(fdata)
- self.assertSameElements(data['lines'].keys(), [])
+ self.assertCountEqual(data['lines'].keys(), [])
arcs = data['arcs']
- self.assertSameElements(arcs['x.py'], X_PY_ARCS_3)
- self.assertSameElements(arcs['y.py'], Y_PY_ARCS_3)
+ self.assertCountEqual(arcs['x.py'], X_PY_ARCS_3)
+ self.assertCountEqual(arcs['y.py'], Y_PY_ARCS_3)
def test_combining_with_aliases(self):
covdata1 = CoverageData()
diff --git a/tests/test_execfile.py b/tests/test_execfile.py
index 7cd8ac4e..2427847e 100644
--- a/tests/test_execfile.py
+++ b/tests/test_execfile.py
@@ -118,11 +118,11 @@ class RunPycFileTest(CoverageTest):
fpyc.write(binary_bytes([0x2a, 0xeb, 0x0d, 0x0a]))
fpyc.close()
- with self.assertRaisesRegexp(NoCode, "Bad magic number in .pyc file"):
+ with self.assertRaisesRegex(NoCode, "Bad magic number in .pyc file"):
run_python_file(pycfile, [pycfile])
def test_no_such_pyc_file(self):
- with self.assertRaisesRegexp(NoCode, "No file to run: 'xyzzy.pyc'"):
+ with self.assertRaisesRegex(NoCode, "No file to run: 'xyzzy.pyc'"):
run_python_file("xyzzy.pyc", [])
@@ -138,22 +138,27 @@ class RunModuleTest(CoverageTest):
def test_runmod1(self):
run_python_module("runmod1", ["runmod1", "hello"])
+ self.assertEqual(self.stderr(), "")
self.assertEqual(self.stdout(), "runmod1: passed hello\n")
def test_runmod2(self):
run_python_module("pkg1.runmod2", ["runmod2", "hello"])
+ self.assertEqual(self.stderr(), "")
self.assertEqual(self.stdout(), "runmod2: passed hello\n")
def test_runmod3(self):
run_python_module("pkg1.sub.runmod3", ["runmod3", "hello"])
+ self.assertEqual(self.stderr(), "")
self.assertEqual(self.stdout(), "runmod3: passed hello\n")
def test_pkg1_main(self):
run_python_module("pkg1", ["pkg1", "hello"])
+ self.assertEqual(self.stderr(), "")
self.assertEqual(self.stdout(), "pkg1.__main__: passed hello\n")
def test_pkg1_sub_main(self):
run_python_module("pkg1.sub", ["pkg1.sub", "hello"])
+ self.assertEqual(self.stderr(), "")
self.assertEqual(self.stdout(), "pkg1.sub.__main__: passed hello\n")
def test_no_such_module(self):
diff --git a/tests/test_farm.py b/tests/test_farm.py
index 261ce4d0..b2ea3697 100644
--- a/tests/test_farm.py
+++ b/tests/test_farm.py
@@ -15,6 +15,10 @@ def test_farm(clean_only=False):
yield (case,)
+# "rU" was deprecated in 3.4
+READ_MODE = "rU" if sys.version_info < (3, 4) else "r"
+
+
class FarmTestCase(object):
"""A test case from the farm tree.
@@ -22,8 +26,8 @@ class FarmTestCase(object):
copy("src", "out")
run('''
- coverage -x white.py
- coverage -a white.py
+ coverage run white.py
+ coverage annotate white.py
''', rundir="out")
compare("out", "gold", "*,cover")
clean("out")
@@ -258,9 +262,9 @@ class FarmTestCase(object):
# ourselves.
text_diff = []
for f in diff_files:
- with open(os.path.join(dir1, f), "rU") as fobj:
+ with open(os.path.join(dir1, f), READ_MODE) as fobj:
left = fobj.read()
- with open(os.path.join(dir2, f), "rU") as fobj:
+ with open(os.path.join(dir2, f), READ_MODE) as fobj:
right = fobj.read()
if scrubs:
left = self._scrub(left, scrubs)
@@ -280,7 +284,7 @@ class FarmTestCase(object):
def _scrub(self, strdata, scrubs):
"""Scrub uninteresting data from the payload in `strdata`.
- `scrubs is a list of (find, replace) pairs of regexes that are used on
+ `scrubs` is a list of (find, replace) pairs of regexes that are used on
`strdata`. A string is returned.
"""
@@ -295,7 +299,8 @@ class FarmTestCase(object):
missing in `filename`.
"""
- text = open(filename, "r").read()
+ with open(filename, "r") as fobj:
+ text = fobj.read()
for s in strlist:
assert s in text, "Missing content in %s: %r" % (filename, s)
@@ -306,7 +311,8 @@ class FarmTestCase(object):
`filename`.
"""
- text = open(filename, "r").read()
+ with open(filename, "r") as fobj:
+ text = fobj.read()
for s in strlist:
assert s not in text, "Forbidden content in %s: %r" % (filename, s)
diff --git a/tests/test_files.py b/tests/test_files.py
index f93feba7..070430ff 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -93,6 +93,12 @@ class MatcherTest(CoverageTest):
for filepath, matches in matches_to_try:
self.assertMatches(fnm, filepath, matches)
+ def test_fnmatch_matcher_overload(self):
+ fnm = FnmatchMatcher(["*x%03d*.txt" % i for i in range(500)])
+ self.assertMatches(fnm, "x007foo.txt", True)
+ self.assertMatches(fnm, "x123foo.txt", True)
+ self.assertMatches(fnm, "x798bar.txt", False)
+
class PathAliasesTest(CoverageTest):
"""Tests for coverage/files.py:PathAliases"""
@@ -131,11 +137,11 @@ class PathAliasesTest(CoverageTest):
def test_cant_have_wildcard_at_end(self):
aliases = PathAliases()
msg = "Pattern must not end with wildcards."
- with self.assertRaisesRegexp(CoverageException, msg):
+ with self.assertRaisesRegex(CoverageException, msg):
aliases.add("/ned/home/*", "fooey")
- with self.assertRaisesRegexp(CoverageException, msg):
+ with self.assertRaisesRegex(CoverageException, msg):
aliases.add("/ned/home/*/", "fooey")
- with self.assertRaisesRegexp(CoverageException, msg):
+ with self.assertRaisesRegex(CoverageException, msg):
aliases.add("/ned/home/*/*/", "fooey")
def test_no_accidental_munging(self):
@@ -177,7 +183,7 @@ class RelativePathAliasesTest(CoverageTest):
aliases.add(d, '/the/source')
the_file = os.path.join(d, 'a.py')
the_file = os.path.expanduser(the_file)
- the_file = os.path.abspath(the_file)
+ the_file = os.path.abspath(os.path.realpath(the_file))
assert '~' not in the_file # to be sure the test is pure.
self.assertEqual(aliases.map(the_file), '/the/source/a.py')
diff --git a/tests/test_html.py b/tests/test_html.py
index 41859382..8e43e7cf 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Tests that HTML generation is awesome."""
-import os.path, re
+import os.path, re, sys
import coverage
import coverage.html
from coverage.misc import CoverageException, NotPython, NoSource
@@ -42,6 +42,13 @@ class HtmlTestHelpers(CoverageTest):
os.remove("htmlcov/helper1.html")
os.remove("htmlcov/helper2.html")
+ def get_html_report_content(self, module):
+ """Return the content of the HTML report for `module`."""
+ filename = module.replace(".py", ".html").replace("/", "_")
+ filename = os.path.join("htmlcov", filename)
+ with open(filename) as f:
+ return f.read()
+
class HtmlDeltaTest(HtmlTestHelpers, CoverageTest):
"""Tests of the HTML delta speed-ups."""
@@ -208,7 +215,7 @@ class HtmlTitleTest(HtmlTestHelpers, CoverageTest):
)
-class HtmlWithUnparsableFilesTest(CoverageTest):
+class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest):
"""Test the behavior when measuring unparsable files."""
def test_dotpy_not_python(self):
@@ -217,7 +224,7 @@ class HtmlWithUnparsableFilesTest(CoverageTest):
self.start_import_stop(cov, "innocuous")
self.make_file("innocuous.py", "<h1>This isn't python!</h1>")
msg = "Couldn't parse '.*innocuous.py' as Python source: .* at line 1"
- with self.assertRaisesRegexp(NotPython, msg):
+ with self.assertRaisesRegex(NotPython, msg):
cov.html_report()
def test_dotpy_not_python_ignored(self):
@@ -267,6 +274,31 @@ class HtmlWithUnparsableFilesTest(CoverageTest):
cov.html_report()
self.assert_exists("htmlcov/index.html")
+ def test_decode_error(self):
+ # imp.load_module won't load a file with an undecodable character
+ # in a comment, though Python will run them. So we'll change the
+ # file after running.
+ self.make_file("main.py", "import sub.not_ascii")
+ self.make_file("sub/__init__.py")
+ self.make_file("sub/not_ascii.py", """\
+ a = 1 # Isn't this great?!
+ """)
+ cov = coverage.coverage()
+ self.start_import_stop(cov, "main")
+
+ # Create the undecodable version of the file.
+ self.make_file("sub/not_ascii.py", """\
+ a = 1 # Isn't this great?\xcb!
+ """)
+ cov.html_report()
+
+ html_report = self.get_html_report_content("sub/not_ascii.py")
+ if sys.version_info < (3, 0):
+ expected = "# Isn&#39;t this great?&#65533;!"
+ else:
+ expected = "# Isn&#39;t this great?&#203;!"
+ self.assertIn(expected, html_report)
+
class HtmlTest(CoverageTest):
"""Moar HTML tests."""
@@ -283,7 +315,7 @@ class HtmlTest(CoverageTest):
missing_file = os.path.join(self.temp_dir, "sub", "another.py")
missing_file = os.path.realpath(missing_file)
msg = "(?i)No source for code: '%s'" % re.escape(missing_file)
- with self.assertRaisesRegexp(NoSource, msg):
+ with self.assertRaisesRegex(NoSource, msg):
cov.html_report()
class HtmlStaticFileTest(CoverageTest):
@@ -340,5 +372,5 @@ class HtmlStaticFileTest(CoverageTest):
cov = coverage.coverage()
self.start_import_stop(cov, "main")
msg = "Couldn't find static file '.*'"
- with self.assertRaisesRegexp(CoverageException, msg):
+ with self.assertRaisesRegex(CoverageException, msg):
cov.html_report()
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 5b90f342..a392ea03 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -13,7 +13,7 @@ class PythonParserTest(CoverageTest):
def parse_source(self, text):
"""Parse `text` as source, and return the `PythonParser` used."""
text = textwrap.dedent(text)
- parser = PythonParser(None, text=text, exclude="nocover")
+ parser = PythonParser(text=text, exclude="nocover")
parser.parse_source()
return parser
@@ -98,7 +98,7 @@ class ParserFileTest(CoverageTest):
def parse_file(self, filename):
"""Parse `text` as source, and return the `PythonParser` used."""
- parser = PythonParser(None, filename=filename, exclude="nocover")
+ parser = PythonParser(filename=filename, exclude="nocover")
parser.parse_source()
return parser
diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py
index e15400b6..4755c167 100644
--- a/tests/test_phystokens.py
+++ b/tests/test_phystokens.py
@@ -97,6 +97,11 @@ if sys.version_info < (3, 0):
source = "# This Python file uses this encoding: utf-8\n"
self.assertEqual(source_encoding(source), 'utf-8')
+ def test_detect_source_encoding_not_in_comment(self):
+ # Should not detect anything here
+ source = 'def parse(src, encoding=None):\n pass'
+ self.assertEqual(source_encoding(source), 'ascii')
+
def test_detect_source_encoding_on_second_line(self):
# A coding declaration should be found despite a first blank line.
source = "\n# coding=cp850\n\n"
diff --git a/tests/test_process.py b/tests/test_process.py
index fa4759a8..d8314982 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -26,7 +26,7 @@ class ProcessTest(CoverageTest):
""")
self.assert_doesnt_exist(".coverage")
- self.run_command("coverage -x mycode.py")
+ self.run_command("coverage run mycode.py")
self.assert_exists(".coverage")
def test_environment(self):
@@ -39,7 +39,7 @@ class ProcessTest(CoverageTest):
""")
self.assert_doesnt_exist(".coverage")
- out = self.run_command("coverage -x mycode.py")
+ out = self.run_command("coverage run mycode.py")
self.assert_exists(".coverage")
self.assertEqual(out, 'done\n')
@@ -55,11 +55,11 @@ class ProcessTest(CoverageTest):
print('done')
""")
- out = self.run_command("coverage -x -p b_or_c.py b")
+ out = self.run_command("coverage run -p b_or_c.py b")
self.assertEqual(out, 'done\n')
self.assert_doesnt_exist(".coverage")
- out = self.run_command("coverage -x -p b_or_c.py c")
+ out = self.run_command("coverage run -p b_or_c.py c")
self.assertEqual(out, 'done\n')
self.assert_doesnt_exist(".coverage")
@@ -67,7 +67,7 @@ class ProcessTest(CoverageTest):
self.assertEqual(self.number_of_data_files(), 2)
# Combine the parallel coverage data files into .coverage .
- self.run_command("coverage -c")
+ self.run_command("coverage combine")
self.assert_exists(".coverage")
# After combining, there should be only the .coverage file.
@@ -91,23 +91,23 @@ class ProcessTest(CoverageTest):
print('done')
""")
- out = self.run_command("coverage -x -p b_or_c.py b")
+ out = self.run_command("coverage run -p b_or_c.py b")
self.assertEqual(out, 'done\n')
self.assert_doesnt_exist(".coverage")
self.assertEqual(self.number_of_data_files(), 1)
# Combine the (one) parallel coverage data file into .coverage .
- self.run_command("coverage -c")
+ self.run_command("coverage combine")
self.assert_exists(".coverage")
self.assertEqual(self.number_of_data_files(), 1)
- out = self.run_command("coverage -x -p b_or_c.py c")
+ out = self.run_command("coverage run --append -p b_or_c.py c")
self.assertEqual(out, 'done\n')
self.assert_exists(".coverage")
self.assertEqual(self.number_of_data_files(), 2)
# Combine the parallel coverage data files into .coverage .
- self.run_command("coverage -c")
+ self.run_command("coverage combine")
self.assert_exists(".coverage")
# After combining, there should be only the .coverage file.
@@ -229,7 +229,7 @@ class ProcessTest(CoverageTest):
self.run_command("coverage run fleeting.py")
os.remove("fleeting.py")
out = self.run_command("coverage html -d htmlcov")
- self.assertRegexpMatches(out, "No source for code: '.*fleeting.py'")
+ self.assertRegex(out, "No source for code: '.*fleeting.py'")
self.assertNotIn("Traceback", out)
# It happens that the code paths are different for *.py and other
@@ -240,14 +240,14 @@ class ProcessTest(CoverageTest):
self.run_command("coverage run fleeting")
os.remove("fleeting")
- status, out = self.run_command_status("coverage html -d htmlcov", 1)
- self.assertRegexpMatches(out, "No source for code: '.*fleeting'")
+ status, out = self.run_command_status("coverage html -d htmlcov")
+ self.assertRegex(out, "No source for code: '.*fleeting'")
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")
+ status, out = self.run_command_status("coverage run xyzzy.py")
+ self.assertRegex(out, "No file to run: .*xyzzy.py")
self.assertNotIn("raceback", out)
self.assertNotIn("rror", out)
self.assertEqual(status, 1)
@@ -265,7 +265,7 @@ class ProcessTest(CoverageTest):
# The important thing is for "coverage run" and "python" to report the
# same traceback.
- status, out = self.run_command_status("coverage run throw.py", 1)
+ status, out = self.run_command_status("coverage run throw.py")
out2 = self.run_command("python throw.py")
if '__pypy__' in sys.builtin_module_names:
# Pypy has an extra frame in the traceback for some reason
@@ -294,8 +294,8 @@ class ProcessTest(CoverageTest):
# The important thing is for "coverage run" and "python" to have the
# same output. No traceback.
- status, out = self.run_command_status("coverage run exit.py", 17)
- status2, out2 = self.run_command_status("python exit.py", 17)
+ status, out = self.run_command_status("coverage run exit.py")
+ status2, out2 = self.run_command_status("python exit.py")
self.assertMultiLineEqual(out, out2)
self.assertMultiLineEqual(out, "about to exit..\n")
self.assertEqual(status, status2)
@@ -310,8 +310,8 @@ class ProcessTest(CoverageTest):
f1()
""")
- status, out = self.run_command_status("coverage run exit_none.py", 0)
- status2, out2 = self.run_command_status("python exit_none.py", 0)
+ status, out = self.run_command_status("coverage run exit_none.py")
+ status2, out2 = self.run_command_status("python exit_none.py")
self.assertMultiLineEqual(out, out2)
self.assertMultiLineEqual(out, "about to exit quietly..\n")
self.assertEqual(status, status2)
@@ -378,7 +378,7 @@ class ProcessTest(CoverageTest):
self.assertEqual(self.number_of_data_files(), 2)
# Combine the parallel coverage data files into .coverage .
- self.run_command("coverage -c")
+ self.run_command("coverage combine")
self.assert_exists(".coverage")
# After combining, there should be only the .coverage file.
@@ -502,6 +502,18 @@ class ProcessTest(CoverageTest):
# about 5.
self.assertGreater(data.summary()['os.py'], 50)
+ def test_deprecation_warnings(self):
+ # Test that coverage doesn't trigger deprecation warnings.
+ # https://bitbucket.org/ned/coveragepy/issue/305/pendingdeprecationwarning-the-imp-module
+ self.make_file("allok.py", """\
+ import warnings
+ warnings.simplefilter('default')
+ import coverage
+ print("No warnings!")
+ """)
+ out = self.run_command("python allok.py")
+ self.assertEqual(out, "No warnings!\n")
+
class AliasedCommandTest(CoverageTest):
"""Tests of the version-specific command aliases."""
@@ -556,32 +568,47 @@ class FailUnderTest(CoverageTest):
def setUp(self):
super(FailUnderTest, self).setUp()
- self.make_file("fifty.py", """\
- # I have 50% coverage!
+ self.make_file("forty_two_plus.py", """\
+ # I have 42.857% (3/7) coverage!
a = 1
- if a > 2:
- b = 3
- c = 4
+ b = 2
+ if a > 3:
+ b = 4
+ c = 5
+ d = 6
+ e = 7
""")
- st, _ = self.run_command_status("coverage run fifty.py", 0)
+ st, _ = self.run_command_status("coverage run forty_two_plus.py")
+ self.assertEqual(st, 0)
+ st, out = self.run_command_status("coverage report")
self.assertEqual(st, 0)
+ self.assertEqual(
+ self.last_line_squeezed(out),
+ "forty_two_plus 7 4 43%"
+ )
def test_report(self):
- st, _ = self.run_command_status("coverage report --fail-under=50", 0)
+ st, _ = self.run_command_status("coverage report --fail-under=42")
+ self.assertEqual(st, 0)
+ st, _ = self.run_command_status("coverage report --fail-under=43")
self.assertEqual(st, 0)
- st, _ = self.run_command_status("coverage report --fail-under=51", 2)
+ st, _ = self.run_command_status("coverage report --fail-under=44")
self.assertEqual(st, 2)
def test_html_report(self):
- st, _ = self.run_command_status("coverage html --fail-under=50", 0)
+ st, _ = self.run_command_status("coverage html --fail-under=42")
self.assertEqual(st, 0)
- st, _ = self.run_command_status("coverage html --fail-under=51", 2)
+ st, _ = self.run_command_status("coverage html --fail-under=43")
+ self.assertEqual(st, 0)
+ st, _ = self.run_command_status("coverage html --fail-under=44")
self.assertEqual(st, 2)
def test_xml_report(self):
- st, _ = self.run_command_status("coverage xml --fail-under=50", 0)
+ st, _ = self.run_command_status("coverage xml --fail-under=42")
+ self.assertEqual(st, 0)
+ st, _ = self.run_command_status("coverage xml --fail-under=43")
self.assertEqual(st, 0)
- st, _ = self.run_command_status("coverage xml --fail-under=51", 2)
+ st, _ = self.run_command_status("coverage xml --fail-under=44")
self.assertEqual(st, 2)
diff --git a/tests/test_summary.py b/tests/test_summary.py
index 29167bf8..7bd1c496 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -21,26 +21,10 @@ class SummaryTest(CoverageTest):
# Parent class saves and restores sys.path, we can just modify it.
sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules'))
- def report_from_command(self, cmd):
- """Return the report from the `cmd`, with some convenience added."""
- report = self.run_command(cmd).replace('\\', '/')
- self.assertNotIn("error", report.lower())
- return report
-
- def line_count(self, report):
- """How many lines are in `report`?"""
- self.assertEqual(report.split('\n')[-1], "")
- return len(report.split('\n')) - 1
-
- def last_line_squeezed(self, report):
- """Return the last line of `report` with the spaces squeezed down."""
- last_line = report.split('\n')[-2]
- return re.sub(r"\s+", " ", last_line)
-
def test_report(self):
- out = self.run_command("coverage -x mycode.py")
+ out = self.run_command("coverage run mycode.py")
self.assertEqual(out, 'done\n')
- report = self.report_from_command("coverage -r")
+ report = self.report_from_command("coverage report")
# Name Stmts Miss Cover
# ---------------------------------------------------------------------
@@ -58,8 +42,24 @@ class SummaryTest(CoverageTest):
def test_report_just_one(self):
# Try reporting just one module
- self.run_command("coverage -x mycode.py")
- report = self.report_from_command("coverage -r mycode.py")
+ self.run_command("coverage run mycode.py")
+ report = self.report_from_command("coverage report mycode.py")
+
+ # Name Stmts Miss Cover
+ # ----------------------------
+ # mycode 4 0 100%
+
+ self.assertEqual(self.line_count(report), 3)
+ self.assertNotIn("/coverage/", report)
+ self.assertNotIn("/tests/modules/covmod1 ", report)
+ self.assertNotIn("/tests/zipmods.zip/covmodzip1 ", report)
+ self.assertIn("mycode ", report)
+ self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%")
+
+ def test_report_wildcard(self):
+ # Try reporting using wildcards to get the modules.
+ self.run_command("coverage run mycode.py")
+ report = self.report_from_command("coverage report my*.py")
# Name Stmts Miss Cover
# ----------------------------
@@ -75,8 +75,10 @@ class SummaryTest(CoverageTest):
def test_report_omitting(self):
# Try reporting while omitting some modules
prefix = os.path.split(__file__)[0]
- self.run_command("coverage -x mycode.py")
- report = self.report_from_command("coverage -r -o '%s/*'" % prefix)
+ self.run_command("coverage run mycode.py")
+ report = self.report_from_command(
+ "coverage report --omit '%s/*'" % prefix
+ )
# Name Stmts Miss Cover
# ----------------------------
@@ -126,13 +128,109 @@ class SummaryTest(CoverageTest):
self.assertEqual(self.last_line_squeezed(report),
"mybranch 5 0 2 1 86%")
+ def test_report_show_missing(self):
+ self.make_file("mymissing.py", """\
+ def missing(x, y):
+ if x:
+ print("x")
+ return x
+ if y:
+ print("y")
+ try:
+ print("z")
+ 1/0
+ print("Never!")
+ except ZeroDivisionError:
+ pass
+ return x
+ missing(0, 1)
+ """)
+ out = self.run_command("coverage run mymissing.py")
+ self.assertEqual(out, 'y\nz\n')
+ report = self.report_from_command("coverage report --show-missing")
+
+ # Name Stmts Miss Cover Missing
+ # -----------------------------------------
+ # mymissing 14 3 79% 3-4, 10
+
+ self.assertEqual(self.line_count(report), 3)
+ self.assertIn("mymissing ", report)
+ self.assertEqual(self.last_line_squeezed(report),
+ "mymissing 14 3 79% 3-4, 10")
+
+ def test_report_show_missing_branches(self):
+ self.make_file("mybranch.py", """\
+ def branch(x, y):
+ if x:
+ print("x")
+ if y:
+ print("y")
+ return x
+ branch(1, 1)
+ """)
+ out = self.run_command("coverage run --branch mybranch.py")
+ self.assertEqual(out, 'x\ny\n')
+ report = self.report_from_command("coverage report --show-missing")
+
+ # Name Stmts Miss Branch BrMiss Cover Missing
+ # -------------------------------------------------------
+ # mybranch 7 0 4 2 82% 2->4, 4->6
+
+ self.assertEqual(self.line_count(report), 3)
+ self.assertIn("mybranch ", report)
+ self.assertEqual(self.last_line_squeezed(report),
+ "mybranch 7 0 4 2 82% 2->4, 4->6")
+
+ def test_report_show_missing_branches_and_lines(self):
+ self.make_file("main.py", """\
+ import mybranch
+ """)
+ self.make_file("mybranch.py", """\
+ def branch(x, y, z):
+ if x:
+ print("x")
+ if y:
+ print("y")
+ if z:
+ if x and y:
+ print("z")
+ return x
+ branch(1, 1, 0)
+ """)
+ out = self.run_command("coverage run --branch main.py")
+ self.assertEqual(out, 'x\ny\n')
+ report = self.report_from_command("coverage report --show-missing")
+
+ # pylint: disable=C0301
+ # Name Stmts Miss Branch BrMiss Cover Missing
+ # -------------------------------------------------------
+ # main 1 0 0 0 100%
+ # mybranch 10 2 8 5 61% 7-8, 2->4, 4->6
+ # -------------------------------------------------------
+ # TOTAL 11 2 8 5 63%
+
+ self.assertEqual(self.line_count(report), 6)
+ squeezed = self.squeezed_lines(report)
+ self.assertEqual(
+ squeezed[2],
+ "main 1 0 0 0 100%"
+ )
+ self.assertEqual(
+ squeezed[3],
+ "mybranch 10 2 8 5 61% 7-8, 2->4, 4->6"
+ )
+ self.assertEqual(
+ squeezed[5],
+ "TOTAL 11 2 8 5 63%"
+ )
+
def test_dotpy_not_python(self):
# We run a .py file, and when reporting, we can't parse it as Python.
# We should get an error message in the report.
self.run_command("coverage run mycode.py")
self.make_file("mycode.py", "This isn't python at all!")
- report = self.report_from_command("coverage -r mycode.py")
+ report = self.report_from_command("coverage report mycode.py")
# pylint: disable=C0301
# Name Stmts Miss Cover
@@ -155,7 +253,7 @@ class SummaryTest(CoverageTest):
# but we've said to ignore errors, so there's no error reported.
self.run_command("coverage run mycode.py")
self.make_file("mycode.py", "This isn't python at all!")
- report = self.report_from_command("coverage -r -i mycode.py")
+ report = self.report_from_command("coverage report -i mycode.py")
# Name Stmts Miss Cover
# ----------------------------
@@ -171,7 +269,7 @@ class SummaryTest(CoverageTest):
self.run_command("coverage run mycode.html")
# Before reporting, change it to be an HTML file.
self.make_file("mycode.html", "<h1>This isn't python at all!</h1>")
- report = self.report_from_command("coverage -r mycode.html")
+ report = self.report_from_command("coverage report mycode.html")
# Name Stmts Miss Cover
# ----------------------------
diff --git a/tests/test_templite.py b/tests/test_templite.py
index 4b1f6e45..a4667a62 100644
--- a/tests/test_templite.py
+++ b/tests/test_templite.py
@@ -35,8 +35,12 @@ class TempliteTest(CoverageTest):
self.assertEqual(actual, result)
def assertSynErr(self, msg):
+ """Assert that a `TempliteSyntaxError` will happen.
+
+ A context manager, and the message should be `msg`.
+ """
pat = "^" + re.escape(msg) + "$"
- return self.assertRaisesRegexp(TempliteSyntaxError, pat)
+ return self.assertRaisesRegex(TempliteSyntaxError, pat)
def test_passthrough(self):
# Strings without variables are passed through unchanged.
diff --git a/tests/test_testing.py b/tests/test_testing.py
index a89a59a9..049a1982 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -12,13 +12,13 @@ class TestingTest(TestCase):
run_in_temp_dir = False
- def test_assert_same_elements(self):
- self.assertSameElements(set(), set())
- self.assertSameElements(set([1,2,3]), set([3,1,2]))
+ def test_assert_count_equal(self):
+ self.assertCountEqual(set(), set())
+ self.assertCountEqual(set([1,2,3]), set([3,1,2]))
with self.assertRaises(AssertionError):
- self.assertSameElements(set([1,2,3]), set())
+ self.assertCountEqual(set([1,2,3]), set())
with self.assertRaises(AssertionError):
- self.assertSameElements(set([1,2,3]), set([4,5,6]))
+ self.assertCountEqual(set([1,2,3]), set([4,5,6]))
class CoverageTestTest(CoverageTest):