1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
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 should
47 database
['creatures'].update(
48 {'magical_powers': {'$exists': False}},
49 {'$set': {'magical_powers': []}},
53 @RegisterMigration(2, TEST_MIGRATION_REGISTRY
)
54 def creature_rename_num_legs_to_num_limbs(database
):
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.
60 # $rename not available till 1.7.2+, Debian Stable only includes
61 # 1.4.4... we should do renames manually for now :(
63 collection
= database
['creatures']
64 target
= collection
.find(
65 {'num_legs': {'$exists': True}})
67 for document
in target
:
68 # A lame manual renaming.
69 document
['num_limbs'] = document
.pop('num_legs')
70 collection
.save(document
)
73 @RegisterMigration(3, TEST_MIGRATION_REGISTRY
)
74 def creature_remove_is_demon(database
):
76 It turns out we don't care much about whether creatures are demons
79 database
['creatures'].update(
80 {'is_demon': {'$exists': True}},
81 {'$unset': {'is_demon': 1}},
85 @RegisterMigration(4, TEST_MIGRATION_REGISTRY
)
86 def level_exits_dict_to_list(database
):
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
92 {'big_door': 'castle_level_id',
93 'trapdoor': 'dungeon_level_id'}
98 'exits_to': 'castle_level_id'},
100 'exits_to': 'dungeon_level_id'}]
102 collection
= database
['levels']
103 target
= collection
.find(
104 {'exits': {'$type': 3}})
108 for exit_name
, exits_to
in level
['exits'].items():
111 'exits_to': exits_to
})
113 level
['exits'] = new_exits
114 collection
.save(level
)
117 CENTIPEDE_OBJECTID
= ObjectId()
118 WOLF_OBJECTID
= ObjectId()
119 WIZARDSNAKE_OBJECTID
= ObjectId()
121 UNMIGRATED_DBDATA
= {
123 {'_id': CENTIPEDE_OBJECTID
,
127 {'_id': WOLF_OBJECTID
,
131 # don't ask me what a wizardsnake is.
132 {'_id': WIZARDSNAKE_OBJECTID
,
133 'name': 'wizardsnake',
138 'name': 'The Necroplex',
139 'description': 'A complex full of pure deathzone.',
141 'deathwell': 'evilstorm',
142 'portal': 'central_park'}},
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.",
151 'portal': 'necroplex'}}]}
154 EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA
= {
156 {'_id': CENTIPEDE_OBJECTID
,
159 'magical_powers': []},
160 {'_id': WOLF_OBJECTID
,
163 # kept around namely to check that it *isn't* removed!
164 'magical_powers': []},
165 {'_id': WIZARDSNAKE_OBJECTID
,
166 'name': 'wizardsnake',
168 'magical_powers': []}],
171 'name': 'The Necroplex',
172 'description': 'A complex full of pure deathzone.',
174 {'name': 'deathwell',
175 'exits_to': 'evilstorm'},
177 'exits_to': 'central_park'}]},
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.",
187 'exits_to': 'necroplex'}]}]}
189 # We want to make sure that if we're at migration 3, migration 3
190 # doesn't get re-run.
192 SEMI_MIGRATED_DBDATA
= {
194 {'_id': CENTIPEDE_OBJECTID
,
197 'magical_powers': []},
198 {'_id': WOLF_OBJECTID
,
201 # kept around namely to check that it *isn't* removed!
204 'ice_breath', 'death_stare']},
205 {'_id': WIZARDSNAKE_OBJECTID
,
206 'name': 'wizardsnake',
209 'death_rattle', 'sneaky_stare',
210 'slithery_smoke', 'treacherous_tremors'],
214 'name': 'The Necroplex',
215 'description': 'A complex full of pure deathzone.',
217 'deathwell': 'evilstorm',
218 'portal': 'central_park'}},
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.",
227 'portal': 'necroplex'}}]}
230 EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA
= {
232 {'_id': CENTIPEDE_OBJECTID
,
235 'magical_powers': []},
236 {'_id': WOLF_OBJECTID
,
239 # kept around namely to check that it *isn't* removed!
242 'ice_breath', 'death_stare']},
243 {'_id': WIZARDSNAKE_OBJECTID
,
244 'name': 'wizardsnake',
247 'death_rattle', 'sneaky_stare',
248 'slithery_smoke', 'treacherous_tremors'],
252 'name': 'The Necroplex',
253 'description': 'A complex full of pure deathzone.',
255 {'name': 'deathwell',
256 'exits_to': 'evilstorm'},
258 'exits_to': 'central_park'}]},
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.",
268 'exits_to': 'necroplex'}]}]}
271 class TestMigrations(object):
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
)
279 self
.empty_migration_manager
= MigrationManager(
280 self
.db
, TEST_EMPTY_MIGRATION_REGISTRY
)
281 self
.run_migrations
= []
284 self
.connection
.drop_database(MIGRATION_DB_NAME
)
286 def _record_migration(self
, migration_number
, migration_func
):
287 self
.run_migrations
.append((migration_number
, migration_func
))
289 def test_migrations_registered_and_sorted(self
):
291 Make sure that migrations get registered and are sorted right
292 in the migration manager
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
== []
306 def test_run_full_migrations(self
):
308 Make sure that running the full migration suite from 0 updates
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
)
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
)]
322 assert_db_meets_expected(
323 self
.db
, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA
)
325 # Make sure the migration is recorded correctly
326 assert self
.migration_manager
.database_current_migration() == 4
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
338 def test_run_partial_migrations(self
):
340 Make sure that running full migration suite from 3 only runs
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
)
348 assert self
.run_migrations
== [
349 (4, level_exits_dict_to_list
)]
351 assert_db_meets_expected(
352 self
.db
, EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA
)
354 # Make sure the migration is recorded correctly
355 assert self
.migration_manager
.database_current_migration() == 4
357 def test_migrations_recorded_as_latest(self
):
359 Make sure that if we don't have a migration_status
360 pre-recorded it's marked as the latest
362 self
.migration_manager
.install_migration_version_if_missing()
363 assert self
.migration_manager
.database_current_migration() == 4
365 def test_no_migrations_recorded_as_zero(self
):
367 Make sure that if we don't have a migration_status
368 but there *are* no migrations that it's marked as 0
370 self
.empty_migration_manager
.install_migration_version_if_missing()
371 assert self
.empty_migration_manager
.database_current_migration() == 0
373 def test_migrations_to_run(self
):
375 Make sure we get the right list of migrations to run
377 self
.migration_manager
.set_current_migration(0)
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
)]
385 self
.migration_manager
.set_current_migration(3)
387 assert self
.migration_manager
.migrations_to_run() == [
388 (4, level_exits_dict_to_list
)]
390 self
.migration_manager
.set_current_migration(4)
392 assert self
.migration_manager
.migrations_to_run() == []
395 def test_no_migrations_raises_exception(self
):
397 If we don't have the current migration set in the database,
398 this should error out.
401 MissingCurrentMigration
,
402 self
.migration_manager
.migrations_to_run
)