Skip to content

Commit a07f47a

Browse files
committed
Added a new TestCase class, subclassing django.test.TestCase, in order to
be able to add fixtures once and for all for all tests, instead of loading them again and again for each test, if running on a database that supports transaction rollbacks. In this case, fixtures specified in the perma_fixtures class attribute will be loaded permanently, and not re-loaded. Fixtures specifice as before, in a fixtures class attribute, will be treated as before. The downside of this is that as fixtures are loaded and not unloaded, they can conflict with each other. The requirements on consistency becomes much greater. The effect of this has been to require quite a bit of changes to the simplified creations of various objects in make_test_data() in cases where identically named objects occur in fixtures. Where completely fictitious objects are created, no conflicts appear. Also re-wrote parts of test_runner.py to permit global fixtures, loaded before any tests are run and shared by all. - Legacy-Id: 6318
1 parent 4805b4b commit a07f47a

File tree

4 files changed

+138
-51
lines changed

4 files changed

+138
-51
lines changed

ietf/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from log import log
44
from cache_foreign_key import FKAsOneToOne
55
from draft_search import normalize_draftname
6+
from test_utils import TestCase
67

78
# See http://docs.python.org/tut/node8.html regarding the use of __all__ and
89
# also regarding the practice of using "from xxx import *" in interactive

ietf/utils/test_data.py

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from ietf.group.models import *
1111
from ietf.person.models import *
1212

13+
import debug
14+
1315
def make_test_data():
1416
# telechat dates
1517
t = datetime.date.today()
@@ -21,7 +23,7 @@ def make_test_data():
2123

