2
require 'models/person'
3
require 'models/reader'
4
require 'models/legacy_thing'
5
require 'models/reference'
7
class LockWithoutDefault < ActiveRecord::Base; end
9
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
10
set_table_name :lock_without_defaults_cust
11
set_locking_column :custom_lock_version
14
class ReadonlyFirstNamePerson < Person
15
attr_readonly :first_name
18
class OptimisticLockingTest < ActiveRecord::TestCase
19
fixtures :people, :legacy_things, :references
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
26
def test_lock_existing
29
assert_equal 0, p1.lock_version
30
assert_equal 0, p2.lock_version
34
assert_equal 1, p1.lock_version
35
assert_equal 0, p2.lock_version
38
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
41
def test_lock_repeating
44
assert_equal 0, p1.lock_version
45
assert_equal 0, p2.lock_version
49
assert_equal 1, p1.lock_version
50
assert_equal 0, p2.lock_version
53
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
54
p2.first_name = 'sue2'
55
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
59
p1 = Person.new(:first_name => 'anika')
60
assert_equal 0, p1.lock_version
62
p1.first_name = 'anika2'
64
p2 = Person.find(p1.id)
65
assert_equal 0, p1.lock_version
66
assert_equal 0, p2.lock_version
68
p1.first_name = 'anika3'
70
assert_equal 1, p1.lock_version
71
assert_equal 0, p2.lock_version
74
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
77
def test_lock_new_with_nil
78
p1 = Person.new(:first_name => 'anika')
80
p1.lock_version = nil # simulate bad fixture or column with no default
82
assert_equal 1, p1.lock_version
86
def test_lock_column_name_existing
87
t1 = LegacyThing.find(1)
88
t2 = LegacyThing.find(1)
89
assert_equal 0, t1.version
90
assert_equal 0, t2.version
92
t1.tps_report_number = 700
94
assert_equal 1, t1.version
95
assert_equal 0, t2.version
97
t2.tps_report_number = 800
98
assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
101
def test_lock_column_is_mass_assignable
102
p1 = Person.create(:first_name => 'bianca')
103
assert_equal 0, p1.lock_version
104
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
106
p1.first_name = 'bianca2'
108
assert_equal 1, p1.lock_version
109
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
112
def test_lock_without_default_sets_version_to_zero
113
t1 = LockWithoutDefault.new
114
assert_equal 0, t1.lock_version
117
def test_lock_with_custom_column_without_default_sets_version_to_zero
118
t1 = LockWithCustomColumnWithoutDefault.new
119
assert_equal 0, t1.custom_lock_version
122
def test_readonly_attributes
123
assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes
125
p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name")
127
assert_equal "unchangeable name", p.first_name
129
p.update_attributes(:first_name => "changed name")
131
assert_equal "unchangeable name", p.first_name
134
{ :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
135
define_method("test_increment_counter_updates_#{name}") do
136
counter_test model, 1 do |id|
137
model.increment_counter :test_count, id
141
define_method("test_decrement_counter_updates_#{name}") do
142
counter_test model, -1 do |id|
143
model.decrement_counter :test_count, id
147
define_method("test_update_counters_updates_#{name}") do
148
counter_test model, 1 do |id|
149
model.update_counters id, :test_count => 1
154
def test_quote_table_name
155
ref = references(:michael_magician)
156
ref.favourite = !ref.favourite
160
# Useful for partial updates, don't only update the lock_version if there
161
# is nothing else being updated.
162
def test_update_without_attributes_does_not_only_update_lock_version
163
assert_nothing_raised do
164
p1 = Person.new(:first_name => 'anika')
165
p1.send(:update_with_lock, [])
171
def add_counter_column_to(model)
172
model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
173
model.reset_column_information
174
# OpenBase does not set a value to existing rows when adding a not null default column
175
model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
178
def remove_counter_column_from(model)
179
model.connection.remove_column model.table_name, :test_count
180
model.reset_column_information
183
def counter_test(model, expected_count)
184
add_counter_column_to(model)
185
object = model.find(:first)
186
assert_equal 0, object.test_count
187
assert_equal 0, object.send(model.locking_column)
190
assert_equal expected_count, object.test_count
191
assert_equal 1, object.send(model.locking_column)
193
remove_counter_column_from(model)
198
# TODO: test against the generated SQL since testing locking behavior itself
199
# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
200
# blocks, so separate script called by Kernel#system is needed.
201
# (See exec vs. async_exec in the PostgreSQL adapter.)
203
# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
205
unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
206
class PessimisticLockingTest < ActiveRecord::TestCase
207
self.use_transactional_fixtures = false
208
fixtures :people, :readers
211
# Avoid introspection queries during tests.
212
Person.columns; Reader.columns
216
def test_sane_find_with_lock
217
assert_nothing_raised do
218
Person.transaction do
219
Person.find 1, :lock => true
225
def test_sane_find_with_scoped_lock
226
assert_nothing_raised do
227
Person.transaction do
228
Person.with_scope(:find => { :lock => true }) do
235
# PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
236
unless current_adapter?(:PostgreSQLAdapter)
237
# Test locked eager find.
238
def test_eager_find_with_lock
239
assert_nothing_raised do
240
Person.transaction do
241
Person.find 1, :include => :readers, :lock => true
247
# Locking a record reloads it.
248
def test_sane_lock_method
249
assert_nothing_raised do
250
Person.transaction do
251
person = Person.find 1
252
old, person.first_name = person.first_name, 'fooman'
254
assert_equal old, person.first_name
259
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
260
use_concurrent_connections
262
def test_no_locks_no_wait
263
first, second = duel { Person.find 1 }
264
assert first.end > second.end
267
def test_second_lock_waits
268
assert [0.2, 1, 5].any? { |zzz|
269
first, second = duel(zzz) { Person.find 1, :lock => true }
270
second.end > first.end
276
t0, t1, t2, t3 = nil, nil, nil, nil
280
Person.transaction do
282
sleep zzz # block thread 2 for zzz seconds
288
sleep zzz / 2.0 # ensure thread 1 tx starts first
290
Person.transaction { yield }
300
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]