summaryrefslogtreecommitdiff
path: root/tests/coveragetest.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/coveragetest.py')
-rw-r--r--tests/coveragetest.py178
1 files changed, 119 insertions, 59 deletions
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index 9410e071..0e6131fb 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -5,13 +5,12 @@
import contextlib
import datetime
-import glob
import os
import random
import re
import shlex
-import shutil
import sys
+import types
from unittest_mixins import (
EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin,
@@ -19,24 +18,51 @@ from unittest_mixins import (
)
import coverage
-from coverage.backunittest import TestCase
+from coverage import env
+from coverage.backunittest import TestCase, unittest
from coverage.backward import StringIO, import_local_file, string_class, shlex_quote
from coverage.cmdline import CoverageScript
-from coverage.debug import _TEST_NAME_FILE, DebugControl
+from coverage.debug import _TEST_NAME_FILE
+from coverage.misc import StopEverything
-from tests.helpers import run_command
+from tests.helpers import run_command, SuperModuleCleaner
# Status returns for the command line.
OK, ERR = 0, 1
+def convert_skip_exceptions(method):
+ """A decorator for test methods to convert StopEverything to SkipTest."""
+ def wrapper(*args, **kwargs):
+ """Run the test method, and convert exceptions."""
+ try:
+ result = method(*args, **kwargs)
+ except StopEverything:
+ raise unittest.SkipTest("StopEverything!")
+ return result
+ return wrapper
+
+
+class SkipConvertingMetaclass(type):
+ """Decorate all test methods to convert StopEverything to SkipTest."""
+ def __new__(mcs, name, bases, attrs):
+ for attr_name, attr_value in attrs.items():
+ if attr_name.startswith('test_') and isinstance(attr_value, types.FunctionType):
+ attrs[attr_name] = convert_skip_exceptions(attr_value)
+
+ return super(SkipConvertingMetaclass, mcs).__new__(mcs, name, bases, attrs)
+
+
+CoverageTestMethodsMixin = SkipConvertingMetaclass('CoverageTestMethodsMixin', (), {})
+
class CoverageTest(
EnvironmentAwareMixin,
StdStreamCapturingMixin,
TempDirMixin,
DelayedAssertionMixin,
- TestCase
+ CoverageTestMethodsMixin,
+ TestCase,
):
"""A base class for coverage.py test cases."""
@@ -46,9 +72,14 @@ class CoverageTest(
# Tell newer unittest implementations to print long helpful messages.
longMessage = True
+ # Let stderr go to stderr, pytest will capture it for us.
+ show_stderr = True
+
def setUp(self):
super(CoverageTest, self).setUp()
+ self.module_cleaner = SuperModuleCleaner()
+
# Attributes for getting info about what happened.
self.last_command_status = None
self.last_command_output = None
@@ -67,25 +98,7 @@ class CoverageTest(
one test.
"""
- # So that we can re-import files, clean them out first.
- self.cleanup_modules()
- # Also have to clean out the .pyc file, since the timestamp
- # resolution is only one second, a changed file might not be
- # picked up.
- for pyc in glob.glob('*.pyc'):
- os.remove(pyc)
- if os.path.exists("__pycache__"):
- shutil.rmtree("__pycache__")
-
- def import_local_file(self, modname, modfile=None):
- """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. `modfile` is the file to
- import if it isn't in the current directory.
-
- """
- return import_local_file(modname, modfile)
+ self.module_cleaner.clean_local_file_imports()
def start_import_stop(self, cov, modname, modfile=None):
"""Start coverage, import a file, then stop coverage.
@@ -100,7 +113,7 @@ class CoverageTest(
cov.start()
try: # pragma: nested
# Import the Python file, executing it.
- mod = self.import_local_file(modname, modfile)
+ mod = import_local_file(modname, modfile)
finally: # pragma: nested
# Stop coverage.py.
cov.stop()
@@ -237,17 +250,17 @@ class CoverageTest(
with self.delayed_assertions():
self.assert_equal_args(
analysis.arc_possibilities(), arcs,
- "Possible arcs differ",
+ "Possible arcs differ: minus is actual, plus is expected"
)
self.assert_equal_args(
analysis.arcs_missing(), arcs_missing,
- "Missing arcs differ"
+ "Missing arcs differ: minus is actual, plus is expected"
)
self.assert_equal_args(
analysis.arcs_unpredicted(), arcs_unpredicted,
- "Unpredicted arcs differ"
+ "Unpredicted arcs differ: minus is actual, plus is expected"
)
if report:
@@ -259,11 +272,27 @@ class CoverageTest(
return cov
@contextlib.contextmanager
- def assert_warnings(self, cov, warnings):
- """A context manager to check that particular warnings happened in `cov`."""
+ def assert_warnings(self, cov, warnings, not_warnings=()):
+ """A context manager to check that particular warnings happened in `cov`.
+
+ `cov` is a Coverage instance. `warnings` is a list of regexes. Every
+ regex must match a warning that was issued by `cov`. It is OK for
+ extra warnings to be issued by `cov` that are not matched by any regex.
+ Warnings that are disabled are still considered issued by this function.
+
+ `not_warnings` is a list of regexes that must not appear in the
+ warnings. This is only checked if there are some positive warnings to
+ test for in `warnings`.
+
+ If `warnings` is empty, then `cov` is not allowed to issue any
+ warnings.
+
+ """
saved_warnings = []
- def capture_warning(msg):
+ def capture_warning(msg, slug=None):
"""A fake implementation of Coverage._warn, to capture warnings."""
+ if slug:
+ msg = "%s (%s)" % (msg, slug)
saved_warnings.append(msg)
original_warn = cov._warn
@@ -274,12 +303,22 @@ class CoverageTest(
except:
raise
else:
- for warning_regex in warnings:
- for saved in saved_warnings:
- if re.search(warning_regex, saved):
- break
- else:
- self.fail("Didn't find warning %r in %r" % (warning_regex, saved_warnings))
+ if warnings:
+ for warning_regex in warnings:
+ for saved in saved_warnings:
+ if re.search(warning_regex, saved):
+ break
+ else:
+ self.fail("Didn't find warning %r in %r" % (warning_regex, saved_warnings))
+ for warning_regex in not_warnings:
+ for saved in saved_warnings:
+ if re.search(warning_regex, saved):
+ self.fail("Found warning %r in %r" % (warning_regex, saved_warnings))
+ else:
+ # No warnings expected. Raise if any warnings happened.
+ if saved_warnings:
+ self.fail("Unexpected warnings: %r" % (saved_warnings,))
+ finally:
cov._warn = original_warn
def nice_file(self, *fparts):
@@ -328,8 +367,7 @@ class CoverageTest(
Returns None.
"""
- script = CoverageScript(_covpkg=_covpkg)
- ret_actual = script.command_line(shlex.split(args))
+ ret_actual = command_line(args, _covpkg=_covpkg)
self.assertEqual(ret_actual, ret)
coverage_command = "coverage"
@@ -373,7 +411,7 @@ class CoverageTest(
"""
# Make sure "python" and "coverage" mean specifically what we want
# them to mean.
- split_commandline = cmd.split(" ", 1)
+ split_commandline = cmd.split()
command_name = split_commandline[0]
command_args = split_commandline[1:]
@@ -383,30 +421,49 @@ class CoverageTest(
# get executed as "python3.3 foo.py". This is important because
# Python 3.x doesn't install as "python", so you might get a Python
# 2 executable instead if you don't use the executable's basename.
- command_name = os.path.basename(sys.executable)
+ command_words = [os.path.basename(sys.executable)]
+
+ elif command_name == "coverage":
+ if env.JYTHON: # pragma: only jython
+ # Jython can't do reporting, so let's skip the test now.
+ if command_args and command_args[0] in ('report', 'html', 'xml', 'annotate'):
+ self.skipTest("Can't run reporting commands in Jython")
+ # Jython can't run "coverage" as a command because the shebang
+ # refers to another shebang'd Python script. So run them as
+ # modules.
+ command_words = "jython -m coverage".split()
+ else:
+ # The invocation requests the Coverage.py program. Substitute the
+ # actual Coverage.py main command name.
+ command_words = [self.coverage_command]
- if command_name == "coverage":
- # The invocation requests the Coverage.py program. Substitute the
- # actual Coverage.py main command name.
- command_name = self.coverage_command
+ else:
+ command_words = [command_name]
- cmd = " ".join([shlex_quote(command_name)] + command_args)
+ cmd = " ".join([shlex_quote(w) for w in command_words] + command_args)
# Add our test modules directory to PYTHONPATH. I'm sure there's too
# much path munging here, but...
- here = os.path.dirname(self.nice_file(coverage.__file__, ".."))
- testmods = self.nice_file(here, 'tests/modules')
- zipfile = self.nice_file(here, 'tests/zipmods.zip')
- pypath = os.getenv('PYTHONPATH', '')
+ pythonpath_name = "PYTHONPATH"
+ if env.JYTHON:
+ pythonpath_name = "JYTHONPATH" # pragma: only jython
+
+ testmods = self.nice_file(self.working_root(), 'tests/modules')
+ zipfile = self.nice_file(self.working_root(), 'tests/zipmods.zip')
+ pypath = os.getenv(pythonpath_name, '')
if pypath:
pypath += os.pathsep
pypath += testmods + os.pathsep + zipfile
- self.set_environ('PYTHONPATH', pypath)
+ self.set_environ(pythonpath_name, pypath)
self.last_command_status, self.last_command_output = run_command(cmd)
print(self.last_command_output)
return self.last_command_status, self.last_command_output
+ def working_root(self):
+ """Where is the root of the coverage.py working tree?"""
+ return os.path.dirname(self.nice_file(coverage.__file__, ".."))
+
def report_from_command(self, cmd):
"""Return the report from the `cmd`, with some convenience added."""
report = self.run_command(cmd).replace('\\', '/')
@@ -433,11 +490,14 @@ class CoverageTest(
return self.squeezed_lines(report)[-1]
-class DebugControlString(DebugControl):
- """A `DebugControl` that writes to a StringIO, for testing."""
- def __init__(self, options):
- super(DebugControlString, self).__init__(options, StringIO())
+def command_line(args, **kwargs):
+ """Run `args` through the CoverageScript command line.
+
+ `kwargs` are the keyword arguments to the CoverageScript constructor.
+
+ Returns the return code from CoverageScript.command_line.
- def get_output(self):
- """Get the output text from the `DebugControl`."""
- return self.output.getvalue()
+ """
+ script = CoverageScript(**kwargs)
+ ret = script.command_line(shlex.split(args))
+ return ret