diff options
author | Robert Collins <robertc@robertcollins.net> | 2014-09-25 15:01:50 +1200 |
---|---|---|
committer | Robert Collins <robertc@robertcollins.net> | 2014-09-25 15:01:50 +1200 |
commit | f61bdd267c9f4a039bad10249e8ae618b29a515e (patch) | |
tree | 40f4b9793826d603b090483d4abbbd7eefb39ba3 /fixtures | |
parent | 9f9d89ce718463b24cd3910b9a99efb60b3c9e1b (diff) | |
download | fixtures-f61bdd267c9f4a039bad10249e8ae618b29a515e.tar.gz |
Migrate to git and pbr.
No functional changes.
Diffstat (limited to 'fixtures')
33 files changed, 2966 insertions, 0 deletions
diff --git a/fixtures/__init__.py b/fixtures/__init__.py new file mode 100644 index 0000000..bfbd3dd --- /dev/null +++ b/fixtures/__init__.py @@ -0,0 +1,126 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + + +"""Fixtures provides a sensible contract for reusable test fixtures. + +It also provides glue for using these in common test runners and acts as a +common repository for widely used Fixture classes. + +See the README for a manual, and the docstrings on individual functions and +methods for details. + +Most users will want to look at TestWithFixtures and Fixture, to start with. +""" + +# same format as sys.version_info: "A tuple containing the five components of +# the version number: major, minor, micro, releaselevel, and serial. All +# values except releaselevel are integers; the release level is 'alpha', +# 'beta', 'candidate', or 'final'. The version_info value corresponding to the +# Python version 2.0 is (2, 0, 0, 'final', 0)." Additionally we use a +# releaselevel of 'dev' for unreleased under-development code. +# +# If the releaselevel is 'alpha' then the major/minor/micro components are not +# established at this point, and setup.py will use a version of next-$(revno). +# If the releaselevel is 'final', then the tarball will be major.minor.micro. +# Otherwise it is major.minor.micro~$(revno). +__version__ = (0, 3, 16, 'final', 0) + +__all__ = [ + 'ByteStream', + 'DetailStream', + 'EnvironmentVariable', + 'EnvironmentVariableFixture', + 'FakeLogger', + 'FakePopen', + 'Fixture', + 'FunctionFixture', + 'LogHandler', + 'LoggerFixture', + 'MethodFixture', + 'MonkeyPatch', + 'NestedTempfile', + 'PackagePathEntry', + 'PopenFixture', + 'PythonPackage', + 'PythonPathEntry', + 'StringStream', + 'TempDir', + 'TempHomeDir', + 'TestWithFixtures', + 'Timeout', + 'TimeoutException', + '__version__', + 'version', + ] + + +import pbr.version + +from fixtures.fixture import ( + Fixture, + FunctionFixture, + MethodFixture, + ) +from fixtures._fixtures import ( + ByteStream, + DetailStream, + EnvironmentVariable, + EnvironmentVariableFixture, + FakeLogger, + FakePopen, + LoggerFixture, + LogHandler, + MonkeyPatch, + NestedTempfile, + PackagePathEntry, + PopenFixture, + PythonPackage, + PythonPathEntry, + StringStream, + TempDir, + TempHomeDir, + Timeout, + TimeoutException, + ) +from fixtures.testcase import TestWithFixtures + +# same format as sys.version_info: "A tuple containing the five components of +# the version number: major, minor, micro, releaselevel, and serial. All +# values except releaselevel are integers; the release level is 'alpha', +# 'beta', 'candidate', or 'final'. The version_info value corresponding to the +# Python version 2.0 is (2, 0, 0, 'final', 0)." Additionally we use a +# releaselevel of 'dev' for unreleased under-development code. +# +# If the releaselevel is 'alpha' then the major/minor/micro components are not +# established at this point, and setup.py will use a version of next-$(revno). +# If the releaselevel is 'final', then the tarball will be major.minor.micro. +# Otherwise it is major.minor.micro~$(revno). + +# Uncomment when pbr 0.11 is released. +#_version = pbr.version.VersionInfo('fixtures').semantic_version() +#__version__ = _version.version_tuple() +#version = _version.release_string() +__version__ = (0, 3, 17, 'alpha', 0) + + +def test_suite(): + import fixtures.tests + return fixtures.tests.test_suite() + + +def load_tests(loader, standard_tests, pattern): + standard_tests.addTests(loader.loadTestsFromNames(["fixtures.tests"])) + return standard_tests diff --git a/fixtures/_fixtures/__init__.py b/fixtures/_fixtures/__init__.py new file mode 100644 index 0000000..1d54858 --- /dev/null +++ b/fixtures/_fixtures/__init__.py @@ -0,0 +1,74 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + + +"""Included fixtures.""" + +__all__ = [ + 'ByteStream', + 'DetailStream', + 'EnvironmentVariable', + 'EnvironmentVariableFixture', + 'FakeLogger', + 'FakePopen', + 'LoggerFixture', + 'LogHandler', + 'MonkeyPatch', + 'NestedTempfile', + 'PackagePathEntry', + 'PopenFixture', + 'PythonPackage', + 'PythonPathEntry', + 'StringStream', + 'TempDir', + 'TempHomeDir', + 'Timeout', + 'TimeoutException', + ] + + +from fixtures._fixtures.environ import ( + EnvironmentVariable, + EnvironmentVariableFixture, + ) +from fixtures._fixtures.logger import ( + FakeLogger, + LoggerFixture, + LogHandler, + ) +from fixtures._fixtures.monkeypatch import MonkeyPatch +from fixtures._fixtures.popen import ( + FakePopen, + PopenFixture, + ) +from fixtures._fixtures.packagepath import PackagePathEntry +from fixtures._fixtures.pythonpackage import PythonPackage +from fixtures._fixtures.pythonpath import PythonPathEntry +from fixtures._fixtures.streams import ( + ByteStream, + DetailStream, + StringStream, + ) +from fixtures._fixtures.tempdir import ( + NestedTempfile, + TempDir, + ) +from fixtures._fixtures.temphomedir import ( + TempHomeDir, + ) +from fixtures._fixtures.timeout import ( + Timeout, + TimeoutException, + ) diff --git a/fixtures/_fixtures/environ.py b/fixtures/_fixtures/environ.py new file mode 100644 index 0000000..5494429 --- /dev/null +++ b/fixtures/_fixtures/environ.py @@ -0,0 +1,58 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'EnvironmentVariable', + 'EnvironmentVariableFixture' + ] + +import os + +from fixtures import Fixture + + +class EnvironmentVariable(Fixture): + """Isolate a specific environment variable.""" + + def __init__(self, varname, newvalue=None): + """Create an EnvironmentVariable fixture. + + :param varname: the name of the variable to isolate. + :param newvalue: A value to set the variable to. If None, the variable + will be deleted. + + During setup the variable will be deleted or assigned the requested + value, and this will be restored in cleanUp. + """ + super(EnvironmentVariable, self).__init__() + self.varname = varname + self.newvalue = newvalue + + def setUp(self): + super(EnvironmentVariable, self).setUp() + varname = self.varname + orig_value = os.environ.get(varname) + if orig_value is not None: + self.addCleanup(os.environ.__setitem__, varname, orig_value) + del os.environ[varname] + else: + self.addCleanup(os.environ.pop, varname, '') + if self.newvalue is not None: + os.environ[varname] = self.newvalue + else: + os.environ.pop(varname, '') + + +EnvironmentVariableFixture = EnvironmentVariable diff --git a/fixtures/_fixtures/logger.py b/fixtures/_fixtures/logger.py new file mode 100644 index 0000000..e46de3a --- /dev/null +++ b/fixtures/_fixtures/logger.py @@ -0,0 +1,109 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +from logging import StreamHandler, getLogger, INFO, Formatter + +from testtools.compat import _u + +from fixtures import Fixture +from fixtures._fixtures.streams import StringStream + +__all__ = [ + 'FakeLogger', + 'LoggerFixture', + 'LogHandler', + ] + + +class LogHandler(Fixture): + """Replace a logger's handlers.""" + + def __init__(self, handler, name="", level=None, nuke_handlers=True): + """Create a LogHandler fixture. + + :param handler: The handler to replace other handlers with. + If nuke_handlers is False, then added as an extra handler. + :param name: The name of the logger to replace. Defaults to "". + :param level: The log level to set, defaults to not changing the level. + :param nuke_handlers: If True remove all existing handles (prevents + existing messages going to e.g. stdout). Defaults to True. + """ + super(LogHandler, self).__init__() + self.handler = handler + self._name = name + self._level = level + self._nuke_handlers = nuke_handlers + + def setUp(self): + super(LogHandler, self).setUp() + logger = getLogger(self._name) + if self._level: + self.addCleanup(logger.setLevel, logger.level) + logger.setLevel(self._level) + if self._nuke_handlers: + for handler in reversed(logger.handlers): + self.addCleanup(logger.addHandler, handler) + logger.removeHandler(handler) + try: + logger.addHandler(self.handler) + finally: + self.addCleanup(logger.removeHandler, self.handler) + + +class FakeLogger(Fixture): + """Replace a logger and capture its output.""" + + def __init__(self, name="", level=INFO, format=None, nuke_handlers=True): + """Create a FakeLogger fixture. + + :param name: The name of the logger to replace. Defaults to "". + :param level: The log level to set, defaults to INFO. + :param format: Logging format to use. Defaults to capturing supplied + messages verbatim. + :param nuke_handlers: If True remove all existing handles (prevents + existing messages going to e.g. stdout). Defaults to True. + + Example: + + def test_log(self) + fixture = self.useFixture(LoggerFixture()) + logging.info('message') + self.assertEqual('message', fixture.output) + """ + super(FakeLogger, self).__init__() + self._name = name + self._level = level + self._format = format + self._nuke_handlers = nuke_handlers + + def setUp(self): + super(FakeLogger, self).setUp() + name = _u("pythonlogging:'%s'") % self._name + output = self.useFixture(StringStream(name)).stream + self._output = output + handler = StreamHandler(output) + if self._format: + handler.setFormatter(Formatter(self._format)) + self.useFixture( + LogHandler(handler, name=self._name, level=self._level, + nuke_handlers=self._nuke_handlers)) + + @property + def output(self): + self._output.seek(0) + return self._output.read() + + +LoggerFixture = FakeLogger diff --git a/fixtures/_fixtures/monkeypatch.py b/fixtures/_fixtures/monkeypatch.py new file mode 100644 index 0000000..bfb7351 --- /dev/null +++ b/fixtures/_fixtures/monkeypatch.py @@ -0,0 +1,79 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'MonkeyPatch' + ] + +import sys +import types + +from fixtures import Fixture + + +class MonkeyPatch(Fixture): + """Replace or delete an attribute.""" + + delete = object() + + def __init__(self, name, new_value=None): + """Create a MonkeyPatch. + + :param name: The fully qualified object name to override. + :param new_value: A value to set the name to. If set to + MonkeyPatch.delete the attribute will be deleted. + + During setup the name will be deleted or assigned the requested value, + and this will be restored in cleanUp. + """ + Fixture.__init__(self) + self.name = name + self.new_value = new_value + + def setUp(self): + Fixture.setUp(self) + location, attribute = self.name.rsplit('.', 1) + # Import, swallowing all errors as any element of location may be + # a class or some such thing. + try: + __import__(location, {}, {}) + except ImportError: + pass + components = location.split('.') + current = __import__(components[0], {}, {}) + for component in components[1:]: + current = getattr(current, component) + sentinel = object() + old_value = getattr(current, attribute, sentinel) + if self.new_value is self.delete: + if old_value is not sentinel: + delattr(current, attribute) + else: + setattr(current, attribute, self.new_value) + if old_value is sentinel: + self.addCleanup(self._safe_delete, current, attribute) + else: + # Python 2's setattr transforms function into instancemethod + if (sys.version_info[0] == 2 and + isinstance(current, (type, types.ClassType)) and + isinstance(old_value, types.FunctionType)): + old_value = staticmethod(old_value) + self.addCleanup(setattr, current, attribute, old_value) + + def _safe_delete(self, obj, attribute): + """Delete obj.attribute handling the case where its missing.""" + sentinel = object() + if getattr(obj, attribute, sentinel) is not sentinel: + delattr(obj, attribute) diff --git a/fixtures/_fixtures/packagepath.py b/fixtures/_fixtures/packagepath.py new file mode 100644 index 0000000..43a9bf3 --- /dev/null +++ b/fixtures/_fixtures/packagepath.py @@ -0,0 +1,48 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'PackagePathEntry' + ] + +import sys + +from fixtures import Fixture + + +class PackagePathEntry(Fixture): + """Add a path to the path of a python package. + + The python package needs to be already imported. + + If this new path is already in the packages __path__ list then the __path__ + list will not be altered. + """ + + def __init__(self, packagename, directory): + """Create a PackagePathEntry. + + :param directory: The directory to add to the package.__path__. + """ + self.packagename = packagename + self.directory = directory + + def setUp(self): + Fixture.setUp(self) + path = sys.modules[self.packagename].__path__ + if self.directory in path: + return + self.addCleanup(path.remove, self.directory) + path.append(self.directory) diff --git a/fixtures/_fixtures/popen.py b/fixtures/_fixtures/popen.py new file mode 100644 index 0000000..728e980 --- /dev/null +++ b/fixtures/_fixtures/popen.py @@ -0,0 +1,125 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'FakePopen', + 'PopenFixture' + ] + +import random +import subprocess + +from fixtures import Fixture + + +class FakeProcess(object): + """A test double process, roughly meeting subprocess.Popen's contract.""" + + def __init__(self, args, info): + self._args = args + self.stdin = info.get('stdin') + self.stdout = info.get('stdout') + self.stderr = info.get('stderr') + self.pid = random.randint(0, 65536) + self._returncode = info.get('returncode', 0) + self.returncode = None + + def communicate(self): + self.returncode = self._returncode + if self.stdout: + out = self.stdout.getvalue() + else: + out = '' + if self.stderr: + err = self.stderr.getvalue() + else: + err = '' + return out, err + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.wait() + + def kill(self): + pass + + def wait(self, timeout=None, endtime=None): + if self.returncode is None: + self.communicate() + return self.returncode + + +class FakePopen(Fixture): + """Replace subprocess.Popen. + + Primarily useful for testing, this fixture replaces subprocess.Popen with a + test double. + + :ivar procs: A list of the processes created by the fixture. + """ + + _unpassed = object() + + def __init__(self, get_info=lambda _:{}): + """Create a PopenFixture + + :param get_info: Optional callback to control the behaviour of the + created process. This callback takes a kwargs dict for the Popen + call, and should return a dict with any desired attributes. + Only parameters that are supplied to the Popen call are in the + dict, making it possible to detect the difference between 'passed + with a default value' and 'not passed at all'. + + e.g. + def get_info(proc_args): + self.assertEqual(subprocess.PIPE, proc_args['stdin']) + return {'stdin': StringIO('foobar')} + + The default behaviour if no get_info is supplied is for the return + process to have returncode of None, empty streams and a random pid. + """ + super(FakePopen, self).__init__() + self.get_info = get_info + + def setUp(self): + super(FakePopen, self).setUp() + self.addCleanup(setattr, subprocess, 'Popen', subprocess.Popen) + subprocess.Popen = self + self.procs = [] + + # The method has the correct signature so we error appropriately if called + # wrongly. + def __call__(self, args, bufsize=_unpassed, executable=_unpassed, + stdin=_unpassed, stdout=_unpassed, stderr=_unpassed, + preexec_fn=_unpassed, close_fds=_unpassed, shell=_unpassed, + cwd=_unpassed, env=_unpassed, universal_newlines=_unpassed, + startupinfo=_unpassed, creationflags=_unpassed): + proc_args = dict(args=args) + local = locals() + for param in [ + "bufsize", "executable", "stdin", "stdout", "stderr", + "preexec_fn", "close_fds", "shell", "cwd", "env", + "universal_newlines", "startupinfo", "creationflags"]: + if local[param] is not FakePopen._unpassed: + proc_args[param] = local[param] + proc_info = self.get_info(proc_args) + result = FakeProcess(proc_args, proc_info) + self.procs.append(result) + return result + + +PopenFixture = FakePopen diff --git a/fixtures/_fixtures/pythonpackage.py b/fixtures/_fixtures/pythonpackage.py new file mode 100644 index 0000000..4fbd278 --- /dev/null +++ b/fixtures/_fixtures/pythonpackage.py @@ -0,0 +1,66 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'PythonPackage' + ] + +import os.path + +from fixtures import Fixture +from fixtures._fixtures.tempdir import TempDir + + +class PythonPackage(Fixture): + """Create a temporary Python package. + + :ivar base: The path of the directory containing the module. E.g. for a + module 'foo', the path base + '/foo/__init__.py' would be the file path + for the module. + """ + + def __init__(self, packagename, modulelist, init=True): + """Create a PythonPackage. + + :param packagename: The name of the package to create - e.g. + 'toplevel.subpackage.' + :param modulelist: List of modules to include in the package. + Each module should be a tuple with the filename and content it + should have. + :param init: If false, do not create a missing __init__.py. When + True, if modulelist does not include an __init__.py, an empty + one is created. + """ + self.packagename = packagename + self.modulelist = modulelist + self.init = init + + def setUp(self): + Fixture.setUp(self) + self.base = self.useFixture(TempDir()).path + base = self.base + root = os.path.join(base, self.packagename) + os.mkdir(root) + init_seen = not self.init + for modulename, contents in self.modulelist: + stream = open(os.path.join(root, modulename), 'wb') + try: + stream.write(contents) + finally: + stream.close() + if modulename == '__init__.py': + init_seen = True + if not init_seen: + open(os.path.join(root, '__init__.py'), 'wb').close() diff --git a/fixtures/_fixtures/pythonpath.py b/fixtures/_fixtures/pythonpath.py new file mode 100644 index 0000000..89c1968 --- /dev/null +++ b/fixtures/_fixtures/pythonpath.py @@ -0,0 +1,43 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'PythonPathEntry' + ] + +import sys + +from fixtures import Fixture + + +class PythonPathEntry(Fixture): + """Add a path to sys.path. + + If the path is already in sys.path, sys.path will not be altered. + """ + + def __init__(self, directory): + """Create a PythonPathEntry. + + :param directory: The directory to add to sys.path. + """ + self.directory = directory + + def setUp(self): + Fixture.setUp(self) + if self.directory in sys.path: + return + self.addCleanup(sys.path.remove, self.directory) + sys.path.append(self.directory) diff --git a/fixtures/_fixtures/streams.py b/fixtures/_fixtures/streams.py new file mode 100644 index 0000000..188d8e3 --- /dev/null +++ b/fixtures/_fixtures/streams.py @@ -0,0 +1,97 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2012, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'ByteStream', + 'DetailStream', + 'StringStream', + ] + +import io +import sys + +from fixtures import Fixture +import testtools + + +class Stream(Fixture): + """Expose a file-like object as a detail. + + :attr stream: The file-like object. + """ + + def __init__(self, detail_name, stream_factory): + """Create a ByteStream. + + :param detail_name: Use this as the name of the stream. + :param stream_factory: Called to construct a pair of streams: + (write_stream, content_stream). + """ + self._detail_name = detail_name + self._stream_factory = stream_factory + + def setUp(self): + super(Stream, self).setUp() + write_stream, read_stream = self._stream_factory() + self.stream = write_stream + self.addDetail(self._detail_name, + testtools.content.content_from_stream(read_stream, seek_offset=0)) + + +def _byte_stream_factory(): + result = io.BytesIO() + return (result, result) + + +def ByteStream(detail_name): + """Provide a file-like object that accepts bytes and expose as a detail. + + :param detail_name: The name of the detail. + :return: A fixture which has an attribute `stream` containing the file-like + object. + """ + return Stream(detail_name, _byte_stream_factory) + + +def _string_stream_factory(): + lower = io.BytesIO() + upper = io.TextIOWrapper(lower, encoding="utf8") + # See http://bugs.python.org/issue7955 + upper._CHUNK_SIZE = 1 + # In theory, this is sufficient and correct, but on Python2, + # upper.write(_b('foo")) will whinge louadly. + if sys.version_info[0] < 3: + upper_write = upper.write + def safe_write(str_or_bytes): + if type(str_or_bytes) is str: + str_or_bytes = str_or_bytes.decode('utf8') + return upper_write(str_or_bytes) + upper.write = safe_write + return upper, lower + + +def StringStream(detail_name): + """Provide a file-like object that accepts strings and expose as a detail. + + :param detail_name: The name of the detail. + :return: A fixture which has an attribute `stream` containing the file-like + object. + """ + return Stream(detail_name, _string_stream_factory) + + +def DetailStream(detail_name): + """Deprecated alias for ByteStream.""" + return ByteStream(detail_name) diff --git a/fixtures/_fixtures/tempdir.py b/fixtures/_fixtures/tempdir.py new file mode 100644 index 0000000..663d3eb --- /dev/null +++ b/fixtures/_fixtures/tempdir.py @@ -0,0 +1,70 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'NestedTempfile', + 'TempDir', + ] + +import os +import shutil +import tempfile + +import fixtures + + +class TempDir(fixtures.Fixture): + """Create a temporary directory. + + :ivar path: The path of the temporary directory. + """ + + def __init__(self, rootdir=None): + """Create a TempDir. + + :param rootdir: If supplied force the temporary directory to be a + child of rootdir. + """ + self.rootdir = rootdir + + def setUp(self): + super(TempDir, self).setUp() + self.path = tempfile.mkdtemp(dir=self.rootdir) + self.addCleanup(shutil.rmtree, self.path, ignore_errors=True) + + def join(self, *children): + """Return an absolute path, given one relative to this ``TempDir``. + + WARNING: This does not do any checking of ``children`` to make sure + they aren't walking up the tree using path segments like '..' or + '/usr'. Use at your own risk. + """ + return os.path.abspath(os.path.join(self.path, *children)) + + +class NestedTempfile(fixtures.Fixture): + """Nest all temporary files and directories inside another directory. + + This temporarily monkey-patches the default location that the `tempfile` + package creates temporary files and directories in to be a new temporary + directory. This new temporary directory is removed when the fixture is torn + down. + """ + + def setUp(self): + super(NestedTempfile, self).setUp() + tempdir = self.useFixture(TempDir()).path + patch = fixtures.MonkeyPatch("tempfile.tempdir", tempdir) + self.useFixture(patch) diff --git a/fixtures/_fixtures/temphomedir.py b/fixtures/_fixtures/temphomedir.py new file mode 100644 index 0000000..2601a8d --- /dev/null +++ b/fixtures/_fixtures/temphomedir.py @@ -0,0 +1,32 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Canonical Ltd. +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'TempHomeDir', + ] + +import fixtures +from fixtures._fixtures.tempdir import TempDir + + +class TempHomeDir(TempDir): + """Create a temporary directory and set it as $HOME + + :ivar path: the path of the temporary directory. + """ + + def setUp(self): + super(TempHomeDir, self).setUp() + self.useFixture(fixtures.EnvironmentVariable("HOME", self.path)) diff --git a/fixtures/_fixtures/timeout.py b/fixtures/_fixtures/timeout.py new file mode 100644 index 0000000..7863b0d --- /dev/null +++ b/fixtures/_fixtures/timeout.py @@ -0,0 +1,68 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (C) 2011, Martin Pool <mbp@sourcefrog.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + + +"""Timeout fixture.""" + + +import signal + +import fixtures + +__all__ = [ + 'Timeout', + 'TimeoutException', + ] + + +class TimeoutException(Exception): + """Timeout expired""" + + +class Timeout(fixtures.Fixture): + """Fixture that aborts the contained code after a number of seconds. + + The interrupt can be either gentle, in which case TimeoutException is + raised, or not gentle, in which case the process will typically be aborted + by SIGALRM. + + Cautions: + * This has no effect on Windows. + * Only one Timeout can be used at any time per process. + """ + + def __init__(self, timeout_secs, gentle): + self.timeout_secs = timeout_secs + self.alarm_fn = getattr(signal, 'alarm', None) + self.gentle = gentle + + def signal_handler(self, signum, frame): + raise TimeoutException() + + def setUp(self): + super(Timeout, self).setUp() + if self.alarm_fn is None: + return # Can't run on Windows + if self.gentle: + # Install a handler for SIGARLM so we can raise an exception rather + # than the default handler executing, which kills the process. + old_handler = signal.signal(signal.SIGALRM, self.signal_handler) + # We add the slarm cleanup before the cleanup for the signal handler, + # otherwise there is a race condition where the signal handler is + # cleaned up but the alarm still fires. + self.addCleanup(lambda: self.alarm_fn(0)) + self.alarm_fn(self.timeout_secs) + if self.gentle: + self.addCleanup(lambda: signal.signal(signal.SIGALRM, old_handler)) diff --git a/fixtures/callmany.py b/fixtures/callmany.py new file mode 100644 index 0000000..23580cb --- /dev/null +++ b/fixtures/callmany.py @@ -0,0 +1,100 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'CallMany', + ] + +import sys + +from testtools.compat import ( + reraise, + ) +from testtools.helpers import try_import + + +class MultipleExceptions(Exception): + """Report multiple exc_info tuples in self.args.""" + +MultipleExceptions = try_import( + "testtools.MultipleExceptions", MultipleExceptions) + + +class CallMany(object): + """A stack of functions which will all be called on __call__. + + CallMany also acts as a context manager for convenience. + + Functions are called in last pushed first executed order. + + This is used by Fixture to manage its addCleanup feature. + """ + + def __init__(self): + self._cleanups = [] + + def push(self, cleanup, *args, **kwargs): + """Add a function to be called from __call__. + + On __call__ all functions are called - see __call__ for details on how + multiple exceptions are handled. + + :param cleanup: A callable to call during cleanUp. + :param *args: Positional args for cleanup. + :param kwargs: Keyword args for cleanup. + :return: None + """ + self._cleanups.append((cleanup, args, kwargs)) + + def __call__(self, raise_errors=True): + """Run all the registered functions. + + :param raise_errors: Deprecated parameter from before testtools gained + MultipleExceptions. raise_errors defaults to True. When True + if exception(s) are raised while running functions, they are + re-raised after all the functions have run. If multiple exceptions + are raised, they are all wrapped into a MultipleExceptions object, + and that is raised. + Thus, to cach a specific exception from a function run by __call__, + you need to catch both the exception and MultipleExceptions, and + then check within a MultipleExceptions instance for an occurance of + the type you wish to catch. + :return: Either None or a list of the exc_info() for each exception + that occured if raise_errors was False. + """ + cleanups = reversed(self._cleanups) + self._cleanups = [] + result = [] + for cleanup, args, kwargs in cleanups: + try: + cleanup(*args, **kwargs) + except Exception: + result.append(sys.exc_info()) + if result and raise_errors: + if 1 == len(result): + error = result[0] + reraise(error[0], error[1], error[2]) + else: + raise MultipleExceptions(*result) + if not raise_errors: + return result + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self() + return False # propogate exceptions from the with body. + diff --git a/fixtures/fixture.py b/fixtures/fixture.py new file mode 100644 index 0000000..2cf966d --- /dev/null +++ b/fixtures/fixture.py @@ -0,0 +1,338 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'Fixture', + 'FunctionFixture', + 'MethodFixture', + 'MultipleExceptions', + ] + +import itertools +import sys + +from testtools.compat import ( + advance_iterator, + reraise, + ) +from testtools.helpers import try_import + +from fixtures.callmany import ( + CallMany, + # Deprecated, imported for compatibility. + MultipleExceptions, + ) + +gather_details = try_import("testtools.testcase.gather_details") + +# This would be better in testtools (or a common library) +def combine_details(source_details, target_details): + """Add every value from source to target deduping common keys.""" + for name, content_object in source_details.items(): + new_name = name + disambiguator = itertools.count(1) + while new_name in target_details: + new_name = '%s-%d' % (name, advance_iterator(disambiguator)) + name = new_name + target_details[name] = content_object + + +class Fixture(object): + """A Fixture representing some state or resource. + + Often used in tests, a Fixture must be setUp before using it, and cleanUp + called after it is finished with (because many Fixture classes have + external resources such as temporary directories). + + The reset() method can be called to perform cleanUp and setUp automatically + and potentially faster. + """ + + def addCleanup(self, cleanup, *args, **kwargs): + """Add a clean function to be called from cleanUp. + + All cleanup functions are called - see cleanUp for details on how + multiple exceptions are handled. + + If for some reason you need to cancel cleanups, call + self._clear_cleanups. + + :param cleanup: A callable to call during cleanUp. + :param *args: Positional args for cleanup. + :param kwargs: Keyword args for cleanup. + :return: None + """ + self._cleanups.push(cleanup, *args, **kwargs) + + def addDetail(self, name, content_object): + """Add a detail to the Fixture. + + This may only be called after setUp has been called. + + :param name: The name for the detail being added. Overrides existing + identically named details. + :param content_object: The content object (meeting the + testtools.content.Content protocol) being added. + """ + self._details[name] = content_object + + def cleanUp(self, raise_first=True): + """Cleanup the fixture. + + This function will free all resources managed by the Fixture, restoring + it (and any external facilities such as databases, temporary + directories and so forth_ to their original state. + + This should not typically be overridden, see addCleanup instead. + + :param raise_first: Deprecated parameter from before testtools gained + MultipleExceptions. raise_first defaults to True. When True + if a single exception is raised, it is reraised after all the + cleanUps have run. If multiple exceptions are raised, they are + all wrapped into a MultipleExceptions object, and that is reraised. + Thus, to cach a specific exception from cleanUp, you need to catch + both the exception and MultipleExceptions, and then check within + a MultipleExceptions instance for the type you're catching. + :return: A list of the exc_info() for each exception that occured if + raise_first was False + """ + try: + return self._cleanups(raise_errors=raise_first) + finally: + self._clear_cleanups() + + def _clear_cleanups(self): + """Clean the cleanup queue without running them. + + This is a helper that can be useful for subclasses which define + reset(): they may perform something equivalent to a typical cleanUp + without actually calling the cleanups. + + This also clears the details dict. + """ + self._cleanups = CallMany() + self._details = {} + self._detail_sources = [] + + def __enter__(self): + self.setUp() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + self._cleanups() + finally: + self._clear_cleanups() + return False # propogate exceptions from the with body. + + def getDetails(self): + """Get the current details registered with the fixture. + + This does not return the internal dictionary: mutating it will have no + effect. If you need to mutate it, just do so directly. + + :return: Dict from name -> content_object. + """ + result = dict(self._details) + for source in self._detail_sources: + combine_details(source.getDetails(), result) + return result + + def setUp(self): + """Prepare the Fixture for use. + + This should be overridden by most concrete fixtures. When overriding + be sure to include self.addCleanup calls to restore the fixture to + an un-setUp state, so that a single Fixture instance can be reused. + + After setUp is called, the fixture will have one or more attributes + which can be used (these depend totally on the concrete subclass). + + :return: None. + """ + self._clear_cleanups() + + def reset(self): + """Reset a setUp Fixture to the 'just setUp' state again. + + The default implementation calls + self.cleanUp() + self.setUp() + + but this function may be overridden to provide an optimised routine to + achieve the same result. + + :return: None. + """ + self.cleanUp() + self.setUp() + + def useFixture(self, fixture): + """Use another fixture. + + The fixture will be setUp, and self.addCleanup(fixture.cleanUp) called. + + :param fixture: The fixture to use. + :return: The fixture, after setting it up and scheduling a cleanup for + it. + """ + try: + fixture.setUp() + except: + # The child failed to come up, capture any details it has (copying + # the content, it may go away anytime). + if gather_details is not None: + gather_details(fixture.getDetails(), self._details) + raise + else: + self.addCleanup(fixture.cleanUp) + # Calls to getDetails while this fixture is setup will return + # details from the child fixture. + self._detail_sources.append(fixture) + return fixture + + +class FunctionFixture(Fixture): + """An adapter to use function(s) as a Fixture. + + Typically used when an existing object or function interface exists but you + wish to use it as a Fixture (e.g. because fixtures are in use in your test + suite and this will fit in better). + + To adapt an object with differently named setUp and cleanUp methods: + fixture = FunctionFixture(object.install, object.__class__.remove) + Note that the indirection via __class__ is to get an unbound method + which can accept the result from install. See also MethodFixture which + is specialised for objects. + + To adapt functions: + fixture = FunctionFixture(tempfile.mkdtemp, shutil.rmtree) + + With a reset function: + fixture = FunctionFixture(setup, cleanup, reset) + + :ivar fn_result: The result of the setup_fn. Undefined outside of the + setUp, cleanUp context. + """ + + def __init__(self, setup_fn, cleanup_fn=None, reset_fn=None): + """Create a FunctionFixture. + + :param setup_fn: A callable which takes no parameters and returns the + thing you want to use. e.g. + def setup_fn(): + return 42 + The result of setup_fn is assigned to the fn_result attribute bu + FunctionFixture.setUp. + :param cleanup_fn: Optional callable which takes a single parameter, which + must be that which is returned from the setup_fn. This is called + from cleanUp. + :param reset_fn: Optional callable which takes a single parameter like + cleanup_fn, but also returns a new object for use as the fn_result: + if defined this replaces the use of cleanup_fn and setup_fn when + reset() is called. + """ + super(FunctionFixture, self).__init__() + self.setup_fn = setup_fn + self.cleanup_fn = cleanup_fn + self.reset_fn = reset_fn + + def setUp(self): + super(FunctionFixture, self).setUp() + fn_result = self.setup_fn() + self._maybe_cleanup(fn_result) + + def reset(self): + if self.reset_fn is None: + super(FunctionFixture, self).reset() + else: + self._clear_cleanups() + fn_result = self.reset_fn(self.fn_result) + self._maybe_cleanup(fn_result) + + def _maybe_cleanup(self, fn_result): + self.addCleanup(delattr, self, 'fn_result') + if self.cleanup_fn is not None: + self.addCleanup(self.cleanup_fn, fn_result) + self.fn_result = fn_result + + +class MethodFixture(Fixture): + """An adapter to use a function as a Fixture. + + Typically used when an existing object exists but you wish to use it as a + Fixture (e.g. because fixtures are in use in your test suite and this will + fit in better). + + To adapt an object with setUp / tearDown methods: + fixture = MethodFixture(object) + If setUp / tearDown / reset are missing, they simply won't be called. + + The object is exposed on fixture.obj. + + To adapt an object with differently named setUp and cleanUp methods: + fixture = MethodFixture(object, setup=object.mySetUp, + teardown=object.myTearDown) + + With a differently named reset function: + fixture = MethodFixture(object, reset=object.myReset) + + :ivar obj: The object which is being wrapped. + """ + + def __init__(self, obj, setup=None, cleanup=None, reset=None): + """Create a MethodFixture. + + :param obj: The object to wrap. Exposed as fixture.obj + :param setup: A method which takes no parameters. e.g. + def setUp(self): + self.value = 42 + If setup is not supplied, and the object has a setUp method, that + method is used, otherwise nothing will happen during fixture.setUp. + :param cleanup: Optional method to cleanup the object's state. If + not supplied the method 'tearDown' is used if it exists. + :param reset: Optional method to reset the wrapped object for use. + If not supplied, then the method 'reset' is used if it exists, + otherwise cleanUp and setUp are called as per Fixture.reset(). + """ + super(MethodFixture, self).__init__() + self.obj = obj + if setup is None: + setup = getattr(obj, 'setUp', None) + if setup is None: + setup = lambda:None + self._setup = setup + if cleanup is None: + cleanup = getattr(obj, 'tearDown', None) + if cleanup is None: + cleanup = lambda:None + self._cleanup = cleanup + if reset is None: + reset = getattr(obj, 'reset', None) + self._reset = reset + + def setUp(self): + super(MethodFixture, self).setUp() + self._setup() + + def cleanUp(self): + super(MethodFixture, self).cleanUp() + self._cleanup() + + def reset(self): + if self._reset is None: + super(MethodFixture, self).reset() + else: + self._reset() diff --git a/fixtures/testcase.py b/fixtures/testcase.py new file mode 100644 index 0000000..1d6a85c --- /dev/null +++ b/fixtures/testcase.py @@ -0,0 +1,60 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +__all__ = [ + 'TestWithFixtures', + ] + +import unittest + +from fixtures.fixture import gather_details + + +class TestWithFixtures(unittest.TestCase): + """A TestCase with a helper function to use fixtures. + + Normally used as a mix-in class to add useFixture. + + Note that test classes such as testtools.TestCase which already have a + ``useFixture`` method do not need this mixed in. + """ + + def useFixture(self, fixture): + """Use fixture in a test case. + + The fixture will be setUp, and self.addCleanup(fixture.cleanUp) called. + + :param fixture: The fixture to use. + :return: The fixture, after setting it up and scheduling a cleanup for + it. + """ + use_details = ( + gather_details is not None and + getattr(self, "addDetail", None) is not None) + try: + fixture.setUp() + except: + if use_details: + # Capture the details now, in case the fixture goes away. + gather_details(fixture.getDetails(), self.getDetails()) + raise + else: + self.addCleanup(fixture.cleanUp) + if use_details: + # Capture the details from the fixture during test teardown; + # this will evaluate the details before tearing down the + # fixture. + self.addCleanup(gather_details, fixture, self) + return fixture diff --git a/fixtures/tests/__init__.py b/fixtures/tests/__init__.py new file mode 100644 index 0000000..5e66653 --- /dev/null +++ b/fixtures/tests/__init__.py @@ -0,0 +1,47 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import doctest +import sys +import unittest + +import fixtures.tests._fixtures + + +def test_suite(): + standard_tests = unittest.TestSuite() + loader = unittest.TestLoader() + return load_tests(loader, standard_tests, None) + + +def load_tests(loader, standard_tests, pattern): + test_modules = [ + 'callmany', + 'fixture', + 'testcase', + ] + prefix = "fixtures.tests.test_" + test_mod_names = [prefix + test_module for test_module in test_modules] + standard_tests.addTests(loader.loadTestsFromNames(test_mod_names)) + if sys.version_info >= (2, 7): + # 2.7 calls load_tests for us + standard_tests.addTests(loader.loadTestsFromName('fixtures.tests._fixtures')) + else: + # We need to call it ourselves. + standard_tests.addTests(fixtures.tests._fixtures.load_tests( + loader, loader.loadTestsFromName('fixtures.tests._fixtures'), pattern)) + doctest.set_unittest_reportflags(doctest.REPORT_ONLY_FIRST_FAILURE) + standard_tests.addTest(doctest.DocFileSuite("../../README")) + return standard_tests diff --git a/fixtures/tests/_fixtures/__init__.py b/fixtures/tests/_fixtures/__init__.py new file mode 100644 index 0000000..e4d9403 --- /dev/null +++ b/fixtures/tests/_fixtures/__init__.py @@ -0,0 +1,33 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +def load_tests(loader, standard_tests, pattern): + test_modules = [ + 'environ', + 'logger', + 'monkeypatch', + 'packagepath', + 'popen', + 'pythonpackage', + 'pythonpath', + 'streams', + 'tempdir', + 'temphomedir', + 'timeout', + ] + prefix = "fixtures.tests._fixtures.test_" + test_mod_names = [prefix + test_module for test_module in test_modules] + standard_tests.addTests(loader.loadTestsFromNames(test_mod_names)) + return standard_tests diff --git a/fixtures/tests/_fixtures/test_environ.py b/fixtures/tests/_fixtures/test_environ.py new file mode 100644 index 0000000..64594c9 --- /dev/null +++ b/fixtures/tests/_fixtures/test_environ.py @@ -0,0 +1,75 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import os + +import testtools + +from fixtures import EnvironmentVariable, TestWithFixtures + + +class TestEnvironmentVariable(testtools.TestCase, TestWithFixtures): + + def test_setup_ignores_missing(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR') + os.environ.pop('FIXTURES_TEST_VAR', '') + self.useFixture(fixture) + self.assertEqual(None, os.environ.get('FIXTURES_TEST_VAR')) + + def test_setup_sets_when_missing(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR', 'bar') + os.environ.pop('FIXTURES_TEST_VAR', '') + self.useFixture(fixture) + self.assertEqual('bar', os.environ.get('FIXTURES_TEST_VAR')) + + def test_setup_deletes(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR') + os.environ['FIXTURES_TEST_VAR'] = 'foo' + self.useFixture(fixture) + self.assertEqual(None, os.environ.get('FIXTURES_TEST_VAR')) + + def test_setup_overrides(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR', 'bar') + os.environ['FIXTURES_TEST_VAR'] = 'foo' + self.useFixture(fixture) + self.assertEqual('bar', os.environ.get('FIXTURES_TEST_VAR')) + + def test_cleanup_deletes_when_missing(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR') + os.environ.pop('FIXTURES_TEST_VAR', '') + with fixture: + os.environ['FIXTURES_TEST_VAR'] = 'foo' + self.assertEqual(None, os.environ.get('FIXTURES_TEST_VAR')) + + def test_cleanup_deletes_when_set(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR', 'bar') + os.environ.pop('FIXTURES_TEST_VAR', '') + with fixture: + os.environ['FIXTURES_TEST_VAR'] = 'foo' + self.assertEqual(None, os.environ.get('FIXTURES_TEST_VAR')) + + def test_cleanup_restores_when_missing(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR') + os.environ['FIXTURES_TEST_VAR'] = 'bar' + with fixture: + os.environ.pop('FIXTURES_TEST_VAR', '') + self.assertEqual('bar', os.environ.get('FIXTURES_TEST_VAR')) + + def test_cleanup_restores_when_set(self): + fixture = EnvironmentVariable('FIXTURES_TEST_VAR') + os.environ['FIXTURES_TEST_VAR'] = 'bar' + with fixture: + os.environ['FIXTURES_TEST_VAR'] = 'quux' + self.assertEqual('bar', os.environ.get('FIXTURES_TEST_VAR')) diff --git a/fixtures/tests/_fixtures/test_logger.py b/fixtures/tests/_fixtures/test_logger.py new file mode 100644 index 0000000..1f69459 --- /dev/null +++ b/fixtures/tests/_fixtures/test_logger.py @@ -0,0 +1,168 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import logging + +from testtools import TestCase +from testtools.compat import StringIO + +from fixtures import ( + FakeLogger, + LogHandler, + TestWithFixtures, + ) + + +class FakeLoggerTest(TestCase, TestWithFixtures): + + def setUp(self): + super(FakeLoggerTest, self).setUp() + self.logger = logging.getLogger() + self.addCleanup(self.removeHandlers, self.logger) + + def removeHandlers(self, logger): + for handler in logger.handlers: + logger.removeHandler(handler) + + def test_output_property_has_output(self): + fixture = self.useFixture(FakeLogger()) + logging.info("some message") + self.assertEqual("some message\n", fixture.output) + + def test_replace_and_restore_handlers(self): + stream = StringIO() + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler(stream)) + logger.setLevel(logging.INFO) + logging.info("one") + fixture = FakeLogger() + with fixture: + logging.info("two") + logging.info("three") + self.assertEqual("two\n", fixture.output) + self.assertEqual("one\nthree\n", stream.getvalue()) + + def test_preserving_existing_handlers(self): + stream = StringIO() + self.logger.addHandler(logging.StreamHandler(stream)) + self.logger.setLevel(logging.INFO) + fixture = FakeLogger(nuke_handlers=False) + with fixture: + logging.info("message") + self.assertEqual("message\n", fixture.output) + self.assertEqual("message\n", stream.getvalue()) + + def test_logging_level_restored(self): + self.logger.setLevel(logging.DEBUG) + fixture = FakeLogger(level=logging.WARNING) + with fixture: + # The fixture won't capture this, because the DEBUG level + # is lower than the WARNING one + logging.debug("debug message") + self.assertEqual(logging.WARNING, self.logger.level) + self.assertEqual("", fixture.output) + self.assertEqual(logging.DEBUG, self.logger.level) + + def test_custom_format(self): + fixture = FakeLogger(format="%(module)s") + self.useFixture(fixture) + logging.info("message") + self.assertEqual("test_logger\n", fixture.output) + + def test_logging_output_included_in_details(self): + fixture = FakeLogger() + detail_name = "pythonlogging:''" + with fixture: + content = fixture.getDetails()[detail_name] + # Output after getDetails is called is included. + logging.info('some message') + self.assertEqual("some message\n", content.as_text()) + # The old content object returns the old usage after cleanUp (not + # strictly needed but convenient). Note that no guarantee is made that + # it will work after setUp is called again. [It does on Python 2.x, not + # on 3.x] + self.assertEqual("some message\n", content.as_text()) + with fixture: + # A new one returns new output: + self.assertEqual("", fixture.getDetails()[detail_name].as_text()) + # The original content object may either fail, or return the old + # content (it must not have been reset..). + try: + self.assertEqual("some message\n", content.as_text()) + except AssertionError: + raise + except: + pass + + +class LogHandlerTest(TestCase, TestWithFixtures): + + class CustomHandler(logging.Handler): + + def __init__(self, *args, **kwargs): + """Create the instance, and add a records attribute.""" + logging.Handler.__init__(self, *args, **kwargs) + self.msgs = [] + + def emit(self, record): + self.msgs.append(record.msg) + + def setUp(self): + super(LogHandlerTest, self).setUp() + self.logger = logging.getLogger() + self.addCleanup(self.removeHandlers, self.logger) + + def removeHandlers(self, logger): + for handler in logger.handlers: + logger.removeHandler(handler) + + def test_captures_logging(self): + fixture = self.useFixture(LogHandler(self.CustomHandler())) + logging.info("some message") + self.assertEqual(["some message"], fixture.handler.msgs) + + def test_replace_and_restore_handlers(self): + stream = StringIO() + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler(stream)) + logger.setLevel(logging.INFO) + logging.info("one") + fixture = LogHandler(self.CustomHandler()) + with fixture: + logging.info("two") + logging.info("three") + self.assertEqual(["two"], fixture.handler.msgs) + self.assertEqual("one\nthree\n", stream.getvalue()) + + def test_preserving_existing_handlers(self): + stream = StringIO() + self.logger.addHandler(logging.StreamHandler(stream)) + self.logger.setLevel(logging.INFO) + fixture = LogHandler(self.CustomHandler(), nuke_handlers=False) + with fixture: + logging.info("message") + self.assertEqual(["message"], fixture.handler.msgs) + self.assertEqual("message\n", stream.getvalue()) + + def test_logging_level_restored(self): + self.logger.setLevel(logging.DEBUG) + fixture = LogHandler(self.CustomHandler(), level=logging.WARNING) + with fixture: + # The fixture won't capture this, because the DEBUG level + # is lower than the WARNING one + logging.debug("debug message") + self.assertEqual(logging.WARNING, self.logger.level) + self.assertEqual([], fixture.handler.msgs) + self.assertEqual(logging.DEBUG, self.logger.level) diff --git a/fixtures/tests/_fixtures/test_monkeypatch.py b/fixtures/tests/_fixtures/test_monkeypatch.py new file mode 100644 index 0000000..1a84d7f --- /dev/null +++ b/fixtures/tests/_fixtures/test_monkeypatch.py @@ -0,0 +1,83 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import testtools + +from fixtures import MonkeyPatch, TestWithFixtures + +reference = 23 + +class C(object): + @staticmethod + def foo(): pass +def bar(): pass + +class TestMonkeyPatch(testtools.TestCase, TestWithFixtures): + + def test_patch_and_restore(self): + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.reference', 45) + self.assertEqual(23, reference) + fixture.setUp() + try: + self.assertEqual(45, reference) + finally: + fixture.cleanUp() + self.assertEqual(23, reference) + + def test_patch_missing_attribute(self): + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.new_attr', True) + self.assertFalse('new_attr' in globals()) + fixture.setUp() + try: + self.assertEqual(True, new_attr) + finally: + fixture.cleanUp() + self.assertFalse('new_attr' in globals()) + + def test_delete_existing_attribute(self): + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.reference', + MonkeyPatch.delete) + self.assertEqual(23, reference) + fixture.setUp() + try: + self.assertFalse('reference' in globals()) + finally: + fixture.cleanUp() + self.assertEqual(23, reference) + + def test_delete_missing_attribute(self): + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.new_attr', + MonkeyPatch.delete) + self.assertFalse('new_attr' in globals()) + fixture.setUp() + try: + self.assertFalse('new_attr' in globals()) + finally: + fixture.cleanUp() + self.assertFalse('new_attr' in globals()) + + def test_patch_staticmethod(self): + oldfoo = C.foo + fixture = MonkeyPatch( + 'fixtures.tests._fixtures.test_monkeypatch.C.foo', + bar) + with fixture: + pass + self.assertEqual(oldfoo, C.foo) + diff --git a/fixtures/tests/_fixtures/test_packagepath.py b/fixtures/tests/_fixtures/test_packagepath.py new file mode 100644 index 0000000..6833fdc --- /dev/null +++ b/fixtures/tests/_fixtures/test_packagepath.py @@ -0,0 +1,43 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import testtools + +import fixtures +from fixtures import ( + PackagePathEntry, + TempDir, + ) + + +class TestPackagePathEntry(testtools.TestCase): + + def test_adds_missing_to_end_package_path(self): + uniquedir = self.useFixture(TempDir()).path + fixture = PackagePathEntry('fixtures', uniquedir) + self.assertFalse(uniquedir in fixtures.__path__) + with fixture: + self.assertTrue(uniquedir in fixtures.__path__) + self.assertFalse(uniquedir in fixtures.__path__) + + def test_doesnt_alter_existing_entry(self): + existingdir = fixtures.__path__[0] + expectedlen = len(fixtures.__path__) + fixture = PackagePathEntry('fixtures', existingdir) + with fixture: + self.assertTrue(existingdir in fixtures.__path__) + self.assertEqual(expectedlen, len(fixtures.__path__)) + self.assertTrue(existingdir in fixtures.__path__) + self.assertEqual(expectedlen, len(fixtures.__path__)) diff --git a/fixtures/tests/_fixtures/test_popen.py b/fixtures/tests/_fixtures/test_popen.py new file mode 100644 index 0000000..98b762f --- /dev/null +++ b/fixtures/tests/_fixtures/test_popen.py @@ -0,0 +1,104 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import subprocess + +import testtools +from testtools.compat import ( + _b, + BytesIO, + ) + +from fixtures import FakePopen, TestWithFixtures +from fixtures._fixtures.popen import FakeProcess + + +class TestFakePopen(testtools.TestCase, TestWithFixtures): + + def test_installs_restores_global(self): + fixture = FakePopen() + popen = subprocess.Popen + fixture.setUp() + try: + self.assertEqual(subprocess.Popen, fixture) + finally: + fixture.cleanUp() + self.assertEqual(subprocess.Popen, popen) + + def test___call___is_recorded(self): + fixture = self.useFixture(FakePopen()) + proc = fixture(['foo', 'bar'], 1, None, 'in', 'out', 'err') + self.assertEqual(1, len(fixture.procs)) + self.assertEqual(dict(args=['foo', 'bar'], bufsize=1, executable=None, + stdin='in', stdout='out', stderr='err'), proc._args) + + def test_inject_content_stdout(self): + def get_info(args): + return {'stdout': 'stdout'} + fixture = self.useFixture(FakePopen(get_info)) + proc = fixture(['foo']) + self.assertEqual('stdout', proc.stdout) + + def test_handles_all_2_7_args(self): + all_args = dict( + args="args", bufsize="bufsize", executable="executable", + stdin="stdin", stdout="stdout", stderr="stderr", + preexec_fn="preexec_fn", close_fds="close_fds", shell="shell", + cwd="cwd", env="env", universal_newlines="universal_newlines", + startupinfo="startupinfo", creationflags="creationflags") + def get_info(proc_args): + self.assertEqual(all_args, proc_args) + return {} + fixture = self.useFixture(FakePopen(get_info)) + proc = fixture(**all_args) + + def test_custom_returncode(self): + def get_info(proc_args): + return dict(returncode=1) + proc = self.useFixture(FakePopen(get_info))(['foo']) + self.assertEqual(None, proc.returncode) + self.assertEqual(1, proc.wait()) + self.assertEqual(1, proc.returncode) + + def test_with_popen_custom(self): + fixture = self.useFixture(FakePopen()) + with subprocess.Popen(['ls -lh']) as proc: + self.assertEqual(None, proc.returncode) + + +class TestFakeProcess(testtools.TestCase): + + def test_wait(self): + proc = FakeProcess({}, {}) + proc.returncode = 45 + self.assertEqual(45, proc.wait()) + + def test_communicate(self): + proc = FakeProcess({}, {}) + self.assertEqual(('', ''), proc.communicate()) + self.assertEqual(0, proc.returncode) + + def test_communicate_with_out(self): + proc = FakeProcess({}, {'stdout': BytesIO(_b('foo'))}) + self.assertEqual((_b('foo'), ''), proc.communicate()) + self.assertEqual(0, proc.returncode) + + def test_kill(self): + proc = FakeProcess({}, {}) + self.assertIs(None, proc.kill()) + + def test_wait_with_timeout_and_endtime(self): + proc = FakeProcess({}, {}) + self.assertEqual(0 , proc.wait(timeout=4, endtime=7)) diff --git a/fixtures/tests/_fixtures/test_pythonpackage.py b/fixtures/tests/_fixtures/test_pythonpackage.py new file mode 100644 index 0000000..4e2160b --- /dev/null +++ b/fixtures/tests/_fixtures/test_pythonpackage.py @@ -0,0 +1,52 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import os.path + +import testtools +from testtools.compat import _b + +from fixtures import PythonPackage, TestWithFixtures + + +class TestPythonPackage(testtools.TestCase, TestWithFixtures): + + def test_has_tempdir(self): + fixture = PythonPackage('foo', []) + fixture.setUp() + try: + self.assertTrue(os.path.isdir(fixture.base)) + finally: + fixture.cleanUp() + + def test_writes_package(self): + fixture = PythonPackage('foo', [('bar.py', _b('woo'))]) + fixture.setUp() + try: + self.assertEqual('', open(os.path.join(fixture.base, 'foo', + '__init__.py')).read()) + self.assertEqual('woo', open(os.path.join(fixture.base, 'foo', + 'bar.py')).read()) + finally: + fixture.cleanUp() + + def test_no__init__(self): + fixture = PythonPackage('foo', [('bar.py', _b('woo'))], init=False) + fixture.setUp() + try: + self.assertFalse(os.path.exists(os.path.join(fixture.base, 'foo', + '__init__.py'))) + finally: + fixture.cleanUp() diff --git a/fixtures/tests/_fixtures/test_pythonpath.py b/fixtures/tests/_fixtures/test_pythonpath.py new file mode 100644 index 0000000..5bb6851 --- /dev/null +++ b/fixtures/tests/_fixtures/test_pythonpath.py @@ -0,0 +1,44 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import sys + +import testtools + +from fixtures import ( + PythonPathEntry, + TempDir, + ) + + +class TestPythonPathEntry(testtools.TestCase): + + def test_adds_missing_to_end_sys_path(self): + uniquedir = self.useFixture(TempDir()).path + fixture = PythonPathEntry(uniquedir) + self.assertFalse(uniquedir in sys.path) + with fixture: + self.assertTrue(uniquedir in sys.path) + self.assertFalse(uniquedir in sys.path) + + def test_doesnt_alter_existing_entry(self): + existingdir = sys.path[0] + expectedlen = len(sys.path) + fixture = PythonPathEntry(existingdir) + with fixture: + self.assertTrue(existingdir in sys.path) + self.assertEqual(expectedlen, len(sys.path)) + self.assertTrue(existingdir in sys.path) + self.assertEqual(expectedlen, len(sys.path)) diff --git a/fixtures/tests/_fixtures/test_streams.py b/fixtures/tests/_fixtures/test_streams.py new file mode 100644 index 0000000..68396cd --- /dev/null +++ b/fixtures/tests/_fixtures/test_streams.py @@ -0,0 +1,105 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2012, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +from testtools import TestCase +from testtools.compat import ( + _b, + _u, + ) +from testtools.matchers import Contains + +from fixtures import ( + ByteStream, + DetailStream, + StringStream, + ) + + +class DetailStreamTest(TestCase): + + def test_doc_mentions_deprecated(self): + self.assertThat(DetailStream.__doc__, Contains('Deprecated')) + + +class TestByteStreams(TestCase): + + def test_empty_detail_stream(self): + detail_name = 'test' + fixture = ByteStream(detail_name) + with fixture: + content = fixture.getDetails()[detail_name] + self.assertEqual(_u(""), content.as_text()) + + def test_stream_content_in_details(self): + detail_name = 'test' + fixture = ByteStream(detail_name) + with fixture: + stream = fixture.stream + content = fixture.getDetails()[detail_name] + # Output after getDetails is called is included. + stream.write(_b("testing 1 2 3")) + self.assertEqual("testing 1 2 3", content.as_text()) + + def test_stream_content_reset(self): + detail_name = 'test' + fixture = ByteStream(detail_name) + with fixture: + stream = fixture.stream + content = fixture.getDetails()[detail_name] + stream.write(_b("testing 1 2 3")) + with fixture: + # The old content object returns the old usage + self.assertEqual(_u("testing 1 2 3"), content.as_text()) + content = fixture.getDetails()[detail_name] + # A new fixture returns the new output: + stream = fixture.stream + stream.write(_b("1 2 3 testing")) + self.assertEqual(_u("1 2 3 testing"), content.as_text()) + + +class TestStringStreams(TestCase): + + def test_empty_detail_stream(self): + detail_name = 'test' + fixture = StringStream(detail_name) + with fixture: + content = fixture.getDetails()[detail_name] + self.assertEqual(_u(""), content.as_text()) + + def test_stream_content_in_details(self): + detail_name = 'test' + fixture = StringStream(detail_name) + with fixture: + stream = fixture.stream + content = fixture.getDetails()[detail_name] + # Output after getDetails is called is included. + stream.write(_u("testing 1 2 3")) + self.assertEqual("testing 1 2 3", content.as_text()) + + def test_stream_content_reset(self): + detail_name = 'test' + fixture = StringStream(detail_name) + with fixture: + stream = fixture.stream + content = fixture.getDetails()[detail_name] + stream.write(_u("testing 1 2 3")) + with fixture: + # The old content object returns the old usage + self.assertEqual(_u("testing 1 2 3"), content.as_text()) + content = fixture.getDetails()[detail_name] + # A new fixture returns the new output: + stream = fixture.stream + stream.write(_u("1 2 3 testing")) + self.assertEqual(_u("1 2 3 testing"), content.as_text()) diff --git a/fixtures/tests/_fixtures/test_tempdir.py b/fixtures/tests/_fixtures/test_tempdir.py new file mode 100644 index 0000000..d0def55 --- /dev/null +++ b/fixtures/tests/_fixtures/test_tempdir.py @@ -0,0 +1,96 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import os +import tempfile + +import testtools +from testtools.matchers import StartsWith + +from fixtures import ( + NestedTempfile, + TempDir, + ) + + +class TestTempDir(testtools.TestCase): + + def test_basic(self): + fixture = TempDir() + sentinel = object() + self.assertEqual(sentinel, getattr(fixture, 'path', sentinel)) + fixture.setUp() + try: + path = fixture.path + self.assertTrue(os.path.isdir(path)) + finally: + fixture.cleanUp() + self.assertFalse(os.path.isdir(path)) + + def test_under_dir(self): + root = self.useFixture(TempDir()).path + fixture = TempDir(root) + fixture.setUp() + with fixture: + self.assertThat(fixture.path, StartsWith(root)) + + def test_join(self): + temp_dir = self.useFixture(TempDir()) + root = temp_dir.path + relpath = 'foo/bar/baz' + self.assertEqual( + os.path.join(root, relpath), temp_dir.join(relpath)) + + def test_join_multiple_children(self): + temp_dir = self.useFixture(TempDir()) + root = temp_dir.path + self.assertEqual( + os.path.join(root, 'foo', 'bar', 'baz'), + temp_dir.join('foo', 'bar', 'baz')) + + def test_join_naughty_children(self): + temp_dir = self.useFixture(TempDir()) + root = temp_dir.path + self.assertEqual( + os.path.abspath(os.path.join(root, '..', 'bar', 'baz')), + temp_dir.join('..', 'bar', 'baz')) + + +class NestedTempfileTest(testtools.TestCase): + """Tests for `NestedTempfile`.""" + + def test_normal(self): + # The temp directory is removed when the context is exited. + starting_tempdir = tempfile.gettempdir() + with NestedTempfile(): + self.assertEqual(tempfile.tempdir, tempfile.gettempdir()) + self.assertNotEqual(starting_tempdir, tempfile.tempdir) + self.assertTrue(os.path.isdir(tempfile.tempdir)) + nested_tempdir = tempfile.tempdir + self.assertEqual(tempfile.tempdir, tempfile.gettempdir()) + self.assertEqual(starting_tempdir, tempfile.tempdir) + self.assertFalse(os.path.isdir(nested_tempdir)) + + def test_exception(self): + # The temp directory is removed when the context is exited, even if + # the code running in context raises an exception. + class ContrivedException(Exception): + pass + try: + with NestedTempfile(): + nested_tempdir = tempfile.tempdir + raise ContrivedException + except ContrivedException: + self.assertFalse(os.path.isdir(nested_tempdir)) diff --git a/fixtures/tests/_fixtures/test_temphomedir.py b/fixtures/tests/_fixtures/test_temphomedir.py new file mode 100644 index 0000000..339ce2c --- /dev/null +++ b/fixtures/tests/_fixtures/test_temphomedir.py @@ -0,0 +1,46 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2011 Canonical Ltd. +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import os + +import testtools +from testtools.matchers import StartsWith + +from fixtures import ( + TempDir, + TempHomeDir, + ) + +class TestTempDir(testtools.TestCase): + + def test_basic(self): + fixture = TempHomeDir() + sentinel = object() + self.assertEqual(sentinel, getattr(fixture, 'path', sentinel)) + fixture.setUp() + try: + path = fixture.path + self.assertTrue(os.path.isdir(path)) + self.assertEqual(path, os.environ.get("HOME")) + finally: + fixture.cleanUp() + self.assertFalse(os.path.isdir(path)) + + def test_under_dir(self): + root = self.useFixture(TempDir()).path + fixture = TempHomeDir(root) + fixture.setUp() + with fixture: + self.assertThat(fixture.path, StartsWith(root)) diff --git a/fixtures/tests/_fixtures/test_timeout.py b/fixtures/tests/_fixtures/test_timeout.py new file mode 100644 index 0000000..266e257 --- /dev/null +++ b/fixtures/tests/_fixtures/test_timeout.py @@ -0,0 +1,66 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (C) 2011, Martin Pool <mbp@sourcefrog.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import signal +import time + +import testtools +from testtools.testcase import ( + TestSkipped, + ) + +import fixtures + + +def sample_timeout_passes(): + with fixtures.Timeout(100, gentle=True): + pass # Timeout shouldn't fire + +def sample_long_delay_with_gentle_timeout(): + with fixtures.Timeout(1, gentle=True): + time.sleep(100) # Expected to be killed here. + +def sample_long_delay_with_harsh_timeout(): + with fixtures.Timeout(1, gentle=False): + time.sleep(100) # Expected to be killed here. + + +class TestTimeout(testtools.TestCase, fixtures.TestWithFixtures): + + def requireUnix(self): + if getattr(signal, 'alarm', None) is None: + raise TestSkipped("no alarm() function") + + def test_timeout_passes(self): + # This can pass even on Windows - the test is skipped. + sample_timeout_passes() + + def test_timeout_gentle(self): + self.requireUnix() + self.assertRaises( + fixtures.TimeoutException, + sample_long_delay_with_gentle_timeout) + + def test_timeout_harsh(self): + self.requireUnix() + # This will normally kill the whole process, which would be + # inconvenient. Let's hook the alarm here so we can observe it. + self.got_alarm = False + def sigalrm_handler(signum, frame): + self.got_alarm = True + old_handler = signal.signal(signal.SIGALRM, sigalrm_handler) + self.addCleanup(signal.signal, signal.SIGALRM, old_handler) + sample_long_delay_with_harsh_timeout() + self.assertTrue(self.got_alarm) diff --git a/fixtures/tests/helpers.py b/fixtures/tests/helpers.py new file mode 100644 index 0000000..ae0d8d3 --- /dev/null +++ b/fixtures/tests/helpers.py @@ -0,0 +1,33 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import fixtures + +class LoggingFixture(fixtures.Fixture): + + def __init__(self, suffix='', calls=None): + super(LoggingFixture, self).__init__() + if calls is None: + calls = [] + self.calls = calls + self.suffix = suffix + + def setUp(self): + super(LoggingFixture, self).setUp() + self.calls.append('setUp' + self.suffix) + self.addCleanup(self.calls.append, 'cleanUp' + self.suffix) + + def reset(self): + self.calls.append('reset' + self.suffix) diff --git a/fixtures/tests/test_callmany.py b/fixtures/tests/test_callmany.py new file mode 100644 index 0000000..2bf28da --- /dev/null +++ b/fixtures/tests/test_callmany.py @@ -0,0 +1,68 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import types + +import testtools + +from fixtures.callmany import CallMany + + +class TestCallMany(testtools.TestCase): + + def test__call__raise_errors_false_callsall_returns_exceptions(self): + calls = [] + def raise_exception1(): + calls.append('1') + raise Exception('woo') + def raise_exception2(): + calls.append('2') + raise Exception('woo') + call = CallMany() + call.push(raise_exception2) + call.push(raise_exception1) + exceptions = call(raise_errors=False) + self.assertEqual(['1', '2'], calls) + # There should be two exceptions + self.assertEqual(2, len(exceptions)) + # They should be a sys.exc_info tuple. + self.assertEqual(3, len(exceptions[0])) + type, value, tb = exceptions[0] + self.assertEqual(Exception, type) + self.assertIsInstance(value, Exception) + self.assertEqual(('woo',), value.args) + self.assertIsInstance(tb, types.TracebackType) + + def test_exit_propogates_exceptions(self): + call = CallMany() + call.__enter__() + self.assertEqual(False, call.__exit__(None, None, None)) + + def test_exit_runs_all_raises_first_exception(self): + calls = [] + def raise_exception1(): + calls.append('1') + raise Exception('woo') + def raise_exception2(): + calls.append('2') + raise Exception('hoo') + call = CallMany() + call.push(raise_exception2) + call.push(raise_exception1) + call.__enter__() + exc = self.assertRaises(Exception, call.__exit__, None, None, None) + self.assertEqual(('woo',), exc.args[0][1].args) + self.assertEqual(('hoo',), exc.args[1][1].args) + self.assertEqual(['1', '2'], calls) diff --git a/fixtures/tests/test_fixture.py b/fixtures/tests/test_fixture.py new file mode 100644 index 0000000..74e6ad0 --- /dev/null +++ b/fixtures/tests/test_fixture.py @@ -0,0 +1,313 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import types + +import testtools +from testtools.content import text_content +from testtools.testcase import skipIf + +import fixtures +from fixtures.fixture import gather_details +from fixtures.tests.helpers import LoggingFixture + + +require_gather_details = skipIf(gather_details is None, + "gather_details() is not available.") + + +# Note: the cleanup related tests are strictly speaking redundant, IFF they are +# replaced with contract tests for correct use of CallMany. +class TestFixture(testtools.TestCase): + + def test_resetCallsSetUpCleanUp(self): + calls = [] + class FixtureWithSetupOnly(fixtures.Fixture): + def setUp(self): + super(FixtureWithSetupOnly, self).setUp() + calls.append('setUp') + self.addCleanup(calls.append, 'cleanUp') + fixture = FixtureWithSetupOnly() + fixture.setUp() + fixture.reset() + fixture.cleanUp() + self.assertEqual(['setUp', 'cleanUp', 'setUp', 'cleanUp'], calls) + + def test_reset_raises_if_cleanup_raises(self): + class FixtureWithSetupOnly(fixtures.Fixture): + def do_raise(self): + raise Exception('foo') + def setUp(self): + super(FixtureWithSetupOnly, self).setUp() + self.addCleanup(self.do_raise) + fixture = FixtureWithSetupOnly() + fixture.setUp() + exc = self.assertRaises(Exception, fixture.reset) + self.assertEqual(('foo',), exc.args) + + def test_cleanUp_raise_first_false_callscleanups_returns_exceptions(self): + calls = [] + def raise_exception1(): + calls.append('1') + raise Exception('woo') + def raise_exception2(): + calls.append('2') + raise Exception('woo') + class FixtureWithException(fixtures.Fixture): + def setUp(self): + super(FixtureWithException, self).setUp() + self.addCleanup(raise_exception2) + self.addCleanup(raise_exception1) + fixture = FixtureWithException() + fixture.setUp() + exceptions = fixture.cleanUp(raise_first=False) + self.assertEqual(['1', '2'], calls) + # There should be two exceptions + self.assertEqual(2, len(exceptions)) + # They should be a sys.exc_info tuple. + self.assertEqual(3, len(exceptions[0])) + type, value, tb = exceptions[0] + self.assertEqual(Exception, type) + self.assertIsInstance(value, Exception) + self.assertEqual(('woo',), value.args) + self.assertIsInstance(tb, types.TracebackType) + + def test_exit_propogates_exceptions(self): + fixture = fixtures.Fixture() + fixture.__enter__() + self.assertEqual(False, fixture.__exit__(None, None, None)) + + def test_exit_runs_all_raises_first_exception(self): + calls = [] + def raise_exception1(): + calls.append('1') + raise Exception('woo') + def raise_exception2(): + calls.append('2') + raise Exception('hoo') + class FixtureWithException(fixtures.Fixture): + def setUp(self): + super(FixtureWithException, self).setUp() + self.addCleanup(raise_exception2) + self.addCleanup(raise_exception1) + fixture = FixtureWithException() + fixture.__enter__() + exc = self.assertRaises(Exception, fixture.__exit__, None, None, None) + self.assertEqual(('woo',), exc.args[0][1].args) + self.assertEqual(('hoo',), exc.args[1][1].args) + self.assertEqual(['1', '2'], calls) + + def test_useFixture(self): + parent = LoggingFixture('-outer') + nested = LoggingFixture('-inner', calls=parent.calls) + parent.setUp() + parent.useFixture(nested) + parent.cleanUp() + self.assertEqual( + ['setUp-outer', 'setUp-inner', 'cleanUp-inner', 'cleanUp-outer'], + parent.calls) + + @require_gather_details + def test_useFixture_details_captured_from_setUp(self): + # Details added during fixture set-up are gathered even if setUp() + # fails with an exception. + class SomethingBroke(Exception): pass + class BrokenFixture(fixtures.Fixture): + def setUp(self): + super(BrokenFixture, self).setUp() + self.addDetail('content', text_content("foobar")) + raise SomethingBroke() + broken_fixture = BrokenFixture() + class SimpleFixture(fixtures.Fixture): + def setUp(self): + super(SimpleFixture, self).setUp() + self.useFixture(broken_fixture) + simple_fixture = SimpleFixture() + self.assertRaises(SomethingBroke, simple_fixture.setUp) + self.assertEqual( + {"content": text_content("foobar")}, + broken_fixture.getDetails()) + self.assertEqual( + {"content": text_content("foobar")}, + simple_fixture.getDetails()) + + def test_getDetails(self): + fixture = fixtures.Fixture() + with fixture: + self.assertEqual({}, fixture.getDetails()) + + def test_details_from_child_fixtures_are_returned(self): + parent = fixtures.Fixture() + with parent: + child = fixtures.Fixture() + parent.useFixture(child) + # Note that we add the detail *after* using the fixture: the parent + # has to query just-in-time. + child.addDetail('foo', 'content') + self.assertEqual({'foo': 'content'}, parent.getDetails()) + # And dropping it from the child drops it from the parent. + del child._details['foo'] + self.assertEqual({}, parent.getDetails()) + # After cleanup the child details are still gone. + child.addDetail('foo', 'content') + self.assertEqual({}, parent.getDetails()) + + def test_duplicate_details_are_disambiguated(self): + parent = fixtures.Fixture() + with parent: + parent.addDetail('foo', 'parent-content') + child = fixtures.Fixture() + parent.useFixture(child) + # Note that we add the detail *after* using the fixture: the parent + # has to query just-in-time. + child.addDetail('foo', 'child-content') + self.assertEqual({'foo': 'parent-content', + 'foo-1': 'child-content',}, parent.getDetails()) + + def test_addDetail(self): + fixture = fixtures.Fixture() + with fixture: + fixture.addDetail('foo', 'content') + self.assertEqual({'foo': 'content'}, fixture.getDetails()) + del fixture._details['foo'] + self.assertEqual({}, fixture.getDetails()) + fixture.addDetail('foo', 'content') + # Cleanup clears the details too. + self.assertEqual({}, fixture.getDetails()) + + +class TestFunctionFixture(testtools.TestCase): + + def test_setup_only(self): + fixture = fixtures.FunctionFixture(lambda: 42) + fixture.setUp() + self.assertEqual(42, fixture.fn_result) + fixture.cleanUp() + self.assertFalse(hasattr(fixture, 'fn_result')) + + def test_cleanup(self): + results = [] + fixture = fixtures.FunctionFixture(lambda: 84, results.append) + fixture.setUp() + self.assertEqual(84, fixture.fn_result) + self.assertEqual([], results) + fixture.cleanUp() + self.assertEqual([84], results) + + def test_reset(self): + results = [] + expected = [21, 7] + def setUp(): + return expected.pop(0) + def reset(result): + results.append(('reset', result)) + return expected.pop(0) + fixture = fixtures.FunctionFixture(setUp, results.append, reset) + fixture.setUp() + self.assertEqual([], results) + fixture.reset() + self.assertEqual([('reset', 21)], results) + self.assertEqual(7, fixture.fn_result) + fixture.cleanUp() + self.assertEqual([('reset', 21), 7], results) + + +class TestMethodFixture(testtools.TestCase): + + def test_no_setup_cleanup(self): + class Stub: + pass + fixture = fixtures.MethodFixture(Stub()) + fixture.setUp() + fixture.reset() + self.assertIsInstance(fixture.obj, Stub) + fixture.cleanUp() + + def test_setup_only(self): + class Stub: + def setUp(self): + self.value = 42 + fixture = fixtures.MethodFixture(Stub()) + fixture.setUp() + self.assertEqual(42, fixture.obj.value) + self.assertIsInstance(fixture.obj, Stub) + fixture.cleanUp() + + def test_cleanup_only(self): + class Stub: + value = None + def tearDown(self): + self.value = 42 + fixture = fixtures.MethodFixture(Stub()) + fixture.setUp() + self.assertEqual(None, fixture.obj.value) + self.assertIsInstance(fixture.obj, Stub) + fixture.cleanUp() + self.assertEqual(42, fixture.obj.value) + + def test_cleanup(self): + class Stub: + def setUp(self): + self.value = 42 + def tearDown(self): + self.value = 84 + fixture = fixtures.MethodFixture(Stub()) + fixture.setUp() + self.assertEqual(42, fixture.obj.value) + self.assertIsInstance(fixture.obj, Stub) + fixture.cleanUp() + self.assertEqual(84, fixture.obj.value) + + def test_custom_setUp(self): + class Stub: + def mysetup(self): + self.value = 42 + obj = Stub() + fixture = fixtures.MethodFixture(obj, setup=obj.mysetup) + fixture.setUp() + self.assertEqual(42, fixture.obj.value) + self.assertEqual(obj, fixture.obj) + fixture.cleanUp() + + def test_custom_cleanUp(self): + class Stub: + value = 42 + def mycleanup(self): + self.value = None + obj = Stub() + fixture = fixtures.MethodFixture(obj, cleanup=obj.mycleanup) + fixture.setUp() + self.assertEqual(42, fixture.obj.value) + self.assertEqual(obj, fixture.obj) + fixture.cleanUp() + self.assertEqual(None, fixture.obj.value) + + def test_reset(self): + class Stub: + def setUp(self): + self.value = 42 + def tearDown(self): + self.value = 84 + def reset(self): + self.value = 126 + obj = Stub() + fixture = fixtures.MethodFixture(obj, reset=obj.reset) + fixture.setUp() + self.assertEqual(obj, fixture.obj) + self.assertEqual(42, obj.value) + fixture.reset() + self.assertEqual(126, obj.value) + fixture.cleanUp() + self.assertEqual(84, obj.value) diff --git a/fixtures/tests/test_testcase.py b/fixtures/tests/test_testcase.py new file mode 100644 index 0000000..3f186c5 --- /dev/null +++ b/fixtures/tests/test_testcase.py @@ -0,0 +1,97 @@ +# fixtures: Fixtures with cleanups for testing and convenience. +# +# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net> +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + +import unittest +import testtools +from testtools.content import text_content +from testtools.testcase import skipIf + +import fixtures +from fixtures import TestWithFixtures +from fixtures.fixture import gather_details +from fixtures.tests.helpers import LoggingFixture + + +class TestTestWithFixtures(unittest.TestCase): + + def test_useFixture(self): + fixture = LoggingFixture() + class SimpleTest(testtools.TestCase, TestWithFixtures): + def test_foo(self): + self.useFixture(fixture) + result = unittest.TestResult() + SimpleTest('test_foo').run(result) + self.assertTrue(result.wasSuccessful()) + self.assertEqual(['setUp', 'cleanUp'], fixture.calls) + + def test_useFixture_uses_raise_first(self): + calls = [] + def raiser(ignored): + calls.append('called') + raise Exception('foo') + fixture = fixtures.FunctionFixture(lambda:None, raiser) + class SimpleTest(testtools.TestCase, TestWithFixtures): + def test_foo(self): + self.useFixture(fixture) + result = unittest.TestResult() + SimpleTest('test_foo').run(result) + self.assertFalse(result.wasSuccessful()) + self.assertEqual(['called'], calls) + + @skipIf(gather_details is None, "gather_details() is not available.") + def test_useFixture_details_captured_from_setUp(self): + # Details added during fixture set-up are gathered even if setUp() + # fails with an exception. + class SomethingBroke(Exception): pass + class BrokenFixture(fixtures.Fixture): + def setUp(self): + super(BrokenFixture, self).setUp() + self.addDetail('content', text_content("foobar")) + raise SomethingBroke() + broken_fixture = BrokenFixture() + class DetailedTestCase(TestWithFixtures, testtools.TestCase): + def setUp(self): + super(DetailedTestCase, self).setUp() + self.useFixture(broken_fixture) + def test(self): + pass + detailed_test_case = DetailedTestCase("test") + self.assertRaises(SomethingBroke, detailed_test_case.setUp) + self.assertEqual( + {"content": text_content("foobar")}, + broken_fixture.getDetails()) + self.assertEqual( + {"content": text_content("foobar")}, + detailed_test_case.getDetails()) + + @skipIf(gather_details is None, "gather_details() is not available.") + def test_useFixture_details_not_captured_from_setUp(self): + # Details added during fixture set-up are not gathered if the test + # case does not have the ability to accept those details. + class SomethingBroke(Exception): pass + class BrokenFixture(fixtures.Fixture): + def setUp(self): + super(BrokenFixture, self).setUp() + self.addDetail('content', text_content("foobar")) + raise SomethingBroke() + broken_fixture = BrokenFixture() + class NonDetailedTestCase(TestWithFixtures, unittest.TestCase): + def setUp(self): + super(NonDetailedTestCase, self).setUp() + self.useFixture(broken_fixture) + def test(self): + pass + non_detailed_test_case = NonDetailedTestCase("test") + self.assertRaises(SomethingBroke, non_detailed_test_case.setUp) |