f6533b539640aa2c32261629ba0854b134389609
[feedcatcher.git] / vendor / rails / activerecord / test / cases / transactions_test.rb
1 require "cases/helper"
2 require 'models/topic'
3 require 'models/reply'
4 require 'models/developer'
5 require 'models/book'
6
7 class TransactionTest < ActiveRecord::TestCase
8 self.use_transactional_fixtures = false
9 fixtures :topics, :developers
10
11 def setup
12 @first, @second = Topic.find(1, 2).sort_by { |t| t.id }
13 end
14
15 def test_successful
16 Topic.transaction do
17 @first.approved = true
18 @second.approved = false
19 @first.save
20 @second.save
21 end
22
23 assert Topic.find(1).approved?, "First should have been approved"
24 assert !Topic.find(2).approved?, "Second should have been unapproved"
25 end
26
27 def transaction_with_return
28 Topic.transaction do
29 @first.approved = true
30 @second.approved = false
31 @first.save
32 @second.save
33 return
34 end
35 end
36
37 def test_successful_with_return
38 class << Topic.connection
39 alias :real_commit_db_transaction :commit_db_transaction
40 def commit_db_transaction
41 $committed = true
42 real_commit_db_transaction
43 end
44 end
45
46 $committed = false
47 transaction_with_return
48 assert $committed
49
50 assert Topic.find(1).approved?, "First should have been approved"
51 assert !Topic.find(2).approved?, "Second should have been unapproved"
52 ensure
53 class << Topic.connection
54 alias :commit_db_transaction :real_commit_db_transaction rescue nil
55 end
56 end
57
58 def test_successful_with_instance_method
59 @first.transaction do
60 @first.approved = true
61 @second.approved = false
62 @first.save
63 @second.save
64 end
65
66 assert Topic.find(1).approved?, "First should have been approved"
67 assert !Topic.find(2).approved?, "Second should have been unapproved"
68 end
69
70 def test_failing_on_exception
71 begin
72 Topic.transaction do
73 @first.approved = true
74 @second.approved = false
75 @first.save
76 @second.save
77 raise "Bad things!"
78 end
79 rescue
80 # caught it
81 end
82
83 assert @first.approved?, "First should still be changed in the objects"
84 assert !@second.approved?, "Second should still be changed in the objects"
85
86 assert !Topic.find(1).approved?, "First shouldn't have been approved"
87 assert Topic.find(2).approved?, "Second should still be approved"
88 end
89
90 def test_raising_exception_in_callback_rollbacks_in_save
91 add_exception_raising_after_save_callback_to_topic
92
93 begin
94 @first.approved = true
95 @first.save
96 flunk
97 rescue => e
98 assert_equal "Make the transaction rollback", e.message
99 assert !Topic.find(1).approved?
100 ensure
101 remove_exception_raising_after_save_callback_to_topic
102 end
103 end
104
105 def test_cancellation_from_before_destroy_rollbacks_in_destroy
106 add_cancelling_before_destroy_with_db_side_effect_to_topic
107 begin
108 nbooks_before_destroy = Book.count
109 status = @first.destroy
110 assert !status
111 assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
112 assert_equal nbooks_before_destroy, Book.count
113 ensure
114 remove_cancelling_before_destroy_with_db_side_effect_to_topic
115 end
116 end
117
118 def test_cancellation_from_before_filters_rollbacks_in_save
119 %w(validation save).each do |filter|
120 send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
121 begin
122 nbooks_before_save = Book.count
123 original_author_name = @first.author_name
124 @first.author_name += '_this_should_not_end_up_in_the_db'
125 status = @first.save
126 assert !status
127 assert_equal original_author_name, @first.reload.author_name
128 assert_equal nbooks_before_save, Book.count
129 ensure
130 send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
131 end
132 end
133 end
134
135 def test_cancellation_from_before_filters_rollbacks_in_save!
136 %w(validation save).each do |filter|
137 send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
138 begin
139 nbooks_before_save = Book.count
140 original_author_name = @first.author_name
141 @first.author_name += '_this_should_not_end_up_in_the_db'
142 @first.save!
143 flunk
144 rescue => e
145 assert_equal original_author_name, @first.reload.author_name
146 assert_equal nbooks_before_save, Book.count
147 ensure
148 send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
149 end
150 end
151 end
152
153 def test_callback_rollback_in_create
154 new_topic = Topic.new(
155 :title => "A new topic",
156 :author_name => "Ben",
157 :author_email_address => "ben@example.com",
158 :written_on => "2003-07-16t15:28:11.2233+01:00",
159 :last_read => "2004-04-15",
160 :bonus_time => "2005-01-30t15:28:00.00+01:00",
161 :content => "Have a nice day",
162 :approved => false)
163 new_record_snapshot = new_topic.new_record?
164 id_present = new_topic.has_attribute?(Topic.primary_key)
165 id_snapshot = new_topic.id
166
167 # Make sure the second save gets the after_create callback called.
168 2.times do
169 begin
170 add_exception_raising_after_create_callback_to_topic
171 new_topic.approved = true
172 new_topic.save
173 flunk
174 rescue => e
175 assert_equal "Make the transaction rollback", e.message
176 assert_equal new_record_snapshot, new_topic.new_record?, "The topic should have its old new_record value"
177 assert_equal id_snapshot, new_topic.id, "The topic should have its old id"
178 assert_equal id_present, new_topic.has_attribute?(Topic.primary_key)
179 ensure
180 remove_exception_raising_after_create_callback_to_topic
181 end
182 end
183 end
184
185 def test_nested_explicit_transactions
186 Topic.transaction do
187 Topic.transaction do
188 @first.approved = true
189 @second.approved = false
190 @first.save
191 @second.save
192 end
193 end
194
195 assert Topic.find(1).approved?, "First should have been approved"
196 assert !Topic.find(2).approved?, "Second should have been unapproved"
197 end
198
199 def test_manually_rolling_back_a_transaction
200 Topic.transaction do
201 @first.approved = true
202 @second.approved = false
203 @first.save
204 @second.save
205
206 raise ActiveRecord::Rollback
207 end
208
209 assert @first.approved?, "First should still be changed in the objects"
210 assert !@second.approved?, "Second should still be changed in the objects"
211
212 assert !Topic.find(1).approved?, "First shouldn't have been approved"
213 assert Topic.find(2).approved?, "Second should still be approved"
214 end
215
216 def test_invalid_keys_for_transaction
217 assert_raise ArgumentError do
218 Topic.transaction :nested => true do
219 end
220 end
221 end
222
223 def test_force_savepoint_in_nested_transaction
224 Topic.transaction do
225 @first.approved = true
226 @second.approved = false
227 @first.save!
228 @second.save!
229
230 begin
231 Topic.transaction :requires_new => true do
232 @first.happy = false
233 @first.save!
234 raise
235 end
236 rescue
237 end
238 end
239
240 assert @first.reload.approved?
241 assert !@second.reload.approved?
242 end if Topic.connection.supports_savepoints?
243
244 def test_no_savepoint_in_nested_transaction_without_force
245 Topic.transaction do
246 @first.approved = true
247 @second.approved = false
248 @first.save!
249 @second.save!
250
251 begin
252 Topic.transaction do
253 @first.approved = false
254 @first.save!
255 raise
256 end
257 rescue
258 end
259 end
260
261 assert !@first.reload.approved?
262 assert !@second.reload.approved?
263 end if Topic.connection.supports_savepoints?
264
265 def test_many_savepoints
266 Topic.transaction do
267 @first.content = "One"
268 @first.save!
269
270 begin
271 Topic.transaction :requires_new => true do
272 @first.content = "Two"
273 @first.save!
274
275 begin
276 Topic.transaction :requires_new => true do
277 @first.content = "Three"
278 @first.save!
279
280 begin
281 Topic.transaction :requires_new => true do
282 @first.content = "Four"
283 @first.save!
284 raise
285 end
286 rescue
287 end
288
289 @three = @first.reload.content
290 raise
291 end
292 rescue
293 end
294
295 @two = @first.reload.content
296 raise
297 end
298 rescue
299 end
300
301 @one = @first.reload.content
302 end
303
304 assert_equal "One", @one
305 assert_equal "Two", @two
306 assert_equal "Three", @three
307 end if Topic.connection.supports_savepoints?
308
309 def test_rollback_when_commit_raises
310 Topic.connection.expects(:begin_db_transaction)
311 Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
312 Topic.connection.expects(:outside_transaction?).returns(false)
313 Topic.connection.expects(:rollback_db_transaction)
314
315 assert_raise RuntimeError do
316 Topic.transaction do
317 # do nothing
318 end
319 end
320 end
321
322 if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
323 def test_outside_transaction_works
324 assert Topic.connection.outside_transaction?
325 Topic.connection.begin_db_transaction
326 assert !Topic.connection.outside_transaction?
327 Topic.connection.rollback_db_transaction
328 assert Topic.connection.outside_transaction?
329 end
330
331 def test_rollback_wont_be_executed_if_no_transaction_active
332 assert_raise RuntimeError do
333 Topic.transaction do
334 Topic.connection.rollback_db_transaction
335 Topic.connection.expects(:rollback_db_transaction).never
336 raise "Rails doesn't scale!"
337 end
338 end
339 end
340
341 def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
342 Topic.transaction do
343 Topic.transaction do
344 Topic.connection.rollback_db_transaction
345 end
346 assert_equal 0, Topic.connection.open_transactions
347 end
348 assert_equal 0, Topic.connection.open_transactions
349 end
350 end
351
352 def test_sqlite_add_column_in_transaction
353 return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
354
355 # Test first if column creation/deletion works correctly when no
356 # transaction is in place.
357 #
358 # We go back to the connection for the column queries because
359 # Topic.columns is cached and won't report changes to the DB
360
361 assert_nothing_raised do
362 Topic.reset_column_information
363 Topic.connection.add_column('topics', 'stuff', :string)
364 assert Topic.column_names.include?('stuff')
365
366 Topic.reset_column_information
367 Topic.connection.remove_column('topics', 'stuff')
368 assert !Topic.column_names.include?('stuff')
369 end
370
371 if Topic.connection.supports_ddl_transactions?
372 assert_nothing_raised do
373 Topic.transaction { Topic.connection.add_column('topics', 'stuff', :string) }
374 end
375 else
376 Topic.transaction do
377 assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
378 raise ActiveRecord::Rollback
379 end
380 end
381 end
382
383 private
384 def add_exception_raising_after_save_callback_to_topic
385 Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
386 end
387
388 def remove_exception_raising_after_save_callback_to_topic
389 Topic.class_eval { remove_method :after_save }
390 end
391
392 def add_exception_raising_after_create_callback_to_topic
393 Topic.class_eval { def after_create() raise "Make the transaction rollback" end }
394 end
395
396 def remove_exception_raising_after_create_callback_to_topic
397 Topic.class_eval { remove_method :after_create }
398 end
399
400 %w(validation save destroy).each do |filter|
401 define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
402 Topic.class_eval "def before_#{filter}() Book.create; false end"
403 end
404
405 define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
406 Topic.class_eval "remove_method :before_#{filter}"
407 end
408 end
409 end
410
411 class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
412 self.use_transactional_fixtures = true
413 fixtures :topics
414
415 def test_automatic_savepoint_in_outer_transaction
416 @first = Topic.find(1)
417
418 begin
419 Topic.transaction do
420 @first.approved = true
421 @first.save!
422 raise
423 end
424 rescue
425 assert !@first.reload.approved?
426 end
427 end
428
429 def test_no_automatic_savepoint_for_inner_transaction
430 @first = Topic.find(1)
431
432 Topic.transaction do
433 @first.approved = true
434 @first.save!
435
436 begin
437 Topic.transaction do
438 @first.approved = false
439 @first.save!
440 raise
441 end
442 rescue
443 end
444 end
445
446 assert !@first.reload.approved?
447 end
448 end if Topic.connection.supports_savepoints?
449
450 if current_adapter?(:PostgreSQLAdapter)
451 class ConcurrentTransactionTest < TransactionTest
452 use_concurrent_connections
453
454 # This will cause transactions to overlap and fail unless they are performed on
455 # separate database connections.
456 def test_transaction_per_thread
457 assert_nothing_raised do
458 threads = (1..3).map do
459 Thread.new do
460 Topic.transaction do
461 topic = Topic.find(1)
462 topic.approved = !topic.approved?
463 topic.save!
464 topic.approved = !topic.approved?
465 topic.save!
466 end
467 end
468 end
469
470 threads.each { |t| t.join }
471 end
472 end
473
474 # Test for dirty reads among simultaneous transactions.
475 def test_transaction_isolation__read_committed
476 # Should be invariant.
477 original_salary = Developer.find(1).salary
478 temporary_salary = 200000
479
480 assert_nothing_raised do
481 threads = (1..3).map do
482 Thread.new do
483 Developer.transaction do
484 # Expect original salary.
485 dev = Developer.find(1)
486 assert_equal original_salary, dev.salary
487
488 dev.salary = temporary_salary
489 dev.save!
490
491 # Expect temporary salary.
492 dev = Developer.find(1)
493 assert_equal temporary_salary, dev.salary
494
495 dev.salary = original_salary
496 dev.save!
497
498 # Expect original salary.
499 dev = Developer.find(1)
500 assert_equal original_salary, dev.salary
501 end
502 end
503 end
504
505 # Keep our eyes peeled.
506 threads << Thread.new do
507 10.times do
508 sleep 0.05
509 Developer.transaction do
510 # Always expect original salary.
511 assert_equal original_salary, Developer.find(1).salary
512 end
513 end
514 end
515
516 threads.each { |t| t.join }
517 end
518
519 assert_equal original_salary, Developer.find(1).salary
520 end
521 end
522 end