Merged updates from trunk into stable branch
[feedcatcher.git] / vendor / rails / activerecord / test / cases / locking_test.rb
1 require "cases/helper"
2 require 'models/person'
3 require 'models/reader'
4 require 'models/legacy_thing'
5 require 'models/reference'
6
7 class LockWithoutDefault < ActiveRecord::Base; end
8
9 class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
10 set_table_name :lock_without_defaults_cust
11 set_locking_column :custom_lock_version
12 end
13
14 class ReadonlyFirstNamePerson < Person
15 attr_readonly :first_name
16 end
17
18 class OptimisticLockingTest < ActiveRecord::TestCase
19 fixtures :people, :legacy_things, :references
20
21 # need to disable transactional fixtures, because otherwise the sqlite3
22 # adapter (at least) chokes when we try and change the schema in the middle
23 # of a test (see test_increment_counter_*).
24 self.use_transactional_fixtures = false
25
26 def test_lock_existing
27 p1 = Person.find(1)
28 p2 = Person.find(1)
29 assert_equal 0, p1.lock_version
30 assert_equal 0, p2.lock_version
31
32 p1.first_name = 'stu'
33 p1.save!
34 assert_equal 1, p1.lock_version
35 assert_equal 0, p2.lock_version
36
37 p2.first_name = 'sue'
38 assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
39 end
40
41 def test_lock_destroy
42 p1 = Person.find(1)
43 p2 = Person.find(1)
44 assert_equal 0, p1.lock_version
45 assert_equal 0, p2.lock_version
46
47 p1.first_name = 'stu'
48 p1.save!
49 assert_equal 1, p1.lock_version
50 assert_equal 0, p2.lock_version
51
52 assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
53
54 assert p1.destroy
55 assert_equal true, p1.frozen?
56 assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
57 end
58
59 def test_lock_repeating
60 p1 = Person.find(1)
61 p2 = Person.find(1)
62 assert_equal 0, p1.lock_version
63 assert_equal 0, p2.lock_version
64
65 p1.first_name = 'stu'
66 p1.save!
67 assert_equal 1, p1.lock_version
68 assert_equal 0, p2.lock_version
69
70 p2.first_name = 'sue'
71 assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
72 p2.first_name = 'sue2'
73 assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
74 end
75
76 def test_lock_new
77 p1 = Person.new(:first_name => 'anika')
78 assert_equal 0, p1.lock_version
79
80 p1.first_name = 'anika2'
81 p1.save!
82 p2 = Person.find(p1.id)
83 assert_equal 0, p1.lock_version
84 assert_equal 0, p2.lock_version
85
86 p1.first_name = 'anika3'
87 p1.save!
88 assert_equal 1, p1.lock_version
89 assert_equal 0, p2.lock_version
90
91 p2.first_name = 'sue'
92 assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
93 end
94
95 def test_lock_new_with_nil
96 p1 = Person.new(:first_name => 'anika')
97 p1.save!
98 p1.lock_version = nil # simulate bad fixture or column with no default
99 p1.save!
100 assert_equal 1, p1.lock_version
101 end
102
103
104 def test_lock_column_name_existing
105 t1 = LegacyThing.find(1)
106 t2 = LegacyThing.find(1)
107 assert_equal 0, t1.version
108 assert_equal 0, t2.version
109
110 t1.tps_report_number = 700
111 t1.save!
112 assert_equal 1, t1.version
113 assert_equal 0, t2.version
114
115 t2.tps_report_number = 800
116 assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
117 end
118
119 def test_lock_column_is_mass_assignable
120 p1 = Person.create(:first_name => 'bianca')
121 assert_equal 0, p1.lock_version
122 assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
123
124 p1.first_name = 'bianca2'
125 p1.save!
126 assert_equal 1, p1.lock_version
127 assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
128 end
129
130 def test_lock_without_default_sets_version_to_zero
131 t1 = LockWithoutDefault.new
132 assert_equal 0, t1.lock_version
133 end
134
135 def test_lock_with_custom_column_without_default_sets_version_to_zero
136 t1 = LockWithCustomColumnWithoutDefault.new
137 assert_equal 0, t1.custom_lock_version
138 end
139
140 def test_readonly_attributes
141 assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes
142
143 p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name")
144 p.reload
145 assert_equal "unchangeable name", p.first_name
146
147 p.update_attributes(:first_name => "changed name")
148 p.reload
149 assert_equal "unchangeable name", p.first_name
150 end
151
152 { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
153 define_method("test_increment_counter_updates_#{name}") do
154 counter_test model, 1 do |id|
155 model.increment_counter :test_count, id
156 end
157 end
158
159 define_method("test_decrement_counter_updates_#{name}") do
160 counter_test model, -1 do |id|
161 model.decrement_counter :test_count, id
162 end
163 end
164
165 define_method("test_update_counters_updates_#{name}") do
166 counter_test model, 1 do |id|
167 model.update_counters id, :test_count => 1
168 end
169 end
170 end
171
172 def test_quote_table_name
173 ref = references(:michael_magician)
174 ref.favourite = !ref.favourite
175 assert ref.save
176 end
177
178 # Useful for partial updates, don't only update the lock_version if there
179 # is nothing else being updated.
180 def test_update_without_attributes_does_not_only_update_lock_version
181 assert_nothing_raised do
182 p1 = Person.new(:first_name => 'anika')
183 p1.send(:update_with_lock, [])
184 end
185 end
186
187 private
188
189 def add_counter_column_to(model)
190 model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
191 model.reset_column_information
192 # OpenBase does not set a value to existing rows when adding a not null default column
193 model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
194 end
195
196 def remove_counter_column_from(model)
197 model.connection.remove_column model.table_name, :test_count
198 model.reset_column_information
199 end
200
201 def counter_test(model, expected_count)
202 add_counter_column_to(model)
203 object = model.find(:first)
204 assert_equal 0, object.test_count
205 assert_equal 0, object.send(model.locking_column)
206 yield object.id
207 object.reload
208 assert_equal expected_count, object.test_count
209 assert_equal 1, object.send(model.locking_column)
210 ensure
211 remove_counter_column_from(model)
212 end
213 end
214
215
216 # TODO: test against the generated SQL since testing locking behavior itself
217 # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
218 # blocks, so separate script called by Kernel#system is needed.
219 # (See exec vs. async_exec in the PostgreSQL adapter.)
220
221 # TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
222
223 unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
224 class PessimisticLockingTest < ActiveRecord::TestCase
225 self.use_transactional_fixtures = false
226 fixtures :people, :readers
227
228 def setup
229 # Avoid introspection queries during tests.
230 Person.columns; Reader.columns
231 end
232
233 # Test typical find.
234 def test_sane_find_with_lock
235 assert_nothing_raised do
236 Person.transaction do
237 Person.find 1, :lock => true
238 end
239 end
240 end
241
242 # Test scoped lock.
243 def test_sane_find_with_scoped_lock
244 assert_nothing_raised do
245 Person.transaction do
246 Person.with_scope(:find => { :lock => true }) do
247 Person.find 1
248 end
249 end
250 end
251 end
252
253 # PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
254 unless current_adapter?(:PostgreSQLAdapter)
255 # Test locked eager find.
256 def test_eager_find_with_lock
257 assert_nothing_raised do
258 Person.transaction do
259 Person.find 1, :include => :readers, :lock => true
260 end
261 end
262 end
263 end
264
265 # Locking a record reloads it.
266 def test_sane_lock_method
267 assert_nothing_raised do
268 Person.transaction do
269 person = Person.find 1
270 old, person.first_name = person.first_name, 'fooman'
271 person.lock!
272 assert_equal old, person.first_name
273 end
274 end
275 end
276
277 if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
278 use_concurrent_connections
279
280 def test_no_locks_no_wait
281 first, second = duel { Person.find 1 }
282 assert first.end > second.end
283 end
284
285 def test_second_lock_waits
286 assert [0.2, 1, 5].any? { |zzz|
287 first, second = duel(zzz) { Person.find 1, :lock => true }
288 second.end > first.end
289 }
290 end
291
292 protected
293 def duel(zzz = 5)
294 t0, t1, t2, t3 = nil, nil, nil, nil
295
296 a = Thread.new do
297 t0 = Time.now
298 Person.transaction do
299 yield
300 sleep zzz # block thread 2 for zzz seconds
301 end
302 t1 = Time.now
303 end
304
305 b = Thread.new do
306 sleep zzz / 2.0 # ensure thread 1 tx starts first
307 t2 = Time.now
308 Person.transaction { yield }
309 t3 = Time.now
310 end
311
312 a.join
313 b.join
314
315 assert t1 > t0 + zzz
316 assert t2 > t0
317 assert t3 > t2
318 [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
319 end
320 end
321 end
322 end