diff options
-rw-r--r-- | coverage/test_helpers.py | 394 | ||||
-rw-r--r-- | requirements/dev.pip | 1 | ||||
-rw-r--r-- | tests/coveragetest.py | 9 | ||||
-rw-r--r-- | tests/goldtest.py | 2 | ||||
-rw-r--r-- | tests/test_farm.py | 2 | ||||
-rw-r--r-- | tests/test_testing.py | 111 | ||||
-rw-r--r-- | tox.ini | 1 |
7 files changed, 9 insertions, 511 deletions
diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py deleted file mode 100644 index 9649ed5d..00000000 --- a/coverage/test_helpers.py +++ /dev/null @@ -1,394 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -"""Mixin classes to help make good tests.""" - -import atexit -import collections -import contextlib -import os -import random -import shutil -import sys -import tempfile -import textwrap - -from coverage.backunittest import TestCase -from coverage.backward import StringIO, to_bytes - - -class Tee(object): - """A file-like that writes to all the file-likes it has.""" - - def __init__(self, *files): - """Make a Tee that writes to all the files in `files.`""" - self._files = files - if hasattr(files[0], "encoding"): - self.encoding = files[0].encoding - - def write(self, data): - """Write `data` to all the files.""" - for f in self._files: - f.write(data) - - def flush(self): - """Flush the data on all the files.""" - for f in self._files: - f.flush() - - def getvalue(self): - """StringIO file-likes have .getvalue()""" - return self._files[0].getvalue() - - if 0: - # Use this if you need to use a debugger, though it makes some tests - # fail, I'm not sure why... - def __getattr__(self, name): - return getattr(self._files[0], name) - - -@contextlib.contextmanager -def change_dir(new_dir): - """Change directory, and then change back. - - Use as a context manager, it will give you the new directory, and later - restore the old one. - - """ - old_dir = os.getcwd() - os.chdir(new_dir) - try: - yield os.getcwd() - finally: - os.chdir(old_dir) - - -@contextlib.contextmanager -def saved_sys_path(): - """Save sys.path, and restore it later.""" - old_syspath = sys.path[:] - try: - yield - finally: - sys.path = old_syspath - - -def setup_with_context_manager(testcase, cm): - """Use a contextmanager to setUp a test case. - - If you have a context manager you like:: - - with ctxmgr(a, b, c) as v: - # do something with v - - and you want to have that effect for a test case, call this function from - your setUp, and it will start the context manager for your test, and end it - when the test is done:: - - def setUp(self): - self.v = setup_with_context_manager(self, ctxmgr(a, b, c)) - - def test_foo(self): - # do something with self.v - - """ - val = cm.__enter__() - testcase.addCleanup(cm.__exit__, None, None, None) - return val - - -class ModuleAwareMixin(TestCase): - """A test case mixin that isolates changes to sys.modules.""" - - def setUp(self): - super(ModuleAwareMixin, self).setUp() - - # Record sys.modules here so we can restore it in cleanup_modules. - self.old_modules = list(sys.modules) - self.addCleanup(self.cleanup_modules) - - def cleanup_modules(self): - """Remove any new modules imported during the test run. - - This lets us import the same source files for more than one test. - - """ - for m in [m for m in sys.modules if m not in self.old_modules]: - del sys.modules[m] - - -class SysPathAwareMixin(TestCase): - """A test case mixin that isolates changes to sys.path.""" - - def setUp(self): - super(SysPathAwareMixin, self).setUp() - setup_with_context_manager(self, saved_sys_path()) - - -class EnvironmentAwareMixin(TestCase): - """A test case mixin that isolates changes to the environment.""" - - def setUp(self): - super(EnvironmentAwareMixin, self).setUp() - - # Record environment variables that we changed with set_environ. - self.environ_undos = {} - - self.addCleanup(self.cleanup_environ) - - def set_environ(self, name, value): - """Set an environment variable `name` to be `value`. - - The environment variable is set, and record is kept that it was set, - so that `cleanup_environ` can restore its original value. - - """ - if name not in self.environ_undos: - self.environ_undos[name] = os.environ.get(name) - os.environ[name] = value - - def cleanup_environ(self): - """Undo all the changes made by `set_environ`.""" - for name, value in self.environ_undos.items(): - if value is None: - del os.environ[name] - else: - os.environ[name] = value - - -class StdStreamCapturingMixin(TestCase): - """A test case mixin that captures stdout and stderr.""" - - def setUp(self): - super(StdStreamCapturingMixin, self).setUp() - - # Capture stdout and stderr so we can examine them in tests. - # nose keeps stdout from littering the screen, so we can safely Tee it, - # but it doesn't capture stderr, so we don't want to Tee stderr to the - # real stderr, since it will interfere with our nice field of dots. - old_stdout = sys.stdout - self.captured_stdout = StringIO() - sys.stdout = Tee(sys.stdout, self.captured_stdout) - - old_stderr = sys.stderr - self.captured_stderr = StringIO() - sys.stderr = self.captured_stderr - - self.addCleanup(self.cleanup_std_streams, old_stdout, old_stderr) - - def cleanup_std_streams(self, old_stdout, old_stderr): - """Restore stdout and stderr.""" - sys.stdout = old_stdout - sys.stderr = old_stderr - - def stdout(self): - """Return the data written to stdout during the test.""" - return self.captured_stdout.getvalue() - - def stderr(self): - """Return the data written to stderr during the test.""" - return self.captured_stderr.getvalue() - - -class DelayedAssertionMixin(TestCase): - """A test case mixin that provides a `delayed_assertions` context manager. - - Use it like this:: - - with self.delayed_assertions(): - self.assertEqual(x, y) - self.assertEqual(z, w) - - All of the assertions will run. The failures will be displayed at the end - of the with-statement. - - NOTE: this only works with some assertions. These are known to work: - - - `assertEqual(str, str)` - - - `assertMultilineEqual(str, str)` - - """ - def __init__(self, *args, **kwargs): - super(DelayedAssertionMixin, self).__init__(*args, **kwargs) - # This mixin only works with assert methods that call `self.fail`. In - # Python 2.7, `assertEqual` didn't, but we can do what Python 3 does, - # and use `assertMultiLineEqual` for comparing strings. - self.addTypeEqualityFunc(str, 'assertMultiLineEqual') - self._delayed_assertions = None - - @contextlib.contextmanager - def delayed_assertions(self): - """The context manager: assert that we didn't collect any assertions.""" - self._delayed_assertions = [] - old_fail = self.fail - self.fail = self._delayed_fail - try: - yield - finally: - self.fail = old_fail - if self._delayed_assertions: - if len(self._delayed_assertions) == 1: - self.fail(self._delayed_assertions[0]) - else: - self.fail( - "{0} failed assertions:\n{1}".format( - len(self._delayed_assertions), - "\n".join(self._delayed_assertions), - ) - ) - - def _delayed_fail(self, msg=None): - """The stand-in for TestCase.fail during delayed_assertions.""" - self._delayed_assertions.append(msg) - - -class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase): - """A test case mixin that creates a temp directory and files in it. - - Includes SysPathAwareMixin and ModuleAwareMixin, because making and using - temp directories like this will also need that kind of isolation. - - """ - - # Our own setting: most of these tests run in their own temp directory. - # Set this to False in your subclass if you don't want a temp directory - # created. - run_in_temp_dir = True - - # Set this if you aren't creating any files with make_file, but still want - # the temp directory. This will stop the test behavior checker from - # complaining. - no_files_in_temp_dir = False - - def setUp(self): - super(TempDirMixin, self).setUp() - - if self.run_in_temp_dir: - # Create a temporary directory. - self.temp_dir = self.make_temp_dir("test_cover") - self.chdir(self.temp_dir) - - # Modules should be importable from this temp directory. We don't - # use '' because we make lots of different temp directories and - # nose's caching importer can get confused. The full path prevents - # problems. - sys.path.insert(0, os.getcwd()) - - class_behavior = self.class_behavior() - class_behavior.tests += 1 - class_behavior.temp_dir = self.run_in_temp_dir - class_behavior.no_files_ok = self.no_files_in_temp_dir - - self.addCleanup(self.check_behavior) - - def make_temp_dir(self, slug="test_cover"): - """Make a temp directory that is cleaned up when the test is done.""" - name = "%s_%08d" % (slug, random.randint(0, 99999999)) - temp_dir = os.path.join(tempfile.gettempdir(), name) - os.makedirs(temp_dir) - self.addCleanup(shutil.rmtree, temp_dir) - return temp_dir - - def chdir(self, new_dir): - """Change directory, and change back when the test is done.""" - old_dir = os.getcwd() - os.chdir(new_dir) - self.addCleanup(os.chdir, old_dir) - - def check_behavior(self): - """Check that we did the right things.""" - - class_behavior = self.class_behavior() - if class_behavior.test_method_made_any_files: - class_behavior.tests_making_files += 1 - - def make_file(self, filename, text="", newline=None): - """Create a file for testing. - - `filename` is the relative path to the file, including directories if - desired, which will be created if need be. - - `text` is the content to create in the file, a native string (bytes in - Python 2, unicode in Python 3). - - If `newline` is provided, it is a string that will be used as the line - endings in the created file, otherwise the line endings are as provided - in `text`. - - Returns `filename`. - - """ - # Tests that call `make_file` should be run in a temp environment. - assert self.run_in_temp_dir - self.class_behavior().test_method_made_any_files = True - - text = textwrap.dedent(text) - if newline: - text = text.replace("\n", newline) - - # Make sure the directories are available. - dirs, _ = os.path.split(filename) - if dirs and not os.path.exists(dirs): - os.makedirs(dirs) - - # Create the file. - with open(filename, 'wb') as f: - f.write(to_bytes(text)) - - return filename - - # 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 directory is used or not. It's easy to forget to set that - # option properly, so we track information about what the tests did, and - # then report at the end of the process on test classes that were set - # wrong. - - class ClassBehavior(object): - """A value object to store per-class.""" - def __init__(self): - self.tests = 0 - self.skipped = 0 - self.temp_dir = True - self.no_files_ok = False - self.tests_making_files = 0 - self.test_method_made_any_files = False - - # Map from class to info about how it ran. - class_behaviors = collections.defaultdict(ClassBehavior) - - @classmethod - def report_on_class_behavior(cls): - """Called at process exit to report on class behavior.""" - for test_class, behavior in cls.class_behaviors.items(): - bad = "" - if behavior.tests <= behavior.skipped: - bad = "" - elif behavior.temp_dir and behavior.tests_making_files == 0: - if not behavior.no_files_ok: - bad = "Inefficient" - elif not behavior.temp_dir and behavior.tests_making_files > 0: - bad = "Unsafe" - - if bad: - if behavior.temp_dir: - where = "in a temp directory" - else: - where = "without a temp directory" - print( - "%s: %s ran %d tests, %d made files %s" % ( - bad, - test_class.__name__, - behavior.tests, - behavior.tests_making_files, - where, - ) - ) - - def class_behavior(self): - """Get the ClassBehavior instance for this test.""" - return self.class_behaviors[self.__class__] - -# When the process ends, find out about bad classes. -atexit.register(TempDirMixin.report_on_class_behavior) diff --git a/requirements/dev.pip b/requirements/dev.pip index dbbcfc5e..eab9acb1 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -14,6 +14,7 @@ mock==2.0.0 PyContracts==1.7.9 pyenchant==1.6.6 pylint==1.4.5 +git+https://github.com/nedbat/unittest-mixins@master#egg=unittest_mixins==0.0 # for kitting. requests==2.10.0 diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 7625ce6c..b38e0a5b 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -13,15 +13,16 @@ import shlex import shutil import sys +from unittest_mixins import ( + EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, + DelayedAssertionMixin, +) + import coverage from coverage.backunittest import TestCase 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.test_helpers import ( - EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, - DelayedAssertionMixin, -) from nose.plugins.skip import SkipTest diff --git a/tests/goldtest.py b/tests/goldtest.py index 39f2bfda..8ebbd591 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -8,7 +8,7 @@ import sys from tests.coveragetest import CoverageTest -from coverage.test_helpers import change_dir # pylint: disable=unused-import +from unittest_mixins import change_dir # pylint: disable=unused-import from tests.test_farm import clean # Import helpers, eventually test_farm.py will go away. from tests.test_farm import ( # pylint: disable=unused-import diff --git a/tests/test_farm.py b/tests/test_farm.py index 0db0af88..ae9e915e 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -15,7 +15,7 @@ import unittest from nose.plugins.skip import SkipTest -from coverage.test_helpers import ModuleAwareMixin, SysPathAwareMixin, change_dir, saved_sys_path +from unittest_mixins import ModuleAwareMixin, SysPathAwareMixin, change_dir, saved_sys_path from tests.helpers import run_command from tests.backtest import execfile # pylint: disable=redefined-builtin diff --git a/tests/test_testing.py b/tests/test_testing.py index 1dafdd0d..c5858bf1 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -6,15 +6,11 @@ import datetime import os -import re import sys -import textwrap import coverage from coverage.backunittest import TestCase -from coverage.backward import to_bytes from coverage.files import actual_path -from coverage.test_helpers import EnvironmentAwareMixin, TempDirMixin, DelayedAssertionMixin from tests.coveragetest import CoverageTest @@ -31,113 +27,6 @@ class TestingTest(TestCase): self.assertCountEqual(set([1,2,3]), set([4,5,6])) -class TempDirMixinTest(TempDirMixin, TestCase): - """Test the methods in TempDirMixin.""" - - def file_text(self, fname): - """Return the text read from a file.""" - with open(fname, "rb") as f: - return f.read().decode('ascii') - - def test_make_file(self): - # A simple file. - self.make_file("fooey.boo", "Hello there") - self.assertEqual(self.file_text("fooey.boo"), "Hello there") - # A file in a sub-directory - self.make_file("sub/another.txt", "Another") - self.assertEqual(self.file_text("sub/another.txt"), "Another") - # A second file in that sub-directory - self.make_file("sub/second.txt", "Second") - self.assertEqual(self.file_text("sub/second.txt"), "Second") - # A deeper directory - self.make_file("sub/deeper/evenmore/third.txt") - self.assertEqual(self.file_text("sub/deeper/evenmore/third.txt"), "") - - def test_make_file_newline(self): - self.make_file("unix.txt", "Hello\n") - self.assertEqual(self.file_text("unix.txt"), "Hello\n") - self.make_file("dos.txt", "Hello\n", newline="\r\n") - self.assertEqual(self.file_text("dos.txt"), "Hello\r\n") - self.make_file("mac.txt", "Hello\n", newline="\r") - self.assertEqual(self.file_text("mac.txt"), "Hello\r") - - def test_make_file_non_ascii(self): - self.make_file("unicode.txt", "tabblo: «ταБЬℓσ»") - with open("unicode.txt", "rb") as f: - text = f.read() - self.assertEqual(text, to_bytes("tabblo: «ταБЬℓσ»")) - - -class EnvironmentAwareMixinTest(EnvironmentAwareMixin, TestCase): - """Tests of test_helpers.EnvironmentAwareMixin.""" - - def test_setting_and_cleaning_env_vars(self): - # The before state. - # Not sure what environment variables are available in all of our - # different testing environments, so try a bunch. - for envvar in ["HOME", "HOMEDIR", "USER", "SYSTEMDRIVE", "TEMP"]: # pragma: part covered - if envvar in os.environ: - original_text = os.environ[envvar] - new_text = "Some Strange Text" - break - # pylint: disable=undefined-loop-variable - self.assertNotEqual(original_text, new_text) - self.assertNotIn("XYZZY_PLUGH", os.environ) - - # Change the environment. - self.set_environ(envvar, new_text) - self.set_environ("XYZZY_PLUGH", "Vogon") - - self.assertEqual(os.environ[envvar], new_text) - self.assertEqual(os.environ["XYZZY_PLUGH"], "Vogon") - - # Do the clean ups early. - self.doCleanups() - - # The environment should be restored. - self.assertEqual(os.environ[envvar], original_text) - self.assertNotIn("XYZZY_PLUGH", os.environ) - - -class DelayedAssertionMixinTest(DelayedAssertionMixin, TestCase): - """Test the `delayed_assertions` method.""" - - def test_delayed_assertions(self): - # Two assertions can be shown at once: - msg = re.escape(textwrap.dedent("""\ - 2 failed assertions: - 'x' != 'y' - - x - + y - - 'w' != 'z' - - w - + z - """)) - with self.assertRaisesRegex(AssertionError, msg): - with self.delayed_assertions(): - self.assertEqual("x", "y") - self.assertEqual("w", "z") - - # It's also OK if only one fails: - msg = re.escape(textwrap.dedent("""\ - 'w' != 'z' - - w - + z - """)) - with self.assertRaisesRegex(AssertionError, msg): - with self.delayed_assertions(): - self.assertEqual("x", "x") - self.assertEqual("w", "z") - - # If an error happens, it gets reported immediately, no special - # handling: - with self.assertRaises(ZeroDivisionError): - with self.delayed_assertions(): - self.assertEqual("x", "y") - self.assertEqual("w", 1/0) - - class CoverageTestTest(CoverageTest): """Test the methods in `CoverageTest`.""" @@ -14,6 +14,7 @@ deps = nose==1.3.7 mock==2.0.0 PyContracts==1.7.9 + git+https://github.com/nedbat/unittest-mixins@master#egg=unittest_mixins==0.0 py26: unittest2==1.1.0 py{27,33,34,35,36}: gevent==1.1.1 py{26,27,33,34,35,36}: eventlet==0.19.0 |