Discover test case method dependencies using load_object function

Refactor fixture manager to avoid reverse control pattern.

test cases method dependency on fixtures is now obtained
from default parameter values discovered from test case
methods signatures.

Change-Id: I4eb04189adb9aab27923b167910f32c49c33d8e0
This commit is contained in:
Federico Ressi 2018-12-19 15:33:29 +01:00
parent 075ea47c20
commit 7f30a885f0
3 changed files with 221 additions and 68 deletions

View File

@ -20,11 +20,15 @@ from tobiko.common.managers import loader as loader_manager
load_object = loader_manager.load_object
load_module = loader_manager.load_module
discover_testcases = testcase_manager.discover_testcases
Fixture = fixture_manager.Fixture
get_fixture = fixture_manager.FIXTURES.get
create_fixture = fixture_manager.FIXTURES.create
delete_fixture = fixture_manager.FIXTURES.delete
discover_testcases = testcase_manager.discover_testcases
fixture = fixture_manager.fixture
is_fixture = fixture_manager.is_fixture
get_fixture = fixture_manager.get_fixture
create_fixture = fixture_manager.create_fixture
delete_fixture = fixture_manager.delete_fixture
get_required_fixtures = fixture_manager.get_required_fixtures
discover_required_fixtures = fixture_manager.discover_required_fixtures
create_fixtures = fixture_manager.create_fixtures
delete_fixtures = fixture_manager.delete_fixtures

View File

@ -13,24 +13,61 @@
# under the License.
from __future__ import absolute_import
import abc
import contextlib
import inspect
import six
import tobiko
def get_fixture_name(obj):
if isinstance(obj, six.string_types):
return obj
elif (isinstance(obj, six.class_types) and
issubclass(obj, Fixture)):
return obj.fixture_name
def fixture(obj):
obj.__tobiko_fixture__ = True
return object
msg = "{!r} is not a string type or a subclass of {!s}".format(
obj, Fixture)
raise TypeError(msg)
def is_fixture(obj):
return getattr(obj, '__tobiko_fixture__', False)
def get_fixture(obj, manager=None):
manager = manager or FIXTURES
return manager.get_fixture(obj)
def create_fixture(obj, manager=None):
manager = manager or FIXTURES
return manager.create_fixture(obj)
def delete_fixture(obj, manager=None):
manager = manager or FIXTURES
return manager.delete_fixture(obj)
def get_required_fixtures(obj, manager=None):
manager = manager or FIXTURES
return manager.get_required_fixtures(obj)
def discover_required_fixtures(objects, manager=None):
manager = manager or FIXTURES
return sorted(set(manager.discover_required_fixtures(objects, manager)))
def create_fixtures(objects, manager=None):
manager = manager or FIXTURES
for _fixture in discover_required_fixtures(objects=objects,
manager=manager):
manager.create_fixture(_fixture)
def delete_fixtures(objects, manager=None):
manager = manager or FIXTURES
for _fixture in discover_required_fixtures(objects=objects,
manager=manager):
return manager.delete_fixture(_fixture)
class FixtureManager(object):
@ -38,60 +75,159 @@ class FixtureManager(object):
def __init__(self):
self.fixtures = {}
def set(self, name, cls):
if not issubclass(cls, Fixture):
msg = "{!r} is not a subclass of {!s}".format(cls, Fixture)
raise TypeError(msg)
fixture = cls()
actual_fixture = self.fixtures.setdefault(name, fixture)
if actual_fixture is not fixture:
msg = "Fixture with named {!r} already registered: {!r}".format(
name, actual_fixture)
raise ValueError(msg)
return fixture
def get_fixture(self, obj):
name = get_object_name(obj)
_fixture = self.fixtures.get(name)
if _fixture is None:
_fixture = self.init_fixture(name=name, obj=obj)
assert isinstance(_fixture, Fixture)
self.fixtures[name] = _fixture
return _fixture
def get(self, cls_or_name):
name = get_fixture_name(cls_or_name)
fixture = self.fixtures.get(name)
if fixture is None:
raise ValueError('Invalid fixture name: {!r}'.format(name))
return fixture
def create_fixture(self, obj):
return self.get_fixture(obj).create_fixture()
def create(self, cls_or_name):
fixture = self.get(cls_or_name)
fixture.create_fixture()
return fixture
def delete_fixture(self, obj):
return self.get_fixture(obj).delete_fixture()
def delete(self, cls_or_name):
fixture = self.get(cls_or_name)
fixture.delete_fixture()
return fixture
def init_fixture(self, obj, name):
if isinstance(obj, six.string_types):
if name != obj:
msg = ("Fixture name mismatch: "
"{!r} != {!r}").format(name, obj.fixture_name)
raise ValueError(msg)
obj = tobiko.load_object(name)
if isinstance(obj, Fixture):
if name != obj.fixture_name:
msg = ("Fixture name mismatch: "
"{!r} != {!r}").format(name, obj.fixture_name)
raise ValueError(msg)
return obj
elif inspect.isclass(obj):
if issubclass(obj, Fixture):
return obj(fixture_name=name)
elif inspect.isgeneratorfunction(obj):
return ContextFixture(
fixture_name=name, context=contextlib.contextmanager(obj))
elif inspect.isfunction(obj):
return FunctionFixture(fixture_name=name, function=obj)
raise TypeError("Invalid fixture object type: {!r}".format(object))
def get_required_fixtures(self, obj):
return sorted(set(self.discover_required_fixtures([obj])))
def discover_required_fixtures(self, objects):
objects = list(objects)
while objects:
obj = objects.pop()
if isinstance(obj, six.string_types):
object_id = obj
obj = tobiko.load_object(object_id)
else:
object_id = get_object_name(obj)
if is_fixture(obj):
yield object_id
elif inspect.isfunction(obj) or inspect.ismethod(obj):
for default in get_default_param_values(obj):
if is_fixture(default):
yield get_object_name(default)
if inspect.ismodule(obj):
members = [obj for _, obj in inspect.getmembers(obj)
if (inspect.isfunction(obj) or
inspect.isclass(obj))]
objects.extend(members)
elif inspect.isclass(obj):
members = [obj for _, obj in inspect.getmembers(obj)
if (inspect.isfunction(obj) or
inspect.ismethod(obj))]
objects.extend(members)
FIXTURES = FixtureManager()
class FixtureMeta(abc.ABCMeta):
def __new__(cls, name, bases, members): # pylint: disable=arguments-differ
fixture_class = super(FixtureMeta, cls).__new__(cls, name, bases,
members)
if not inspect.isabstract(fixture_class):
fixture_name = getattr(fixture_class, 'fixture_name', None)
if fixture_name is None:
fixture_class.fixture_name = fixture_name = (
fixture_class.__module__ + '.' +
fixture_class.__name__)
FIXTURES.set(fixture_name, fixture_class)
return fixture_class
@six.add_metaclass(FixtureMeta)
class Fixture(object):
__tobiko_fixture__ = True
def __init__(self, fixture_name):
self.fixture_name = fixture_name
@abc.abstractmethod
def create_fixture(self):
pass
def delete_fixture(self):
pass
class FunctionFixture(Fixture):
def __init__(self, fixture_name, function):
super(FunctionFixture, self).__init__(fixture_name=fixture_name)
assert callable(function)
self.function = function
def create_fixture(self):
return self.function()
class ContextFixture(Fixture):
def __init__(self, fixture_name, context):
super(ContextFixture, self).__init__(fixture_name=fixture_name)
self.context = context
def create_fixture(self):
return self.context.__enter__()
def delete_fixture(self):
return self.context.__exit__(None, None, None)
def get_object_name(obj):
if isinstance(obj, six.string_types):
return obj
if is_fixture(obj):
name = getattr(obj, 'fixture_name', None)
if name:
return name
name = getattr(obj, '__qualname__', None)
if name:
return obj.__module__ + '.' + name
module = inspect.getmodule(obj).__name__
if inspect.isclass(obj):
return module + '.' + obj.__name__
parent_class = getattr(obj, 'im_class', None)
if parent_class:
return module + parent_class.__name__ + '.' + obj.__name__
if inspect.isfunction(obj):
return module + '.' + obj.func_name
msg = "Unable to get fixture name from object {!r}".format(obj)
raise TypeError(msg)
def get_default_param_values(obj):
if hasattr(inspect, 'signature'):
try:
signature = inspect.signature(obj)
except ValueError:
pass
else:
return [param.default
for param in signature.parameters.values()]
# Use old deprecated function 'getargspec'
return list(inspect.getargspec(obj).defaults or # pylint: disable=W1505
tuple())