2224
# groups
2325
secretariat, created = Group.objects.get_or_create(
24-
name="Secretariat",
26+
name="IETF Secretariat",
2527
acronym="secretariat",
2628
state_id="active",
2729
type_id="ietf",
@@ -32,27 +34,39 @@ def make_test_data():
3234
state_id="active",
3335
type_id="ietf",
3436
parent=None)
35-
for x in ["irtf", "iab", "ise", "iesg"]:
36-
Group.objects.get_or_create(
37-
name=x.upper(),
38-
acronym=x,
39-
state_id="active",
40-
type_id="ietf",
41-
parent=None)
42-
area, created = Group.objects.get_or_create(
37+
38+
# XXX As given below, the group objects created doesn't match what's in the
39+
# fixtures, so if both are used, things blow up. The code below should
40+
# probably be updated to match what's in the fixtures, making the fixtures
41+
# unnecessary for a number of test cases.
42+
43+
# irtf, created = Group.objects.get_or_create(
44+
# name="IRTF",
45+
# acronym="irtf",
46+
# state_id="active",
47+
# type_id="irtf",
48+
# parent=None)
49+
# for g,t,n,p in [("iab","ietf", "Internet Architecture Board",1), ("ise","ietf", "Independent Submission Editor", None), ("iesg","ietf", "Internet Engineering Steering Group", 1), ]:
50+
# Group.objects.get_or_create(
51+
# name=n,
52+
# acronym=g,
53+
# state_id="active",
54+
# type_id=t,
55+
# parent_id=p)
56+
area = Group.objects.create(
4357
name="Far Future",
4458
acronym="farfut",
4559
state_id="active",
4660
type_id="area",
4761
parent=ietf)
48-
individ, created = Group.objects.get_or_create(
49-
name="Individual submissions",
50-
acronym="none",
51-
state_id="active",
52-
type_id="individ",
53-
parent=None)
62+
# individ, created = Group.objects.get_or_create(
63+
# name="Individual submissions",
64+
# acronym="none",
65+
# state_id="active",
66+
# type_id="individ",
67+
# parent=None)
5468
# mars WG
55-
group, created = Group.objects.get_or_create(
69+
group = Group.objects.create(
5670
name="Martian Special Interest Group",
5771
acronym="mars",
5872
state_id="active",
@@ -61,7 +75,7 @@ def make_test_data():
6175
list_email="mars-wg@ietf.org",
6276
)
6377
mars_wg = group
64-
charter, created = Document.objects.get_or_create(
78+
charter = Document.objects.create(
6579
name="charter-ietf-" + group.acronym,
6680
type_id="charter",
6781
title=group.name,
@@ -111,7 +125,7 @@ def make_test_data():
111125
# persons
112126

113127
# system
114-
system_person = Person.objects.create(
128+
system_person, created = Person.objects.get_or_create(
115129
# id=0, # special value
116130
name="(System)",
117131
ascii="(System)",
@@ -120,36 +134,35 @@ def make_test_data():
120134
system_person.save()
121135

122136
# IANA and RFC Editor groups
123-
iana = Group.objects.create(
137+
iana, created = Group.objects.get_or_create(
124138
name="IANA",
125139
acronym="iana",
126140
state_id="active",
127141
type_id="ietf",
128142
parent=None,
129143
)
130-
rfc_editor = Group.objects.create(
144+
rfc_editor, created = Group.objects.get_or_create(
131145
name="RFC Editor",
132146
acronym="rfceditor",
133147
state_id="active",
134-
type_id="ietf",
148+
type_id="rfcedtyp",
135149
parent=None,
136150
)
137-
138-
if system_person.id != 0: # work around bug in Django
139-
Person.objects.filter(id=system_person.id).update(id=0)
140-
system_person = Person.objects.get(id=0)
141151

142-
alias = Alias(person=system_person, name=system_person.name)
143-
alias.save()
152+
# if system_person.id != 0: # work around bug in Django
153+
# Person.objects.filter(id=system_person.id).update(id=0)
154+
# system_person = Person.objects.get(id=0)
155+
156+
Alias.objects.get_or_create(person=system_person, name=system_person.name)
144157
Email.objects.get_or_create(address="", person=system_person)
145158

146159
# plain IETF'er
147-
u = User.objects.create(username="plain")
148-
plainman = Person.objects.create(
160+
u, created = User.objects.get_or_create(username="plain")
161+
plainman, created = Person.objects.get_or_create(
149162
name="Plain Man",
150163
ascii="Plain Man",
151164
user=u)
152-
email = Email.objects.create(
165+
email, created = Email.objects.get_or_create(
153166
address="plain@example.com",
154167
person=plainman)
155168

@@ -308,15 +321,15 @@ def make_test_data():
308321
)
309322

310323
# Secretariat user
311-
u = User.objects.create(id=509, username="wnl")
312-
p = Person.objects.create(
324+
u, created = User.objects.get_or_create(id=509, username="wnl")
325+
p, created = Person.objects.get_or_create(
313326
name="Wanda Lo",
314327
ascii="Wanda Lo",
315328
user=u)
316-
email = Email.objects.create(
329+
email, created = Email.objects.get_or_create(
317330
address="wnl@amsl.com",
318331
person=p)
319-
Role.objects.create(
332+
Role.objects.get_or_create(
320333
name_id="auth",
321334
group=secretariat,
322335
email=email,

ietf/utils/test_runner.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from django.conf import settings
3838
from django.template import TemplateDoesNotExist
3939
from django.test.simple import run_tests as django_run_tests
40+
from django.core.management import call_command
4041

4142
import debug
4243

@@ -50,12 +51,14 @@
5051
def safe_create_1(self, verbosity, *args, **kwargs):
5152
global test_database_name, old_create
5253
print " Creating test database..."
53-
settings.DATABASES["default"]["OPTIONS"] = settings.DATABASE_TEST_OPTIONS
54-
print " Using OPTIONS: %s" % settings.DATABASES["default"]["OPTIONS"]
55-
x = old_create(self, 0, *args, **kwargs)
56-
print " Saving test database name "+settings.DATABASES["default"]["NAME"]+"..."
57-
test_database_name = settings.DATABASES["default"]["NAME"]
58-
return x
54+
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.mysql':
55+
settings.DATABASES["default"]["OPTIONS"] = settings.DATABASE_TEST_OPTIONS
56+
print " Using OPTIONS: %s" % settings.DATABASES["default"]["OPTIONS"]
57+
test_database_name = old_create(self, 0, *args, **kwargs)
58+
if settings.GLOBAL_TEST_FIXTURES:
59+
print " Loading global test fixtures: %s" % ", ".join(settings.GLOBAL_TEST_FIXTURES)
60+
call_command('loaddata', *settings.GLOBAL_TEST_FIXTURES, verbosity=0, commit=False, database="default")
61+
return test_database_name
5962

6063
def safe_destroy_0_1(*args, **kwargs):
6164
global test_database_name, old_destroy

ietf/utils/test_utils.py

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,21 @@
3535
import os
3636
import re
3737
import sys
38-
import django
39-
from django.db import connection
40-
from django.test import TestCase
41-
from django.test.client import Client
42-
import ietf.settings
43-
from django.conf import settings
4438
from datetime import datetime
4539
import urllib2 as urllib
4640
from difflib import unified_diff
4741

48-
real_database_name = ietf.settings.DATABASES["default"]["NAME"]
42+
import django.test
43+
from django.db import connection, connections, transaction, DEFAULT_DB_ALIAS
44+
from django.test.testcases import connections_support_transactions
45+
from django.test.testcases import disable_transaction_methods
46+
from django.test.client import Client
47+
from django.conf import settings
48+
from django.core.management import call_command
49+
50+
import debug
51+
52+
real_database_name = settings.DATABASES["default"]["NAME"]
4953

5054
import traceback
5155

@@ -57,7 +61,7 @@ def setUpRealDatabase(self):
5761
self._setDatabaseName(newdb)
5862

5963
def tearDownRealDatabase(self):
60-
curdb = self._getDatabaseName()
64+
#curdb = self._getDatabaseName()
6165
#print " Switching database from "+curdb+" to "+self._original_testdb
6266
self._setDatabaseName(self._original_testdb)
6367

@@ -105,7 +109,7 @@ def split_url(url):
105109
args = {}
106110
return url, args
107111

108-
class SimpleUrlTestCase(TestCase,RealDatabaseTest):
112+
class SimpleUrlTestCase(django.test.TestCase,RealDatabaseTest):
109113

110114
def setUp(self):
111115
self.setUpRealDatabase()
@@ -158,7 +162,8 @@ def doTestUrl(self, tuple):
158162
if code in codes:
159163
sys.stdout.write(".")
160164
else:
161-
sys.stdout.write("E")
165+
sys.stdout.write("F")
166+
failed = True
162167
elif self.verbosity > 1:
163168
if code in codes:
164169
print "OK %s %s" % (code, url)
@@ -232,7 +237,72 @@ def login_testing_unauthorized(tc, remote_user, url):
232237

233238
tc.client.login(remote_user=remote_user)
234239

235-
class ReverseLazyTest(TestCase):
240+
class ReverseLazyTest(django.test.TestCase):
236241
def test_redirect_with_lazy_reverse(self):
237242
response = self.client.get('/ipr/update/')
238243
self.assertRedirects(response, "/ipr/", status_code=301)
244+
245+
loaded_fixtures = []
246+
247+
class TestCase(django.test.TestCase):
248+
"""
249+
Does basically the same as django.test.TestCase, but if the test database has
250+
support for transactions, it loads perma_fixtures before the transaction which
251+
surrounds every test. This causes the perma_fixtures to stay in the database
252+
after the transaction which surrounds each test is rolled back, which lets us
253+
load each preload_fixture only once. However, it requires that all the
254+
perma_fixtures are consistent with each other and with all regular fixtures
255+
across all applications. You could view the perma_fixtures as on_the_fly
256+
initial_data for tests.
257+
258+
Regular fixtures are re-loaded for each TestCase, as usual.
259+
260+
We don't flush the database, as that triggers a re-load of initial_data.
261+
"""
262+
263+
def _fixture_setup(self):
264+
global loaded_fixtures
265+
266+
if not connections_support_transactions():
267+
if hasattr(self, 'perma_fixtures'):
268+
if not hasattr(self, 'fixtures'):
269+
self.fixtures = self.perma_fixtures
270+
else:
271+
self.fixtures += self.perma_fixtures
272+
return super(TestCase, self)._fixture_setup()
273+
274+
# If the test case has a multi_db=True flag, setup all databases.
275+
# Otherwise, just use default.
276+
if getattr(self, 'multi_db', False):
277+
databases = connections
278+
else:
279+
databases = [DEFAULT_DB_ALIAS]
280+
281+
for db in databases:
282+
if hasattr(self, 'perma_fixtures'):
283+
fixtures = [ fixture for fixture in self.perma_fixtures if not fixture in loaded_fixtures ]
284+
if fixtures:
285+
call_command('loaddata', *fixtures, **{
286+
'verbosity': 0,
287+
'commit': False,
288+
'database': db
289+
})
290+
loaded_fixtures += fixtures
291+
292+
for db in databases:
293+
transaction.enter_transaction_management(using=db)
294+
transaction.managed(True, using=db)
295+
disable_transaction_methods()
296+
297+
from django.contrib.sites.models import Site
298+
Site.objects.clear_cache()
299+
300+
for db in databases:
301+
if hasattr(self, 'fixtures'):
302+
call_command('loaddata', *self.fixtures, **{
303+
'verbosity': 0,
304+
'commit': False,
305+
'database': db
306+
})
307+
308+

0 commit comments

Comments
 (0)