From 19db9b615852af773dc5890157ea8b93b6db1389 Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 6 Jan 2014 18:52:42 +0100 Subject: [PATCH] Add the possibility to uninstall fixtures This should be used while tearing down tests. It is complementary to install_fixtures. --- charlatan/fixtures_manager.py | 127 ++++++++++++++++-- charlatan/testcase.py | 43 ++++-- charlatan/tests/test_fixtures_manager.py | 61 +++++++++ .../tests/test_fixtures_manager_mixin.py | 73 ++++++++++ charlatan/tests/test_testcase.py | 74 ++++++++++ docs/hooks.rst | 12 ++ docs/quickstart.rst | 17 ++- 7 files changed, 380 insertions(+), 27 deletions(-) create mode 100644 charlatan/tests/test_fixtures_manager_mixin.py create mode 100644 charlatan/tests/test_testcase.py diff --git a/charlatan/fixtures_manager.py b/charlatan/fixtures_manager.py index b2354a3..39db0af 100644 --- a/charlatan/fixtures_manager.py +++ b/charlatan/fixtures_manager.py @@ -39,6 +39,7 @@ class FixturesManager(object): def __init__(self): self.hooks = {} + self.installed_keys = [] def load(self, filename, db_session=None, models_package=""): """Pre-load the fixtures. @@ -118,6 +119,7 @@ def add_to_graph(fixture): def clean_cache(self): """Clean the cache.""" self.cache = {} + self.installed_keys = [] def save_instance(self, instance): """Save a fixture instance. @@ -144,6 +146,35 @@ def save_instance(self, instance): self._get_hook("after_save")(instance) + def delete_instance(self, instance): + """Delete a fixture instance. + + If it's a SQLAlchemy model, it will be deleted from the session and the + session will be committed. + + Otherwise, :meth:`delete_instance` will be run first. If the instance + does not have it, :meth:`delete` will be run. If the instance does not + have it, nothing will happen. + + Before and after the process, the :func:`before_delete` and + :func:`after_delete` hook are run. + + """ + + self._get_hook("before_delete")(instance) + + if self.session and is_sqlalchemy_model(instance): + self.session.delete(instance) + self.session.commit() + + else: + try: + getattr(instance, "delete_instance")() + except AttributeError: + getattr(instance, "delete", lambda: None)() + + self._get_hook("after_delete")(instance) + def install_fixture(self, fixture_key, do_not_save=False, include_relationships=True, attrs=None): @@ -189,12 +220,8 @@ def install_fixtures(self, fixture_keys, do_not_save=False, :rtype: list of :data:`fixture_instance` """ - - if isinstance(fixture_keys, basestring): - fixture_keys = (fixture_keys, ) - instances = [] - for f in fixture_keys: + for f in self.make_list(fixture_keys): instances.append(self.install_fixture( f, do_not_save=do_not_save, @@ -218,6 +245,59 @@ def install_all_fixtures(self, do_not_save=False, do_not_save=do_not_save, include_relationships=include_relationships) + def uninstall_fixture(self, fixture_key): + """Uninstall a fixture. + + :param str fixture_key: + + :rtype: :data:`fixture_instance` or None if no instance was installed + with the given key + """ + + try: + self._get_hook("before_uninstall")() + instance = self.cache.get(fixture_key) + if instance: + self.delete_instance(instance) + self.cache.pop(fixture_key, None) + self.installed_keys.remove(fixture_key) + + except Exception as exc: + self._get_hook("after_uninstall")(exc) + raise + + else: + self._get_hook("after_uninstall")(None) + return instance + + def uninstall_fixtures(self, fixture_keys): + """Uninstall a list of installed fixtures. + + If a given fixture was not previously installed, nothing happens and + its instance is not part of the returned list. + + :param fixture_keys: fixtures to be uninstalled + :type fixture_keys: str or list of strs + + :rtype: list of :data:`fixture_instance` + """ + instances = [] + for fixture_key in self.make_list(fixture_keys): + instance = self.uninstall_fixture(fixture_key) + if instance: + instances.append(instance) + + return instances + + def uninstall_all_fixtures(self): + """Uninstall all installed fixtures. + + :rtype: list of :data:`fixture_instance` + """ + installed_fixture_keys = list(self.installed_keys) + installed_fixture_keys.reverse() + return self.uninstall_fixtures(installed_fixture_keys) + def get_fixture(self, fixture_key, include_relationships=True, attrs=None): """Return a fixture instance (but do not save it). @@ -260,6 +340,7 @@ def get_fixture(self, fixture_key, include_relationships=True, attrs=None): ) self.cache[fixture_key] = instance + self.installed_keys.append(fixture_key) # If any arguments are passed in, set them before returning. But do not # set them on a list of fixtures (they are already set on all elements) @@ -306,6 +387,12 @@ def set_hook(self, hookname, func): self.hooks[hookname] = func + def make_list(self, obj): + """Return list of objects if necessary.""" + if isinstance(obj, (list, tuple)): + return obj + return (obj, ) + FIXTURES_MANAGER = FixturesManager() @@ -340,12 +427,7 @@ def install_fixture(self, fixture_key, do_not_save=False, @copy_docstring_from(FixturesManager) def install_fixtures(self, fixture_keys, do_not_save=False, include_relationships=True): - - # Let's be forgiving - if isinstance(fixture_keys, basestring): - fixture_keys = (fixture_keys, ) - - for f in fixture_keys: + for f in FIXTURES_MANAGER.make_list(fixture_keys): self.install_fixture(f, do_not_save=do_not_save, include_relationships=include_relationships) @@ -357,6 +439,29 @@ def install_all_fixtures(self, do_not_save=False, do_not_save=do_not_save, include_relationships=include_relationships) + @copy_docstring_from(FixturesManager) + def uninstall_fixture(self, fixture_key): + instance = FIXTURES_MANAGER.uninstall_fixture(fixture_key) + if instance: + delattr(self, fixture_key) + return instance + + @copy_docstring_from(FixturesManager) + def uninstall_fixtures(self, fixture_keys): + instances = [] + for fixture_key in FIXTURES_MANAGER.make_list(fixture_keys): + instance = self.uninstall_fixture(fixture_key) + if instance: + instances.append(instance) + + return instances + + @copy_docstring_from(FixturesManager) + def uninstall_all_fixtures(self): + installed_keys = list(FIXTURES_MANAGER.installed_keys) + installed_keys.reverse() + return self.uninstall_fixtures(installed_keys) + def clean_fixtures_cache(self): """Clean the cache.""" FIXTURES_MANAGER.clean_cache() diff --git a/charlatan/testcase.py b/charlatan/testcase.py index 0aa4df7..faa731e 100644 --- a/charlatan/testcase.py +++ b/charlatan/testcase.py @@ -14,7 +14,7 @@ def use_fixtures_manager(self, fixtures_manager): if hasattr(self, "fixtures"): self.install_fixtures(self.fixtures) - def install_fixtures(self, fixtures, do_not_save=False): + def install_fixtures(self, fixtures=None, do_not_save=False): """Install required fixtures. :param fixtures: fixtures key @@ -25,11 +25,7 @@ def install_fixtures(self, fixtures, do_not_save=False): """ if fixtures: - # Be forgiving - if not isinstance(fixtures, (list, tuple)): - fixtures = (fixtures, ) - fixtures_to_install = fixtures - + fixtures_to_install = self.__fixtures_manager.make_list(fixtures) else: fixtures_to_install = self.fixtures @@ -50,10 +46,7 @@ def install_fixture(self, fixture_name): def create_all_fixtures(self): """Create all available fixtures but do not save them.""" - - # Adding fixtures to the class - for f in self.__fixtures_manager.install_all(do_not_save=True): - setattr(self, f[0], f[1]) + return self.install_fixtures(do_not_save=True) def get_fixture(self, fixture_name): """Return a fixture instance (but do not save it). @@ -61,3 +54,33 @@ def get_fixture(self, fixture_name): :param str fixture_name: fixture key """ return self.__fixtures_manager.get_fixture(fixture_name) + + def uninstall_fixtures(self, fixtures=None): + """Uninstall fixtures. + + :param fixtures: fixtures key + :type fixtures: list of strings + + If :data:`fixtures` is not provided, the method will look for a class + property named :attr:`fixtures`. + """ + + if not fixtures: + # copy and reverse the list in order to remove objects with + # relationships first + fixtures = list(self.__fixtures_manager.installed_keys) + fixtures.reverse() + + fixtures = self.__fixtures_manager.make_list(fixtures) + uninstalled = self.__fixtures_manager.uninstall_fixtures(fixtures) + + # Removing fixtures from the class + for fixture in fixtures: + delattr(self, fixture) + + # Return list of fixture instances + return uninstalled + + def uninstall_fixture(self, fixture_name): + """Uninstall a fixture and return it.""" + return self.uninstall_fixtures(fixture_name)[0] diff --git a/charlatan/tests/test_fixtures_manager.py b/charlatan/tests/test_fixtures_manager.py index 4c1e6c2..0d45905 100644 --- a/charlatan/tests/test_fixtures_manager.py +++ b/charlatan/tests/test_fixtures_manager.py @@ -21,6 +21,67 @@ def test_install_fixture(self): 'field2': 2, }) + def test_uninstall_fixture(self): + """uninstall_fixture should return the fixture""" + + fixtures_manager = FixturesManager() + fixtures_manager.load( + './charlatan/tests/data/relationships_without_models.yaml') + + fixtures_manager.install_fixture('simple_dict') + fixture = fixtures_manager.uninstall_fixture('simple_dict') + self.assertEqual(fixture, { + 'field1': 'lolin', + 'field2': 2, + }) + + # verify we are forgiving with list inputs + fixtures = fixtures_manager.install_fixtures('simple_dict') + self.assertEqual(len(fixtures), 1) + + fixtures = fixtures_manager.uninstall_fixtures('simple_dict') + self.assertEqual(len(fixtures), 1) + self.assertEqual(fixtures[0], { + 'field1': 'lolin', + 'field2': 2, + }) + + def test_uninstall_non_installed_fixture(self): + """ + uninstall_fixture should return None if the fixture has not been + previously installed. + """ + + fixtures_manager = FixturesManager() + fixtures_manager.load( + './charlatan/tests/data/relationships_without_models.yaml') + + fixture = fixtures_manager.uninstall_fixture('simple_dict') + self.assertEqual(fixture, None) + + def test_uninstall_fixtures(self): + """ + uninstall_fixtures should return the list of previously installed + fixtures that are now uninstalled. + """ + fixtures_manager = FixturesManager() + fixtures_manager.load( + './charlatan/tests/data/relationships_without_models.yaml') + + fixture_keys = ('simple_dict', 'dict_with_nest') + + fixtures_manager.install_fixtures(fixture_keys) + self.assertEqual(len(fixtures_manager.cache.keys()), 2) + + fixtures = fixtures_manager.uninstall_fixtures(fixture_keys) + self.assertEqual(len(fixtures), 2) + self.assertEqual(len(fixtures_manager.cache.keys()), 0) + + # uninstalling non-exiting fixtures should not raise an exception + fixtures = fixtures_manager.uninstall_fixtures(fixture_keys) + self.assertEqual(len(fixtures), 0) + self.assertEqual(len(fixtures_manager.cache.keys()), 0) + def test_dependency_parsing(self): fm = FixturesManager() fm.load( diff --git a/charlatan/tests/test_fixtures_manager_mixin.py b/charlatan/tests/test_fixtures_manager_mixin.py new file mode 100644 index 0000000..5e38114 --- /dev/null +++ b/charlatan/tests/test_fixtures_manager_mixin.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import + +from charlatan import testing +from charlatan import fixtures_manager +from charlatan import FixturesManagerMixin + + +class TestFixturesManagerMixin(testing.TestCase, FixturesManagerMixin): + + def _pre_setup(self): + fixtures_manager.load( + './charlatan/tests/data/relationships_without_models.yaml') + self.install_all_fixtures() + + def _post_teardown(self): + self.uninstall_all_fixtures() + + def test_get_fixture(self): + simple_dict = self.get_fixture('simple_dict') + self.assertEqual(simple_dict['field1'], 'lolin') + self.assertEqual(simple_dict['field2'], 2) + + dict_with_nest = self.get_fixture('dict_with_nest') + self.assertEqual(dict_with_nest['field1'], 'asdlkf') + self.assertEqual(dict_with_nest['field2'], 4) + + def test_get_fixtures(self): + fixtures = self.get_fixtures(('simple_dict', 'dict_with_nest')) + self.assertEqual(len(fixtures), 2) + + self.assertEqual(fixtures[0]['field1'], 'lolin') + self.assertEqual(fixtures[0]['field2'], 2) + + self.assertEqual(fixtures[1]['field1'], 'asdlkf') + self.assertEqual(fixtures[1]['field2'], 4) + + def test_install_fixture(self): + self.uninstall_all_fixtures() + + simple_dict = self.install_fixture('simple_dict') + self.assertEqual(simple_dict['field1'], 'lolin') + self.assertEqual(simple_dict['field2'], 2) + + def test_install_fixtures(self): + self.uninstall_all_fixtures() + + self.install_fixtures(('simple_dict', 'dict_with_nest')) + fixtures = self.get_fixtures(('simple_dict', 'dict_with_nest')) + self.assertEqual(len(fixtures), 2) + + def test_uninstall_fixture(self): + simple_dict = self.uninstall_fixture('simple_dict') + self.assertEqual(simple_dict['field1'], 'lolin') + self.assertEqual(simple_dict['field2'], 2) + + dict_with_nest = self.uninstall_fixture('dict_with_nest') + self.assertEqual(dict_with_nest['field1'], 'asdlkf') + self.assertEqual(dict_with_nest['field2'], 4) + + # there is one more left to uninstall + fixtures = self.uninstall_all_fixtures() + self.assertEqual(len(fixtures), 1) + + def test_uninstall_all_fixtures(self): + fixtures = self.uninstall_all_fixtures() + self.assertEqual(len(fixtures), 3) + + fixtures = self.uninstall_all_fixtures() + self.assertEqual(len(fixtures), 0) + + def test_clean_fixtures_cache(self): + self.clean_fixtures_cache() + self.assertEqual(len(fixtures_manager.cache.keys()), 0) diff --git a/charlatan/tests/test_testcase.py b/charlatan/tests/test_testcase.py new file mode 100644 index 0000000..bb9c4ff --- /dev/null +++ b/charlatan/tests/test_testcase.py @@ -0,0 +1,74 @@ +from __future__ import absolute_import + +from charlatan import testcase +from charlatan import testing +from charlatan import FixturesManager + + +class TestTestCase(testing.TestCase, testcase.FixturesMixin): + + fixtures = ( + 'simple_dict', + 'dict_with_nest',) + + def _pre_setup(self): + self.fixtures_manager = FixturesManager() + self.fixtures_manager.load( + './charlatan/tests/data/relationships_without_models.yaml') + self.use_fixtures_manager(self.fixtures_manager) + + def _post_teardown(self): + self.uninstall_fixtures() + + def test_uninstall_fixtures(self): + fixtures = self.uninstall_fixtures() + self.assertEqual(len(fixtures), 2) + + fixtures = self.uninstall_fixtures() + self.assertEqual(len(fixtures), 0) + + def test_uninstall_fixture(self): + simple_dict = self.uninstall_fixture('simple_dict') + self.assertEqual(simple_dict['field1'], 'lolin') + self.assertEqual(simple_dict['field2'], 2) + + dict_with_nest = self.uninstall_fixture('dict_with_nest') + self.assertEqual(dict_with_nest['field1'], 'asdlkf') + self.assertEqual(dict_with_nest['field2'], 4) + + fixtures = self.uninstall_fixtures() + self.assertEqual(len(fixtures), 0) + + def test_get_fixture(self): + simple_dict = self.get_fixture('simple_dict') + self.assertEqual(simple_dict['field1'], 'lolin') + self.assertEqual(simple_dict['field2'], 2) + + dict_with_nest = self.get_fixture('dict_with_nest') + self.assertEqual(dict_with_nest['field1'], 'asdlkf') + self.assertEqual(dict_with_nest['field2'], 4) + + def test_install_fixtures(self): + self.uninstall_fixtures() + + fixtures = self.install_fixtures(('simple_dict', 'dict_with_nest')) + self.assertEqual(len(fixtures), 2) + + def test_install_fixtures_with_no_arguments(self): + self.uninstall_fixtures() + + fixtures = self.install_fixtures() + self.assertEqual(len(fixtures), 2) + + def test_install_fixture(self): + self.uninstall_fixtures() + + simple_dict = self.install_fixture('simple_dict') + self.assertEqual(simple_dict['field1'], 'lolin') + self.assertEqual(simple_dict['field2'], 2) + + def test_create_all_fixtures(self): + self.uninstall_fixtures() + + fixtures = self.create_all_fixtures() + self.assertEqual(len(fixtures), 2) diff --git a/docs/hooks.rst b/docs/hooks.rst index 50e77af..9654ab5 100644 --- a/docs/hooks.rst +++ b/docs/hooks.rst @@ -17,6 +17,18 @@ The following hooks are available: single argument that will be the exception that may have been raised during the whole process. This function is guaranteed to be called. +* ``before_uninstall``: called before uninstalling fixtures. The callback takes + no argument. +* ``before_delete``: called before deleting an instance using either the + SQLAlchemy session or in the following order `delete_instance` and `delete`. + The callback takes a single argument which is the instance being deleted. +* ``after_delete``: called after deleting an instance using either the + SQLAlchemy session or in the following order `delete_instance` and `delete`. + The callback takes a single argument which is the instance that was deleted. +* ``after_uninstall``: called after uninstalling fixtures. The callback must + accept a single argument that will be the exception that may have been raised + during the whole process. This function is guaranteed to be called. + You can register them using :meth:`charlatan.set_hook`. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8728930..45ed1b8 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -65,8 +65,8 @@ Using fixtures There are multiple ways to require and use fixtures. -For each tests, in setUp -"""""""""""""""""""""""" +For each tests, in setUp and tearDown +""""""""""""""""""""""""""""""""""""" .. code-block:: python @@ -76,16 +76,21 @@ For each tests, in setUp # This will create self.client and self.driver self.install_fixtures(("client", "driver")) + def tearDown(self): + # This will delete self.client and self.driver + self.uninstall_fixtures(("client", "driver")) + For a single test """"""""""""""""" .. code-block:: python - class MyTest(FixturesMixin): - - def test_toaster(self): - self.install_fixtures("toaster") + class MyTest(FixturesMixin): + def test_toaster(self): + self.install_fixtures("toaster") + # do things... and optionally uninstall it once you're done + self.uninstall_fixtures("toaster") Getting a fixture without saving it """""""""""""""""""""""""""""""""""