aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/locking_test.rb
diff options
context:
space:
mode:
authorJeremy Kemper <jeremy@bitsweat.net>2008-01-18 07:30:42 +0000
committerJeremy Kemper <jeremy@bitsweat.net>2008-01-18 07:30:42 +0000
commit42b39ae3f2991692672364d7e09b1e4002e66261 (patch)
treecddaf1eb2dbf7be27430bde882432db3b1cc0407 /activerecord/test/cases/locking_test.rb
parent105a27f39ee9dbfd7fdb2b25e5ba38b00708b66c (diff)
downloadrails-42b39ae3f2991692672364d7e09b1e4002e66261.tar.gz
rails-42b39ae3f2991692672364d7e09b1e4002e66261.tar.bz2
rails-42b39ae3f2991692672364d7e09b1e4002e66261.zip
Move tests to cases
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8660 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/test/cases/locking_test.rb')
-rw-r--r--activerecord/test/cases/locking_test.rb282
1 files changed, 282 insertions, 0 deletions
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
new file mode 100644
index 0000000000..da2b594c5d
--- /dev/null
+++ b/activerecord/test/cases/locking_test.rb
@@ -0,0 +1,282 @@
+require 'abstract_unit'
+require 'fixtures/person'
+require 'fixtures/reader'
+require 'fixtures/legacy_thing'
+
+class LockWithoutDefault < ActiveRecord::Base; end
+
+class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
+ set_table_name :lock_without_defaults_cust
+ set_locking_column :custom_lock_version
+end
+
+class ReadonlyFirstNamePerson < Person
+ attr_readonly :first_name
+end
+
+class OptimisticLockingTest < ActiveSupport::TestCase
+ fixtures :people, :legacy_things
+
+ # need to disable transactional fixtures, because otherwise the sqlite3
+ # adapter (at least) chokes when we try and change the schema in the middle
+ # of a test (see test_increment_counter_*).
+ self.use_transactional_fixtures = false
+
+ def test_lock_existing
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_repeating
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_new
+ p1 = Person.new(:first_name => 'anika')
+ assert_equal 0, p1.lock_version
+
+ p1.save!
+ p2 = Person.find(p1.id)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_new_with_nil
+ p1 = Person.new(:first_name => 'anika')
+ p1.save!
+ p1.lock_version = nil # simulate bad fixture or column with no default
+ p1.save!
+ assert_equal 1, p1.lock_version
+ end
+
+
+ def test_lock_column_name_existing
+ t1 = LegacyThing.find(1)
+ t2 = LegacyThing.find(1)
+ assert_equal 0, t1.version
+ assert_equal 0, t2.version
+
+ t1.save!
+ assert_equal 1, t1.version
+ assert_equal 0, t2.version
+
+ assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
+ end
+
+ def test_lock_column_is_mass_assignable
+ p1 = Person.create(:first_name => 'bianca')
+ assert_equal 0, p1.lock_version
+ assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
+
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
+ end
+
+ def test_lock_without_default_sets_version_to_zero
+ t1 = LockWithoutDefault.new
+ assert_equal 0, t1.lock_version
+ end
+
+ def test_lock_with_custom_column_without_default_sets_version_to_zero
+ t1 = LockWithCustomColumnWithoutDefault.new
+ assert_equal 0, t1.custom_lock_version
+ end
+
+ def test_readonly_attributes
+ assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes
+
+ p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name")
+ p.reload
+ assert_equal "unchangeable name", p.first_name
+
+ p.update_attributes(:first_name => "changed name")
+ p.reload
+ assert_equal "unchangeable name", p.first_name
+ end
+
+ { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
+ define_method("test_increment_counter_updates_#{name}") do
+ counter_test model, 1 do |id|
+ model.increment_counter :test_count, id
+ end
+ end
+
+ define_method("test_decrement_counter_updates_#{name}") do
+ counter_test model, -1 do |id|
+ model.decrement_counter :test_count, id
+ end
+ end
+
+ define_method("test_update_counters_updates_#{name}") do
+ counter_test model, 1 do |id|
+ model.update_counters id, :test_count => 1
+ end
+ end
+ end
+
+ private
+
+ def add_counter_column_to(model)
+ model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
+ model.reset_column_information
+ # OpenBase does not set a value to existing rows when adding a not null default column
+ model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
+ end
+
+ def remove_counter_column_from(model)
+ model.connection.remove_column model.table_name, :test_count
+ model.reset_column_information
+ end
+
+ def counter_test(model, expected_count)
+ add_counter_column_to(model)
+ object = model.find(:first)
+ assert_equal 0, object.test_count
+ assert_equal 0, object.send(model.locking_column)
+ yield object.id
+ object.reload
+ assert_equal expected_count, object.test_count
+ assert_equal 1, object.send(model.locking_column)
+ ensure
+ remove_counter_column_from(model)
+ end
+end
+
+
+# TODO: test against the generated SQL since testing locking behavior itself
+# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
+# blocks, so separate script called by Kernel#system is needed.
+# (See exec vs. async_exec in the PostgreSQL adapter.)
+
+# TODO: The SQL Server, Sybase, and OpenBase adapters currently have no support for pessimistic locking
+
+unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
+ class PessimisticLockingTest < ActiveSupport::TestCase
+ self.use_transactional_fixtures = false
+ fixtures :people, :readers
+
+ def setup
+ # Avoid introspection queries during tests.
+ Person.columns; Reader.columns
+
+ @allow_concurrency = ActiveRecord::Base.allow_concurrency
+ ActiveRecord::Base.allow_concurrency = true
+ end
+
+ def teardown
+ ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ end
+
+ # Test typical find.
+ def test_sane_find_with_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.find 1, :lock => true
+ end
+ end
+ end
+
+ # Test scoped lock.
+ def test_sane_find_with_scoped_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.with_scope(:find => { :lock => true }) do
+ Person.find 1
+ end
+ end
+ end
+ end
+
+ # PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
+ unless current_adapter?(:PostgreSQLAdapter)
+ # Test locked eager find.
+ def test_eager_find_with_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.find 1, :include => :readers, :lock => true
+ end
+ end
+ end
+ end
+
+ # Locking a record reloads it.
+ def test_sane_lock_method
+ assert_nothing_raised do
+ Person.transaction do
+ person = Person.find 1
+ old, person.first_name = person.first_name, 'fooman'
+ person.lock!
+ assert_equal old, person.first_name
+ end
+ end
+ end
+
+ if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
+ def test_no_locks_no_wait
+ first, second = duel { Person.find 1 }
+ assert first.end > second.end
+ end
+
+ def test_second_lock_waits
+ assert [0.2, 1, 5].any? { |zzz|
+ first, second = duel(zzz) { Person.find 1, :lock => true }
+ second.end > first.end
+ }
+ end
+
+ protected
+ def duel(zzz = 5)
+ t0, t1, t2, t3 = nil, nil, nil, nil
+
+ a = Thread.new do
+ t0 = Time.now
+ Person.transaction do
+ yield
+ sleep zzz # block thread 2 for zzz seconds
+ end
+ t1 = Time.now
+ end
+
+ b = Thread.new do
+ sleep zzz / 2.0 # ensure thread 1 tx starts first
+ t2 = Time.now
+ Person.transaction { yield }
+ t3 = Time.now
+ end
+
+ a.join
+ b.join
+
+ assert t1 > t0 + zzz
+ assert t2 > t0
+ assert t3 > t2
+ [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
+ end
+ end
+ end
+end