Ticket #11665: better_constraint_checks_during_testing.v4.diff

File better_constraint_checks_during_testing.v4.diff, 21.3 KB (added by Jim Dalton, 13 years ago)
  • django/db/__init__.py

    diff --git a/django/db/__init__.py b/django/db/__init__.py
    index 8395468..c6f28da 100644
    a b if DEFAULT_DB_ALIAS not in settings.DATABASES:  
    1212    raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
    1313
    1414connections = ConnectionHandler(settings.DATABASES)
     15connections._ignore_num_queries = False
    1516
    1617router = ConnectionRouter(settings.DATABASE_ROUTERS)
    1718
  • django/db/backends/__init__.py

    diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
    index 23ddedb..c9a6d7a 100644
    a b class BaseDatabaseWrapper(local):  
    3434        self.transaction_state = []
    3535        self.savepoint_state = 0
    3636        self._dirty = None
     37       
     38        self.constraint_checking_disabled = False
    3739
    3840    def __eq__(self, other):
    3941        return self.alias == other.alias
    class BaseDatabaseWrapper(local):  
    238240        """
    239241        if self.savepoint_state:
    240242            self._savepoint_commit(sid)
    241 
     243   
    242244    @contextmanager
    243245    def constraint_checks_disabled(self):
    244         disabled = self.disable_constraint_checking()
     246        self.disable_constraint_checking()
    245247        try:
    246248            yield
    247249        finally:
    248             if disabled:
     250            if self.constraint_checking_disabled:
    249251                self.enable_constraint_checking()
    250 
     252       
     253   
    251254    def disable_constraint_checking(self):
    252255        """
    253256        Backends can implement as needed to temporarily disable foreign key constraint
    254257        checking.
    255258        """
    256         pass
     259        self.constraint_checking_disabled = True
    257260
    258261    def enable_constraint_checking(self):
    259262        """
    260263        Backends can implement as needed to re-enable foreign key constraint checking.
    261264        """
    262         pass
    263 
     265        self.constraint_checking_disabled = False
     266   
    264267    def check_constraints(self, table_names=None):
    265268        """
    266269        Backends can override this method if they can apply constraint checking (e.g. via "SET CONSTRAINTS
  • django/db/backends/mysql/base.py

    diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
    index f4523e4..0d719b7 100644
    a b class DatabaseWrapper(BaseDatabaseWrapper):  
    356356        to indicate constraint checks need to be re-enabled.
    357357        """
    358358        self.cursor().execute('SET foreign_key_checks=0')
    359         return True
     359        self.constraint_checking_disabled = True
    360360
    361361    def enable_constraint_checking(self):
    362362        """
    363363        Re-enable foreign key checks after they have been disabled.
    364364        """
    365365        self.cursor().execute('SET foreign_key_checks=1')
    366 
     366        self.constraint_checking_disabled = False
     367   
    367368    def check_constraints(self, table_names=None):
    368369        """
    369370        Checks each table name in table-names for rows with invalid foreign key references. This method is
    class DatabaseWrapper(BaseDatabaseWrapper):  
    389390                    SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
    390391                    LEFT JOIN `%s` as REFERRED
    391392                    ON (REFERRING.`%s` = REFERRED.`%s`)
    392                     WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL"""
     393                    WHERE REFERRING.`%s` IS NOT NULL
     394                        AND REFERRED.`%s` IS NULL"""
    393395                    % (primary_key_column_name, column_name, table_name, referenced_table_name,
    394                     column_name, referenced_column_name, column_name, referenced_column_name))
     396                       column_name, referenced_column_name, column_name, referenced_column_name))
    395397                for bad_row in cursor.fetchall():
    396                     raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid "
    397                         "foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
    398                         % (table_name, bad_row[0],
    399                         table_name, column_name, bad_row[1],
    400                         referenced_table_name, referenced_column_name))
     398                    raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid \
     399foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
     400                                         % (table_name, bad_row[0], table_name, column_name, bad_row[1],
     401                                            referenced_table_name, referenced_column_name))
  • django/db/backends/postgresql_psycopg2/base.py

    diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
    index 37aa072..691c736 100644
    a b class DatabaseWrapper(BaseDatabaseWrapper):  
    105105        self.introspection = DatabaseIntrospection(self)
    106106        self.validation = BaseDatabaseValidation(self)
    107107        self._pg_version = None
     108   
     109    def check_constraints(self, table_names=None):
     110        """
     111        To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they
     112        are returned to deferred.
     113        """
     114        self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE')
     115        self.cursor().execute('SET CONSTRAINTS ALL DEFERRED')
    108116
    109117    def check_constraints(self, table_names=None):
    110118        """
  • django/db/backends/sqlite3/introspection.py

    diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py
    index 9652a4d..b1427c0 100644
    a b class DatabaseIntrospection(BaseDatabaseIntrospection):  
    156156            name = info[0][2] # seqno, cid, name
    157157            indexes[name]['unique'] = True
    158158        return indexes
     159   
     160    def get_primary_key_column(self, cursor, table_name):
     161        """
     162        Get the column name of the primary key for the given table.
     163        """
     164        # Don't use PRAGMA because that causes issues with some transactions
     165        cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"])
     166        results = cursor.fetchone()[0].strip()
     167        results = results[results.index('(')+1:results.rindex(')')]
     168        for field_desc in results.split(','):
     169            field_desc = field_desc.strip()
     170            m = re.search('"(.*)".*PRIMARY KEY$', field_desc)
     171            if m:
     172                return m.groups()[0]
     173        return None
    159174
    160175    def get_primary_key_column(self, cursor, table_name):
    161176        """
  • django/db/backends/util.py

    diff --git a/django/db/backends/util.py b/django/db/backends/util.py
    index 0766f87..e037a5a 100644
    a b class CursorWrapper(object):  
    2929class CursorDebugWrapper(CursorWrapper):
    3030
    3131    def execute(self, sql, params=()):
     32        from django.db import connections
    3233        start = time()
    3334        try:
    3435            return self.cursor.execute(sql, params)
    class CursorDebugWrapper(CursorWrapper):  
    3637            stop = time()
    3738            duration = stop - start
    3839            sql = self.db.ops.last_executed_query(self.cursor, sql, params)
    39             self.db.queries.append({
    40                 'sql': sql,
    41                 'time': "%.3f" % duration,
    42             })
     40            if not connections._ignore_num_queries:
     41                self.db.queries.append({
     42                    'sql': sql,
     43                    'time': "%.3f" % duration,
     44                })
    4345            logger.debug('(%.3f) %s; args=%s' % (duration, sql, params),
    4446                extra={'duration':duration, 'sql':sql, 'params':params}
    4547            )
    def format_number(value, max_digits, decimal_places):  
    143145        context.prec = max_digits
    144146        return u'%s' % str(value.quantize(decimal.Decimal(".1") ** decimal_places, context=context))
    145147    else:
    146         return u"%.*f" % (decimal_places, value)
     148        return u"%.*f" % (decimal_places, value)
     149 No newline at end of file
  • django/test/__init__.py

    diff --git a/django/test/__init__.py b/django/test/__init__.py
    index 68aea9a..04d985d 100644
    a b Django Unit Test and Doctest framework.  
    33"""
    44
    55from django.test.client import Client, RequestFactory
    6 from django.test.testcases import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
     6from django.test.testcases import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature, ignore_num_queries
    77from django.test.utils import Approximate
  • django/test/testcases.py

    diff --git a/django/test/testcases.py b/django/test/testcases.py
    index 5a06e3f..e93ead6 100644
    a b from django.utils.encoding import smart_str  
    2323__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
    2424           'skipIfDBFeature', 'skipUnlessDBFeature')
    2525
     26
     27def ignore_num_queries(fn):
     28    @wraps(fn)
     29    def num_queries_ignored(*args, **kwargs):
     30        connections._ignore_num_queries = True
     31        try:
     32            return fn(*args, **kwargs)
     33        finally:
     34            connections._ignore_num_queries = False
     35    return num_queries_ignored
     36
    2637normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
    2738normalize_decimals = lambda s: re.sub(r"Decimal\('(\d+(\.\d*)?)'\)", lambda m: "Decimal(\"%s\")" % m.groups()[0], s)
    2839
    def to_list(value):  
    3950
    4051real_commit = transaction.commit
    4152real_rollback = transaction.rollback
     53real_commit_unless_managed = transaction.commit_unless_managed
     54real_rollback_unless_managed = transaction.rollback_unless_managed
    4255real_enter_transaction_management = transaction.enter_transaction_management
    4356real_leave_transaction_management = transaction.leave_transaction_management
    4457real_managed = transaction.managed
    real_managed = transaction.managed  
    4659def nop(*args, **kwargs):
    4760    return
    4861
     62@ignore_num_queries
     63def check_constraints(using=None):
     64    """
     65    Emulate the constraint check behavior that normally occurs when a transaction is rolled back or committed.
     66    """
     67    if using is None:
     68        using = DEFAULT_DB_ALIAS
     69    connection = connections[using]
     70    # Don't check constraints if they have been manually disabled
     71    if not connection.constraint_checking_disabled:
     72        connection.check_constraints()
     73
    4974def disable_transaction_methods():
    50     transaction.commit = nop
     75    transaction.commit = check_constraints
    5176    transaction.rollback = nop
     77    transaction.commit_unless_managed = check_constraints
     78    transaction.rollback_unless_managed = nop
    5279    transaction.enter_transaction_management = nop
    5380    transaction.leave_transaction_management = nop
    5481    transaction.managed = nop
    def disable_transaction_methods():  
    5683def restore_transaction_methods():
    5784    transaction.commit = real_commit
    5885    transaction.rollback = real_rollback
     86    transaction.commit_unless_managed = real_commit_unless_managed
     87    transaction.rollback_unless_managed = real_rollback_unless_managed
    5988    transaction.enter_transaction_management = real_enter_transaction_management
    6089    transaction.leave_transaction_management = real_leave_transaction_management
    6190    transaction.managed = real_managed
    class TestCase(TransactionTestCase):  
    578607
    579608        from django.contrib.sites.models import Site
    580609        Site.objects.clear_cache()
     610       
     611        from django.contrib.contenttypes.models import ContentType
     612        ContentType.objects.clear_cache()
    581613
    582614        for db in databases:
    583615            if hasattr(self, 'fixtures'):
  • tests/regressiontests/backends/tests.py

    diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py
    index 27d3dfd..be5d6f4 100644
    a b class FkConstraintsTests(TransactionTestCase):  
    347347        except IntegrityError:
    348348            return
    349349        self.skipTest("This backend does not support integrity checks.")
    350 
     350   
    351351    def test_disable_constraint_checks_manually(self):
    352352        """
    353         When constraint checks are disabled, should be able to write bad data without IntegrityErrors.
     353        When constraint checks are disabled, should be able to write bad data without IntegrityErrors. Also,
     354        should set disabled flag.
    354355        """
    355356        with transaction.commit_manually():
    356357            # Create an Article.
    class FkConstraintsTests(TransactionTestCase):  
    360361            a.reporter_id = 30
    361362            try:
    362363                connection.disable_constraint_checking()
     364                self.assertTrue(connection.constraint_checking_disabled)
    363365                a.save()
    364366                connection.enable_constraint_checking()
    365367            except IntegrityError:
    366368                self.fail("IntegrityError should not have occurred.")
    367369            finally:
    368370                transaction.rollback()
    369 
     371   
    370372    def test_disable_constraint_checks_context_manager(self):
    371373        """
    372374        When constraint checks are disabled (using context manager), should be able to write bad data without IntegrityErrors.
    class FkConstraintsTests(TransactionTestCase):  
    379381            a.reporter_id = 30
    380382            try:
    381383                with connection.constraint_checks_disabled():
     384                    self.assertTrue(connection.constraint_checking_disabled)
    382385                    a.save()
    383386            except IntegrityError:
    384387                self.fail("IntegrityError should not have occurred.")
    385388            finally:
    386389                transaction.rollback()
    387 
     390   
    388391    def test_check_constraints(self):
    389392        """
    390393        Constraint checks should raise an IntegrityError when bad data is in the DB.
  • tests/regressiontests/comment_tests/tests/templatetag_tests.py

    diff --git a/tests/regressiontests/comment_tests/tests/templatetag_tests.py b/tests/regressiontests/comment_tests/tests/templatetag_tests.py
    index 0ee34ac..835017c 100644
    a b class CommentTemplateTagTests(CommentTestCase):  
    4242    def testRenderCommentFormFromObjectWithQueryCount(self):
    4343        def test():
    4444            self.testRenderCommentFormFromObject()
    45         self.assertNumQueries(1, test)
     45        # 1 to select object
     46        # 1 to get the contenttype
     47        self.assertNumQueries(2, test)
    4648
    4749    def testGetCommentCount(self, tag=None):
    4850        self.createSomeComments()
  • new file tests/regressiontests/fixtures_regress/fixtures/forward_ref.json

    diff --git a/tests/regressiontests/fixtures_regress/fixtures/forward_ref.json b/tests/regressiontests/fixtures_regress/fixtures/forward_ref.json
    new file mode 100644
    index 0000000..237b076
    - +  
     1[
     2    {
     3        "pk": 1,
     4        "model": "fixtures_regress.book",
     5        "fields": {
     6            "name": "Cryptonomicon",
     7            "author": 4
     8        }
     9    },
     10    {
     11        "pk": "4",
     12        "model": "fixtures_regress.person",
     13        "fields": {
     14            "name": "Neal Stephenson"
     15        }
     16    }
     17]
     18 No newline at end of file
  • new file tests/regressiontests/fixtures_regress/fixtures/forward_ref_bad_data.json

    diff --git a/tests/regressiontests/fixtures_regress/fixtures/forward_ref_bad_data.json b/tests/regressiontests/fixtures_regress/fixtures/forward_ref_bad_data.json
    new file mode 100644
    index 0000000..3a3fb64
    - +  
     1[
     2    {
     3        "pk": 1,
     4        "model": "fixtures_regress.book",
     5        "fields": {
     6            "name": "Cryptonomicon",
     7            "author": 3
     8        }
     9    },
     10    {
     11        "pk": "4",
     12        "model": "fixtures_regress.person",
     13        "fields": {
     14            "name": "Neal Stephenson"
     15        }
     16    }
     17]
     18 No newline at end of file
  • tests/regressiontests/fixtures_regress/tests.py

    diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py
    index b67155d..07eff1d 100644
    a b class TestFixtures(TestCase):  
    361361            """[{"pk": %d, "model": "fixtures_regress.widget", "fields": {"name": "grommet"}}]"""
    362362            % widget.pk
    363363            )
     364   
     365    def test_loaddata_works_when_fixture_has_forward_refs(self):
     366        """
     367        Regression for #3615 - Forward references cause fixtures not to load in MySQL (InnoDB)
     368        """
     369        management.call_command(
     370            'loaddata',
     371            'forward_ref.json',
     372            verbosity=0,
     373            commit=False
     374        )
     375        self.assertEqual(Book.objects.all()[0].id, 1)
     376        self.assertEqual(Person.objects.all()[0].id, 4)
     377   
     378    def test_loaddata_raises_error_when_fixture_has_invalid_foreign_key(self):
     379        """
     380        Regression for #3615 - Ensure data with nonexistent child key references raises error
     381        """
     382        stderr = StringIO()
     383        management.call_command(
     384            'loaddata',
     385            'forward_ref_bad_data.json',
     386            verbosity=0,
     387            commit=False,
     388            stderr=stderr,
     389        )
     390        self.assertTrue(
     391            stderr.getvalue().startswith('Problem installing fixture')
     392        )
    364393
    365394    def test_loaddata_works_when_fixture_has_forward_refs(self):
    366395        """
  • tests/regressiontests/multiple_database/tests.py

    diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py
    index 02ee34f..3a368bf 100644
    a b class RouterTestCase(TestCase):  
    973973        # Make the 'other' database appear to be a slave of the 'default'
    974974        self.old_routers = router.routers
    975975        router.routers = [TestRouter()]
     976       
     977        # Disable constraint checking, for now
     978        for connection_name in connections:
     979            connection = connections[connection_name]
     980            connection.disable_constraint_checking()
    976981
    977982    def tearDown(self):
    978983        # Restore the 'other' database as an independent database
    979984        router.routers = self.old_routers
     985       
     986        # Re-enable constraint checking
     987        for connection_name in connections:
     988            connection = connections[connection_name]
     989            connection.enable_constraint_checking()
    980990
    981991    def test_db_selection(self):
    982992        "Check that querysets obey the router for db suggestions"
    class SignalTests(TestCase):  
    16921702
    16931703    def setUp(self):
    16941704        self.old_routers = router.routers
     1705       
     1706        # Disable constraint checking, for now
     1707        for connection_name in connections:
     1708            connection = connections[connection_name]
     1709            connection.disable_constraint_checking()
    16951710
    16961711    def tearDown(self):
    16971712        router.routers = self.old_routers
     1713       
     1714        # Re-enable constraint checking
     1715        for connection_name in connections:
     1716            connection = connections[connection_name]
     1717            connection.enable_constraint_checking()
    16981718
    16991719    def _write_to_other(self):
    17001720        "Sends all writes to 'other'."
  • tests/regressiontests/test_utils/models.py

    diff --git a/tests/regressiontests/test_utils/models.py b/tests/regressiontests/test_utils/models.py
    index 4da7a07..7a7a94e 100644
    a b from django.db import models  
    33
    44class Person(models.Model):
    55    name = models.CharField(max_length=100)
     6
     7class Pet(models.Model):
     8    name = models.CharField(max_length=100)
     9    owner = models.ForeignKey(Person)
     10
     11    def __unicode__(self):
     12        return self.name
     13
     14    class Meta:
     15        ordering = ('name',)
  • tests/regressiontests/test_utils/tests.py

    diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py
    index a35b0bc..6e26b24 100644
    a b  
    11from __future__ import with_statement
    22
    3 from django.test import TestCase, skipUnlessDBFeature
     3from django.test import TestCase, skipUnlessDBFeature, ignore_num_queries
    44from django.utils.unittest import skip
     5from django.db import IntegrityError
    56
    6 from models import Person
     7from models import Person, Pet
    78
    89
    910class SkippingTestCase(TestCase):
    class AssertNumQueriesTests(TestCase):  
    4748            self.client.get("/test_utils/get_person/%s/" % person.pk)
    4849            self.client.get("/test_utils/get_person/%s/" % person.pk)
    4950        self.assertNumQueries(2, test_func)
     51   
     52    def test_assert_num_queries_ignore_decorator(self):
     53        person = Person.objects.create(name='test')
     54       
     55        @ignore_num_queries
     56        def test_func():
     57            self.client.get("/test_utils/get_person/%s/" % person.pk)
     58            self.client.get("/test_utils/get_person/%s/" % person.pk)
     59        self.assertNumQueries(0, test_func)
    5060
    5161class AssertNumQueriesContextManagerTests(TestCase):
    5262    urls = 'regressiontests.test_utils.urls'
    class AssertNumQueriesContextManagerTests(TestCase):  
    8696            self.client.get("/test_utils/get_person/%s/" % person.pk)
    8797
    8898
     99class TransactionPatchingTests(TestCase):
     100    def test_bad_data_should_raise_data_integrity_error(self):
     101        """
     102        Ensure bad data cannot be saved to DB during tests.
     103        """
     104        bill = Person.objects.create(name="Bill")
     105        dog = Pet.objects.create(name="Spot", owner=bill)
     106        dog.owner_id = 20 # Does not exist
     107        with self.assertRaises(IntegrityError):
     108            dog.save()
     109
    89110class SaveRestoreWarningState(TestCase):
    90111    def test_save_restore_warnings_state(self):
    91112        """
Back to Top