with_advisory_lock 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +80 -0
- data/.gitignore +3 -0
- data/.tool-versions +1 -0
- data/Appraisals +37 -11
- data/CHANGELOG.md +131 -0
- data/Gemfile +0 -12
- data/README.md +85 -171
- data/gemfiles/{activerecord_5.1.gemfile → activerecord_6.1.gemfile} +4 -2
- data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +4 -2
- data/gemfiles/activerecord_7.1.gemfile +14 -0
- data/lib/with_advisory_lock/base.rb +16 -2
- data/lib/with_advisory_lock/concern.rb +13 -2
- data/lib/with_advisory_lock/database_adapter_support.rb +10 -3
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +7 -0
- data/lib/with_advisory_lock/flock.rb +4 -3
- data/lib/with_advisory_lock/mysql.rb +6 -16
- data/lib/with_advisory_lock/postgresql.rb +9 -7
- data/lib/with_advisory_lock/version.rb +3 -1
- data/lib/with_advisory_lock.rb +1 -1
- data/test/concern_test.rb +23 -10
- data/test/lock_test.rb +61 -28
- data/test/nesting_test.rb +14 -46
- data/test/options_test.rb +35 -33
- data/test/parallelism_test.rb +35 -37
- data/test/shared_test.rb +93 -90
- data/test/test_helper.rb +52 -0
- data/test/test_models.rb +9 -7
- data/test/thread_test.rb +23 -22
- data/test/transaction_test.rb +34 -36
- data/with_advisory_lock.gemspec +24 -23
- metadata +27 -41
- data/.travis.install-mysql-5.7.sh +0 -11
- data/.travis.yml +0 -32
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/lib/with_advisory_lock/nested_advisory_lock_error.rb +0 -14
- data/test/database.yml +0 -17
- data/test/minitest_helper.rb +0 -40
- data/tests.sh +0 -11
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "activerecord", "~>
|
5
|
+
gem "activerecord", "~> 7.0.0"
|
6
6
|
|
7
7
|
platforms :ruby do
|
8
|
+
gem "sqlite3"
|
8
9
|
gem "mysql2"
|
10
|
+
gem "trilogy"
|
11
|
+
gem "activerecord-trilogy-adapter"
|
9
12
|
gem "pg"
|
10
|
-
gem "sqlite3"
|
11
13
|
end
|
12
14
|
|
13
15
|
platforms :jruby do
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'zlib'
|
2
4
|
|
3
5
|
module WithAdvisoryLock
|
@@ -19,17 +21,18 @@ module WithAdvisoryLock
|
|
19
21
|
LockStackItem = Struct.new(:name, :shared)
|
20
22
|
|
21
23
|
class Base
|
22
|
-
attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction
|
24
|
+
attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction, :disable_query_cache
|
23
25
|
|
24
26
|
def initialize(connection, lock_name, options)
|
25
27
|
options = { timeout_seconds: options } unless options.respond_to?(:fetch)
|
26
|
-
options.assert_valid_keys :timeout_seconds, :shared, :transaction
|
28
|
+
options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache
|
27
29
|
|
28
30
|
@connection = connection
|
29
31
|
@lock_name = lock_name
|
30
32
|
@timeout_seconds = options.fetch(:timeout_seconds, nil)
|
31
33
|
@shared = options.fetch(:shared, false)
|
32
34
|
@transaction = options.fetch(:transaction, false)
|
35
|
+
@disable_query_cache = options.fetch(:disable_query_cache, false)
|
33
36
|
end
|
34
37
|
|
35
38
|
def lock_str
|
@@ -51,6 +54,16 @@ module WithAdvisoryLock
|
|
51
54
|
end
|
52
55
|
|
53
56
|
def with_advisory_lock_if_needed(&block)
|
57
|
+
if disable_query_cache
|
58
|
+
return lock_and_yield do
|
59
|
+
ActiveRecord::Base.uncached(&block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
lock_and_yield(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def lock_and_yield(&block)
|
54
67
|
if already_locked?
|
55
68
|
Result.new(true, yield)
|
56
69
|
elsif timeout_seconds == 0
|
@@ -75,6 +88,7 @@ module WithAdvisoryLock
|
|
75
88
|
while @timeout_seconds.nil? || Time.now < give_up_at
|
76
89
|
r = yield_with_lock(&block)
|
77
90
|
return r if r.lock_was_acquired?
|
91
|
+
|
78
92
|
# Randomizing sleep time may help reduce contention.
|
79
93
|
sleep(rand(0.05..0.15))
|
80
94
|
end
|
@@ -1,16 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/concern'
|
2
4
|
|
3
5
|
module WithAdvisoryLock
|
4
6
|
module Concern
|
5
7
|
extend ActiveSupport::Concern
|
6
|
-
delegate :with_advisory_lock, :advisory_lock_exists?, to: 'self.class'
|
8
|
+
delegate :with_advisory_lock, :with_advisory_lock!, :advisory_lock_exists?, to: 'self.class'
|
7
9
|
|
8
|
-
|
10
|
+
class_methods do
|
9
11
|
def with_advisory_lock(lock_name, options = {}, &block)
|
10
12
|
result = with_advisory_lock_result(lock_name, options, &block)
|
11
13
|
result.lock_was_acquired? ? result.result : false
|
12
14
|
end
|
13
15
|
|
16
|
+
def with_advisory_lock!(lock_name, options = {}, &block)
|
17
|
+
result = with_advisory_lock_result(lock_name, options, &block)
|
18
|
+
unless result.lock_was_acquired?
|
19
|
+
raise WithAdvisoryLock::FailedToAcquireLock, lock_name
|
20
|
+
end
|
21
|
+
|
22
|
+
result.result
|
23
|
+
end
|
24
|
+
|
14
25
|
def with_advisory_lock_result(lock_name, options = {}, &block)
|
15
26
|
impl = impl_class.new(connection, lock_name, options)
|
16
27
|
impl.with_advisory_lock_if_needed(&block)
|
@@ -1,11 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WithAdvisoryLock
|
2
4
|
class DatabaseAdapterSupport
|
5
|
+
# Caches nested lock support by MySQL reported version
|
6
|
+
@@mysql_nl_cache = {}
|
7
|
+
@@mysql_nl_cache_mutex = Mutex.new
|
8
|
+
|
3
9
|
def initialize(connection)
|
4
|
-
@
|
10
|
+
@connection = connection
|
11
|
+
@sym_name = connection.adapter_name.downcase.to_sym
|
5
12
|
end
|
6
13
|
|
7
14
|
def mysql?
|
8
|
-
%i[
|
15
|
+
%i[mysql2 trilogy].include? @sym_name
|
9
16
|
end
|
10
17
|
|
11
18
|
def postgresql?
|
@@ -13,7 +20,7 @@ module WithAdvisoryLock
|
|
13
20
|
end
|
14
21
|
|
15
22
|
def sqlite?
|
16
|
-
|
23
|
+
@sym_name == :sqlite3
|
17
24
|
end
|
18
25
|
end
|
19
26
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'fileutils'
|
2
4
|
|
3
5
|
module WithAdvisoryLock
|
@@ -19,9 +21,8 @@ module WithAdvisoryLock
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def try_lock
|
22
|
-
if transaction
|
23
|
-
|
24
|
-
end
|
24
|
+
raise ArgumentError, 'transaction level locks are not supported on SQLite' if transaction
|
25
|
+
|
25
26
|
0 == file_io.flock((shared ? File::LOCK_SH : File::LOCK_EX) | File::LOCK_NB)
|
26
27
|
end
|
27
28
|
|
@@ -1,17 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WithAdvisoryLock
|
2
4
|
class MySQL < Base
|
3
|
-
# See
|
5
|
+
# See https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
|
4
6
|
def try_lock
|
5
|
-
unless lock_stack.empty?
|
6
|
-
raise NestedAdvisoryLockError.new(
|
7
|
-
"MySQL doesn't support nested Advisory Locks",
|
8
|
-
lock_stack.dup
|
9
|
-
)
|
10
|
-
end
|
11
7
|
raise ArgumentError, 'shared locks are not supported on MySQL' if shared
|
12
|
-
if transaction
|
13
|
-
|
14
|
-
end
|
8
|
+
raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
|
9
|
+
|
15
10
|
execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)")
|
16
11
|
end
|
17
12
|
|
@@ -21,12 +16,7 @@ module WithAdvisoryLock
|
|
21
16
|
|
22
17
|
def execute_successful?(mysql_function)
|
23
18
|
sql = "SELECT #{mysql_function} AS #{unique_column_name}"
|
24
|
-
connection.select_value(sql).to_i
|
25
|
-
end
|
26
|
-
|
27
|
-
# MySQL doesn't support nested locks:
|
28
|
-
def already_locked?
|
29
|
-
lock_stack.last == lock_stack_item
|
19
|
+
connection.select_value(sql).to_i.positive?
|
30
20
|
end
|
31
21
|
|
32
22
|
# MySQL wants a string as the lock key.
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WithAdvisoryLock
|
2
4
|
class PostgreSQL < Base
|
3
5
|
# See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
@@ -8,10 +10,12 @@ module WithAdvisoryLock
|
|
8
10
|
|
9
11
|
def release_lock
|
10
12
|
return if transaction
|
13
|
+
|
11
14
|
pg_function = "pg_advisory_unlock#{shared ? '_shared' : ''}"
|
12
15
|
execute_successful?(pg_function)
|
13
16
|
rescue ActiveRecord::StatementInvalid => e
|
14
17
|
raise unless e.message =~ / ERROR: +current transaction is aborted,/
|
18
|
+
|
15
19
|
begin
|
16
20
|
connection.rollback_db_transaction
|
17
21
|
execute_successful?(pg_function)
|
@@ -21,20 +25,18 @@ module WithAdvisoryLock
|
|
21
25
|
end
|
22
26
|
|
23
27
|
def execute_successful?(pg_function)
|
24
|
-
comment = lock_name.gsub(
|
28
|
+
comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--')
|
25
29
|
sql = "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
|
26
30
|
result = connection.select_value(sql)
|
27
31
|
# MRI returns 't', jruby returns true. YAY!
|
28
|
-
|
32
|
+
['t', true].include?(result)
|
29
33
|
end
|
30
34
|
|
31
35
|
# PostgreSQL wants 2 32bit integers as the lock key.
|
32
36
|
def lock_keys
|
33
|
-
@lock_keys ||=
|
34
|
-
|
35
|
-
|
36
|
-
ea.to_i & 0x7fffffff
|
37
|
-
end
|
37
|
+
@lock_keys ||= [stable_hashcode(lock_name), ENV['WITH_ADVISORY_LOCK_PREFIX']].map do |ea|
|
38
|
+
# pg advisory args must be 31 bit ints
|
39
|
+
ea.to_i & 0x7fffffff
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
data/lib/with_advisory_lock.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'with_advisory_lock/version'
|
2
2
|
require 'active_support'
|
3
|
+
require_relative 'with_advisory_lock/failed_to_acquire_lock'
|
3
4
|
|
4
5
|
module WithAdvisoryLock
|
5
6
|
extend ActiveSupport::Autoload
|
@@ -9,7 +10,6 @@ module WithAdvisoryLock
|
|
9
10
|
autoload :DatabaseAdapterSupport
|
10
11
|
autoload :Flock
|
11
12
|
autoload :MySQL, 'with_advisory_lock/mysql'
|
12
|
-
autoload :NestedAdvisoryLockError
|
13
13
|
autoload :PostgreSQL, 'with_advisory_lock/postgresql'
|
14
14
|
end
|
15
15
|
|
data/test/concern_test.rb
CHANGED
@@ -1,20 +1,33 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class WithAdvisoryLockConcernTest < GemTestCase
|
6
|
+
test 'adds with_advisory_lock to ActiveRecord classes' do
|
7
|
+
assert_respond_to(Tag, :with_advisory_lock)
|
6
8
|
end
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
test 'adds with_advisory_lock to ActiveRecord instances' do
|
11
|
+
assert_respond_to(Label.new, :with_advisory_lock)
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
test 'adds advisory_lock_exists? to ActiveRecord classes' do
|
15
|
+
assert_respond_to(Tag, :advisory_lock_exists?)
|
14
16
|
end
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
+
test 'adds advisory_lock_exists? to ActiveRecord instances' do
|
19
|
+
assert_respond_to(Label.new, :advisory_lock_exists?)
|
18
20
|
end
|
21
|
+
end
|
19
22
|
|
23
|
+
class ActiveRecordQueryCacheTest < GemTestCase
|
24
|
+
test 'does not disable quary cache by default' do
|
25
|
+
ActiveRecord::Base.expects(:uncached).never
|
26
|
+
Tag.with_advisory_lock('lock') { Tag.first }
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'can disable ActiveRecord query cache' do
|
30
|
+
ActiveRecord::Base.expects(:uncached).once
|
31
|
+
Tag.with_advisory_lock('a-lock', disable_query_cache: true) { Tag.first }
|
32
|
+
end
|
20
33
|
end
|
data/test/lock_test.rb
CHANGED
@@ -1,47 +1,80 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
let(:lock_name) { 'test lock' }
|
3
|
+
require 'test_helper'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
class LockTest < GemTestCase
|
6
|
+
setup do
|
7
|
+
@lock_name = 'test lock'
|
8
|
+
@return_val = 1900
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'returns nil outside an advisory lock request' do
|
12
|
+
assert_nil(Tag.current_advisory_lock)
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'returns the name of the last lock acquired' do
|
16
|
+
Tag.with_advisory_lock(@lock_name) do
|
17
|
+
assert_match(/#{@lock_name}/, Tag.current_advisory_lock)
|
9
18
|
end
|
19
|
+
end
|
10
20
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
21
|
+
test 'can obtain a lock with a name that attempts to disrupt a SQL comment' do
|
22
|
+
dangerous_lock_name = 'test */ lock /*'
|
23
|
+
Tag.with_advisory_lock(dangerous_lock_name) do
|
24
|
+
assert_match(/#{Regexp.escape(dangerous_lock_name)}/, Tag.current_advisory_lock)
|
16
25
|
end
|
26
|
+
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
Tag.current_advisory_lock.must_match(/#{Regexp.escape(dangerous_lock_name)}/)
|
22
|
-
end
|
28
|
+
test 'returns false for an unacquired lock' do
|
29
|
+
refute(Tag.advisory_lock_exists?(@lock_name))
|
30
|
+
end
|
23
31
|
|
32
|
+
test 'returns true for an acquired lock' do
|
33
|
+
Tag.with_advisory_lock(@lock_name) do
|
34
|
+
assert(Tag.advisory_lock_exists?(@lock_name))
|
24
35
|
end
|
25
36
|
end
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
|
38
|
+
test 'returns block return value if lock successful' do
|
39
|
+
assert_equal(@return_val, Tag.with_advisory_lock!(@lock_name) { @return_val })
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'returns false on lock acquisition failure' do
|
43
|
+
thread_with_lock = Thread.new do
|
44
|
+
Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do
|
45
|
+
@locked_elsewhere = true
|
46
|
+
loop { sleep 0.01 }
|
47
|
+
end
|
30
48
|
end
|
31
49
|
|
32
|
-
|
33
|
-
|
34
|
-
|
50
|
+
sleep 0.01 until @locked_elsewhere
|
51
|
+
assert_not(Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) { @return_val })
|
52
|
+
|
53
|
+
thread_with_lock.kill
|
54
|
+
end
|
55
|
+
|
56
|
+
test 'raises an error on lock acquisition failure' do
|
57
|
+
thread_with_lock = Thread.new do
|
58
|
+
Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do
|
59
|
+
@locked_elsewhere = true
|
60
|
+
loop { sleep 0.01 }
|
35
61
|
end
|
36
62
|
end
|
63
|
+
|
64
|
+
sleep 0.01 until @locked_elsewhere
|
65
|
+
assert_raises(WithAdvisoryLock::FailedToAcquireLock) do
|
66
|
+
Tag.with_advisory_lock!(@lock_name, timeout_seconds: 0) { @return_val }
|
67
|
+
end
|
68
|
+
|
69
|
+
thread_with_lock.kill
|
37
70
|
end
|
38
71
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
expected
|
44
|
-
end.must_equal expected
|
72
|
+
test 'attempts the lock exactly once with no timeout' do
|
73
|
+
expected = SecureRandom.base64
|
74
|
+
actual = Tag.with_advisory_lock(@lock_name, 0) do
|
75
|
+
expected
|
45
76
|
end
|
77
|
+
|
78
|
+
assert_equal(expected, actual)
|
46
79
|
end
|
47
80
|
end
|
data/test/nesting_test.rb
CHANGED
@@ -1,60 +1,28 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class LockNestingTest < GemTestCase
|
6
|
+
setup do
|
6
7
|
@prior_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX']
|
7
8
|
ENV['WITH_ADVISORY_LOCK_PREFIX'] = nil
|
8
9
|
end
|
9
10
|
|
10
|
-
|
11
|
+
teardown do
|
11
12
|
ENV['WITH_ADVISORY_LOCK_PREFIX'] = @prior_prefix
|
12
13
|
end
|
13
14
|
|
14
|
-
|
15
|
+
test "doesn't request the same lock twice" do
|
15
16
|
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
16
|
-
impl.lock_stack
|
17
|
-
Tag.with_advisory_lock(
|
18
|
-
impl.lock_stack.map(&:name)
|
17
|
+
assert_empty(impl.lock_stack)
|
18
|
+
Tag.with_advisory_lock('first') do
|
19
|
+
assert_equal(%w[first], impl.lock_stack.map(&:name))
|
19
20
|
# Even MySQL should be OK with this:
|
20
|
-
Tag.with_advisory_lock(
|
21
|
-
impl.lock_stack.map(&:name)
|
22
|
-
end
|
23
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
24
|
-
end
|
25
|
-
impl.lock_stack.must_be_empty
|
26
|
-
end
|
27
|
-
|
28
|
-
it "raises errors with MySQL when acquiring nested lock" do
|
29
|
-
skip unless env_db == :mysql
|
30
|
-
exc = proc {
|
31
|
-
Tag.with_advisory_lock("first") do
|
32
|
-
Tag.with_advisory_lock("second") do
|
33
|
-
end
|
34
|
-
end
|
35
|
-
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
36
|
-
exc.lock_stack.map(&:name).must_equal %w(first)
|
37
|
-
end
|
38
|
-
|
39
|
-
it "supports nested advisory locks with !MySQL" do
|
40
|
-
skip if env_db == :mysql
|
41
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
42
|
-
impl.lock_stack.must_be_empty
|
43
|
-
Tag.with_advisory_lock("first") do
|
44
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
45
|
-
Tag.with_advisory_lock("second") do
|
46
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
47
|
-
Tag.with_advisory_lock("first") do
|
48
|
-
# Shouldn't ask for another lock:
|
49
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
50
|
-
Tag.with_advisory_lock("second") do
|
51
|
-
# Shouldn't ask for another lock:
|
52
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
53
|
-
end
|
54
|
-
end
|
21
|
+
Tag.with_advisory_lock('first') do
|
22
|
+
assert_equal(%w[first], impl.lock_stack.map(&:name))
|
55
23
|
end
|
56
|
-
impl.lock_stack.map(&:name)
|
24
|
+
assert_equal(%w[first], impl.lock_stack.map(&:name))
|
57
25
|
end
|
58
|
-
impl.lock_stack
|
26
|
+
assert_empty(impl.lock_stack)
|
59
27
|
end
|
60
28
|
end
|
data/test/options_test.rb
CHANGED
@@ -1,64 +1,66 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class OptionsParsingTest < GemTestCase
|
4
6
|
def parse_options(options)
|
5
7
|
WithAdvisoryLock::Base.new(mock, mock, options)
|
6
8
|
end
|
7
9
|
|
8
|
-
|
10
|
+
test 'defaults (empty hash)' do
|
9
11
|
impl = parse_options({})
|
10
|
-
impl.timeout_seconds
|
11
|
-
impl.shared
|
12
|
-
impl.transaction
|
12
|
+
assert_nil(impl.timeout_seconds)
|
13
|
+
assert_not(impl.shared)
|
14
|
+
assert_not(impl.transaction)
|
13
15
|
end
|
14
16
|
|
15
|
-
|
17
|
+
test 'nil sets timeout to nil' do
|
16
18
|
impl = parse_options(nil)
|
17
|
-
impl.timeout_seconds
|
18
|
-
impl.shared
|
19
|
-
impl.transaction
|
19
|
+
assert_nil(impl.timeout_seconds)
|
20
|
+
assert_not(impl.shared)
|
21
|
+
assert_not(impl.transaction)
|
20
22
|
end
|
21
23
|
|
22
|
-
|
24
|
+
test 'integer sets timeout to value' do
|
23
25
|
impl = parse_options(42)
|
24
|
-
impl.timeout_seconds
|
25
|
-
impl.shared
|
26
|
-
impl.transaction
|
26
|
+
assert_equal(42, impl.timeout_seconds)
|
27
|
+
assert_not(impl.shared)
|
28
|
+
assert_not(impl.transaction)
|
27
29
|
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
+
test 'hash with invalid key errors' do
|
32
|
+
assert_raises(ArgumentError) do
|
31
33
|
parse_options(foo: 42)
|
32
|
-
|
34
|
+
end
|
33
35
|
end
|
34
36
|
|
35
|
-
|
37
|
+
test 'hash with timeout_seconds sets timeout to value' do
|
36
38
|
impl = parse_options(timeout_seconds: 123)
|
37
|
-
impl.timeout_seconds
|
38
|
-
impl.shared
|
39
|
-
impl.transaction
|
39
|
+
assert_equal(123, impl.timeout_seconds)
|
40
|
+
assert_not(impl.shared)
|
41
|
+
assert_not(impl.transaction)
|
40
42
|
end
|
41
43
|
|
42
|
-
|
44
|
+
test 'hash with shared option sets shared to true' do
|
43
45
|
impl = parse_options(shared: true)
|
44
|
-
impl.timeout_seconds
|
45
|
-
impl.shared
|
46
|
-
impl.transaction
|
46
|
+
assert_nil(impl.timeout_seconds)
|
47
|
+
assert(impl.shared)
|
48
|
+
assert_not(impl.transaction)
|
47
49
|
end
|
48
50
|
|
49
|
-
|
51
|
+
test 'hash with transaction option set transaction to true' do
|
50
52
|
impl = parse_options(transaction: true)
|
51
|
-
impl.timeout_seconds
|
52
|
-
impl.shared
|
53
|
-
impl.transaction
|
53
|
+
assert_nil(impl.timeout_seconds)
|
54
|
+
assert_not(impl.shared)
|
55
|
+
assert(impl.transaction)
|
54
56
|
end
|
55
57
|
|
56
|
-
|
58
|
+
test 'hash with multiple keys sets options' do
|
57
59
|
foo = mock
|
58
60
|
bar = mock
|
59
61
|
impl = parse_options(timeout_seconds: foo, shared: bar)
|
60
|
-
impl.timeout_seconds
|
61
|
-
impl.shared
|
62
|
-
impl.transaction
|
62
|
+
assert_equal(foo, impl.timeout_seconds)
|
63
|
+
assert_equal(bar, impl.shared)
|
64
|
+
assert_not(impl.transaction)
|
63
65
|
end
|
64
66
|
end
|