Merge branch 'i507_beaker_cache'
[mediagoblin.git] / mediagoblin / tests / test_migrations.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17
18 from nose.tools import assert_raises
19 from pymongo import Connection
20
21 from mediagoblin.tests.tools import (
22 install_fixtures_simple, assert_db_meets_expected)
23 from mediagoblin.db.util import (
24 RegisterMigration, MigrationManager, ObjectId,
25 MissingCurrentMigration)
26
27 # This one will get filled with local migrations
28 TEST_MIGRATION_REGISTRY = {}
29 # this one won't get filled
30 TEST_EMPTY_MIGRATION_REGISTRY = {}
31
32 MIGRATION_DB_NAME = u'__mediagoblin_test_migrations__'
33
34
35 ######################
36 # Fake test migrations
37 ######################
38
39 @RegisterMigration(1, TEST_MIGRATION_REGISTRY)
40 def creature_add_magical_powers(database):
41 """
42 Add lists of magical powers.
43
44 This defaults to [], an empty list. Since we haven't declared any
45 magical powers, all existing monsters, setting to an empty list is
46 fine.
47 """
48 database['creatures'].update(
49 {'magical_powers': {'$exists': False}},
50 {'$set': {'magical_powers': []}},
51 multi=True)
52
53
54 @RegisterMigration(2, TEST_MIGRATION_REGISTRY)
55 def creature_rename_num_legs_to_num_limbs(database):
56 """
57 It turns out we want to track how many limbs a creature has, not
58 just how many legs. We don't care about the ambiguous distinction
59 between arms/legs currently.
60 """
61 # $rename not available till 1.7.2+, Debian Stable only includes
62 # 1.4.4... we should do renames manually for now :(
63
64 collection = database['creatures']
65 target = collection.find(
66 {'num_legs': {'$exists': True}})
67
68 for document in target:
69 # A lame manual renaming.
70 document['num_limbs'] = document.pop('num_legs')
71 collection.save(document)
72
73
74 @RegisterMigration(3, TEST_MIGRATION_REGISTRY)
75 def creature_remove_is_demon(database):
76 """
77 It turns out we don't care much about whether creatures are demons
78 or not.
79 """
80 database['creatures'].update(
81 {'is_demon': {'$exists': True}},
82 {'$unset': {'is_demon': 1}},
83 multi=True)
84
85
86 @RegisterMigration(4, TEST_MIGRATION_REGISTRY)
87 def level_exits_dict_to_list(database):
88 """
89 For the sake of the indexes we want to write, and because we
90 intend to add more flexible fields, we want to move level exits
91 from like:
92
93 {'big_door': 'castle_level_id',
94 'trapdoor': 'dungeon_level_id'}
95
96 to like:
97
98 [{'name': 'big_door',
99 'exits_to': 'castle_level_id'},
100 {'name': 'trapdoor',
101 'exits_to': 'dungeon_level_id'}]
102 """
103 collection = database['levels']
104 target = collection.find(
105 {'exits': {'$type': 3}})
106
107 for level in target:
108 new_exits = []
109 for exit_name, exits_to in level['exits'].items():
110 new_exits.append(
111 {'name': exit_name,
112 'exits_to': exits_to})
113
114 level['exits'] = new_exits
115 collection.save(level)
116
117
118 CENTIPEDE_OBJECTID = ObjectId()
119 WOLF_OBJECTID = ObjectId()
120 WIZARDSNAKE_OBJECTID = ObjectId()
121
122 UNMIGRATED_DBDATA = {
123 'creatures': [
124 {'_id': CENTIPEDE_OBJECTID,
125 'name': 'centipede',
126 'num_legs': 100,
127 'is_demon': False},
128 {'_id': WOLF_OBJECTID,
129 'name': 'wolf',
130 'num_legs': 4,
131 'is_demon': False},
132 # don't ask me what a wizardsnake is.
133 {'_id': WIZARDSNAKE_OBJECTID,
134 'name': 'wizardsnake',
135 'num_legs': 0,
136 'is_demon': True}],
137 'levels': [
138 {'_id': 'necroplex',
139 'name': 'The Necroplex',
140 'description': 'A complex full of pure deathzone.',
141 'exits': {
142 'deathwell': 'evilstorm',
143 'portal': 'central_park'}},
144 {'_id': 'evilstorm',
145 'name': 'Evil Storm',
146 'description': 'A storm full of pure evil.',
147 'exits': {}}, # you can't escape the evilstorm
148 {'_id': 'central_park',
149 'name': 'Central Park, NY, NY',
150 'description': "New York's friendly Central Park.",
151 'exits': {
152 'portal': 'necroplex'}}]}
153
154
155 EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA = {
156 'creatures': [
157 {'_id': CENTIPEDE_OBJECTID,
158 'name': 'centipede',
159 'num_limbs': 100,
160 'magical_powers': []},
161 {'_id': WOLF_OBJECTID,
162 'name': 'wolf',
163 'num_limbs': 4,
164 # kept around namely to check that it *isn't* removed!
165 'magical_powers': []},
166 {'_id': WIZARDSNAKE_OBJECTID,
167 'name': 'wizardsnake',
168 'num_limbs': 0,
169 'magical_powers': []}],
170 'levels': [
171 {'_id': 'necroplex',
172 'name': 'The Necroplex',
173 'description': 'A complex full of pure deathzone.',
174 'exits': [
175 {'name': 'deathwell',
176 'exits_to': 'evilstorm'},
177 {'name': 'portal',
178 'exits_to': 'central_park'}]},
179 {'_id': 'evilstorm',
180 'name': 'Evil Storm',
181 'description': 'A storm full of pure evil.',
182 'exits': []}, # you can't escape the evilstorm
183 {'_id': 'central_park',
184 'name': 'Central Park, NY, NY',
185 'description': "New York's friendly Central Park.",
186 'exits': [
187 {'name': 'portal',
188 'exits_to': 'necroplex'}]}]}
189
190 # We want to make sure that if we're at migration 3, migration 3
191 # doesn't get re-run.
192
193 SEMI_MIGRATED_DBDATA = {
194 'creatures': [
195 {'_id': CENTIPEDE_OBJECTID,
196 'name': 'centipede',
197 'num_limbs': 100,
198 'magical_powers': []},
199 {'_id': WOLF_OBJECTID,
200 'name': 'wolf',
201 'num_limbs': 4,
202 # kept around namely to check that it *isn't* removed!
203 'is_demon': False,
204 'magical_powers': [
205 'ice_breath', 'death_stare']},
206 {'_id': WIZARDSNAKE_OBJECTID,
207 'name': 'wizardsnake',
208 'num_limbs': 0,
209 'magical_powers': [
210 'death_rattle', 'sneaky_stare',
211 'slithery_smoke', 'treacherous_tremors'],
212 'is_demon': True}],
213 'levels': [
214 {'_id': 'necroplex',
215 'name': 'The Necroplex',
216 'description': 'A complex full of pure deathzone.',
217 'exits': {
218 'deathwell': 'evilstorm',
219 'portal': 'central_park'}},
220 {'_id': 'evilstorm',
221 'name': 'Evil Storm',
222 'description': 'A storm full of pure evil.',
223 'exits': {}}, # you can't escape the evilstorm
224 {'_id': 'central_park',
225 'name': 'Central Park, NY, NY',
226 'description': "New York's friendly Central Park.",
227 'exits': {
228 'portal': 'necroplex'}}]}
229
230
231 EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA = {
232 'creatures': [
233 {'_id': CENTIPEDE_OBJECTID,
234 'name': 'centipede',
235 'num_limbs': 100,
236 'magical_powers': []},
237 {'_id': WOLF_OBJECTID,
238 'name': 'wolf',
239 'num_limbs': 4,
240 # kept around namely to check that it *isn't* removed!
241 'is_demon': False,
242 'magical_powers': [
243 'ice_breath', 'death_stare']},
244 {'_id': WIZARDSNAKE_OBJECTID,
245 'name': 'wizardsnake',
246 'num_limbs': 0,
247 'magical_powers': [
248 'death_rattle', 'sneaky_stare',
249 'slithery_smoke', 'treacherous_tremors'],
250 'is_demon': True}],
251 'levels': [
252 {'_id': 'necroplex',
253 'name': 'The Necroplex',
254 'description': 'A complex full of pure deathzone.',
255 'exits': [
256 {'name': 'deathwell',
257 'exits_to': 'evilstorm'},
258 {'name': 'portal',
259 'exits_to': 'central_park'}]},
260 {'_id': 'evilstorm',
261 'name': 'Evil Storm',
262 'description': 'A storm full of pure evil.',
263 'exits': []}, # you can't escape the evilstorm
264 {'_id': 'central_park',
265 'name': 'Central Park, NY, NY',
266 'description': "New York's friendly Central Park.",
267 'exits': [
268 {'name': 'portal',
269 'exits_to': 'necroplex'}]}]}
270
271
272 class TestMigrations(object):
273 def setUp(self):
274 # Set up the connection, drop an existing possible database
275 self.connection = Connection()
276 self.connection.drop_database(MIGRATION_DB_NAME)
277 self.db = Connection()[MIGRATION_DB_NAME]
278 self.migration_manager = MigrationManager(
279 self.db, TEST_MIGRATION_REGISTRY)
280 self.empty_migration_manager = MigrationManager(
281 self.db, TEST_EMPTY_MIGRATION_REGISTRY)
282 self.run_migrations = []
283
284 def tearDown(self):
285 self.connection.drop_database(MIGRATION_DB_NAME)
286
287 def _record_migration(self, migration_number, migration_func):
288 self.run_migrations.append((migration_number, migration_func))
289
290 def test_migrations_registered_and_sorted(self):
291 """
292 Make sure that migrations get registered and are sorted right
293 in the migration manager
294 """
295 assert TEST_MIGRATION_REGISTRY == {
296 1: creature_add_magical_powers,
297 2: creature_rename_num_legs_to_num_limbs,
298 3: creature_remove_is_demon,
299 4: level_exits_dict_to_list}
300 assert self.migration_manager.sorted_migrations == [
301 (1, creature_add_magical_powers),
302 (2, creature_rename_num_legs_to_num_limbs),
303 (3, creature_remove_is_demon),
304 (4, level_exits_dict_to_list)]
305 assert self.empty_migration_manager.sorted_migrations == []
306
307 def test_run_full_migrations(self):
308 """
309 Make sure that running the full migration suite from 0 updates
310 everything
311 """
312 self.migration_manager.set_current_migration(0)
313 assert self.migration_manager.database_current_migration() == 0
314 install_fixtures_simple(self.db, UNMIGRATED_DBDATA)
315 self.migration_manager.migrate_new(post_callback=self._record_migration)
316
317 assert self.run_migrations == [
318 (1, creature_add_magical_powers),
319 (2, creature_rename_num_legs_to_num_limbs),
320 (3, creature_remove_is_demon),
321 (4, level_exits_dict_to_list)]
322
323 assert_db_meets_expected(
324 self.db, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA)
325
326 # Make sure the migration is recorded correctly
327 assert self.migration_manager.database_current_migration() == 4
328
329 # run twice! It should do nothing the second time.
330 # ------------------------------------------------
331 self.run_migrations = []
332 self.migration_manager.migrate_new(post_callback=self._record_migration)
333 assert self.run_migrations == []
334 assert_db_meets_expected(
335 self.db, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA)
336 assert self.migration_manager.database_current_migration() == 4
337
338
339 def test_run_partial_migrations(self):
340 """
341 Make sure that running full migration suite from 3 only runs
342 last migration
343 """
344 self.migration_manager.set_current_migration(3)
345 assert self.migration_manager.database_current_migration() == 3
346 install_fixtures_simple(self.db, SEMI_MIGRATED_DBDATA)
347 self.migration_manager.migrate_new(post_callback=self._record_migration)
348
349 assert self.run_migrations == [
350 (4, level_exits_dict_to_list)]
351
352 assert_db_meets_expected(
353 self.db, EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA)
354
355 # Make sure the migration is recorded correctly
356 assert self.migration_manager.database_current_migration() == 4
357
358 def test_migrations_recorded_as_latest(self):
359 """
360 Make sure that if we don't have a migration_status
361 pre-recorded it's marked as the latest
362 """
363 self.migration_manager.install_migration_version_if_missing()
364 assert self.migration_manager.database_current_migration() == 4
365
366 def test_no_migrations_recorded_as_zero(self):
367 """
368 Make sure that if we don't have a migration_status
369 but there *are* no migrations that it's marked as 0
370 """
371 self.empty_migration_manager.install_migration_version_if_missing()
372 assert self.empty_migration_manager.database_current_migration() == 0
373
374 def test_migrations_to_run(self):
375 """
376 Make sure we get the right list of migrations to run
377 """
378 self.migration_manager.set_current_migration(0)
379
380 assert self.migration_manager.migrations_to_run() == [
381 (1, creature_add_magical_powers),
382 (2, creature_rename_num_legs_to_num_limbs),
383 (3, creature_remove_is_demon),
384 (4, level_exits_dict_to_list)]
385
386 self.migration_manager.set_current_migration(3)
387
388 assert self.migration_manager.migrations_to_run() == [
389 (4, level_exits_dict_to_list)]
390
391 self.migration_manager.set_current_migration(4)
392
393 assert self.migration_manager.migrations_to_run() == []
394
395
396 def test_no_migrations_raises_exception(self):
397 """
398 If we don't have the current migration set in the database,
399 this should error out.
400 """
401 assert_raises(
402 MissingCurrentMigration,
403 self.migration_manager.migrations_to_run)