Froze rails gems
[depot.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 uses_mocha 'mocking connection.commit_db_transaction' do
217 def test_rollback_when_commit_raises
218 Topic.connection.expects(:begin_db_transaction)
219 Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
220 Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
221 Topic.connection.expects(:rollback_db_transaction)
222
223 assert_raise RuntimeError do
224 Topic.transaction do
225 # do nothing
226 end
227 end
228 end
229 end
230
231 def test_sqlite_add_column_in_transaction_raises_statement_invalid
232 return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
233
234 # Test first if column creation/deletion works correctly when no
235 # transaction is in place.
236 #
237 # We go back to the connection for the column queries because
238 # Topic.columns is cached and won't report changes to the DB
239
240 assert_nothing_raised do
241 Topic.reset_column_information
242 Topic.connection.add_column('topics', 'stuff', :string)
243 assert Topic.column_names.include?('stuff')
244
245 Topic.reset_column_information
246 Topic.connection.remove_column('topics', 'stuff')
247 assert !Topic.column_names.include?('stuff')
248 end
249
250 # Test now inside a transaction: add_column should raise a StatementInvalid
251 Topic.transaction do
252 assert_raises(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
253 raise ActiveRecord::Rollback
254 end
255 end
256
257 private
258 def add_exception_raising_after_save_callback_to_topic
259 Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
260 end
261
262 def remove_exception_raising_after_save_callback_to_topic
263 Topic.class_eval { remove_method :after_save }
264 end
265
266 def add_exception_raising_after_create_callback_to_topic
267 Topic.class_eval { def after_create() raise "Make the transaction rollback" end }
268 end
269
270 def remove_exception_raising_after_create_callback_to_topic
271 Topic.class_eval { remove_method :after_create }
272 end
273
274 %w(validation save destroy).each do |filter|
275 define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
276 Topic.class_eval "def before_#{filter}() Book.create; false end"
277 end
278
279 define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
280 Topic.class_eval "remove_method :before_#{filter}"
281 end
282 end
283 end
284
285 if current_adapter?(:PostgreSQLAdapter)
286 class ConcurrentTransactionTest < TransactionTest
287 use_concurrent_connections
288
289 # This will cause transactions to overlap and fail unless they are performed on
290 # separate database connections.
291 def test_transaction_per_thread
292 assert_nothing_raised do
293 threads = (1..3).map do
294 Thread.new do
295 Topic.transaction do
296 topic = Topic.find(1)
297 topic.approved = !topic.approved?
298 topic.save!
299 topic.approved = !topic.approved?
300 topic.save!
301 end
302 end
303 end
304
305 threads.each { |t| t.join }
306 end
307 end
308
309 # Test for dirty reads among simultaneous transactions.
310 def test_transaction_isolation__read_committed
311 # Should be invariant.
312 original_salary = Developer.find(1).salary
313 temporary_salary = 200000
314
315 assert_nothing_raised do
316 threads = (1..3).map do
317 Thread.new do
318 Developer.transaction do
319 # Expect original salary.
320 dev = Developer.find(1)
321 assert_equal original_salary, dev.salary
322
323 dev.salary = temporary_salary
324 dev.save!
325
326 # Expect temporary salary.
327 dev = Developer.find(1)
328 assert_equal temporary_salary, dev.salary
329
330 dev.salary = original_salary
331 dev.save!
332
333 # Expect original salary.
334 dev = Developer.find(1)
335 assert_equal original_salary, dev.salary
336 end
337 end
338 end
339
340 # Keep our eyes peeled.
341 threads << Thread.new do
342 10.times do
343 sleep 0.05
344 Developer.transaction do
345 # Always expect original salary.
346 assert_equal original_salary, Developer.find(1).salary
347 end
348 end
349 end
350
351 threads.each { |t| t.join }
352 end
353
354 assert_equal original_salary, Developer.find(1).salary
355 end
356 end
357 end