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