1
1
import os
2
- import warnings
3
2
4
3
from django .conf import settings
4
+ from django .core .management .color import no_style
5
5
from django .core .management .commands .loaddata import Command
6
6
from django .db import connections , DEFAULT_DB_ALIAS
7
- from django .db .backends .creation import TEST_DATABASE_PREFIX
8
7
from django .db .backends .mysql import creation as mysql
9
8
10
9
import django_nose
@@ -15,8 +14,9 @@ def uses_mysql(connection):
15
14
16
15
17
16
_old_handle = Command .handle
18
- def _new_handle (self , * fixture_labels , ** options ):
19
- """Wrap the the stock loaddata to ignore foreign key checks.
17
+ def _foreign_key_ignoring_handle (self , * fixture_labels , ** options ):
18
+ """Wrap the the stock loaddata to ignore foreign key checks so we can load
19
+ circular references from fixtures.
20
20
21
21
This is monkeypatched into place in setup_databases().
22
22
@@ -39,45 +39,24 @@ def _new_handle(self, *fixture_labels, **options):
39
39
connection .close ()
40
40
41
41
42
- # XXX: hard-coded to mysql.
43
42
class SkipDatabaseCreation (mysql .DatabaseCreation ):
43
+ """Database creation class that skips both creation and flushing
44
44
45
- def _create_test_db ( self , verbosity , autoclobber ):
46
- ### Oh yes, let's copy from django/db/backends/creation.py
47
- suffix = self . sql_table_creation_suffix ()
45
+ The idea is to re-use the perfectly good test DB already created by an
46
+ earlier test run, cutting the time spent before any tests run from 5-13
47
+ (depending on your I/O luck) down to 3.
48
48
49
- if self .connection .settings_dict ['TEST_NAME' ]:
50
- test_database_name = self .connection .settings_dict ['TEST_NAME' ]
51
- else :
52
- test_database_name = TEST_DATABASE_PREFIX + self .connection .settings_dict ['NAME' ]
53
- qn = self .connection .ops .quote_name
54
-
55
- # Create the test database and connect to it. We need to autocommit
56
- # if the database supports it because PostgreSQL doesn't allow
57
- # CREATE/DROP DATABASE statements within transactions.
58
- cursor = self .connection .cursor ()
59
- self .set_autocommit ()
60
-
61
- ### That's enough copying.
62
-
63
- # If we couldn't create the test db, assume it already exists.
64
- try :
65
- cursor .execute ("CREATE DATABASE %s %s" %
66
- (qn (test_database_name ), suffix ))
67
- except Exception , e :
68
- print '...Skipping setup of %s!' % test_database_name
69
- print '...Try FORCE_DB=true if you need fresh databases.'
70
- return test_database_name
71
-
72
- # Drop the db we just created, then do the normal setup.
73
- cursor .execute ("DROP DATABASE %s" % qn (test_database_name ))
74
- return super (SkipDatabaseCreation , self )._create_test_db (
75
- verbosity , autoclobber )
49
+ """
50
+ def create_test_db (self , verbosity = 1 , autoclobber = False ):
51
+ # Notice that the DB supports transactions. Originally, this was done
52
+ # in the method this overrides.
53
+ self .connection .features .confirm ()
54
+ return self ._get_test_db_name ()
76
55
77
56
78
57
class RadicalTestSuiteRunner (django_nose .NoseTestSuiteRunner ):
79
58
"""This is a test runner that monkeypatches connection.creation to skip
80
- database creation if it appears that the db already exists. Your tests
59
+ database creation if it appears that the DB already exists. Your tests
81
60
will run much faster.
82
61
83
62
To force the normal database creation, define the environment variable
@@ -86,22 +65,82 @@ class RadicalTestSuiteRunner(django_nose.NoseTestSuiteRunner):
86
65
87
66
"""
88
67
def setup_databases (self ):
89
- using_mysql = False
68
+ def should_create_database (connection ):
69
+ """Return whether we should recreate the given DB.
70
+
71
+ This is true if the DB doesn't exist or if the FORCE_DB env var is
72
+ truthy.
73
+
74
+ """
75
+ # TODO: Notice when the Model classes change and return True. Worst
76
+ # case, we can generate sqlall and hash it, though it's a bit slow
77
+ # (2 secs) and hits the DB for no good reason. Until we find a
78
+ # faster way, I'm inclined to keep making people explicitly saying
79
+ # FORCE_DB if they want a new DB.
80
+
81
+ # Notice whether the DB exists, and create it if it doesn't:
82
+ try :
83
+ connection .cursor ()
84
+ except StandardError : # TODO: Be more discerning but still DB
85
+ # agnostic.
86
+ return True
87
+ return not not os .getenv ('FORCE_DB' )
88
+
89
+ def sql_reset_sequences (connection ):
90
+ """Return a list of SQL statements needed to reset all sequences
91
+ for Django tables."""
92
+ # TODO: This is MySQL-specific--see below. It should also work with
93
+ # SQLite but not Postgres. :-(
94
+ tables = connection .introspection .django_table_names (
95
+ only_existing = True )
96
+ flush_statements = connection .ops .sql_flush (
97
+ no_style (), tables , connection .introspection .sequence_list ())
98
+
99
+ # connection.ops.sequence_reset_sql() is not implemented for MySQL,
100
+ # and the base class just returns []. TODO: Implement it by pulling
101
+ # the relevant bits out of sql_flush().
102
+ return [s for s in flush_statements if s .startswith ('ALTER' )]
103
+ # Being overzealous and resetting the sequences on non-empty tables
104
+ # like django_content_type seems to be fine in MySQL: adding a row
105
+ # afterward does find the correct sequence number rather than
106
+ # crashing into an existing row.
107
+
90
108
for alias in connections :
91
109
connection = connections [alias ]
92
- if not os .getenv ('FORCE_DB' ):
93
- if uses_mysql (connection ):
94
- connection .creation .__class__ = SkipDatabaseCreation
95
- else :
96
- warnings .warn ('NOT skipping db creation for %s' %
97
- connection .settings_dict ['ENGINE' ])
98
-
99
- Command .handle = _new_handle
110
+ creation = connection .creation
111
+ test_db_name = creation ._get_test_db_name ()
112
+
113
+ # Mess with the DB name so other things operate on a test DB
114
+ # rather than the real one. This is done in create_test_db when
115
+ # we don't monkeypatch it away with SkipDatabaseCreation.
116
+ orig_db_name = connection .settings_dict ['NAME' ]
117
+ connection .settings_dict ['NAME' ] = test_db_name
118
+
119
+ if not should_create_database (connection ):
120
+ print ('Reusing old database "%s". Set env var FORCE_DB=1 if '
121
+ 'you need fresh DBs.' % test_db_name )
122
+
123
+ # Reset auto-increment sequences. Apparently, SUMO's tests are
124
+ # horrid and coupled to certain numbers.
125
+ cursor = connection .cursor ()
126
+ for statement in sql_reset_sequences (connection ):
127
+ cursor .execute (statement )
128
+ connection .commit_unless_managed () # which it is
129
+
130
+ creation .__class__ = SkipDatabaseCreation
131
+ else :
132
+ # We're not using SkipDatabaseCreation, so put the DB name
133
+ # back.
134
+ connection .settings_dict ['NAME' ] = orig_db_name
135
+
136
+ Command .handle = _foreign_key_ignoring_handle
137
+
138
+ # With our class patch, does nothing but return some connection
139
+ # objects:
100
140
return super (RadicalTestSuiteRunner , self ).setup_databases ()
101
141
102
- def teardown_databases (self , old_config ):
103
- if os .getenv ('FORCE_DB' ):
104
- super (RadicalTestSuiteRunner , self ).teardown_databases (old_config )
142
+ def teardown_databases (self , old_config , ** kwargs ):
143
+ """Leave those poor, reusable databases alone."""
105
144
106
145
def setup_test_environment (self , ** kwargs ):
107
146
# If we have a settings_test.py let's roll it into our settings.
@@ -113,4 +152,3 @@ def setup_test_environment(self, **kwargs):
113
152
except ImportError :
114
153
pass
115
154
super (RadicalTestSuiteRunner , self ).setup_test_environment (** kwargs )
116
-
0 commit comments