def test_clone(self): """Testing DatabaseState.clone""" database_state = DatabaseState(db_name='default') cloned_state = database_state.clone() self.assertEqual(cloned_state.db_name, database_state.db_name) self.assertEqual(cloned_state._tables, database_state._tables)
def test_clone(self): """Testing DatabaseState.clone""" database_state = DatabaseState(db_name='default') cloned_state = database_state.clone() self.assertEqual(cloned_state.db_name, database_state.db_name) self.assertEqual(cloned_state._tables, database_state._tables)
class EvolutionTestCase(TestCase): """Base class for test cases that need to evolve the database.""" default_model_name = 'TestModel' default_base_model = None default_pre_extra_models = [] default_extra_models = [] def setUp(self): super(EvolutionTestCase, self).setUp() self.base_model = None self.pre_extra_models = [] self.extra_models = [] self.start = None self.start_sig = None self.database_state = None self.test_database_state = None self._models_registered = False if self.default_base_model: self.set_base_model(base_model=self.default_base_model, pre_extra_models=self.default_pre_extra_models, extra_models=self.default_extra_models) def tearDown(self): if self._models_registered: unregister_app('tests') super(EvolutionTestCase, self).tearDown() def default_create_test_data(self, db_name): """Default function for creating test data for base models. By default, this won't do anything. Args: db_name (unicode): The name of the database to create models on. """ pass def set_base_model(self, base_model, name=None, extra_models=[], pre_extra_models=[], db_name=None): """Set the base model(s) that will be mutated in a test. These models will be registered in Django's model registry and queued up to be written to the database. Starting signatures based on these models will be provided, which the test is expected to mutate. Args: base_model (type): The base :py:class:`~django.db.models.Model` to register and write to the database that the test will then mutate. name (unicode, optional): The name to register for the model. This defaults to :py:attr:`default_model_name`. extra_models (list of type, optional): The list of extra models to register and write to the database after writing ``base_model``. These may form relations to ``base_model``. pre_extra_models (list of type, optional): The list of extra models to write to the database before writing ``base_model``. ``base_model`` may form relations to these models. db_name (unicode, optional): The name of the database to write the models to. This defaults to :py:attr:`default_database_name`. """ name = name or self.default_model_name db_name = db_name or self.default_database_name if self.base_model: unregister_app('tests') self.base_model = base_model self.pre_extra_models = pre_extra_models self.extra_models = extra_models self.database_state = DatabaseState(db_name) self.start = self.register_model(model=base_model, name=name, register_indexes=True, db_name=db_name) self.start_sig = self.create_test_proj_sig(model=base_model, name=name) def make_end_signatures(self, dest_model, model_name, db_name=None): """Return signatures for a model representing the end of a mutation. Callers should construct a model that reflects the expected result of any mutations and provide that. This will register that model and construct a signature from it. Args: dest_model (type): The destination :py:class:`~django.db.models.Model` representing the expected result of an evolution. model_name (unicode): The name to register for the model. db_name (unicode, optional): The name of the database to write the models to. This defaults to :py:attr:`default_database_name`. Returns: tuple: A tuple containing: 1. A :py:class:`collections.OrderedDict` mapping the model name to the model class. 2. A :py:class:`django_evolution.signature.ProjectSignature` for the provided model. """ db_name = db_name or self.default_database_name end = self.register_model(model=dest_model, name=model_name, db_name=db_name) end_sig = self.create_test_proj_sig(model=dest_model, name=model_name) return end, end_sig def perform_evolution_tests(self, dest_model, evolutions, diff_text=None, expected_hint=None, sql_name=None, model_name=None, end=None, end_sig=None, expect_noop=False, rescan_indexes=True, use_hinted_evolutions=False, perform_simulations=True, perform_mutations=True, db_name=None, create_test_data_func=None): """Perform test evolutions and validate results. This is used for most common evolution-related tests. It handles generating signatures for a base model and an expected post-evolution model, ensuring that the mutations result in an empty diff. It then optionally simulates the evolutions on the signatures (using :py:meth:`perform_simulations)`, and then optionally performs the actual evolutions on the database (using :py:meth:`perform_mutations`), verifying all results. Args: dest_model (type): The destination :py:class:`~django.db.models.Model` representing the expected result of an evolution. evolutions (list of django_evolution.mutations.BaseMutation): The combined series of evolutions (list of mutations) to apply to the base model. diff_text (unicode, optional): The expected generated text describing a diff that must match, if provided. expected_hint (unicode, optional): The expected generated hint text that must match, if provided. sql_name (unicode, optional): The name of the registered SQL content for the database being tested. This must be provided if ``perform_mutations`` is ``True`. model_name (unicode, optional): The name of the model to register. This defaults to :py:attr:`default_model_name`. end (collections.OrderedDict, optional): The expected model map at the end of the evolution. This is generated by :py:meth:`make_end_signatures`. If not provided, one will be generated. end_sig (django_evolution.signature.ProjectSignature, optional): The expected project signature at the end of the evolution. This is generated by :py:meth:`make_end_signatures`. If not provided, one will be generated. expect_noop (bool, optional): Whether the evolution is expected not to change anything. rescan_indexes (bool, optional): Whether to re-scan the list of table indexes after performing mutations. This is ignored if ``perform_mutations`` is ``False``. use_hinted_evolutions (bool, optional): Whether to use the hinted evolutions generated by the signatures. This cannot be used if ``evolutions`` is non-empty. perform_simulations (bool, optional): Whether to simulate the evolution and compare results. perform_mutations (bool, optional): Whether to apply the mutations and compare results. db_name (unicode, optional): The name of the database to apply evolutions to. This defaults to :py:attr:`default_database_name`. create_test_data_func (callable, optional): A function to call in order to create data in the database for the initial models, before applying mutations. It must take a database name. If not provided, :py:meth:`default_create_test_data` will be used. Raises: AssertionError: A diff, simulation, or mutation test has failed. """ model_name = model_name or self.default_model_name db_name = db_name or self.default_database_name if end is None or end_sig is None: end, end_sig = self.make_end_signatures(dest_model=dest_model, model_name=model_name, db_name=db_name) # See if the diff between signatures contains the contents we expect. d = self.perform_diff_test(end_sig=end_sig, diff_text=diff_text, expected_hint=expected_hint, expect_empty=expect_noop) if use_hinted_evolutions: assert not evolutions, ( 'The evolutions= argument cannot be provided when providing ' 'use_hinted_evolutions=True') evolutions = d.evolution()['tests'] if perform_simulations: self.perform_simulations(evolutions=evolutions, end_sig=end_sig, db_name=db_name) if perform_mutations: self.perform_mutations( evolutions=evolutions, end=end, end_sig=end_sig, sql_name=sql_name, rescan_indexes=rescan_indexes, db_name=db_name, create_test_data_func=(create_test_data_func or self.default_create_test_data)) def perform_diff_test(self, end_sig, diff_text=None, expected_hint=None, expect_empty=False): """Generate a diff between signatures and check for expected results. The registered base signature and the provided ending signature will be diffed, asserted to be empty/not empty (depending on the arguments), and then checked against the provided diff text and hint. Args: end_sig (django_evolution.signature.ProjectSignature): The expected project signature at the end of the evolution. This is generated by :py:meth:`make_end_signatures`. diff_text (unicode, optional): The expected generated text describing a diff that must match, if provided. expected_hint (unicode, optional): The expected generated hint text that must match, if provided. expect_empty (bool, optional): Whether the diff is expected to be empty. Returns: django_evolution.diff.Diff: The resulting diff. Raises: AssertionError: One of the expectations has failed. """ d = Diff(self.start_sig, end_sig) self.assertEqual(d.is_empty(), expect_empty) if not expect_empty: if diff_text is not None: self.assertEqual(str(d), diff_text) if expected_hint is not None: self.assertEqual([str(e) for e in d.evolution()['tests']], expected_hint) return d def perform_simulations(self, evolutions, end_sig, ignore_apps=False, db_name=None): """Run simulations and verify that they result in an end signature. This will run through an evolution chain, simulating each one on a copy of the starting signature, and then verifying that the signature is properly transformed into the expected ending signature. Args: evolutions (list of django_evolution.mutations.BaseMutation): The evolution chain to run simulations on. end_sig (django_evolution.signature.ProjectSignature): The expected ending signature. ignore_apps (bool, optional): Whether to ignore changes to the list of applications. db_name (unicode, optional): The name of the database to perform the simulations against. Returns: django_evolution.signature.ProjectSignature: The resulting modified signature. Raises: AssertionError: The modified signature did not match the expected end signature. """ db_name = db_name or self.default_database_name self.test_database_state = self.database_state.clone() test_sig = self.start_sig.clone() for mutation in evolutions: mutation.run_simulation(app_label='tests', project_sig=test_sig, database_state=self.test_database_state, database=db_name) # Check that the simulation's changes results in an empty diff. d = Diff(test_sig, end_sig) self.assertTrue(d.is_empty(ignore_apps=ignore_apps)) return test_sig def perform_mutations(self, evolutions, end, end_sig, sql_name=None, rescan_indexes=True, db_name=None, create_test_data_func=None): """Apply mutations that and verify the results. This will run through the evolution chain, applying each mutation on the database and against the signature, and then verifying the resulting signature and generated SQL. Args: evolutions (list of django_evolution.mutations.BaseMutation): The evolution chain to run simulations on. end (collections.OrderedDict): The expected model map at the end of the evolution. This is generated by :py:meth:`make_end_signatures`. end_sig (django_evolution.signature.ProjectSignature): The expected ending signature. This is generated by :py:meth:`make_end_signatures`. sql_name (unicode, optional): The name of the registered SQL content for the database being tested. If not provided, the SQL won't be compared. rescan_indexes (bool, optional): Whether to re-scan the list of table indexes after applying the mutations. db_name (unicode, optional): The name of the database to apply the evolutions against. create_test_data_func (callable, optional): A function to call in order to create data in the database for the initial models, before applying mutations. It must take a database name. Raises: AssertionError: The resulting SQL did not match. django.db.utils.OperationalError: There was an error executing SQL. """ app_label = 'tests' def run_mutations(): if rescan_indexes: self.test_database_state.rescan_tables() app_mutator = AppMutator(app_label=app_label, project_sig=test_sig, database_state=self.test_database_state, database=db_name) app_mutator.run_mutations(evolutions) return app_mutator.to_sql() db_name = db_name or self.default_database_name self.test_database_state = self.database_state.clone() test_sig = self.start_sig.clone() with ensure_test_db(model_entries=six.iteritems(self.start), end_model_entries=six.iteritems(end), app_label=app_label, database=db_name): if create_test_data_func: create_test_data_func(db_name) sql = execute_test_sql(run_mutations(), database=db_name) if sql_name is not None: self.assertSQLMappingEqual(sql, sql_name, database=db_name) def register_model(self, model, name, db_name=None, **kwargs): """Register a model for the test. This will register not only this model, but any models in :py:attr:`pre_extra_models` and :py:attr:`extra_models`. It will not include :py:attr:`base_model`. Args: model (type): The main :py:class:`~django.db.models.Model` to register. name (unicode): The name to use when for the model when registering. This doesn't have to match the model's actual name. db_name (unicode, optional): The name of the database to register this model on. **kwargs (dict): Additional keyword arguments to pass to :py:func:`~django_evolution.tests.utils.register_models`. Returns: collections.OrderedDict: A dictionary of registered models. The keys are model names, and the values are the models. """ self._models_registered = True models = self.pre_extra_models + [(name, model)] + self.extra_models return register_models(database_state=self.database_state, models=models, new_app_label='tests', db_name=db_name or self.default_database_name, **kwargs) def create_test_proj_sig(self, model, name, extra_models=[], pre_extra_models=[]): """Create a project signature for the given models. The signature will include not only these models, but any models in :py:attr:`pre_extra_models` and :py:attr:`extra_models`. It will not include :py:attr:`base_model`. Args: model (type): The main :py:class:`~django.db.models.Model` to include in the signature. name (unicode): The name to use when for the model. This doesn't have to match the model's actual name. extra_models (list of type, optional): An additional list of extra models to register after ``model`` (but before the class-defined :py:attr:`extra_models`). pre_extra_models (list of type, optional): An additional list of extra models to register before ``model`` (but after :py:attr:`pre_extra_models`). Returns: django_evolution.signature.ProjectSignature: The generated project signature. """ return create_test_project_sig( models=(self.pre_extra_models + pre_extra_models + [(name, model)] + extra_models + self.extra_models)) def copy_models(self, models): """Copy a list of models. This will be a deep copy, allowing any of the copied models to be altered without affecting the originals. Args: models (list of type): The list of :py:class:`~django.db.models.Model` classes. Returns: list of type: The copied list of :py:class:`~django.db.models.Model` classes. """ return copy.deepcopy(models) @contextmanager def override_db_routers(self, routers): """Override database routers for a test. This clears the router cache before and after the test, allowing custom routers to be used during unit tests. Args: routers (list): The list of router class paths or instances. Yields: The context. """ try: with override_settings(DATABASE_ROUTERS=routers): self.clear_routers_cache() yield finally: self.clear_routers_cache() def clear_routers_cache(self): """Clear the router cache.""" router.routers = ConnectionRouter().routers
class EvolutionTestCase(TestCase): """Base class for test cases that need to evolve the database.""" sql_mapping_key = None default_database_name = 'default' default_model_name = 'TestModel' default_base_model = None default_pre_extra_models = [] default_extra_models = [] # Allow for diffs for large dictionary structures, to help debug # signature failures. maxDiff = 10000 def setUp(self): self.base_model = None self.pre_extra_models = [] self.extra_models = [] self.start = None self.start_sig = None self.database_state = None self.test_database_state = None self._models_registered = False if self.default_base_model: self.set_base_model(self.default_base_model, pre_extra_models=self.default_pre_extra_models, extra_models=self.default_extra_models) def tearDown(self): if self._models_registered: unregister_app('tests') def set_base_model(self, model, name=None, extra_models=[], pre_extra_models=[], db_name=None): name = name or self.default_model_name db_name = db_name or self.default_database_name if self.base_model: unregister_app('tests') self.base_model = model self.pre_extra_models = pre_extra_models self.extra_models = extra_models self.database_state = DatabaseState(db_name) self.start = self.register_model(model, name, register_indexes=True, db_name=db_name) self.start_sig = self.create_test_proj_sig(model, name) def make_end_signatures(self, model, model_name, db_name=None): db_name = db_name or self.default_database_name end = self.register_model(model, name=model_name, db_name=db_name) end_sig = self.create_test_proj_sig(model, name=model_name) return end, end_sig def perform_evolution_tests(self, model, evolutions, diff_text=None, expected_hint=None, sql_name=None, model_name=None, end=None, end_sig=None, expect_noop=False, rescan_indexes=True, use_hinted_evolutions=False, perform_simulations=True, perform_mutations=True, db_name=None): model_name = model_name or self.default_model_name db_name = db_name or self.default_database_name if end is None or end_sig is None: end, end_sig = self.make_end_signatures(model, model_name, db_name) # See if the diff between signatures contains the contents we expect. d = self.perform_diff_test(end_sig, diff_text, expected_hint, expect_empty=expect_noop) if use_hinted_evolutions: assert not evolutions evolutions = d.evolution()['tests'] if perform_simulations: self.perform_simulations(evolutions, end_sig, db_name=db_name) if perform_mutations: self.perform_mutations(evolutions, end, end_sig, sql_name, rescan_indexes=rescan_indexes, db_name=db_name) def perform_diff_test(self, end_sig, diff_text, expected_hint, expect_empty=False): d = Diff(self.start_sig, end_sig) self.assertEqual(d.is_empty(), expect_empty) if not expect_empty: if diff_text is not None: self.assertEqual(str(d), diff_text) if expected_hint is not None: self.assertEqual( [str(e) for e in d.evolution()['tests']], expected_hint) return d def perform_simulations(self, evolutions, end_sig, ignore_apps=False, db_name=None): db_name = db_name or self.default_database_name self.test_database_state = self.database_state.clone() test_sig = self.start_sig.clone() for mutation in evolutions: mutation.run_simulation(app_label='tests', project_sig=test_sig, database_state=self.test_database_state, database=db_name) # Check that the simulation's changes results in an empty diff. d = Diff(test_sig, end_sig) self.assertTrue(d.is_empty(ignore_apps=ignore_apps)) def perform_mutations(self, evolutions, end, end_sig, sql_name, rescan_indexes=True, db_name=None): def run_mutations(): if rescan_indexes: self.test_database_state.rescan_indexes() app_mutator = AppMutator(app_label='tests', project_sig=test_sig, database_state=self.test_database_state, database=db_name) app_mutator.run_mutations(evolutions) return app_mutator.to_sql() db_name = db_name or self.default_database_name self.test_database_state = self.database_state.clone() test_sig = self.start_sig.clone() sql = execute_test_sql(self.start, end, run_mutations, database=db_name) if sql_name is not None: # Normalize the generated and expected SQL so that we are # guaranteed to have a list with one item per line. generated_sql = '\n'.join(sql).splitlines() expected_sql = self.get_sql_mapping(sql_name, db_name).splitlines() # Output the statements one-by-one, to help with diagnosing # differences. print() print("** Comparing SQL against '%s'" % sql_name) print('** Generated:') print() for line in generated_sql: print(' %s' % line) print() print('** Expected:') print() for line in expected_sql: print(' %s' % line) print() # Compare as lists, so that we can better spot inconsistencies. self.assertListEqual(generated_sql, expected_sql) def get_sql_mapping(self, name, db_name=None): db_name = db_name or self.default_database_name return get_sql_mappings(self.sql_mapping_key, db_name)[name] def register_model(self, model, name, db_name=None, **kwargs): self._models_registered = True models = self.pre_extra_models + [(name, model)] + self.extra_models return register_models(database_state=self.database_state, models=models, new_app_label='tests', db_name=db_name or self.default_database_name, **kwargs) def create_test_proj_sig(self, model, name, extra_models=[], pre_extra_models=[]): return create_test_project_sig(models=( self.pre_extra_models + pre_extra_models + [(name, model)] + extra_models + self.extra_models )) def copy_models(self, models): return copy.deepcopy(models) @contextmanager def override_db_routers(self, routers): """Override database routers for a test. This clears the router cache before and after the test, allowing custom routers to be used during unit tests. Args: routers (list): The list of router class paths or instances. Yields: The context. """ with override_settings(DATABASE_ROUTERS=routers): self.clear_routers_cache() yield self.clear_routers_cache() def clear_routers_cache(self): """Clear the router cache.""" router.routers = ConnectionRouter().routers