offroad 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/LICENSE +674 -674
  2. data/README.rdoc +29 -29
  3. data/Rakefile +75 -75
  4. data/TODO +42 -42
  5. data/lib/app/models/offroad/group_state.rb +85 -85
  6. data/lib/app/models/offroad/mirror_info.rb +53 -53
  7. data/lib/app/models/offroad/model_state.rb +36 -36
  8. data/lib/app/models/offroad/received_record_state.rb +115 -115
  9. data/lib/app/models/offroad/sendable_record_state.rb +91 -91
  10. data/lib/app/models/offroad/system_state.rb +33 -33
  11. data/lib/cargo_streamer.rb +222 -222
  12. data/lib/controller_extensions.rb +74 -74
  13. data/lib/exceptions.rb +16 -16
  14. data/lib/migrate/20100512164608_create_offroad_tables.rb +72 -72
  15. data/lib/mirror_data.rb +376 -376
  16. data/lib/model_extensions.rb +378 -377
  17. data/lib/module_funcs.rb +94 -94
  18. data/lib/offroad.rb +41 -41
  19. data/lib/version.rb +3 -3
  20. data/lib/view_helper.rb +7 -7
  21. data/templates/offline.rb +36 -36
  22. data/templates/offline_database.yml +7 -7
  23. data/templates/offroad.yml +6 -6
  24. data/test/app_root/app/controllers/application_controller.rb +2 -2
  25. data/test/app_root/app/controllers/group_controller.rb +28 -28
  26. data/test/app_root/app/models/global_record.rb +10 -10
  27. data/test/app_root/app/models/group.rb +12 -12
  28. data/test/app_root/app/models/group_owned_record.rb +68 -68
  29. data/test/app_root/app/models/guest.rb +7 -7
  30. data/test/app_root/app/models/subrecord.rb +12 -12
  31. data/test/app_root/app/models/unmirrored_record.rb +4 -4
  32. data/test/app_root/app/views/group/download_down_mirror.html.erb +3 -3
  33. data/test/app_root/app/views/group/download_initial_down_mirror.html.erb +3 -3
  34. data/test/app_root/app/views/group/download_up_mirror.html.erb +5 -5
  35. data/test/app_root/app/views/layouts/mirror.html.erb +8 -8
  36. data/test/app_root/config/boot.rb +115 -115
  37. data/test/app_root/config/database-pg.yml +8 -8
  38. data/test/app_root/config/database.yml +5 -5
  39. data/test/app_root/config/environment.rb +24 -24
  40. data/test/app_root/config/environments/test.rb +17 -17
  41. data/test/app_root/config/offroad.yml +6 -6
  42. data/test/app_root/config/routes.rb +4 -4
  43. data/test/app_root/db/migrate/20100529235049_create_tables.rb +64 -64
  44. data/test/app_root/lib/common_hobo.rb +15 -15
  45. data/test/app_root/vendor/plugins/offroad/init.rb +2 -2
  46. data/test/functional/mirror_operations_test.rb +148 -148
  47. data/test/test_helper.rb +453 -453
  48. data/test/unit/app_state_tracking_test.rb +275 -275
  49. data/test/unit/cargo_streamer_test.rb +332 -332
  50. data/test/unit/global_data_test.rb +102 -102
  51. data/test/unit/group_controller_test.rb +152 -152
  52. data/test/unit/group_data_test.rb +442 -435
  53. data/test/unit/group_single_test.rb +136 -136
  54. data/test/unit/hobo_permissions_test.rb +57 -57
  55. data/test/unit/mirror_data_test.rb +1283 -1283
  56. data/test/unit/mirror_info_test.rb +31 -31
  57. data/test/unit/module_funcs_test.rb +37 -37
  58. data/test/unit/pathological_model_test.rb +62 -62
  59. data/test/unit/test_framework_test.rb +86 -86
  60. data/test/unit/unmirrored_data_test.rb +14 -14
  61. metadata +6 -8
