1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
18 from nose
.tools
import assert_raises
19 from pymongo
import Connection
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
)
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
= {}
32 MIGRATION_DB_NAME
= u
'__mediagoblin_test_migrations__'
35 ######################
36 # Fake test migrations
37 ######################
39 @RegisterMigration(1, TEST_MIGRATION_REGISTRY
)
40 def creature_add_magical_powers(database
):
42 Add lists of magical powers.
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
48 database
['creatures'].update(
49 {'magical_powers': {'$exists': False}},
50 {'$set': {'magical_powers': []}},
54 @RegisterMigration(2, TEST_MIGRATION_REGISTRY
)
55 def creature_rename_num_legs_to_num_limbs(database
):
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.
61 # $rename not available till 1.7.2+, Debian Stable only includes
62 # 1.4.4... we should do renames manually for now :(
64 collection
= database
['creatures']
65 target
= collection
.find(
66 {'num_legs': {'$exists': True}})
68 for document
in target
:
69 # A lame manual renaming.
70 document
['num_limbs'] = document
.pop('num_legs')
71 collection
.save(document
)
74 @RegisterMigration(3, TEST_MIGRATION_REGISTRY
)
75 def creature_remove_is_demon(database
):
77 It turns out we don't care much about whether creatures are demons
80 database
['creatures'].update(
81 {'is_demon': {'$exists': True}},
82 {'$unset': {'is_demon': 1}},
86 @RegisterMigration(4, TEST_MIGRATION_REGISTRY
)
87 def level_exits_dict_to_list(database
):
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
93 {'big_door': 'castle_level_id',
94 'trapdoor': 'dungeon_level_id'}
99 'exits_to': 'castle_level_id'},
101 'exits_to': 'dungeon_level_id'}]
103 collection
= database
['levels']
104 target
= collection
.find(
105 {'exits': {'$type': 3}})
109 for exit_name
, exits_to
in level
['exits'].items():
112 'exits_to': exits_to
})
114 level
['exits'] = new_exits
115 collection
.save(level
)
118 CENTIPEDE_OBJECTID
= ObjectId()
119 WOLF_OBJECTID
= ObjectId()
120 WIZARDSNAKE_OBJECTID
= ObjectId()
122 UNMIGRATED_DBDATA
= {
124 {'_id': CENTIPEDE_OBJECTID
,
128 {'_id': WOLF_OBJECTID
,
132 # don't ask me what a wizardsnake is.
133 {'_id': WIZARDSNAKE_OBJECTID
,
134 'name': 'wizardsnake',
139 'name': 'The Necroplex',
140 'description': 'A complex full of pure deathzone.',
142 'deathwell': 'evilstorm',
143 'portal': 'central_park'}},
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.",
152 'portal': 'necroplex'}}]}
155 EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA
= {
157 {'_id': CENTIPEDE_OBJECTID
,
160 'magical_powers': []},
161 {'_id': WOLF_OBJECTID
,
164 # kept around namely to check that it *isn't* removed!
165 'magical_powers': []},
166 {'_id': WIZARDSNAKE_OBJECTID
,
167 'name': 'wizardsnake',
169 'magical_powers': []}],
172 'name': 'The Necroplex',
173 'description': 'A complex full of pure deathzone.',
175 {'name': 'deathwell',
176 'exits_to': 'evilstorm'},
178 'exits_to': 'central_park'}]},
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.",
188 'exits_to': 'necroplex'}]}]}
190 # We want to make sure that if we're at migration 3, migration 3
191 # doesn't get re-run.
193 SEMI_MIGRATED_DBDATA
= {
195 {'_id': CENTIPEDE_OBJECTID
,
198 'magical_powers': []},
199 {'_id': WOLF_OBJECTID
,
202 # kept around namely to check that it *isn't* removed!
205 'ice_breath', 'death_stare']},
206 {'_id': WIZARDSNAKE_OBJECTID
,
207 'name': 'wizardsnake',
210 'death_rattle', 'sneaky_stare',
211 'slithery_smoke', 'treacherous_tremors'],
215 'name': 'The Necroplex',
216 'description': 'A complex full of pure deathzone.',
218 'deathwell': 'evilstorm',
219 'portal': 'central_park'}},
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.",
228 'portal': 'necroplex'}}]}
231 EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA
= {
233 {'_id': CENTIPEDE_OBJECTID
,
236 'magical_powers': []},
237 {'_id': WOLF_OBJECTID
,
240 # kept around namely to check that it *isn't* removed!
243 'ice_breath', 'death_stare']},
244 {'_id': WIZARDSNAKE_OBJECTID
,
245 'name': 'wizardsnake',
248 'death_rattle', 'sneaky_stare',
249 'slithery_smoke', 'treacherous_tremors'],
253 'name': 'The Necroplex',
254 'description': 'A complex full of pure deathzone.',
256 {'name': 'deathwell',
257 'exits_to': 'evilstorm'},
259 'exits_to': 'central_park'}]},
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.",
269 'exits_to': 'necroplex'}]}]}
272 class TestMigrations(object):
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
= []
285 self
.connection
.drop_database(MIGRATION_DB_NAME
)
287 def _record_migration(self
, migration_number
, migration_func
):
288 self
.run_migrations
.append((migration_number
, migration_func
))
290 def test_migrations_registered_and_sorted(self
):
292 Make sure that migrations get registered and are sorted right
293 in the migration manager
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
== []
307 def test_run_full_migrations(self
):
309 Make sure that running the full migration suite from 0 updates
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
)
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
)]
323 assert_db_meets_expected(
324 self
.db
, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA
)
326 # Make sure the migration is recorded correctly
327 assert self
.migration_manager
.database_current_migration() == 4
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
339 def test_run_partial_migrations(self
):
341 Make sure that running full migration suite from 3 only runs
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
)
349 assert self
.run_migrations
== [
350 (4, level_exits_dict_to_list
)]
352 assert_db_meets_expected(
353 self
.db
, EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA
)
355 # Make sure the migration is recorded correctly
356 assert self
.migration_manager
.database_current_migration() == 4
358 def test_migrations_recorded_as_latest(self
):
360 Make sure that if we don't have a migration_status
361 pre-recorded it's marked as the latest
363 self
.migration_manager
.install_migration_version_if_missing()
364 assert self
.migration_manager
.database_current_migration() == 4
366 def test_no_migrations_recorded_as_zero(self
):
368 Make sure that if we don't have a migration_status
369 but there *are* no migrations that it's marked as 0
371 self
.empty_migration_manager
.install_migration_version_if_missing()
372 assert self
.empty_migration_manager
.database_current_migration() == 0
374 def test_migrations_to_run(self
):
376 Make sure we get the right list of migrations to run
378 self
.migration_manager
.set_current_migration(0)
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
)]
386 self
.migration_manager
.set_current_migration(3)
388 assert self
.migration_manager
.migrations_to_run() == [
389 (4, level_exits_dict_to_list
)]
391 self
.migration_manager
.set_current_migration(4)
393 assert self
.migration_manager
.migrations_to_run() == []
396 def test_no_migrations_raises_exception(self
):
398 If we don't have the current migration set in the database,
399 this should error out.
402 MissingCurrentMigration
,
403 self
.migration_manager
.migrations_to_run
)