4 require 'models/developer'
7 class TransactionTest
< ActiveRecord
::TestCase
8 self.use_transactional_fixtures
= false
9 fixtures
:topics, :developers
12 @first, @second = Topic
.find(1, 2).sort_by
{ |t
| t
.id
}
17 @first.approved
= true
18 @second.approved
= false
23 assert Topic
.find(1).approved
?, "First should have been approved"
24 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
27 def transaction_with_return
29 @first.approved
= true
30 @second.approved
= false
37 def test_successful_with_return
38 class << Topic
.connection
39 alias :real_commit_db_transaction :commit_db_transaction
40 def commit_db_transaction
42 real_commit_db_transaction
47 transaction_with_return
50 assert Topic
.find(1).approved
?, "First should have been approved"
51 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
53 class << Topic
.connection
54 alias :commit_db_transaction :real_commit_db_transaction rescue nil
58 def test_successful_with_instance_method
60 @first.approved
= true
61 @second.approved
= false
66 assert Topic
.find(1).approved
?, "First should have been approved"
67 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
70 def test_failing_on_exception
73 @first.approved
= true
74 @second.approved
= false
83 assert
@first.approved
?, "First should still be changed in the objects"
84 assert
!@second.approved
?, "Second should still be changed in the objects"
86 assert
!Topic
.find(1).approved
?, "First shouldn't have been approved"
87 assert Topic
.find(2).approved
?, "Second should still be approved"
90 def test_raising_exception_in_callback_rollbacks_in_save
91 add_exception_raising_after_save_callback_to_topic
94 @first.approved
= true
98 assert_equal
"Make the transaction rollback", e
.message
99 assert
!Topic
.find(1).approved
?
101 remove_exception_raising_after_save_callback_to_topic
105 def test_cancellation_from_before_destroy_rollbacks_in_destroy
106 add_cancelling_before_destroy_with_db_side_effect_to_topic
108 nbooks_before_destroy
= Book
.count
109 status
= @first.destroy
111 assert_nothing_raised(ActiveRecord
::RecordNotFound) { @first.reload
}
112 assert_equal nbooks_before_destroy
, Book
.count
114 remove_cancelling_before_destroy_with_db_side_effect_to_topic
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")
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'
127 assert_equal original_author_name
, @first.reload
.author_name
128 assert_equal nbooks_before_save
, Book
.count
130 send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
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")
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'
145 assert_equal original_author_name
, @first.reload
.author_name
146 assert_equal nbooks_before_save
, Book
.count
148 send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
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",
163 new_record_snapshot
= new_topic
.new_record
?
164 id_present
= new_topic
.has_attribute
?(Topic
.primary_key
)
165 id_snapshot
= new_topic
.id
167 # Make sure the second save gets the after_create callback called.
170 add_exception_raising_after_create_callback_to_topic
171 new_topic
.approved
= true
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
)
180 remove_exception_raising_after_create_callback_to_topic
185 def test_nested_explicit_transactions
188 @first.approved
= true
189 @second.approved
= false
195 assert Topic
.find(1).approved
?, "First should have been approved"
196 assert
!Topic
.find(2).approved
?, "Second should have been unapproved"
199 def test_manually_rolling_back_a_transaction
201 @first.approved
= true
202 @second.approved
= false
206 raise ActiveRecord
::Rollback
209 assert
@first.approved
?, "First should still be changed in the objects"
210 assert
!@second.approved
?, "Second should still be changed in the objects"
212 assert
!Topic
.find(1).approved
?, "First shouldn't have been approved"
213 assert Topic
.find(2).approved
?, "Second should still be approved"
216 def test_invalid_keys_for_transaction
217 assert_raise ArgumentError
do
218 Topic
.transaction
:nested => true do
223 def test_force_savepoint_in_nested_transaction
225 @first.approved
= true
226 @second.approved
= false
231 Topic
.transaction
:requires_new => true do
240 assert
@first.reload
.approved
?
241 assert
!@second.reload
.approved
?
242 end if Topic
.connection
.supports_savepoints
?
244 def test_no_savepoint_in_nested_transaction_without_force
246 @first.approved
= true
247 @second.approved
= false
253 @first.approved
= false
261 assert
!@first.reload
.approved
?
262 assert
!@second.reload
.approved
?
263 end if Topic
.connection
.supports_savepoints
?
265 def test_many_savepoints
267 @first.content
= "One"
271 Topic
.transaction
:requires_new => true do
272 @first.content
= "Two"
276 Topic
.transaction
:requires_new => true do
277 @first.content
= "Three"
281 Topic
.transaction
:requires_new => true do
282 @first.content
= "Four"
289 @three = @first.reload
.content
295 @two = @first.reload
.content
301 @one = @first.reload
.content
304 assert_equal
"One", @one
305 assert_equal
"Two", @two
306 assert_equal
"Three", @three
307 end if Topic
.connection
.supports_savepoints
?
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)
315 assert_raise RuntimeError
do
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
?
331 def test_rollback_wont_be_executed_if_no_transaction_active
332 assert_raise RuntimeError
do
334 Topic
.connection
.rollback_db_transaction
335 Topic
.connection
.expects(:rollback_db_transaction).never
336 raise "Rails doesn't scale!"
341 def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
344 Topic
.connection
.rollback_db_transaction
346 assert_equal
0, Topic
.connection
.open_transactions
348 assert_equal
0, Topic
.connection
.open_transactions
352 def test_sqlite_add_column_in_transaction
353 return true unless current_adapter
?(:SQLite3Adapter, :SQLiteAdapter)
355 # Test first if column creation/deletion works correctly when no
356 # transaction is in place.
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
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')
366 Topic
.reset_column_information
367 Topic
.connection
.remove_column('topics', 'stuff')
368 assert
!Topic
.column_names
.include?('stuff')
371 if Topic
.connection
.supports_ddl_transactions
?
372 assert_nothing_raised
do
373 Topic
.transaction
{ Topic
.connection
.add_column('topics', 'stuff', :string) }
377 assert_raise(ActiveRecord
::StatementInvalid) { Topic
.connection
.add_column('topics', 'stuff', :string) }
378 raise ActiveRecord
::Rollback
384 def add_exception_raising_after_save_callback_to_topic
385 Topic
.class_eval
{ def after_save() raise "Make the transaction rollback" end }
388 def remove_exception_raising_after_save_callback_to_topic
389 Topic
.class_eval
{ remove_method
:after_save }
392 def add_exception_raising_after_create_callback_to_topic
393 Topic
.class_eval
{ def after_create() raise "Make the transaction rollback" end }
396 def remove_exception_raising_after_create_callback_to_topic
397 Topic
.class_eval
{ remove_method
:after_create }
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"
405 define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
406 Topic
.class_eval
"remove_method :before_#{filter}"
411 class TransactionsWithTransactionalFixturesTest
< ActiveRecord
::TestCase
412 self.use_transactional_fixtures
= true
415 def test_automatic_savepoint_in_outer_transaction
416 @first = Topic
.find(1)
420 @first.approved
= true
425 assert
!@first.reload
.approved
?
429 def test_no_automatic_savepoint_for_inner_transaction
430 @first = Topic
.find(1)
433 @first.approved
= true
438 @first.approved
= false
446 assert
!@first.reload
.approved
?
448 end if Topic
.connection
.supports_savepoints
?
450 if current_adapter
?(:PostgreSQLAdapter)
451 class ConcurrentTransactionTest
< TransactionTest
452 use_concurrent_connections
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
461 topic
= Topic
.find(1)
462 topic
.approved
= !topic
.approved
?
464 topic
.approved
= !topic
.approved
?
470 threads
.each
{ |t
| t
.join
}
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
480 assert_nothing_raised
do
481 threads
= (1..3).map
do
483 Developer
.transaction
do
484 # Expect original salary.
485 dev
= Developer
.find(1)
486 assert_equal original_salary
, dev
.salary
488 dev
.salary
= temporary_salary
491 # Expect temporary salary.
492 dev
= Developer
.find(1)
493 assert_equal temporary_salary
, dev
.salary
495 dev
.salary
= original_salary
498 # Expect original salary.
499 dev
= Developer
.find(1)
500 assert_equal original_salary
, dev
.salary
505 # Keep our eyes peeled.
506 threads
<< Thread
.new
do
509 Developer
.transaction
do
510 # Always expect original salary.
511 assert_equal original_salary
, Developer
.find(1).salary
516 threads
.each
{ |t
| t
.join
}
519 assert_equal original_salary
, Developer
.find(1).salary