data/test/test_helper.rb CHANGED
@@ -1,453 +1,453 @@
1
- ENV['RAILS_ENV'] = 'test'
2
-
3
- prev_dir = Dir.getwd
4
- begin
5
- Dir.chdir("#{File.dirname(__FILE__)}/..")
6
-
7
- begin
8
- # Used when running test files directly
9
- $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib"
10
- require "#{File.dirname(__FILE__)}/app_root/config/environment"
11
- rescue LoadError
12
- # This is needed for root-level rake task 'test'
13
- require "app_root/config/environment"
14
- end
15
- ensure
16
- Dir.chdir(prev_dir)
17
- end
18
-
19
- require 'rubygems'
20
- require 'test/unit/util/backtracefilter'
21
- require 'test_help'
22
-
23
- # Try to load the redgreen test console outputter, if it's available
24
- begin
25
- require 'redgreen'
26
- rescue LoadError
27
- end
28
-
29
- # Monkey patch the backtrace filter to include project source files
30
- module Test::Unit::Util::BacktraceFilter
31
- def filter_backtrace(backtrace, prefix = nil)
32
- backtrace = backtrace.select do |e|
33
- if ENV['FULL_BACKTRACE']
34
- true
35
- else
36
- e.include?("offroad") || !(e.include?("/ruby/") || e.include?("/gems/"))
37
- end
38
- end
39
-
40
- common_prefix = nil
41
- backtrace.each do |elem|
42
- next if elem.start_with? "./"
43
- if common_prefix
44
- until elem.start_with? common_prefix
45
- common_prefix.chop!
46
- end
47
- else
48
- common_prefix = String.new(elem)
49
- end
50
- end
51
-
52
- return backtrace.map do |element|
53
- if element.start_with? common_prefix && common_prefix.size < element.size
54
- element[common_prefix.size, element.size]
55
- elsif element.start_with? "./"
56
- element[2, element.size]
57
- elsif element.start_with?(Dir.getwd)
58
- element[Dir.getwd.size+1, element.size]
59
- else
60
- element
61
- end
62
- end
63
- end
64
- end
65
-
66
- def force_save_and_reload(*records)
67
- records.each do |record|
68
- record.class.delete(record.id) unless record.new_record?
69
- record.class.import([record], :validate => false, :timestamps => false)
70
- if record.new_record?
71
- record.id = record.class.last(:select => record.class.primary_key, :order => record.class.primary_key).id
72
- end
73
- record.reload
74
- end
75
- end
76
-
77
- class VirtualTestDatabase
78
- @@current_database = nil
79
- @@test_instance = nil
80
-
81
- def initialize(prefix, test_class)
82
- ActiveRecord::Base.connection.clear_query_cache
83
-
84
- @prefix = prefix
85
- @test_instance_vars = {}
86
- @test_instance_var_names = {}
87
-
88
- if @@current_database != nil
89
- @@current_database.send(:put_away)
90
- delete_all_rows
91
- end
92
-
93
- @@test_instance = test_class
94
- setup
95
- backup_as_fresh
96
- @@current_database = self
97
- end
98
-
99
- def bring_forward(test_class, fresh_flag = false)
100
- ActiveRecord::Base.connection.clear_query_cache
101
- @@current_database.send(:put_away)
102
- @@test_instance = test_class
103
- fresh_flag ? restore_fresh : restore
104
- @@current_database = self
105
- end
106
-
107
- def delete_all_rows
108
- tables = ActiveRecord::Base.connection.tables
109
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
110
- tables << "sqlite_sequence"
111
- end
112
-
113
- tables.each do |table|
114
- next if table.downcase.start_with?("virtual_")
115
- next if table == "schema_migrations"
116
- ActiveRecord::Base.connection.execute "DELETE FROM #{table}"
117
- end
118
-
119
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
120
- # Reset all sequences so that autoincremented ids start from 1 again
121
- seqnames = ActiveRecord::Base.connection.select_values "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
122
- seqnames.each do |s|
123
- ActiveRecord::Base.connection.execute "SELECT setval('#{s}', 1, false)"
124
- end
125
- end
126
- end
127
-
128
- protected
129
-
130
- def setup
131
- delete_all_rows
132
- end
133
-
134
- private
135
-
136
- def normal_prefix
137
- "virtual_normal_#{@prefix}_"
138
- end
139
-
140
- def fresh_prefix
141
- "virtual_fresh_#{@prefix}_"
142
- end
143
-
144
- def put_away
145
- copy_tables("", normal_prefix)
146
- backup_instance_vars(normal_prefix)
147
- delete_instance_vars
148
- end
149
-
150
- def backup_as_fresh
151
- copy_tables("", fresh_prefix)
152
- backup_instance_vars(fresh_prefix)
153
- end
154
-
155
- def restore
156
- copy_tables(normal_prefix, "")
157
- restore_instance_vars(normal_prefix)
158
- end
159
-
160
- def restore_fresh
161
- copy_tables(fresh_prefix, "")
162
- restore_instance_vars(fresh_prefix)
163
- end
164
-
165
- def copy_tables(src_prefix, dst_prefix)
166
- tables = ActiveRecord::Base.connection.tables
167
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
168
- tables << "sqlite_sequence"
169
- end
170
- tables.each do |src_table|
171
- next if src_table.end_with?("schema_migrations")
172
- next unless src_table.start_with?(src_prefix)
173
- next if dst_prefix != "" && src_table.start_with?(dst_prefix)
174
- dst_table = dst_prefix + src_table[(src_prefix.size)..(src_table.size)]
175
- next if src_table.downcase.start_with?("virtual_") && dst_table.downcase.start_with?("virtual_")
176
- if tables.include?(dst_table)
177
- ActiveRecord::Base.connection.execute "DELETE FROM #{dst_table}"
178
- ActiveRecord::Base.connection.execute "INSERT INTO #{dst_table} SELECT * FROM #{src_table}"
179
- else
180
- ActiveRecord::Base.connection.execute "CREATE TABLE #{dst_table} AS SELECT * FROM #{src_table}"
181
- end
182
- end
183
-
184
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
185
- # Manage postgresql sequences by keeping non-current sequence vals backed up in Ruby
186
- @pg_seq_vals ||= {}
187
- if src_prefix.blank?
188
- # PG Sequences -> Ruby
189
- @pg_seq_vals[dst_prefix] ||= {}
190
- seqnames = ActiveRecord::Base.connection.select_values "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
191
- seqnames.each do |s|
192
- @pg_seq_vals[dst_prefix][s] = ActiveRecord::Base.connection.select_value "SELECT last_value FROM \"#{s}\""
193
- end
194
- else
195
- # Ruby -> PG Sequences
196
- @pg_seq_vals[src_prefix].each do |s, v|
197
- ActiveRecord::Base.connection.execute "SELECT setval('#{s}', #{v}, true)"
198
- end
199
- end
200
- end
201
- end
202
-
203
- def backup_instance_vars(key)
204
- @test_instance_vars ||= {}
205
- @test_instance_vars[key] = {}
206
- @@test_instance.instance_variables.each do |varname|
207
- next unless @test_instance_var_names.has_key?(varname)
208
- value = @@test_instance.instance_variable_get(varname.to_sym)
209
- next unless value
210
- @test_instance_vars[key][varname] = value.dup
211
- end
212
- end
213
-
214
- def restore_instance_vars(key)
215
- delete_instance_vars
216
- @test_instance_vars[key].each_pair do |varname, value|
217
- restored_val = nil
218
- if value.is_a?(ActiveRecord::Base)
219
- if !value.destroyed? && (value.changed? || value.new_record?)
220
- restored_val = value.class.find(value.id)
221
- else
222
- restored_val = value
223
- end
224
- else
225
- restored_val = value.dup
226
- end
227
-
228
- # Using find_by_id so that if the record was destroyed earlier in the test, RecordNotFound isn't raised here
229
- restored_val = value.is_a?(ActiveRecord::Base) ? value.class.find_by_id(value.id) : value.dup
230
-
231
- @@test_instance.instance_variable_set(varname.to_sym, restored_val)
232
- end
233
- end
234
-
235
- def delete_instance_vars
236
- @@test_instance.instance_variables.each do |varname|
237
- next unless @test_instance_var_names.has_key?(varname)
238
- @@test_instance.instance_variable_set(varname.to_sym, nil)
239
- end
240
- end
241
-
242
- def setup_ivar(key, value)
243
- @test_instance_var_names[key.to_s] = true
244
- @@test_instance.instance_variable_set(key, value)
245
- end
246
- end
247
-
248
- class OnlineTestDatabase < VirtualTestDatabase
249
- def initialize(test_class)
250
- super("online", test_class)
251
- end
252
-
253
- def self.initial_mirror_data
254
- @@initial_mirror_data
255
- end
256
-
257
- protected
258
-
259
- def setup
260
- super
261
-
262
- unused_offline_group = Group.create(:name => "Unused Offline Group")
263
- unused_online_group = Group.create(:name => "Unused Online Group")
264
-
265
- offline_group = Group.create(:name => "An Offline Group")
266
- online_group = Group.create(:name => "An Online Group")
267
- setup_ivar(:@offline_group, offline_group)
268
- setup_ivar(:@online_group, online_group)
269
-
270
- offline_data = GroupOwnedRecord.create( :description => "Sam", :group => offline_group)
271
- online_data = GroupOwnedRecord.create(:description => "Max", :group => online_group)
272
- setup_ivar(:@offline_group_data, offline_data)
273
- setup_ivar(:@online_group_data, online_data)
274
-
275
- indirect_offline_data = SubRecord.create( :description => "Boris", :group_owned_record => offline_data)
276
- indirect_online_data = SubRecord.create( :description => "Natasha", :group_owned_record => online_data)
277
- setup_ivar(:@offline_indirect_data, indirect_offline_data)
278
- setup_ivar(:@online_indirect_data, indirect_online_data)
279
-
280
- setup_ivar(:@editable_group, online_group)
281
- setup_ivar(:@editable_group_data, online_data)
282
- setup_ivar(:@editable_indirect_data, indirect_online_data)
283
-
284
- unused_offline_group.group_offline = true
285
- offline_group.group_offline = true
286
-
287
- @@initial_mirror_data ||= Offroad::MirrorData.new(offline_group, :initial_mode => true).write_downwards_data
288
- end
289
- end
290
-
291
- class OfflineTestDatabase < VirtualTestDatabase
292
- def initialize(test_class)
293
- super("offline", test_class)
294
- end
295
-
296
- protected
297
-
298
- def setup
299
- super
300
-
301
- Offroad::MirrorData.new(nil, :initial_mode => true).load_downwards_data OnlineTestDatabase::initial_mirror_data
302
-
303
- offline_group = Group.first
304
- offline_data = GroupOwnedRecord.first
305
- offline_indirect_data = SubRecord.first
306
-
307
- setup_ivar(:@offline_group, offline_group)
308
- setup_ivar(:@offline_group_data, offline_data)
309
- setup_ivar(:@offline_indirect_data, offline_indirect_data)
310
-
311
- setup_ivar(:@editable_group, offline_group)
312
- setup_ivar(:@editable_group_data, offline_data)
313
- setup_ivar(:@editable_indirect_data, offline_indirect_data)
314
- end
315
- end
316
-
317
- class Test::Unit::TestCase
318
- @@online_database = nil
319
- @@offline_database = nil
320
- @@initial_setup = false
321
-
322
- include Test::Unit::Util::BacktraceFilter
323
-
324
- def setup
325
- begin
326
- unless @@initial_setup
327
- if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgresql")
328
- puts "Dropping all postgresql tables"
329
- tables = ActiveRecord::Base.connection.tables.each do |table|
330
- ActiveRecord::Base.connection.execute "DROP TABLE #{table}"
331
- end
332
- end
333
- @@initial_setup = true
334
- end
335
-
336
- unless ActiveRecord::Base.connection.table_exists?("schema_migrations")
337
- # FIXME : Figure out why ActionController::TestCase keeps on deleting all the tables before each method
338
-
339
- # First time the setup method has ran, create our test databases
340
- ActiveRecord::Migration.verbose = false
341
- ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate") # Migrations for the testing pseudo-app
342
- ActiveRecord::Migrator.migrate("#{File.dirname(__FILE__)}/../lib/migrate/") # Offroad internal tables
343
-
344
- Offroad::config_app_online(true)
345
- @@online_database = OnlineTestDatabase.new(self)
346
-
347
- Offroad::config_app_online(false)
348
- @@offline_database = OfflineTestDatabase.new(self)
349
-
350
- Offroad::config_app_online(nil)
351
- end
352
- rescue Exception => e
353
- puts ""
354
- puts "!!!!! Test framework setup error: #{e.to_s}"
355
- puts " " + filter_backtrace(e.backtrace).join("\n ")
356
- puts ""
357
- raise SystemExit
358
- end
359
- end
360
-
361
- def in_online_app(fresh_flag = false, delete_all_rows = false, &block)
362
- begin
363
- Offroad::config_app_online(true)
364
- @@online_database.bring_forward(self, fresh_flag)
365
- if delete_all_rows
366
- @@online_database.delete_all_rows
367
- end
368
- instance_eval &block
369
- ensure
370
- Offroad::config_app_online(nil)
371
- end
372
- end
373
-
374
- def in_offline_app(fresh_flag = false, delete_all_rows = false, &block)
375
- begin
376
- Offroad::config_app_online(false)
377
- @@offline_database.bring_forward(self, fresh_flag)
378
- if delete_all_rows
379
- @@offline_database.delete_all_rows
380
- end
381
- instance_eval &block
382
- ensure
383
- Offroad::config_app_online(nil)
384
- end
385
- end
386
-
387
- def restore_all_from_fresh
388
- @@offline_database.bring_forward(self, true)
389
- @@online_database.bring_forward(self, true)
390
- end
391
- end
392
-
393
- def define_wrapped_test(name, wrapper_proc, block)
394
- method_name = "test_" + name.to_s.gsub(/[^\w ]/, '_').gsub(' ', '_')
395
- define_method method_name.to_sym, &block
396
- if wrapper_proc
397
- define_method "wrapped_#{method_name}".to_sym do
398
- wrapper_proc.call(self) { send "unwrapped_#{method_name}".to_sym }
399
- end
400
- alias_method "unwrapped_#{method_name}".to_sym, method_name.to_sym
401
- alias_method method_name.to_sym, "wrapped_#{method_name}".to_sym
402
- end
403
- end
404
-
405
- # Convenience methods to create tests that apply to particular environments or situations
406
-
407
- # Test that should be run in the online environment
408
- def online_test(name, &block)
409
- wrapper = Proc.new do |t|
410
- t.in_online_app(true, &block)
411
- end
412
-
413
- define_wrapped_test("ONLINE #{name}", wrapper, block)
414
- end
415
-
416
- # Test that is ran in the online environment, but with no preset records
417
- def empty_online_test(name, &block)
418
- wrapper = Proc.new do |t|
419
- t.in_online_app(false, true, &block)
420
- end
421
-
422
- define_wrapped_test("EMPTY ONLINE #{name}", wrapper, block)
423
- end
424
-
425
- # Test that should be run in the offline environment
426
- def offline_test(name, &block)
427
- wrapper = Proc.new do |t|
428
- t.in_offline_app(true, &block)
429
- end
430
-
431
- define_wrapped_test("OFFLINE #{name}", wrapper, block)
432
- end
433
-
434
- # Test that should be run twice, once online and once offline
435
- def double_test(name, &block)
436
- online_test(name, &block)
437
- offline_test(name, &block)
438
- end
439
-
440
- # Test that shouldn't care what environment it is started in
441
- def agnostic_test(name, &block)
442
- define_wrapped_test("AGNOSTIC #{name}", nil, block)
443
- end
444
-
445
- # Test that involves both environments (within test, use in_online_app and in_offline_app)
446
- def cross_test(name, &block)
447
- wrapper = Proc.new do |t|
448
- t.restore_all_from_fresh
449
- t.instance_eval &block
450
- end
451
-
452
- define_wrapped_test("CROSS #{name}", wrapper, block)
453
- end
1
+ ENV['RAILS_ENV'] = 'test'
2
+
3
+ prev_dir = Dir.getwd
4
+ begin
5
+ Dir.chdir("#{File.dirname(__FILE__)}/..")
6
+
7
+ begin
8
+ # Used when running test files directly
9
+ $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib"
10
+ require "#{File.dirname(__FILE__)}/app_root/config/environment"
11
+ rescue LoadError
12
+ # This is needed for root-level rake task 'test'
13
+ require "app_root/config/environment"
14
+ end
15
+ ensure
16
+ Dir.chdir(prev_dir)
17
+ end
18
+
19
+ require 'rubygems'
20
+ require 'test/unit/util/backtracefilter'
21
+ require 'test_help'
22
+
23
+ # Try to load the redgreen test console outputter, if it's available
24
+ begin
25
+ require 'redgreen'
26
+ rescue LoadError
27
+ end
28
+
29
+ # Monkey patch the backtrace filter to include project source files
30
+ module Test::Unit::Util::BacktraceFilter
31
+ def filter_backtrace(backtrace, prefix = nil)
32
+ backtrace = backtrace.select do |e|
33
+ if ENV['FULL_BACKTRACE']
34
+ true
35
+ else
36
+ e.include?("offroad") || !(e.include?("/ruby/") || e.include?("/gems/"))
37
+ end
38
+ end
39
+
40
+ common_prefix = nil
41
+ backtrace.each do |elem|
42
+ next if elem.start_with? "./"
43
+ if common_prefix
44
+ until elem.start_with? common_prefix
45
+ common_prefix.chop!
46
+ end
47
+ else
48
+ common_prefix = String.new(elem)
49
+ end
50
+ end
51
+
52
+ return backtrace.map do |element|
53
+ if element.start_with? common_prefix && common_prefix.size < element.size
54
+ element[common_prefix.size, element.size]
55
+ elsif element.start_with? "./"
56
+ element[2, element.size]
57
+ elsif element.start_with?(Dir.getwd)
58
+ element[Dir.getwd.size+1, element.size]
59
+ else
60
+ element
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def force_save_and_reload(*records)
67
+ records.each do |record|
68
+ record.class.delete(record.id) unless record.new_record?
69
+ record.class.import([record], :validate => false, :timestamps => false)
70
+ if record.new_record?
71
+ record.id = record.class.last(:select => record.class.primary_key, :order => record.class.primary_key).id
72
+ end
73
+ record.reload
74
+ end
75
+ end
76
+
77
+ class VirtualTestDatabase
78
+ @@current_database = nil
79
+ @@test_instance = nil
80
+
81
+ def initialize(prefix, test_class)
82
+ ActiveRecord::Base.connection.clear_query_cache
83
+
84
+ @prefix = prefix
85
+ @test_instance_vars = {}
86
+ @test_instance_var_names = {}
87
+
88
+ if @@current_database != nil
89
+ @@current_database.send(:put_away)
90
+ delete_all_rows
91
+ end
92
+
93
+ @@test_instance = test_class
94
+ setup
95
+ backup_as_fresh
96
+ @@current_database = self
97
+ end
98
+
99
+ def bring_forward(test_class, fresh_flag = false)
100
+ ActiveRecord::Base.connection.clear_query_cache
101
+ @@current_database.send(:put_away)
102
+ @@test_instance = test_class
103
+ fresh_flag ? restore_fresh : restore
104
+ @@current_database = self
105
+ end
106
+
107
+ def delete_all_rows
108
+ tables = ActiveRecord::Base.connection.tables
109
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
110
+ tables << "sqlite_sequence"
111
+ end
112
+
113
+ tables.each do |table|
114
+ next if table.downcase.start_with?("virtual_")
115
+ next if table == "schema_migrations"
116
+ ActiveRecord::Base.connection.execute "DELETE FROM #{table}"
117
+ end
118
+
119
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
120
+ # Reset all sequences so that autoincremented ids start from 1 again
121
+ seqnames = ActiveRecord::Base.connection.select_values "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
122
+ seqnames.each do |s|
123
+ ActiveRecord::Base.connection.execute "SELECT setval('#{s}', 1, false)"
124
+ end
125
+ end
126
+ end
127
+
128
+ protected
129
+
130
+ def setup
131
+ delete_all_rows
132
+ end
133
+
134
+ private
135
+
136
+ def normal_prefix
137
+ "virtual_normal_#{@prefix}_"
138
+ end
139
+
140
+ def fresh_prefix
141
+ "virtual_fresh_#{@prefix}_"
142
+ end
143
+
144
+ def put_away
145
+ copy_tables("", normal_prefix)
146
+ backup_instance_vars(normal_prefix)
147
+ delete_instance_vars
148
+ end
149
+
150
+ def backup_as_fresh
151
+ copy_tables("", fresh_prefix)
152
+ backup_instance_vars(fresh_prefix)
153
+ end
154
+
155
+ def restore
156
+ copy_tables(normal_prefix, "")
157
+ restore_instance_vars(normal_prefix)
158
+ end
159
+
160
+ def restore_fresh
161
+ copy_tables(fresh_prefix, "")
162
+ restore_instance_vars(fresh_prefix)
163
+ end
164
+
165
+ def copy_tables(src_prefix, dst_prefix)
166
+ tables = ActiveRecord::Base.connection.tables
167
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
168
+ tables << "sqlite_sequence"
169
+ end
170
+ tables.each do |src_table|
171
+ next if src_table.end_with?("schema_migrations")
172
+ next unless src_table.start_with?(src_prefix)
173
+ next if dst_prefix != "" && src_table.start_with?(dst_prefix)
174
+ dst_table = dst_prefix + src_table[(src_prefix.size)..(src_table.size)]
175
+ next if src_table.downcase.start_with?("virtual_") && dst_table.downcase.start_with?("virtual_")
176
+ if tables.include?(dst_table)
177
+ ActiveRecord::Base.connection.execute "DELETE FROM #{dst_table}"
178
+ ActiveRecord::Base.connection.execute "INSERT INTO #{dst_table} SELECT * FROM #{src_table}"
179
+ else
180
+ ActiveRecord::Base.connection.execute "CREATE TABLE #{dst_table} AS SELECT * FROM #{src_table}"
181
+ end
182
+ end
183
+
184
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgres")
185
+ # Manage postgresql sequences by keeping non-current sequence vals backed up in Ruby
186
+ @pg_seq_vals ||= {}
187
+ if src_prefix.blank?
188
+ # PG Sequences -> Ruby
189
+ @pg_seq_vals[dst_prefix] ||= {}
190
+ seqnames = ActiveRecord::Base.connection.select_values "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
191
+ seqnames.each do |s|
192
+ @pg_seq_vals[dst_prefix][s] = ActiveRecord::Base.connection.select_value "SELECT last_value FROM \"#{s}\""
193
+ end
194
+ else
195
+ # Ruby -> PG Sequences
196
+ @pg_seq_vals[src_prefix].each do |s, v|
197
+ ActiveRecord::Base.connection.execute "SELECT setval('#{s}', #{v}, true)"
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ def backup_instance_vars(key)
204
+ @test_instance_vars ||= {}
205
+ @test_instance_vars[key] = {}
206
+ @@test_instance.instance_variables.each do |varname|
207
+ next unless @test_instance_var_names.has_key?(varname)
208
+ value = @@test_instance.instance_variable_get(varname.to_sym)
209
+ next unless value
210
+ @test_instance_vars[key][varname] = value.dup
211
+ end
212
+ end
213
+
214
+ def restore_instance_vars(key)
215
+ delete_instance_vars
216
+ @test_instance_vars[key].each_pair do |varname, value|
217
+ restored_val = nil
218
+ if value.is_a?(ActiveRecord::Base)
219
+ if !value.destroyed? && (value.changed? || value.new_record?)
220
+ restored_val = value.class.find(value.id)
221
+ else
222
+ restored_val = value
223
+ end
224
+ else
225
+ restored_val = value.dup
226
+ end
227
+
228
+ # Using find_by_id so that if the record was destroyed earlier in the test, RecordNotFound isn't raised here
229
+ restored_val = value.is_a?(ActiveRecord::Base) ? value.class.find_by_id(value.id) : value.dup
230
+
231
+ @@test_instance.instance_variable_set(varname.to_sym, restored_val)
232
+ end
233
+ end
234
+
235
+ def delete_instance_vars
236
+ @@test_instance.instance_variables.each do |varname|
237
+ next unless @test_instance_var_names.has_key?(varname)
238
+ @@test_instance.instance_variable_set(varname.to_sym, nil)
239
+ end
240
+ end
241
+
242
+ def setup_ivar(key, value)
243
+ @test_instance_var_names[key.to_s] = true
244
+ @@test_instance.instance_variable_set(key, value)
245
+ end
246
+ end
247
+
248
+ class OnlineTestDatabase < VirtualTestDatabase
249
+ def initialize(test_class)
250
+ super("online", test_class)
251
+ end
252
+
253
+ def self.initial_mirror_data
254
+ @@initial_mirror_data
255
+ end
256
+
257
+ protected
258
+
259
+ def setup
260
+ super
261
+
262
+ unused_offline_group = Group.create(:name => "Unused Offline Group")
263
+ unused_online_group = Group.create(:name => "Unused Online Group")
264
+
265
+ offline_group = Group.create(:name => "An Offline Group")
266
+ online_group = Group.create(:name => "An Online Group")
267
+ setup_ivar(:@offline_group, offline_group)
268
+ setup_ivar(:@online_group, online_group)
269
+
270
+ offline_data = GroupOwnedRecord.create( :description => "Sam", :group => offline_group)
271
+ online_data = GroupOwnedRecord.create(:description => "Max", :group => online_group)
272
+ setup_ivar(:@offline_group_data, offline_data)
273
+ setup_ivar(:@online_group_data, online_data)
274
+
275
+ indirect_offline_data = SubRecord.create( :description => "Boris", :group_owned_record => offline_data)
276
+ indirect_online_data = SubRecord.create( :description => "Natasha", :group_owned_record => online_data)
277
+ setup_ivar(:@offline_indirect_data, indirect_offline_data)
278
+ setup_ivar(:@online_indirect_data, indirect_online_data)
279
+
280
+ setup_ivar(:@editable_group, online_group)
281
+ setup_ivar(:@editable_group_data, online_data)
282
+ setup_ivar(:@editable_indirect_data, indirect_online_data)
283
+
284
+ unused_offline_group.group_offline = true
285
+ offline_group.group_offline = true
286
+
287
+ @@initial_mirror_data ||= Offroad::MirrorData.new(offline_group, :initial_mode => true).write_downwards_data
288
+ end
289
+ end
290
+
291
+ class OfflineTestDatabase < VirtualTestDatabase
292
+ def initialize(test_class)
293
+ super("offline", test_class)
294
+ end
295
+
296
+ protected
297
+
298
+ def setup
299
+ super
300
+
301
+ Offroad::MirrorData.new(nil, :initial_mode => true).load_downwards_data OnlineTestDatabase::initial_mirror_data
302
+
303
+ offline_group = Group.first
304
+ offline_data = GroupOwnedRecord.first
305
+ offline_indirect_data = SubRecord.first
306
+
307
+ setup_ivar(:@offline_group, offline_group)
308
+ setup_ivar(:@offline_group_data, offline_data)
309
+ setup_ivar(:@offline_indirect_data, offline_indirect_data)
310
+
311
+ setup_ivar(:@editable_group, offline_group)
312
+ setup_ivar(:@editable_group_data, offline_data)
313
+ setup_ivar(:@editable_indirect_data, offline_indirect_data)
314
+ end
315
+ end
316
+
317
+ class Test::Unit::TestCase
318
+ @@online_database = nil
319
+ @@offline_database = nil
320
+ @@initial_setup = false
321
+
322
+ include Test::Unit::Util::BacktraceFilter
323
+
324
+ def setup
325
+ begin
326
+ unless @@initial_setup
327
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?("postgresql")
328
+ puts "Dropping all postgresql tables"
329
+ tables = ActiveRecord::Base.connection.tables.each do |table|
330
+ ActiveRecord::Base.connection.execute "DROP TABLE #{table}"
331
+ end
332
+ end
333
+ @@initial_setup = true
334
+ end
335
+
336
+ unless ActiveRecord::Base.connection.table_exists?("schema_migrations")
337
+ # FIXME : Figure out why ActionController::TestCase keeps on deleting all the tables before each method
338
+
339
+ # First time the setup method has ran, create our test databases
340
+ ActiveRecord::Migration.verbose = false
341
+ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate") # Migrations for the testing pseudo-app
342
+ ActiveRecord::Migrator.migrate("#{File.dirname(__FILE__)}/../lib/migrate/") # Offroad internal tables
343
+
344
+ Offroad::config_app_online(true)
345
+ @@online_database = OnlineTestDatabase.new(self)
346
+
347
+ Offroad::config_app_online(false)
348
+ @@offline_database = OfflineTestDatabase.new(self)
349
+
350
+ Offroad::config_app_online(nil)
351
+ end
352
+ rescue Exception => e
353
+ puts ""
354
+ puts "!!!!! Test framework setup error: #{e.to_s}"
355
+ puts " " + filter_backtrace(e.backtrace).join("\n ")
356
+ puts ""
357
+ raise SystemExit
358
+ end
359
+ end
360
+
361
+ def in_online_app(fresh_flag = false, delete_all_rows = false, &block)
362
+ begin
363
+ Offroad::config_app_online(true)
364
+ @@online_database.bring_forward(self, fresh_flag)
365
+ if delete_all_rows
366
+ @@online_database.delete_all_rows
367
+ end
368
+ instance_eval &block
369
+ ensure
370
+ Offroad::config_app_online(nil)
371
+ end
372
+ end
373
+
374
+ def in_offline_app(fresh_flag = false, delete_all_rows = false, &block)
375
+ begin
376
+ Offroad::config_app_online(false)
377
+ @@offline_database.bring_forward(self, fresh_flag)
378
+ if delete_all_rows
379
+ @@offline_database.delete_all_rows
380
+ end
381
+ instance_eval &block
382
+ ensure
383
+ Offroad::config_app_online(nil)
384
+ end
385
+ end
386
+
387
+ def restore_all_from_fresh
388
+ @@offline_database.bring_forward(self, true)
389
+ @@online_database.bring_forward(self, true)
390
+ end
391
+ end
392
+
393
+ def define_wrapped_test(name, wrapper_proc, block)
394
+ method_name = "test_" + name.to_s.gsub(/[^\w ]/, '_').gsub(' ', '_')
395
+ define_method method_name.to_sym, &block
396
+ if wrapper_proc
397
+ define_method "wrapped_#{method_name}".to_sym do
398
+ wrapper_proc.call(self) { send "unwrapped_#{method_name}".to_sym }
399
+ end
400
+ alias_method "unwrapped_#{method_name}".to_sym, method_name.to_sym
401
+ alias_method method_name.to_sym, "wrapped_#{method_name}".to_sym
402
+ end
403
+ end
404
+
405
+ # Convenience methods to create tests that apply to particular environments or situations
406
+
407
+ # Test that should be run in the online environment
408
+ def online_test(name, &block)
409
+ wrapper = Proc.new do |t|
410
+ t.in_online_app(true, &block)
411
+ end
412
+
413
+ define_wrapped_test("ONLINE #{name}", wrapper, block)
414
+ end
415
+
416
+ # Test that is ran in the online environment, but with no preset records
417
+ def empty_online_test(name, &block)
418
+ wrapper = Proc.new do |t|
419
+ t.in_online_app(false, true, &block)
420
+ end
421
+
422
+ define_wrapped_test("EMPTY ONLINE #{name}", wrapper, block)
423
+ end
424
+
425
+ # Test that should be run in the offline environment
426
+ def offline_test(name, &block)
427
+ wrapper = Proc.new do |t|
428
+ t.in_offline_app(true, &block)
429
+ end
430
+
431
+ define_wrapped_test("OFFLINE #{name}", wrapper, block)
432
+ end
433
+
434
+ # Test that should be run twice, once online and once offline
435
+ def double_test(name, &block)
436
+ online_test(name, &block)
437
+ offline_test(name, &block)
438
+ end
439
+
440
+ # Test that shouldn't care what environment it is started in
441
+ def agnostic_test(name, &block)
442
+ define_wrapped_test("AGNOSTIC #{name}", nil, block)
443
+ end
444
+
445
+ # Test that involves both environments (within test, use in_online_app and in_offline_app)
446
+ def cross_test(name, &block)
447
+ wrapper = Proc.new do |t|
448
+ t.restore_all_from_fresh
449
+ t.instance_eval &block
450
+ end
451
+
452
+ define_wrapped_test("CROSS #{name}", wrapper, block)
453
+ end