View File

@ -19,17 +19,20 @@ from tobiko.tests import base
class TestFixture(tobiko.Fixture):
def __init__(self):
created = False
deleted = False
def reset(self):
self.created = False
self.deleted = False
reset = __init__
def create_fixture(self):
self.created = True
return 'created'
def delete_fixture(self):
self.deleted = True
return 'deleted'
class FixtureTypeTest(base.TobikoTest):
@ -71,10 +74,10 @@ class FixtureTypeTest(base.TobikoTest):
self._test_create_fixture(self.fixture_type)
def _test_create_fixture(self, obj):
fixture = tobiko.create_fixture(obj)
self.assertIs(self.fixture, fixture)
self.assertTrue(fixture.created)
self.assertFalse(fixture.deleted)
result = tobiko.create_fixture(obj)
self.assertEqual('created', result)
self.assertTrue(self.fixture.created)
self.assertFalse(self.fixture.deleted)
def test_delete_fixture_by_name(self):
self._test_delete_fixture(self.fixture_name)
@ -83,7 +86,17 @@ class FixtureTypeTest(base.TobikoTest):
self._test_delete_fixture(self.fixture_type)
def _test_delete_fixture(self, obj=TestFixture):
fixture = tobiko.delete_fixture(obj)
self.assertIs(self.fixture, fixture)
self.assertFalse(fixture.created)
self.assertTrue(fixture.deleted)
result = tobiko.delete_fixture(obj)
self.assertEqual('deleted', result)
self.assertFalse(self.fixture.created)
self.assertTrue(self.fixture.deleted)
def test_get_required_fixtures_from_method_by_type(
self, _required_fixture=TestFixture):
result = tobiko.get_required_fixtures(self.id())
self.assertEqual([self.fixture_name], result)
def test_get_required_fixtures_from_test_class(
self, _required_fixture=TestFixture):
result = tobiko.get_required_fixtures(FixtureTypeTest)
self.assertEqual([self.fixture_name], result)