familia 2.0.0.pre26 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +94 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +12 -2
- data/README.md +1 -3
- data/docs/guides/feature-encrypted-fields.md +1 -1
- data/docs/guides/feature-expiration.md +1 -1
- data/docs/guides/feature-quantization.md +1 -1
- data/docs/guides/writing-migrations.md +345 -0
- data/docs/overview.md +7 -7
- data/docs/reference/api-technical.md +103 -7
- data/examples/migrations/v1_to_v2_serialization_migration.rb +374 -0
- data/examples/schemas/customer.json +33 -0
- data/examples/schemas/session.json +27 -0
- data/familia.gemspec +3 -2
- data/lib/familia/features/schema_validation.rb +139 -0
- data/lib/familia/migration/base.rb +447 -0
- data/lib/familia/migration/errors.rb +31 -0
- data/lib/familia/migration/model.rb +418 -0
- data/lib/familia/migration/pipeline.rb +226 -0
- data/lib/familia/migration/rake_tasks.rake +3 -0
- data/lib/familia/migration/rake_tasks.rb +160 -0
- data/lib/familia/migration/registry.rb +364 -0
- data/lib/familia/migration/runner.rb +311 -0
- data/lib/familia/migration/script.rb +234 -0
- data/lib/familia/migration.rb +43 -0
- data/lib/familia/schema_registry.rb +173 -0
- data/lib/familia/settings.rb +63 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -0
- data/try/features/schema_registry_try.rb +193 -0
- data/try/features/schema_validation_feature_try.rb +218 -0
- data/try/migration/base_try.rb +226 -0
- data/try/migration/errors_try.rb +67 -0
- data/try/migration/integration_try.rb +451 -0
- data/try/migration/model_try.rb +431 -0
- data/try/migration/pipeline_try.rb +460 -0
- data/try/migration/rake_tasks_try.rb +61 -0
- data/try/migration/registry_try.rb +199 -0
- data/try/migration/runner_try.rb +311 -0
- data/try/migration/schema_validation_try.rb +201 -0
- data/try/migration/script_try.rb +192 -0
- data/try/migration/v1_to_v2_serialization_try.rb +513 -0
- data/try/performance/benchmarks_try.rb +11 -12
- metadata +45 -27
- data/docs/migrating/v2.0.0-pre.md +0 -84
- data/docs/migrating/v2.0.0-pre11.md +0 -253
- data/docs/migrating/v2.0.0-pre12.md +0 -306
- data/docs/migrating/v2.0.0-pre13.md +0 -95
- data/docs/migrating/v2.0.0-pre14.md +0 -37
- data/docs/migrating/v2.0.0-pre18.md +0 -58
- data/docs/migrating/v2.0.0-pre19.md +0 -197
- data/docs/migrating/v2.0.0-pre22.md +0 -241
- data/docs/migrating/v2.0.0-pre5.md +0 -131
- data/docs/migrating/v2.0.0-pre6.md +0 -154
- data/docs/migrating/v2.0.0-pre7.md +0 -222
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# try/migration/integration_try.rb
|
|
2
|
+
#
|
|
3
|
+
# Integration tests for complete migration workflow scenarios
|
|
4
|
+
#
|
|
5
|
+
# frozen_string_literal: true
|
|
6
|
+
|
|
7
|
+
require_relative '../support/helpers/test_helpers'
|
|
8
|
+
require_relative '../../lib/familia/migration'
|
|
9
|
+
|
|
10
|
+
Familia.debug = false
|
|
11
|
+
|
|
12
|
+
# Setup - unique prefix for this test run
|
|
13
|
+
@redis = Familia.dbclient
|
|
14
|
+
@prefix = "familia:test:integration:#{Process.pid}:#{Time.now.to_i}"
|
|
15
|
+
@registry = Familia::Migration::Registry.new(redis: @redis, prefix: @prefix)
|
|
16
|
+
|
|
17
|
+
# Store initial migrations
|
|
18
|
+
@initial_migrations = Familia::Migration.migrations.dup
|
|
19
|
+
|
|
20
|
+
# Helper to reset state
|
|
21
|
+
def reset_state
|
|
22
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
reset_state
|
|
26
|
+
|
|
27
|
+
# ============================================
|
|
28
|
+
# Define all migration classes upfront
|
|
29
|
+
# ============================================
|
|
30
|
+
|
|
31
|
+
# Scenario 1: Simple migration lifecycle
|
|
32
|
+
class IntegrationSimpleMigration < Familia::Migration::Base
|
|
33
|
+
self.migration_id = 'integration_simple'
|
|
34
|
+
self.description = 'Simple integration test'
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
attr_accessor :ran_count
|
|
38
|
+
end
|
|
39
|
+
self.ran_count = 0
|
|
40
|
+
|
|
41
|
+
def migration_needed?
|
|
42
|
+
self.class.ran_count < 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def migrate
|
|
46
|
+
self.class.ran_count += 1
|
|
47
|
+
track_stat(:executed)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Scenario 2: Migration with dependencies
|
|
52
|
+
class IntegrationDepA < Familia::Migration::Base
|
|
53
|
+
self.migration_id = 'integration_dep_a'
|
|
54
|
+
self.dependencies = []
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
attr_accessor :order
|
|
58
|
+
end
|
|
59
|
+
self.order = []
|
|
60
|
+
|
|
61
|
+
def migration_needed?; true; end
|
|
62
|
+
def migrate
|
|
63
|
+
self.class.order << :a
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class IntegrationDepB < Familia::Migration::Base
|
|
68
|
+
self.migration_id = 'integration_dep_b'
|
|
69
|
+
self.dependencies = ['integration_dep_a']
|
|
70
|
+
|
|
71
|
+
def migration_needed?; true; end
|
|
72
|
+
def migrate
|
|
73
|
+
IntegrationDepA.order << :b
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Scenario 3: Dry run mode
|
|
78
|
+
class IntegrationDryRun < Familia::Migration::Base
|
|
79
|
+
self.migration_id = 'integration_dry'
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
attr_accessor :executed
|
|
83
|
+
end
|
|
84
|
+
self.executed = false
|
|
85
|
+
|
|
86
|
+
def migration_needed?; true; end
|
|
87
|
+
def migrate
|
|
88
|
+
self.class.executed = true
|
|
89
|
+
track_stat(:would_run)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Scenario 4: Rollback flow
|
|
94
|
+
class IntegrationRollback < Familia::Migration::Base
|
|
95
|
+
self.migration_id = 'integration_rollback'
|
|
96
|
+
|
|
97
|
+
class << self
|
|
98
|
+
attr_accessor :state
|
|
99
|
+
end
|
|
100
|
+
self.state = :initial
|
|
101
|
+
|
|
102
|
+
def migration_needed?; true; end
|
|
103
|
+
|
|
104
|
+
def migrate
|
|
105
|
+
self.class.state = :migrated
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def down
|
|
109
|
+
self.class.state = :rolled_back
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Scenario 7: Error handling
|
|
114
|
+
class IntegrationFailingMigration < Familia::Migration::Base
|
|
115
|
+
self.migration_id = 'integration_failing'
|
|
116
|
+
|
|
117
|
+
def migration_needed?; true; end
|
|
118
|
+
|
|
119
|
+
def migrate
|
|
120
|
+
raise "Intentional failure for testing"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Scenario 8: Migration status reporting
|
|
125
|
+
class IntegrationStatusA < Familia::Migration::Base
|
|
126
|
+
self.migration_id = 'integration_status_a'
|
|
127
|
+
self.description = 'Status test A'
|
|
128
|
+
|
|
129
|
+
def migration_needed?; true; end
|
|
130
|
+
def migrate; end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class IntegrationStatusB < Familia::Migration::Base
|
|
134
|
+
self.migration_id = 'integration_status_b'
|
|
135
|
+
self.description = 'Status test B'
|
|
136
|
+
|
|
137
|
+
def migration_needed?; true; end
|
|
138
|
+
def migrate; end
|
|
139
|
+
def down; end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Scenario 9: for_realsies_this_time? guard
|
|
143
|
+
class IntegrationGuardedMigration < Familia::Migration::Base
|
|
144
|
+
self.migration_id = 'integration_guarded'
|
|
145
|
+
|
|
146
|
+
class << self
|
|
147
|
+
attr_accessor :guarded_executed
|
|
148
|
+
end
|
|
149
|
+
self.guarded_executed = false
|
|
150
|
+
|
|
151
|
+
def migration_needed?; true; end
|
|
152
|
+
|
|
153
|
+
def migrate
|
|
154
|
+
for_realsies_this_time? do
|
|
155
|
+
self.class.guarded_executed = true
|
|
156
|
+
end
|
|
157
|
+
track_stat(:checked)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Scenario 10: Validate dependencies
|
|
162
|
+
class IntegrationOrphanMigration < Familia::Migration::Base
|
|
163
|
+
self.migration_id = 'integration_orphan'
|
|
164
|
+
self.dependencies = ['nonexistent_parent']
|
|
165
|
+
|
|
166
|
+
def migration_needed?; true; end
|
|
167
|
+
def migrate; end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Scenario 11: Run with limit
|
|
171
|
+
class IntegrationLimitA < Familia::Migration::Base
|
|
172
|
+
self.migration_id = 'integration_limit_a'
|
|
173
|
+
self.dependencies = []
|
|
174
|
+
def migration_needed?; true; end
|
|
175
|
+
def migrate; end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class IntegrationLimitB < Familia::Migration::Base
|
|
179
|
+
self.migration_id = 'integration_limit_b'
|
|
180
|
+
self.dependencies = []
|
|
181
|
+
def migration_needed?; true; end
|
|
182
|
+
def migrate; end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
class IntegrationLimitC < Familia::Migration::Base
|
|
186
|
+
self.migration_id = 'integration_limit_c'
|
|
187
|
+
self.dependencies = []
|
|
188
|
+
def migration_needed?; true; end
|
|
189
|
+
def migrate; end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# ============================================
|
|
193
|
+
# Scenario 1: Simple migration lifecycle
|
|
194
|
+
# ============================================
|
|
195
|
+
|
|
196
|
+
## Scenario 1: Migration runs and is recorded
|
|
197
|
+
runner = Familia::Migration::Runner.new(
|
|
198
|
+
migrations: [IntegrationSimpleMigration],
|
|
199
|
+
registry: @registry
|
|
200
|
+
)
|
|
201
|
+
results = runner.run(dry_run: false)
|
|
202
|
+
[results.first[:status], @registry.applied?('integration_simple')]
|
|
203
|
+
#=> [:success, true]
|
|
204
|
+
|
|
205
|
+
## Scenario 1: Re-run skips already applied migration
|
|
206
|
+
runner = Familia::Migration::Runner.new(
|
|
207
|
+
migrations: [IntegrationSimpleMigration],
|
|
208
|
+
registry: @registry
|
|
209
|
+
)
|
|
210
|
+
runner.pending.empty?
|
|
211
|
+
#=> true
|
|
212
|
+
|
|
213
|
+
reset_state
|
|
214
|
+
IntegrationSimpleMigration.ran_count = 0
|
|
215
|
+
|
|
216
|
+
# ============================================
|
|
217
|
+
# Scenario 2: Migration with dependencies
|
|
218
|
+
# ============================================
|
|
219
|
+
|
|
220
|
+
## Scenario 2: Dependencies run in correct order
|
|
221
|
+
IntegrationDepA.order = []
|
|
222
|
+
runner = Familia::Migration::Runner.new(
|
|
223
|
+
migrations: [IntegrationDepB, IntegrationDepA],
|
|
224
|
+
registry: @registry
|
|
225
|
+
)
|
|
226
|
+
runner.run(dry_run: false)
|
|
227
|
+
IntegrationDepA.order
|
|
228
|
+
#=> [:a, :b]
|
|
229
|
+
|
|
230
|
+
## Scenario 2: Both are recorded as applied
|
|
231
|
+
[@registry.applied?('integration_dep_a'), @registry.applied?('integration_dep_b')]
|
|
232
|
+
#=> [true, true]
|
|
233
|
+
|
|
234
|
+
reset_state
|
|
235
|
+
IntegrationDepA.order = []
|
|
236
|
+
|
|
237
|
+
# ============================================
|
|
238
|
+
# Scenario 3: Dry run mode
|
|
239
|
+
# ============================================
|
|
240
|
+
|
|
241
|
+
## Scenario 3: Dry run does not persist
|
|
242
|
+
IntegrationDryRun.executed = false
|
|
243
|
+
runner = Familia::Migration::Runner.new(
|
|
244
|
+
migrations: [IntegrationDryRun],
|
|
245
|
+
registry: @registry
|
|
246
|
+
)
|
|
247
|
+
@dry_run_results = runner.run(dry_run: true)
|
|
248
|
+
[@dry_run_results.first[:dry_run], @registry.applied?('integration_dry')]
|
|
249
|
+
#=> [true, false]
|
|
250
|
+
|
|
251
|
+
## Scenario 3: Dry run still returns success status
|
|
252
|
+
@dry_run_results.first[:status]
|
|
253
|
+
#=> :success
|
|
254
|
+
|
|
255
|
+
reset_state
|
|
256
|
+
IntegrationDryRun.executed = false
|
|
257
|
+
|
|
258
|
+
# ============================================
|
|
259
|
+
# Scenario 4: Rollback flow
|
|
260
|
+
# ============================================
|
|
261
|
+
|
|
262
|
+
## Scenario 4: Rollback executes down method
|
|
263
|
+
IntegrationRollback.state = :initial
|
|
264
|
+
runner = Familia::Migration::Runner.new(
|
|
265
|
+
migrations: [IntegrationRollback],
|
|
266
|
+
registry: @registry
|
|
267
|
+
)
|
|
268
|
+
runner.run(dry_run: false)
|
|
269
|
+
runner.rollback('integration_rollback')
|
|
270
|
+
IntegrationRollback.state
|
|
271
|
+
#=> :rolled_back
|
|
272
|
+
|
|
273
|
+
## Scenario 4: Registry shows rollback
|
|
274
|
+
@registry.applied?('integration_rollback')
|
|
275
|
+
#=> false
|
|
276
|
+
|
|
277
|
+
## Scenario 4: Metadata shows rolled_back status
|
|
278
|
+
@rollback_meta = @registry.metadata('integration_rollback')
|
|
279
|
+
@rollback_meta[:status]
|
|
280
|
+
#=> 'rolled_back'
|
|
281
|
+
|
|
282
|
+
reset_state
|
|
283
|
+
IntegrationRollback.state = :initial
|
|
284
|
+
|
|
285
|
+
# ============================================
|
|
286
|
+
# Scenario 5: Lua script atomicity
|
|
287
|
+
# ============================================
|
|
288
|
+
|
|
289
|
+
## Scenario 5: rename_field is atomic
|
|
290
|
+
@test_key = "#{@prefix}:script_test"
|
|
291
|
+
@redis.hset(@test_key, 'old_field', 'test_value')
|
|
292
|
+
@redis.hset(@test_key, 'other_field', 'keep_me')
|
|
293
|
+
|
|
294
|
+
Familia::Migration::Script.execute(
|
|
295
|
+
@redis,
|
|
296
|
+
:rename_field,
|
|
297
|
+
keys: [@test_key],
|
|
298
|
+
argv: ['old_field', 'new_field']
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
[
|
|
302
|
+
@redis.hexists(@test_key, 'old_field'),
|
|
303
|
+
@redis.hget(@test_key, 'new_field'),
|
|
304
|
+
@redis.hget(@test_key, 'other_field')
|
|
305
|
+
]
|
|
306
|
+
#=> [false, 'test_value', 'keep_me']
|
|
307
|
+
|
|
308
|
+
## Scenario 5: backup_and_modify_field creates backup
|
|
309
|
+
@backup_key = "#{@prefix}:backup_test"
|
|
310
|
+
@hash_key = "#{@prefix}:data_test"
|
|
311
|
+
@redis.hset(@hash_key, 'target', 'original')
|
|
312
|
+
|
|
313
|
+
Familia::Migration::Script.execute(
|
|
314
|
+
@redis,
|
|
315
|
+
:backup_and_modify_field,
|
|
316
|
+
keys: [@hash_key, @backup_key],
|
|
317
|
+
argv: ['target', 'modified', '3600']
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
[
|
|
321
|
+
@redis.hget(@hash_key, 'target'),
|
|
322
|
+
@redis.hget(@backup_key, "#{@hash_key}:target")
|
|
323
|
+
]
|
|
324
|
+
#=> ['modified', 'original']
|
|
325
|
+
|
|
326
|
+
reset_state
|
|
327
|
+
|
|
328
|
+
# ============================================
|
|
329
|
+
# Scenario 6: Configuration
|
|
330
|
+
# ============================================
|
|
331
|
+
|
|
332
|
+
## Scenario 6: Configuration defaults are set
|
|
333
|
+
config = Familia::Migration.config
|
|
334
|
+
[config.migrations_key, config.backup_ttl, config.batch_size]
|
|
335
|
+
#=> ['familia:migrations', 86400, 1000]
|
|
336
|
+
|
|
337
|
+
## Scenario 6: Configuration can be changed
|
|
338
|
+
Familia::Migration.configure do |c|
|
|
339
|
+
c.batch_size = 500
|
|
340
|
+
end
|
|
341
|
+
Familia::Migration.config.batch_size
|
|
342
|
+
#=> 500
|
|
343
|
+
|
|
344
|
+
# Reset config
|
|
345
|
+
Familia::Migration.config.batch_size = 1000
|
|
346
|
+
|
|
347
|
+
# ============================================
|
|
348
|
+
# Scenario 7: Error handling
|
|
349
|
+
# ============================================
|
|
350
|
+
|
|
351
|
+
## Scenario 7: Failed migration returns error in result
|
|
352
|
+
runner = Familia::Migration::Runner.new(
|
|
353
|
+
migrations: [IntegrationFailingMigration],
|
|
354
|
+
registry: @registry
|
|
355
|
+
)
|
|
356
|
+
@fail_result = runner.run(dry_run: false).first
|
|
357
|
+
[@fail_result[:status], @fail_result[:error].include?('Intentional failure')]
|
|
358
|
+
#=> [:failed, true]
|
|
359
|
+
|
|
360
|
+
## Scenario 7: Failed migration is not recorded as applied
|
|
361
|
+
@registry.applied?('integration_failing')
|
|
362
|
+
#=> false
|
|
363
|
+
|
|
364
|
+
reset_state
|
|
365
|
+
|
|
366
|
+
# ============================================
|
|
367
|
+
# Scenario 8: Migration status reporting
|
|
368
|
+
# ============================================
|
|
369
|
+
|
|
370
|
+
## Scenario 8: Status shows pending and applied correctly
|
|
371
|
+
runner = Familia::Migration::Runner.new(
|
|
372
|
+
migrations: [IntegrationStatusA, IntegrationStatusB],
|
|
373
|
+
registry: @registry
|
|
374
|
+
)
|
|
375
|
+
runner.run_one(IntegrationStatusA, dry_run: false)
|
|
376
|
+
@status_list = runner.status
|
|
377
|
+
@statuses = @status_list.map { |s| [s[:migration_id], s[:status]] }.to_h
|
|
378
|
+
[@statuses['integration_status_a'], @statuses['integration_status_b']]
|
|
379
|
+
#=> [:applied, :pending]
|
|
380
|
+
|
|
381
|
+
## Scenario 8: Status correctly reports reversibility
|
|
382
|
+
@reversible_map = @status_list.map { |s| [s[:migration_id], s[:reversible]] }.to_h
|
|
383
|
+
[@reversible_map['integration_status_a'], @reversible_map['integration_status_b']]
|
|
384
|
+
#=> [false, true]
|
|
385
|
+
|
|
386
|
+
reset_state
|
|
387
|
+
|
|
388
|
+
# ============================================
|
|
389
|
+
# Scenario 9: for_realsies_this_time? guard
|
|
390
|
+
# ============================================
|
|
391
|
+
|
|
392
|
+
## Scenario 9: Guarded block skipped in dry run
|
|
393
|
+
IntegrationGuardedMigration.guarded_executed = false
|
|
394
|
+
runner = Familia::Migration::Runner.new(
|
|
395
|
+
migrations: [IntegrationGuardedMigration],
|
|
396
|
+
registry: @registry
|
|
397
|
+
)
|
|
398
|
+
runner.run(dry_run: true)
|
|
399
|
+
IntegrationGuardedMigration.guarded_executed
|
|
400
|
+
#=> false
|
|
401
|
+
|
|
402
|
+
## Scenario 9: Guarded block executes in actual run
|
|
403
|
+
IntegrationGuardedMigration.guarded_executed = false
|
|
404
|
+
reset_state
|
|
405
|
+
runner = Familia::Migration::Runner.new(
|
|
406
|
+
migrations: [IntegrationGuardedMigration],
|
|
407
|
+
registry: @registry
|
|
408
|
+
)
|
|
409
|
+
runner.run(dry_run: false)
|
|
410
|
+
IntegrationGuardedMigration.guarded_executed
|
|
411
|
+
#=> true
|
|
412
|
+
|
|
413
|
+
reset_state
|
|
414
|
+
IntegrationGuardedMigration.guarded_executed = false
|
|
415
|
+
|
|
416
|
+
# ============================================
|
|
417
|
+
# Scenario 10: Validate dependencies
|
|
418
|
+
# ============================================
|
|
419
|
+
|
|
420
|
+
## Scenario 10: Validate detects missing dependencies
|
|
421
|
+
runner = Familia::Migration::Runner.new(
|
|
422
|
+
migrations: [IntegrationOrphanMigration],
|
|
423
|
+
registry: @registry
|
|
424
|
+
)
|
|
425
|
+
issues = runner.validate
|
|
426
|
+
issues.any? { |i| i[:type] == :missing_dependency && i[:dependency] == 'nonexistent_parent' }
|
|
427
|
+
#=> true
|
|
428
|
+
|
|
429
|
+
reset_state
|
|
430
|
+
|
|
431
|
+
# ============================================
|
|
432
|
+
# Scenario 11: Run with limit
|
|
433
|
+
# ============================================
|
|
434
|
+
|
|
435
|
+
## Scenario 11: Run with limit applies only N migrations
|
|
436
|
+
runner = Familia::Migration::Runner.new(
|
|
437
|
+
migrations: [IntegrationLimitA, IntegrationLimitB, IntegrationLimitC],
|
|
438
|
+
registry: @registry
|
|
439
|
+
)
|
|
440
|
+
@limit_results = runner.run(dry_run: false, limit: 2)
|
|
441
|
+
[@limit_results.size, runner.pending.size]
|
|
442
|
+
#=> [2, 1]
|
|
443
|
+
|
|
444
|
+
reset_state
|
|
445
|
+
|
|
446
|
+
# ============================================
|
|
447
|
+
# Teardown
|
|
448
|
+
# ============================================
|
|
449
|
+
|
|
450
|
+
reset_state
|
|
451
|
+
Familia::Migration.migrations.replace(@initial_migrations)
|