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