with_advisory_lock 4.6.0 → 5.1.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 +4 -4
- data/.github/workflows/ci.yml +80 -0
- data/.github/workflows/release.yml +20 -0
- data/.gitignore +2 -0
- data/.release-please-manifest.json +1 -0
- data/.tool-versions +1 -1
- data/Appraisals +34 -18
- data/CHANGELOG.md +31 -0
- data/Gemfile +0 -12
- data/README.md +17 -6
- data/gemfiles/{activerecord_6.0.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 +17 -3
- data/lib/with_advisory_lock/concern.rb +13 -17
- data/lib/with_advisory_lock/database_adapter_support.rb +4 -41
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +9 -0
- data/lib/with_advisory_lock/flock.rb +4 -3
- data/lib/with_advisory_lock/mysql.rb +5 -5
- data/lib/with_advisory_lock/postgresql.rb +9 -7
- data/lib/with_advisory_lock/version.rb +3 -1
- data/lib/with_advisory_lock.rb +8 -10
- data/release-please-config.json +9 -0
- data/test/concern_test.rb +23 -10
- data/test/lock_test.rb +61 -28
- data/test/nesting_test.rb +14 -79
- 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 +29 -23
- metadata +32 -28
- data/.travis.yml +0 -38
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/gemfiles/activerecord_5.1.gemfile +0 -19
- data/lib/with_advisory_lock/mysql_no_nesting.rb +0 -20
- 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
data/lib/with_advisory_lock.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
require 'with_advisory_lock/version'
|
2
2
|
require 'active_support'
|
3
|
+
require 'zeitwerk'
|
3
4
|
|
4
|
-
|
5
|
-
|
5
|
+
loader = Zeitwerk::Loader.for_gem
|
6
|
+
loader.inflector.inflect(
|
7
|
+
'mysql' => 'MySQL',
|
8
|
+
'postgresql' => 'PostgreSQL',
|
9
|
+
)
|
10
|
+
loader.setup
|
6
11
|
|
7
|
-
|
8
|
-
autoload :Base
|
9
|
-
autoload :DatabaseAdapterSupport
|
10
|
-
autoload :Flock
|
11
|
-
autoload :MySQL, 'with_advisory_lock/mysql'
|
12
|
-
autoload :MySQLNoNesting, 'with_advisory_lock/mysql_no_nesting'
|
13
|
-
autoload :NestedAdvisoryLockError
|
14
|
-
autoload :PostgreSQL, 'with_advisory_lock/postgresql'
|
12
|
+
module WithAdvisoryLock
|
15
13
|
end
|
16
14
|
|
17
15
|
ActiveSupport.on_load :active_record do
|
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,93 +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 < 5.7.5 when acquiring nested lock" do
|
29
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
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 "does not raise errors with MySQL < 5.7.5 when acquiring nested error force enabled" do
|
40
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
41
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
42
|
-
impl.lock_stack.must_be_empty
|
43
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: true) do
|
44
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
45
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: true) do
|
46
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
47
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: true) 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", force_nested_lock_support: true) 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
|
59
|
-
end
|
60
|
-
|
61
|
-
it "supports nested advisory locks with !MySQL 5.6" do
|
62
|
-
skip if env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
63
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
64
|
-
impl.lock_stack.must_be_empty
|
65
|
-
Tag.with_advisory_lock("first") do
|
66
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
67
|
-
Tag.with_advisory_lock("second") do
|
68
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
69
|
-
Tag.with_advisory_lock("first") do
|
70
|
-
# Shouldn't ask for another lock:
|
71
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
72
|
-
Tag.with_advisory_lock("second") do
|
73
|
-
# Shouldn't ask for another lock:
|
74
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
79
|
-
end
|
80
|
-
impl.lock_stack.must_be_empty
|
81
|
-
end
|
82
|
-
|
83
|
-
it "raises with !MySQL 5.6 and nested error force disabled" do
|
84
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
85
|
-
exc = proc {
|
86
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: false) do
|
87
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: false) do
|
88
|
-
end
|
89
|
-
end
|
90
|
-
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
91
|
-
exc.lock_stack.map(&:name).must_equal %w(first)
|
26
|
+
assert_empty(impl.lock_stack)
|
92
27
|
end
|
93
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
|
data/test/parallelism_test.rb
CHANGED
@@ -1,35 +1,37 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
2
4
|
require 'forwardable'
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
6
|
+
class FindOrCreateWorker
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def initialize(name, use_advisory_lock)
|
11
|
+
@name = name
|
12
|
+
@use_advisory_lock = use_advisory_lock
|
13
|
+
@thread = Thread.new { work_later }
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
16
|
+
def work_later
|
17
|
+
sleep
|
18
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
19
|
+
if @use_advisory_lock
|
20
|
+
Tag.with_advisory_lock(@name) { work }
|
21
|
+
else
|
22
|
+
work
|
23
23
|
end
|
24
24
|
end
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
27
|
+
def work
|
28
|
+
Tag.transaction do
|
29
|
+
Tag.where(name: @name).first_or_create
|
30
30
|
end
|
31
31
|
end
|
32
|
+
end
|
32
33
|
|
34
|
+
class ParallelismTest < GemTestCase
|
33
35
|
def run_workers
|
34
36
|
@names = @iterations.times.map { |iter| "iteration ##{iter}" }
|
35
37
|
@names.each do |name|
|
@@ -37,9 +39,7 @@ describe 'parallelism' do
|
|
37
39
|
FindOrCreateWorker.new(name, @use_advisory_lock)
|
38
40
|
end
|
39
41
|
# Wait for all the threads to get ready:
|
40
|
-
until workers.all? { |ea| ea.status == 'sleep' }
|
41
|
-
sleep(0.1)
|
42
|
-
end
|
42
|
+
sleep(0.1) until workers.all? { |ea| ea.status == 'sleep' }
|
43
43
|
# OK, GO!
|
44
44
|
workers.each(&:wakeup)
|
45
45
|
# Then wait for them to finish:
|
@@ -49,29 +49,27 @@ describe 'parallelism' do
|
|
49
49
|
ActiveRecord::Base.connection_pool.connection
|
50
50
|
end
|
51
51
|
|
52
|
-
|
52
|
+
setup do
|
53
53
|
ActiveRecord::Base.connection.reconnect!
|
54
54
|
@workers = 10
|
55
55
|
end
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
it 'creates multiple duplicate rows without advisory locks' do
|
60
|
-
skip if env_db == :sqlite
|
57
|
+
test 'creates multiple duplicate rows without advisory locks' do
|
58
|
+
skip if %i[sqlite3 jdbcsqlite3].include?(env_db)
|
61
59
|
@use_advisory_lock = false
|
62
60
|
@iterations = 1
|
63
61
|
run_workers
|
64
|
-
Tag.all.size
|
65
|
-
TagAudit.all.size
|
66
|
-
Label.all.size
|
62
|
+
assert_operator(Tag.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
|
63
|
+
assert_operator(TagAudit.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
|
64
|
+
assert_operator(Label.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
|
67
65
|
end
|
68
66
|
|
69
|
-
|
67
|
+
test "doesn't create multiple duplicate rows with advisory locks" do
|
70
68
|
@use_advisory_lock = true
|
71
69
|
@iterations = 10
|
72
70
|
run_workers
|
73
|
-
Tag.all.size
|
74
|
-
TagAudit.all.size
|
75
|
-
Label.all.size
|
71
|
+
assert_equal(@iterations, Tag.all.size) # <- any duplicated rows will NOT make me happy.
|
72
|
+
assert_equal(@iterations, TagAudit.all.size) # <- any duplicated rows will NOT make me happy.
|
73
|
+
assert_equal(@iterations, Label.all.size) # <- any duplicated rows will NOT make me happy.
|
76
74
|
end
|
77
75
|
end
|