Refactor required fixtures listing

- test methods requires fixtures of test class
- test class requires fixtures of RequiredFixtureProperty
- fix some minor problems (like getting object names)

Change-Id: Ic7e50d5e3b0299202dd6b28de243cfe888293401
This commit is contained in:
Federico Ressi 2019-04-15 17:11:47 +02:00
parent 1b962fafa1
commit 3f3efbbb9e
2 changed files with 96 additions and 62 deletions

View File

@ -91,49 +91,90 @@ def cleanup_fixture(obj, manager=None):
return fixture
def iter_required_fixtures(objects):
objects = list(objects)
def get_name_and_object(obj):
if isinstance(obj, six.string_types):
return obj, tobiko.load_object(obj)
else:
return get_object_name(obj), obj
def visit_objects(objects):
if not isinstance(objects, list):
raise TypeError("parameter 'objects' is not a list")
visited = set()
while objects:
obj = objects.pop()
if isinstance(obj, six.string_types):
object_id = obj
obj = tobiko.load_object(object_id)
try:
name, obj = get_name_and_object(obj)
except Exception:
LOG.exception('Unable to get (name, object) pair from {!r}'.format(
obj))
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) or
isinstance(obj, RequiredFixtureProperty))]
objects.extend(members)
elif isinstance(obj, RequiredFixtureProperty):
objects.append(obj.obj)
if name not in visited:
visited.add(name)
yield name, obj
def list_required_fixtures(objects):
return sorted(set(iter_required_fixtures(objects)))
result = []
objects = list(objects)
for name, obj in visit_objects(objects):
if is_fixture(obj):
result.append(name)
continue
if is_test_method(obj):
# Test methods also require test class fixtures
if '.' in name:
parent_name = name.rsplit('.', 1)[0]
objects.append(parent_name)
objects.extend(get_required_fixture(obj))
result.sort()
return result
def is_test_method(obj):
return ((inspect.isfunction(obj) or inspect.ismethod(obj)) and
obj.__name__.startswith('test_'))
def get_required_fixture(obj):
required_fixtures = getattr(obj, '__tobiko_required_fixtures__', None)
if required_fixtures is None:
required_fixtures = []
try:
# try to cache list for later use
obj.__tobiko_required_fixtures__ = required_fixtures
except AttributeError:
pass
if is_test_method(obj):
for default in get_default_param_values(obj):
if is_fixture(default):
required_fixtures.append(get_fixture_name(default))
elif inspect.isclass(obj):
# inspect.getmembers() would iterate over such many
# testtools.TestCase members too, so let exclude members from
# very base classes
mro_index = obj.__mro__.index(testtools.TestCase)
if mro_index > 0:
member_names = sorted(set(
[name
for cls in obj.__mro__[:mro_index]
for name in cls.__dict__]))
for member_name in member_names:
member = getattr(obj, member_name)
if isinstance(member, RequiredFixtureProperty):
required_fixtures.append(member.fixture)
return required_fixtures
def init_fixture(obj, name):
if isinstance(obj, six.string_types):
obj = tobiko.load_object(name)
if (inspect.isclass(obj) and issubclass(obj, fixtures.Fixture)):
obj = obj()
@ -142,7 +183,7 @@ def init_fixture(obj, name):
obj.__tobiko_fixture_name__ = name
return obj
raise TypeError("Invalid fixture object type: {!r}".format(object))
raise TypeError("Invalid fixture object type: {!r}".format(obj))
def required_fixture(obj):
@ -161,7 +202,9 @@ def get_object_name(obj):
if name:
return name
if not inspect.isclass(obj):
if (not inspect.isfunction(obj) and
not inspect.ismethod(obj) and
not inspect.isclass(obj)):
obj = type(obj)
module = inspect.getmodule(obj).__name__
@ -175,7 +218,7 @@ def get_object_name(obj):
method_class = getattr(obj, 'im_class', None)
if method_class:
# This doesn't work for nested classes
return module + method_class.__name__ + '.' + obj.__name__
return module + '.' + method_class.__name__ + '.' + obj.__name__
if inspect.isfunction(obj):
return module + '.' + obj.func_name
@ -211,7 +254,7 @@ class FixtureManager(object):
self.fixtures = {}
def get_fixture(self, obj, init=init_fixture):
name = get_object_name(obj)
name, obj = get_name_and_object(obj)
fixture = self.fixtures.get(name)
if fixture is None:
self.fixtures[name] = fixture = init(name=name, obj=obj)
@ -297,20 +340,24 @@ class SharedFixture(fixtures.Fixture):
class RequiredFixtureProperty(object):
def __init__(self, obj):
self.obj = obj
def __init__(self, fixture):
self.fixture = fixture
def __get__(self, instance, owner):
def __get__(self, instance, _):
if instance is None:
return self
else:
return self.get_fixture()
def get_fixture(self):
return self.obj
return get_fixture(self.fixture)
@property
def __tobiko_required_fixtures__(self):
return [self.fixture]
class RequiredSetupFixtureProperty(RequiredFixtureProperty):
def get_fixture(self):
return setup_fixture(self.obj)
return setup_fixture(self.fixture)

View File

@ -128,31 +128,19 @@ class FixtureManagerTest(unit.TobikoUnitTest):
def test_list_required_fixtures_from_module(self):
result = tobiko.list_required_fixtures([__name__])
self.assertEqual(
[MY_FIXTURE_NAME,
MY_REQUIRED_FIXTURE_NAME,
MY_REQUIRED_SETUP_FIXTURE_NAME],
result)
self.assertEqual([], result)
def test_list_required_fixtures_from_testcase_type(self):
result = tobiko.list_required_fixtures([FixtureManagerTest])
self.assertEqual([MY_FIXTURE_NAME], result)
self.assertEqual([], result)
def test_list_required_fixtures_from_fixture_type(self):
result = tobiko.list_required_fixtures([MyFixture])
self.assertEqual(
[MY_FIXTURE_NAME,
MY_REQUIRED_FIXTURE_NAME,
MY_REQUIRED_SETUP_FIXTURE_NAME],
result)
self.assertEqual([MY_FIXTURE_NAME], result)
def test_list_required_fixtures_from_fixture_name(self):
result = tobiko.list_required_fixtures([MY_FIXTURE_NAME])
self.assertEqual(
[MY_FIXTURE_NAME,
MY_REQUIRED_FIXTURE_NAME,
MY_REQUIRED_SETUP_FIXTURE_NAME],
result)
self.assertEqual([MY_FIXTURE_NAME], result)
def test_list_required_fixtures_from_method(
self, fixture_type=MyFixture):
@ -177,9 +165,8 @@ class SharedFixtureTest(unit.TobikoUnitTest):
fixture = MyFixture()
fixture.setup_fixture.assert_not_called()
fixture.cleanup_fixture.assert_not_called()
self.assertEqual(
MyRequiredFixture,
fixture.req_fixture)
self.assertIs(tobiko.get_fixture(MyRequiredFixture),
fixture.req_fixture)
fixture.req_setup_fixture.setup_fixture.assert_called_once()
def test_get(self):