freeipa/tests/util.py

607 lines
16 KiB
Python
Raw Normal View History

# Authors:
# Jason Gerard DeRose <jderose@redhat.com>
#
# Copyright (C) 2008 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
2008-10-07 23:20:00 -05:00
Common utility functions and classes for unit tests.
"""
import inspect
import os
from os import path
import tempfile
import shutil
2009-12-17 07:16:18 -06:00
import re
import ipalib
from ipalib.plugable import Plugin
from ipalib.request import context
class TempDir(object):
def __init__(self):
self.__path = tempfile.mkdtemp(prefix='ipa.tests.')
assert self.path == self.__path
def __get_path(self):
assert path.abspath(self.__path) == self.__path
assert self.__path.startswith('/tmp/ipa.tests.')
assert path.isdir(self.__path) and not path.islink(self.__path)
return self.__path
path = property(__get_path)
def rmtree(self):
if self.__path is not None:
shutil.rmtree(self.path)
self.__path = None
def makedirs(self, *parts):
d = self.join(*parts)
if not path.exists(d):
os.makedirs(d)
assert path.isdir(d) and not path.islink(d)
return d
def touch(self, *parts):
d = self.makedirs(*parts[:-1])
f = path.join(d, parts[-1])
assert not path.exists(f)
open(f, 'w').close()
assert path.isfile(f) and not path.islink(f)
return f
def write(self, content, *parts):
d = self.makedirs(*parts[:-1])
f = path.join(d, parts[-1])
assert not path.exists(f)
open(f, 'w').write(content)
assert path.isfile(f) and not path.islink(f)
return f
def join(self, *parts):
return path.join(self.path, *parts)
def __del__(self):
self.rmtree()
class TempHome(TempDir):
def __init__(self):
super(TempHome, self).__init__()
self.__home = os.environ['HOME']
os.environ['HOME'] = self.path
class ExceptionNotRaised(Exception):
"""
Exception raised when an *expected* exception is *not* raised during a
unit test.
"""
msg = 'expected %s'
def __init__(self, expected):
self.expected = expected
def __str__(self):
return self.msg % self.expected.__name__
def assert_equal(val1, val2):
"""
Assert ``val1`` and ``val2`` are the same type and of equal value.
"""
assert type(val1) is type(val2), '%r != %r' % (val1, val2)
assert val1 == val2, '%r != %r' % (val1, val2)
2009-12-17 07:16:18 -06:00
class Fuzzy(object):
"""
Perform a fuzzy (non-strict) equality tests.
2009-12-17 07:16:18 -06:00
`Fuzzy` instances will likely be used when comparing nesting data-structures
using `assert_deepequal()`.
By default a `Fuzzy` instance is equal to everything. For example, all of
these evaluate to ``True``:
>>> Fuzzy() == False
True
>>> 7 == Fuzzy() # Order doesn't matter
True
>>> Fuzzy() == u'Hello False, Lucky 7!'
True
The first optional argument *regex* is a regular expression pattern to
match. For example, you could match a phone number like this:
>>> phone = Fuzzy('^\d{3}-\d{3}-\d{4}$')
>>> u'123-456-7890' == phone
True
Use of a regular expression by default implies the ``unicode`` type, so
comparing with an ``str`` instance will evaluate to ``False``:
>>> phone.type
<type 'unicode'>
>>> '123-456-7890' == phone
False
The *type* kwarg allows you to specify a type constraint, so you can force
the above to work on ``str`` instances instead:
>>> '123-456-7890' == Fuzzy('^\d{3}-\d{3}-\d{4}$', type=str)
True
You can also use the *type* constraint on its own without the *regex*, for
example:
>>> 42 == Fuzzy(type=int)
True
>>> 42.0 == Fuzzy(type=int)
False
>>> 42.0 == Fuzzy(type=(int, float))
True
Finally the *test* kwarg is an optional callable that will be called to
perform the loose equality test. For example:
>>> 42 == Fuzzy(test=lambda other: other > 42)
False
>>> 43 == Fuzzy(test=lambda other: other > 42)
True
You can use *type* and *test* together. For example:
>>> 43 == Fuzzy(type=float, test=lambda other: other > 42)
False
>>> 42.5 == Fuzzy(type=float, test=lambda other: other > 42)
True
The *regex*, *type*, and *test* kwargs are all availabel via attributes on
the `Fuzzy` instance:
>>> fuzzy = Fuzzy('.+', type=str, test=lambda other: True)
>>> fuzzy.regex
'.+'
>>> fuzzy.type
<type 'str'>
>>> fuzzy.test # doctest:+ELLIPSIS
<function <lambda> at 0x...>
To aid debugging, `Fuzzy.__repr__()` revealse these kwargs as well:
>>> fuzzy # doctest:+ELLIPSIS
Fuzzy('.+', <type 'str'>, <function <lambda> at 0x...>)
2009-12-17 07:16:18 -06:00
"""
def __init__(self, regex=None, type=None, test=None):
"""
Initialize.
:param regex: A regular expression pattern to match, e.g.
``u'^\d+foo'``
:param type: A type or tuple of types to test using ``isinstance()``,
e.g. ``(int, float)``
:param test: A callable used to perform equality test, e.g.
``lambda other: other >= 18``
"""
assert regex is None or isinstance(regex, basestring)
assert test is None or callable(test)
if regex is None:
self.re = None
else:
self.re = re.compile(regex)
if type is None:
type = unicode
assert type in (unicode, str, basestring)
2009-12-17 07:16:18 -06:00
self.regex = regex
self.type = type
self.test = test
def __repr__(self):
return '%s(%r, %r, %r)' % (
2009-12-17 07:16:18 -06:00
self.__class__.__name__, self.regex, self.type, self.test
)
def __eq__(self, other):
if not (self.type is None or isinstance(other, self.type)):
return False
if not (self.re is None or self.re.search(other)):
return False
if not (self.test is None or self.test(other)):
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
VALUE = """assert_deepequal: expected != got.
%s
expected = %r
got = %r
path = %r"""
TYPE = """assert_deepequal: type(expected) is not type(got).
%s
type(expected) = %r
type(got) = %r
expected = %r
got = %r
path = %r"""
LEN = """assert_deepequal: list length mismatch.
%s
len(expected) = %r
len(got) = %r
expected = %r
got = %r
path = %r"""
KEYS = """assert_deepequal: dict keys mismatch.
%s
missing keys = %r
extra keys = %r
expected = %r
got = %r
path = %r"""
def assert_deepequal(expected, got, doc='', stack=tuple()):
"""
Recursively check for type and equality.
If the tests fails, it will raise an ``AssertionError`` with detailed
information, including the path to the offending value. For example:
>>> expected = [u'Hello', dict(world=u'how are you?')]
>>> got = [u'Hello', dict(world='how are you?')]
>>> expected == got
True
>>> assert_deepequal(expected, got, doc='Testing my nested data')
Traceback (most recent call last):
...
AssertionError: assert_deepequal: type(expected) is not type(got).
Testing my nested data
type(expected) = <type 'unicode'>
type(got) = <type 'str'>
expected = u'how are you?'
got = 'how are you?'
path = (1, 'world')
"""
2009-12-17 07:16:18 -06:00
if isinstance(expected, tuple):
expected = list(expected)
if isinstance(got, tuple):
got = list(got)
if not (isinstance(expected, Fuzzy) or type(expected) is type(got)):
raise AssertionError(
TYPE % (doc, type(expected), type(got), expected, got, stack)
)
if isinstance(expected, (list, tuple)):
if len(expected) != len(got):
raise AssertionError(
LEN % (doc, len(expected), len(got), expected, got, stack)
)
for (i, e_sub) in enumerate(expected):
g_sub = got[i]
assert_deepequal(e_sub, g_sub, doc, stack + (i,))
elif isinstance(expected, dict):
missing = set(expected).difference(got)
extra = set(got).difference(expected)
if missing or extra:
raise AssertionError(KEYS % (
doc, sorted(missing), sorted(extra), expected, got, stack
)
)
for key in sorted(expected):
e_sub = expected[key]
g_sub = got[key]
assert_deepequal(e_sub, g_sub, doc, stack + (key,))
2009-12-17 07:16:18 -06:00
elif expected != got:
raise AssertionError(
VALUE % (doc, expected, got, stack)
)
def raises(exception, callback, *args, **kw):
"""
Tests that the expected exception is raised; raises ExceptionNotRaised
if test fails.
"""
raised = False
try:
callback(*args, **kw)
except exception, e:
raised = True
if not raised:
raise ExceptionNotRaised(exception)
return e
def getitem(obj, key):
"""
Works like getattr but for dictionary interface. Use this in combination
with raises() to test that, for example, KeyError is raised.
"""
return obj[key]
def setitem(obj, key, value):
"""
Works like setattr but for dictionary interface. Use this in combination
with raises() to test that, for example, TypeError is raised.
"""
obj[key] = value
def delitem(obj, key):
"""
Works like delattr but for dictionary interface. Use this in combination
with raises() to test that, for example, TypeError is raised.
"""
del obj[key]
def no_set(obj, name, value='some_new_obj'):
"""
Tests that attribute cannot be set.
"""
raises(AttributeError, setattr, obj, name, value)
def no_del(obj, name):
"""
Tests that attribute cannot be deleted.
"""
raises(AttributeError, delattr, obj, name)
def read_only(obj, name, value='some_new_obj'):
"""
Tests that attribute is read-only. Returns attribute.
"""
# Test that it cannot be set:
no_set(obj, name, value)
# Test that it cannot be deleted:
no_del(obj, name)
# Return the attribute
return getattr(obj, name)
def is_prop(prop):
return type(prop) is property
class ClassChecker(object):
__cls = None
__subcls = None
def __get_cls(self):
if self.__cls is None:
self.__cls = self._cls
assert inspect.isclass(self.__cls)
return self.__cls
cls = property(__get_cls)
def __get_subcls(self):
if self.__subcls is None:
self.__subcls = self.get_subcls()
assert inspect.isclass(self.__subcls)
return self.__subcls
subcls = property(__get_subcls)
def get_subcls(self):
raise NotImplementedError(
self.__class__.__name__,
'get_subcls()'
)
def tearDown(self):
"""
nose tear-down fixture.
"""
2010-03-08 21:42:26 -06:00
context.__dict__.clear()
def check_TypeError(value, type_, name, callback, *args, **kw):
"""
Tests a standard TypeError raised with `errors.raise_TypeError`.
"""
e = raises(TypeError, callback, *args, **kw)
assert e.value is value
assert e.type is type_
assert e.name == name
assert type(e.name) is str
assert str(e) == ipalib.errors.TYPE_FORMAT % (name, type_, value)
return e
def get_api(**kw):
"""
Returns (api, home) tuple.
This function returns a tuple containing an `ipalib.plugable.API`
instance and a `TempHome` instance.
"""
home = TempHome()
api = ipalib.create_api(mode='unit_test')
api.env.in_tree = True
for (key, value) in kw.iteritems():
api.env[key] = value
return (api, home)
def create_test_api(**kw):
"""
Returns (api, home) tuple.
This function returns a tuple containing an `ipalib.plugable.API`
instance and a `TempHome` instance.
"""
home = TempHome()
api = ipalib.create_api(mode='unit_test')
api.env.in_tree = True
for (key, value) in kw.iteritems():
api.env[key] = value
return (api, home)
class PluginTester(object):
__plugin = None
def __get_plugin(self):
if self.__plugin is None:
self.__plugin = self._plugin
assert issubclass(self.__plugin, Plugin)
return self.__plugin
plugin = property(__get_plugin)
def register(self, *plugins, **kw):
"""
Create a testing api and register ``self.plugin``.
This method returns an (api, home) tuple.
:param plugins: Additional \*plugins to register.
:param kw: Additional \**kw args to pass to `create_test_api`.
"""
(api, home) = create_test_api(**kw)
api.register(self.plugin)
for p in plugins:
api.register(p)
return (api, home)
def finalize(self, *plugins, **kw):
(api, home) = self.register(*plugins, **kw)
api.finalize()
return (api, home)
def instance(self, namespace, *plugins, **kw):
(api, home) = self.finalize(*plugins, **kw)
o = api[namespace][self.plugin.__name__]
return (o, api, home)
def tearDown(self):
"""
nose tear-down fixture.
"""
2010-03-08 21:42:26 -06:00
context.__dict__.clear()
class dummy_ugettext(object):
__called = False
def __init__(self, translation=None):
if translation is None:
translation = u'The translation'
self.translation = translation
assert type(self.translation) is unicode
def __call__(self, message):
assert self.__called is False
self.__called = True
assert type(message) is str
assert not hasattr(self, 'message')
self.message = message
assert type(self.translation) is unicode
return self.translation
def called(self):
return self.__called
def reset(self):
assert type(self.translation) is unicode
assert type(self.message) is str
del self.message
assert self.__called is True
self.__called = False
class dummy_ungettext(object):
__called = False
def __init__(self):
self.translation_singular = u'The singular translation'
self.translation_plural = u'The plural translation'
def __call__(self, singular, plural, n):
assert type(singular) is str
assert type(plural) is str
assert type(n) is int
assert self.__called is False
self.__called = True
self.singular = singular
self.plural = plural
self.n = n
if n == 1:
return self.translation_singular
return self.translation_plural
class DummyMethod(object):
def __init__(self, callback, name):
self.__callback = callback
self.__name = name
def __call__(self, *args, **kw):
return self.__callback(self.__name, args, kw)
class DummyClass(object):
def __init__(self, *calls):
self.__calls = calls
self.__i = 0
for (name, args, kw, result) in calls:
method = DummyMethod(self.__process, name)
setattr(self, name, method)
def __process(self, name_, args_, kw_):
if self.__i >= len(self.__calls):
raise AssertionError(
'extra call: %s, %r, %r' % (name, args, kw)
)
(name, args, kw, result) = self.__calls[self.__i]
self.__i += 1
i = self.__i
if name_ != name:
raise AssertionError(
'call %d should be to method %r; got %r' % (i, name, name_)
)
if args_ != args:
raise AssertionError(
'call %d to %r should have args %r; got %r' % (i, name, args, args_)
)
if kw_ != kw:
raise AssertionError(
'call %d to %r should have kw %r, got %r' % (i, name, kw, kw_)
)
if isinstance(result, Exception):
raise result
return result
def _calledall(self):
return self.__i == len(self.__calls)