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
)
26 from mediagoblin
.db
.migrations
import add_table_field
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
= {}
33 MIGRATION_DB_NAME
= u
'__mediagoblin_test_migrations__'
36 ######################
37 # Fake test migrations
38 ######################
40 @RegisterMigration(1, TEST_MIGRATION_REGISTRY
)
41 def creature_add_magical_powers(database
):
43 Add lists of magical powers.
45 This defaults to [], an empty list. Since we haven't declared any
46 magical powers, all existing monsters, setting to an empty list is
49 add_table_field(database
, 'creatures', 'magical_powers', [])
52 @RegisterMigration(2, TEST_MIGRATION_REGISTRY
)
53 def creature_rename_num_legs_to_num_limbs(database
):
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.
59 # $rename not available till 1.7.2+, Debian Stable only includes
60 # 1.4.4... we should do renames manually for now :(
62 collection
= database
['creatures']
63 target
= collection
.find(
64 {'num_legs': {'$exists': True}})
66 for document
in target
:
67 # A lame manual renaming.
68 document
['num_limbs'] = document
.pop('num_legs')
69 collection
.save(document
)
72 @RegisterMigration(3, TEST_MIGRATION_REGISTRY
)
73 def creature_remove_is_demon(database
):
75 It turns out we don't care much about whether creatures are demons
78 database
['creatures'].update(
79 {'is_demon': {'$exists': True}},
80 {'$unset': {'is_demon': 1}},
84 @RegisterMigration(4, TEST_MIGRATION_REGISTRY
)
85 def level_exits_dict_to_list(database
):
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
91 {'big_door': 'castle_level_id',
92 'trapdoor': 'dungeon_level_id'}
97 'exits_to': 'castle_level_id'},
99 'exits_to': 'dungeon_level_id'}]
101 collection
= database
['levels']
102 target
= collection
.find(
103 {'exits': {'$type': 3}})
107 for exit_name
, exits_to
in level
['exits'].items():
110 'exits_to': exits_to
})
112 level
['exits'] = new_exits
113 collection
.save(level
)
116 CENTIPEDE_OBJECTID
= ObjectId()
117 WOLF_OBJECTID
= ObjectId()
118 WIZARDSNAKE_OBJECTID
= ObjectId()
120 UNMIGRATED_DBDATA
= {
122 {'_id': CENTIPEDE_OBJECTID
,
126 {'_id': WOLF_OBJECTID
,
130 # don't ask me what a wizardsnake is.
131 {'_id': WIZARDSNAKE_OBJECTID
,
132 'name': 'wizardsnake',
137 'name': 'The Necroplex',
138 'description': 'A complex full of pure deathzone.',
140 'deathwell': 'evilstorm',
141 'portal': 'central_park'}},
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.",
150 'portal': 'necroplex'}}]}
153 EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA
= {
155 {'_id': CENTIPEDE_OBJECTID
,
158 'magical_powers': []},
159 {'_id': WOLF_OBJECTID
,
162 # kept around namely to check that it *isn't* removed!
163 'magical_powers': []},
164 {'_id': WIZARDSNAKE_OBJECTID
,
165 'name': 'wizardsnake',
167 'magical_powers': []}],
170 'name': 'The Necroplex',
171 'description': 'A complex full of pure deathzone.',
173 {'name': 'deathwell',
174 'exits_to': 'evilstorm'},
176 'exits_to': 'central_park'}]},
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.",
186 'exits_to': 'necroplex'}]}]}
188 # We want to make sure that if we're at migration 3, migration 3
189 # doesn't get re-run.
191 SEMI_MIGRATED_DBDATA
= {
193 {'_id': CENTIPEDE_OBJECTID
,
196 'magical_powers': []},
197 {'_id': WOLF_OBJECTID
,
200 # kept around namely to check that it *isn't* removed!
203 'ice_breath', 'death_stare']},
204 {'_id': WIZARDSNAKE_OBJECTID
,
205 'name': 'wizardsnake',
208 'death_rattle', 'sneaky_stare',
209 'slithery_smoke', 'treacherous_tremors'],
213 'name': 'The Necroplex',
214 'description': 'A complex full of pure deathzone.',
216 'deathwell': 'evilstorm',
217 'portal': 'central_park'}},
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.",
226 'portal': 'necroplex'}}]}
229 EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA
= {
231 {'_id': CENTIPEDE_OBJECTID
,
234 'magical_powers': []},
235 {'_id': WOLF_OBJECTID
,
238 # kept around namely to check that it *isn't* removed!
241 'ice_breath', 'death_stare']},
242 {'_id': WIZARDSNAKE_OBJECTID
,
243 'name': 'wizardsnake',
246 'death_rattle', 'sneaky_stare',
247 'slithery_smoke', 'treacherous_tremors'],
251 'name': 'The Necroplex',
252 'description': 'A complex full of pure deathzone.',
254 {'name': 'deathwell',
255 'exits_to': 'evilstorm'},
257 'exits_to': 'central_park'}]},
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.",
267 'exits_to': 'necroplex'}]}]}
270 class TestMigrations(object):
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
)
278 self
.empty_migration_manager
= MigrationManager(
279 self
.db
, TEST_EMPTY_MIGRATION_REGISTRY
)
280 self
.run_migrations
= []
283 self
.connection
.drop_database(MIGRATION_DB_NAME
)
285 def _record_migration(self
, migration_number
, migration_func
):
286 self
.run_migrations
.append((migration_number
, migration_func
))
288 def test_migrations_registered_and_sorted(self
):
290 Make sure that migrations get registered and are sorted right
291 in the migration manager
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
== []
305 def test_run_full_migrations(self
):
307 Make sure that running the full migration suite from 0 updates
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
)
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
)]
321 assert_db_meets_expected(
322 self
.db
, EXPECTED_POST_MIGRATION_UNMIGRATED_DBDATA
)
324 # Make sure the migration is recorded correctly
325 assert self
.migration_manager
.database_current_migration() == 4
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
337 def test_run_partial_migrations(self
):
339 Make sure that running full migration suite from 3 only runs
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
)
347 assert self
.run_migrations
== [
348 (4, level_exits_dict_to_list
)]
350 assert_db_meets_expected(
351 self
.db
, EXPECTED_POST_MIGRATION_SEMI_MIGRATED_DBDATA
)
353 # Make sure the migration is recorded correctly
354 assert self
.migration_manager
.database_current_migration() == 4
356 def test_migrations_recorded_as_latest(self
):
358 Make sure that if we don't have a migration_status
359 pre-recorded it's marked as the latest
361 self
.migration_manager
.install_migration_version_if_missing()
362 assert self
.migration_manager
.database_current_migration() == 4
364 def test_no_migrations_recorded_as_zero(self
):
366 Make sure that if we don't have a migration_status
367 but there *are* no migrations that it's marked as 0
369 self
.empty_migration_manager
.install_migration_version_if_missing()
370 assert self
.empty_migration_manager
.database_current_migration() == 0
372 def test_migrations_to_run(self
):
374 Make sure we get the right list of migrations to run
376 self
.migration_manager
.set_current_migration(0)
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
)]
384 self
.migration_manager
.set_current_migration(3)
386 assert self
.migration_manager
.migrations_to_run() == [
387 (4, level_exits_dict_to_list
)]
389 self
.migration_manager
.set_current_migration(4)
391 assert self
.migration_manager
.migrations_to_run() == []
394 def test_no_migrations_raises_exception(self
):
396 If we don't have the current migration set in the database,
397 this should error out.
400 MissingCurrentMigration
,
401 self
.migration_manager
.migrations_to_run
)