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