closure_tree 7.4.0 → 8.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 +4 -4
- data/.github/workflows/ci.yml +30 -56
- data/.github/workflows/ci_jruby.yml +68 -0
- data/.github/workflows/ci_truffleruby.yml +71 -0
- data/.github/workflows/release.yml +17 -0
- data/.gitignore +1 -1
- data/.release-please-manifest.json +1 -0
- data/.tool-versions +1 -0
- data/Appraisals +9 -53
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -3
- data/README.md +21 -9
- data/Rakefile +11 -16
- data/closure_tree.gemspec +16 -9
- data/lib/closure_tree/active_record_support.rb +3 -14
- data/lib/closure_tree/digraphs.rb +1 -1
- data/lib/closure_tree/finders.rb +1 -1
- data/lib/closure_tree/hash_tree.rb +1 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +3 -6
- data/lib/closure_tree/model.rb +3 -3
- data/lib/closure_tree/numeric_deterministic_ordering.rb +3 -8
- data/lib/closure_tree/support.rb +3 -7
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/migration_generator.rb +1 -4
- data/release-please-config.json +4 -0
- data/test/closure_tree/cache_invalidation_test.rb +36 -0
- data/test/closure_tree/cuisine_type_test.rb +42 -0
- data/test/closure_tree/generator_test.rb +49 -0
- data/test/closure_tree/has_closure_tree_root_test.rb +80 -0
- data/test/closure_tree/hierarchy_maintenance_test.rb +56 -0
- data/test/closure_tree/label_test.rb +674 -0
- data/test/closure_tree/metal_test.rb +59 -0
- data/test/closure_tree/model_test.rb +9 -0
- data/test/closure_tree/namespace_type_test.rb +13 -0
- data/test/closure_tree/parallel_test.rb +162 -0
- data/test/closure_tree/pool_test.rb +33 -0
- data/test/closure_tree/support_test.rb +18 -0
- data/test/closure_tree/tag_test.rb +8 -0
- data/test/closure_tree/user_test.rb +175 -0
- data/test/closure_tree/uuid_tag_test.rb +8 -0
- data/test/support/query_counter.rb +25 -0
- data/test/support/tag_examples.rb +923 -0
- data/test/test_helper.rb +99 -0
- metadata +52 -21
- data/_config.yml +0 -1
- data/tests.sh +0 -11
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
describe Metal do
|
6
|
+
describe '#find_or_create_by_path' do
|
7
|
+
def assert_correctness(grandchild)
|
8
|
+
assert(Metal, grandchild)
|
9
|
+
assert_equal 'slag', grandchild.description
|
10
|
+
child = grandchild.parent
|
11
|
+
assert(Unobtanium, child)
|
12
|
+
assert_equal 'frames', child.description
|
13
|
+
assert_equal 'child', child.value
|
14
|
+
parent = child.parent
|
15
|
+
assert(Adamantium, parent)
|
16
|
+
assert_equal 'claws', parent.description
|
17
|
+
assert_equal 'parent', parent.value
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:attr_path) do
|
21
|
+
[
|
22
|
+
{ value: 'parent', description: 'claws', metal_type: 'Adamantium' },
|
23
|
+
{ value: 'child', description: 'frames', metal_type: 'Unobtanium' },
|
24
|
+
{ value: 'grandchild', description: 'slag', metal_type: 'Metal' }
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
if false
|
29
|
+
before do
|
30
|
+
# ensure the correct root is used in find_or_create_by_path:
|
31
|
+
[Metal, Adamantium, Unobtanium].each do |metal|
|
32
|
+
metal.find_or_create_by_path(%w[parent child grandchild])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'creates children from the proper root' do
|
38
|
+
assert_correctness(Metal.find_or_create_by_path(attr_path))
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'supports STI from the base class' do
|
42
|
+
assert_correctness(Metal.find_or_create_by_path(attr_path))
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'supports STI from a subclass' do
|
46
|
+
parent = Adamantium.create!(value: 'parent', description: 'claws')
|
47
|
+
assert_correctness(parent.find_or_create_by_path(attr_path.last(2)))
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'maintains the current STI subclass if attributes are not specified' do
|
51
|
+
leaf = Unobtanium.find_or_create_by_path(%w[a b c d])
|
52
|
+
assert(Unobtanium, leaf)
|
53
|
+
assert_equal %w[c b a], leaf.ancestors.map(&:value)
|
54
|
+
leaf.ancestors.each do |anc|
|
55
|
+
assert(Unobtanium, anc)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
describe Namespace::Type do
|
6
|
+
describe 'class injection' do
|
7
|
+
it 'should build hierarchy classname correctly' do
|
8
|
+
assert_equal 'Namespace::TypeHierarchy', Namespace::Type.hierarchy_class.to_s
|
9
|
+
assert_equal 'Namespace::TypeHierarchy', Namespace::Type._ct.hierarchy_class_name
|
10
|
+
assert_equal 'TypeHierarchy', Namespace::Type._ct.short_hierarchy_class_name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
# We don't need to run the expensive parallel tests for every combination of prefix/suffix.
|
6
|
+
# Those affect SQL generation, not parallelism.
|
7
|
+
# SQLite doesn't support concurrency reliably, either.
|
8
|
+
def run_parallel_tests?
|
9
|
+
!sqlite? &&
|
10
|
+
ActiveRecord::Base.table_name_prefix.empty? &&
|
11
|
+
ActiveRecord::Base.table_name_suffix.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
def max_threads
|
15
|
+
5
|
16
|
+
end
|
17
|
+
|
18
|
+
class WorkerBase
|
19
|
+
extend Forwardable
|
20
|
+
attr_reader :name
|
21
|
+
|
22
|
+
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
23
|
+
|
24
|
+
def log(msg)
|
25
|
+
puts("#{Thread.current}: #{msg}") if ENV["VERBOSE"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(target, name)
|
29
|
+
@target = target
|
30
|
+
@name = name
|
31
|
+
@thread = Thread.new do
|
32
|
+
ActiveRecord::Base.connection_pool.with_connection { before_work } if respond_to? :before_work
|
33
|
+
log "going to sleep..."
|
34
|
+
sleep
|
35
|
+
log "woke up..."
|
36
|
+
ActiveRecord::Base.connection_pool.with_connection { work }
|
37
|
+
log "done."
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class FindOrCreateWorker < WorkerBase
|
43
|
+
def work
|
44
|
+
path = [name, :a, :b, :c]
|
45
|
+
log "making #{path}..."
|
46
|
+
t = (@target || Tag).find_or_create_by_path(path)
|
47
|
+
log "made #{t.id}, #{t.ancestry_path}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class SiblingPrependerWorker < WorkerBase
|
52
|
+
def before_work
|
53
|
+
@target.reload
|
54
|
+
@sibling = Label.new(name: SecureRandom.hex(10))
|
55
|
+
end
|
56
|
+
|
57
|
+
def work
|
58
|
+
@target.prepend_sibling @sibling
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "Concurrent creation" do
|
63
|
+
before do
|
64
|
+
@target = nil
|
65
|
+
@iterations = 5
|
66
|
+
end
|
67
|
+
|
68
|
+
def log(msg)
|
69
|
+
puts(msg) if ENV["VERBOSE"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def run_workers(worker_class = FindOrCreateWorker)
|
73
|
+
@names = @iterations.times.map { |iter| "iteration ##{iter}" }
|
74
|
+
@names.each do |name|
|
75
|
+
workers = max_threads.times.map { worker_class.new(@target, name) }
|
76
|
+
# Wait for all the threads to get ready:
|
77
|
+
while true
|
78
|
+
unready_workers = workers.select { |ea| ea.status != "sleep" }
|
79
|
+
if unready_workers.empty?
|
80
|
+
break
|
81
|
+
else
|
82
|
+
log "Not ready to wakeup: #{unready_workers.map { |ea| [ea.to_s, ea.status] }}"
|
83
|
+
sleep(0.1)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
sleep(0.25)
|
87
|
+
# OK, GO!
|
88
|
+
log "Calling .wakeup on all workers..."
|
89
|
+
workers.each(&:wakeup)
|
90
|
+
sleep(0.25)
|
91
|
+
# Then wait for them to finish:
|
92
|
+
log "Calling .join on all workers..."
|
93
|
+
workers.each(&:join)
|
94
|
+
end
|
95
|
+
# Ensure we're still connected:
|
96
|
+
ActiveRecord::Base.connection_pool.connection
|
97
|
+
end
|
98
|
+
|
99
|
+
it "will not create dupes from class methods" do
|
100
|
+
skip("unsupported") unless run_parallel_tests?
|
101
|
+
|
102
|
+
run_workers
|
103
|
+
assert_equal @names.sort, Tag.roots.collect { |ea| ea.name }.sort
|
104
|
+
# No dupe children:
|
105
|
+
%w[a b c].each do |ea|
|
106
|
+
assert_equal @iterations, Tag.where(name: ea).size
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "will not create dupes from instance methods" do
|
111
|
+
skip("unsupported") unless run_parallel_tests?
|
112
|
+
|
113
|
+
@target = Tag.create!(name: "root")
|
114
|
+
run_workers
|
115
|
+
assert_equal @names.sort, @target.reload.children.collect { |ea| ea.name }.sort
|
116
|
+
assert_equal @iterations, Tag.where(name: @names).size
|
117
|
+
%w[a b c].each do |ea|
|
118
|
+
assert_equal @iterations, Tag.where(name: ea).size
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
it "creates dupe roots without advisory locks" do
|
123
|
+
skip("unsupported") unless run_parallel_tests?
|
124
|
+
|
125
|
+
# disable with_advisory_lock:
|
126
|
+
Tag.stub(:with_advisory_lock, ->(_lock_name, &block) { block.call }) do
|
127
|
+
run_workers
|
128
|
+
# duplication from at least one iteration:
|
129
|
+
assert Tag.where(name: @names).size > @iterations
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it "fails to deadlock while simultaneously deleting items from the same hierarchy" do
|
134
|
+
skip("unsupported") unless run_parallel_tests?
|
135
|
+
|
136
|
+
target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s })
|
137
|
+
emails = target.self_and_ancestors.to_a.map(&:email).shuffle
|
138
|
+
Parallel.map(emails, in_threads: max_threads) do |email|
|
139
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
140
|
+
User.transaction do
|
141
|
+
log "Destroying #{email}..."
|
142
|
+
User.where(email: email).destroy_all
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
User.connection.reconnect!
|
147
|
+
assert User.all.empty?
|
148
|
+
end
|
149
|
+
|
150
|
+
it "fails to deadlock from prepending siblings" do
|
151
|
+
skip("unsupported") unless run_parallel_tests?
|
152
|
+
|
153
|
+
@target = Label.find_or_create_by_path %w[root parent]
|
154
|
+
run_workers(SiblingPrependerWorker)
|
155
|
+
children = Label.roots
|
156
|
+
uniq_order_values = children.collect { |ea| ea.order_value }.uniq
|
157
|
+
assert_equal uniq_order_values.size, children.size
|
158
|
+
|
159
|
+
# The only non-root node should be "root":
|
160
|
+
assert_equal([@target.parent], Label.all.select { |ea| ea.root? })
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
describe 'Configuration' do
|
6
|
+
it 'returns connection to the pool after has_closure_tree setup' do
|
7
|
+
class TypeDuplicate < ActiveRecord::Base
|
8
|
+
self.table_name = "namespace_type#{table_name_suffix}"
|
9
|
+
has_closure_tree
|
10
|
+
end
|
11
|
+
|
12
|
+
refute ActiveRecord::Base.connection_pool.active_connection?
|
13
|
+
# +false+ in AR 4, +nil+ in AR 5
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns connection to the pool after has_closure_tree setup with order' do
|
17
|
+
class MetalDuplicate < ActiveRecord::Base
|
18
|
+
self.table_name = "#{table_name_prefix}metal#{table_name_suffix}"
|
19
|
+
has_closure_tree order: 'sort_order', name_column: 'value'
|
20
|
+
end
|
21
|
+
|
22
|
+
refute ActiveRecord::Base.connection_pool.active_connection?
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns connection to the pool after has_closure_tree_root setup' do
|
26
|
+
class GroupDuplicate < ActiveRecord::Base
|
27
|
+
self.table_name = "#{table_name_prefix}group#{table_name_suffix}"
|
28
|
+
has_closure_tree_root :root_user
|
29
|
+
end
|
30
|
+
|
31
|
+
refute ActiveRecord::Base.connection_pool.active_connection?
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
describe ClosureTree::Support do
|
6
|
+
let(:sut) { Tag._ct }
|
7
|
+
|
8
|
+
it 'passes through table names without prefix and suffix' do
|
9
|
+
expected = 'some_random_table_name'
|
10
|
+
assert_equal expected, sut.remove_prefix_and_suffix(expected)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'extracts through table name with prefix and suffix' do
|
14
|
+
expected = 'some_random_table_name'
|
15
|
+
tn = ActiveRecord::Base.table_name_prefix + expected + ActiveRecord::Base.table_name_suffix
|
16
|
+
assert_equal expected, sut.remove_prefix_and_suffix(tn)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
describe "empty db" do
|
6
|
+
describe "empty db" do
|
7
|
+
it "should return no entities" do
|
8
|
+
assert User.roots.empty?
|
9
|
+
assert User.leaves.empty?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "1 user db" do
|
14
|
+
it "should return the only entity as a root and leaf" do
|
15
|
+
a = User.create!(email: "me@domain.com")
|
16
|
+
assert_equal [a], User.roots
|
17
|
+
assert_equal [a], User.leaves
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "2 user db" do
|
22
|
+
it "should return a simple root and leaf" do
|
23
|
+
root = User.create!(email: "first@t.co")
|
24
|
+
leaf = root.children.create!(email: "second@t.co")
|
25
|
+
assert_equal [root], User.roots
|
26
|
+
assert_equal [leaf], User.leaves
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "3 User collection.create db" do
|
31
|
+
before do
|
32
|
+
@root = User.create! email: "poppy@t.co"
|
33
|
+
@mid = @root.children.create! email: "matt@t.co"
|
34
|
+
@leaf = @mid.children.create! email: "james@t.co"
|
35
|
+
@root_id = @root.id
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should create all Users" do
|
39
|
+
assert_equal [@root, @mid, @leaf], User.all.to_a.sort
|
40
|
+
end
|
41
|
+
|
42
|
+
it "orders self_and_ancestor_ids nearest generation first" do
|
43
|
+
assert_equal [@leaf.id, @mid.id, @root.id], @leaf.self_and_ancestor_ids
|
44
|
+
end
|
45
|
+
|
46
|
+
it "orders self_and_descendant_ids nearest generation first" do
|
47
|
+
assert_equal [@root.id, @mid.id, @leaf.id], @root.self_and_descendant_ids
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should have children" do
|
51
|
+
assert_equal [@mid], @root.children.to_a
|
52
|
+
assert_equal [@leaf], @mid.children.to_a
|
53
|
+
assert_equal [], @leaf.children.to_a
|
54
|
+
end
|
55
|
+
|
56
|
+
it "roots should have children" do
|
57
|
+
assert_equal [@mid], User.roots.first.children.to_a
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should return a root and leaf without middle User" do
|
61
|
+
assert_equal [@root], User.roots.to_a
|
62
|
+
assert_equal [@leaf], User.leaves.to_a
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should delete leaves" do
|
66
|
+
User.leaves.destroy_all
|
67
|
+
assert_equal [@root], User.roots.to_a # untouched
|
68
|
+
assert_equal [@mid], User.leaves.to_a
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should delete roots and maintain hierarchies" do
|
72
|
+
User.roots.destroy_all
|
73
|
+
assert_mid_and_leaf_remain
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should root all children" do
|
77
|
+
@root.destroy
|
78
|
+
assert_mid_and_leaf_remain
|
79
|
+
end
|
80
|
+
|
81
|
+
def assert_mid_and_leaf_remain
|
82
|
+
assert ReferralHierarchy.where(ancestor_id: @root_id).empty?
|
83
|
+
assert ReferralHierarchy.where(descendant_id: @root_id).empty?
|
84
|
+
assert_equal %w[matt@t.co], @mid.ancestry_path
|
85
|
+
assert_equal %w[matt@t.co james@t.co], @leaf.ancestry_path
|
86
|
+
assert_equal [@mid, @leaf].sort, @mid.self_and_descendants.to_a.sort
|
87
|
+
assert_equal [@mid], User.roots
|
88
|
+
assert_equal [@leaf], User.leaves
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it "supports users with contracts" do
|
93
|
+
u = User.find_or_create_by_path(%w[a@t.co b@t.co c@t.co])
|
94
|
+
assert_equal [], u.descendant_ids
|
95
|
+
assert_equal [u.parent.id, u.root.id], u.ancestor_ids
|
96
|
+
assert_equal [u.id, u.parent.id, u.root.id], u.self_and_ancestor_ids
|
97
|
+
assert_equal [u.parent.id, u.id], u.root.descendant_ids
|
98
|
+
assert_equal [], u.root.ancestor_ids
|
99
|
+
assert_equal [u.root.id], u.root.self_and_ancestor_ids
|
100
|
+
c1 = u.contracts.create!
|
101
|
+
c2 = u.parent.contracts.create!
|
102
|
+
assert_equal [c1, c2].sort, u.root.indirect_contracts.to_a.sort
|
103
|
+
end
|
104
|
+
|
105
|
+
it "supports << on shallow unsaved hierarchies" do
|
106
|
+
a = User.new(email: "a")
|
107
|
+
b = User.new(email: "b")
|
108
|
+
a.children << b
|
109
|
+
a.save
|
110
|
+
assert_equal [a], User.roots
|
111
|
+
assert_equal [b], User.leaves
|
112
|
+
assert_equal %w[a b], b.ancestry_path
|
113
|
+
end
|
114
|
+
|
115
|
+
it "supports << on deep unsaved hierarchies" do
|
116
|
+
a = User.new(email: "a")
|
117
|
+
b1 = User.new(email: "b1")
|
118
|
+
a.children << b1
|
119
|
+
b2 = User.new(email: "b2")
|
120
|
+
a.children << b2
|
121
|
+
c1 = User.new(email: "c1")
|
122
|
+
b2.children << c1
|
123
|
+
c2 = User.new(email: "c2")
|
124
|
+
b2.children << c2
|
125
|
+
d = User.new(email: "d")
|
126
|
+
c2.children << d
|
127
|
+
|
128
|
+
a.save
|
129
|
+
assert_equal [a], User.roots.to_a
|
130
|
+
assert_equal [b1, c1, d].sort, User.leaves.to_a.sort
|
131
|
+
assert_equal %w[a b2 c2 d], d.ancestry_path
|
132
|
+
end
|
133
|
+
|
134
|
+
it "supports siblings" do
|
135
|
+
refute User._ct.order_option?
|
136
|
+
a = User.create(email: "a")
|
137
|
+
b1 = a.children.create(email: "b1")
|
138
|
+
b2 = a.children.create(email: "b2")
|
139
|
+
b3 = a.children.create(email: "b3")
|
140
|
+
assert a.siblings.empty?
|
141
|
+
assert_equal [b2, b3].sort, b1.siblings.to_a.sort
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "when a user is not yet saved" do
|
145
|
+
it "supports siblings" do
|
146
|
+
refute User._ct.order_option?
|
147
|
+
a = User.create(email: "a")
|
148
|
+
b1 = a.children.new(email: "b1")
|
149
|
+
b2 = a.children.create(email: "b2")
|
150
|
+
b3 = a.children.create(email: "b3")
|
151
|
+
assert a.siblings.empty?
|
152
|
+
assert_equal [b2, b3].sort, b1.siblings.to_a.sort
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "properly nullifies descendents" do
|
157
|
+
c = User.find_or_create_by_path %w[a b c]
|
158
|
+
b = c.parent
|
159
|
+
c.root.destroy
|
160
|
+
assert b.reload.root?
|
161
|
+
assert_equal [c.id], b.child_ids
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "roots" do
|
165
|
+
it "works on models without ordering" do
|
166
|
+
expected = ("a".."z").to_a
|
167
|
+
expected.shuffle.each do |ea|
|
168
|
+
User.create! do |u|
|
169
|
+
u.email = ea
|
170
|
+
end
|
171
|
+
end
|
172
|
+
assert_equal(expected, User.roots.collect { |ea| ea.email }.sort)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# https://stackoverflow.com/a/43810063/1683557
|
4
|
+
|
5
|
+
module QueryCounter
|
6
|
+
def sql_queries(&block)
|
7
|
+
queries = []
|
8
|
+
counter = lambda { |*, payload|
|
9
|
+
queries << payload.fetch(:sql) unless %w[CACHE SCHEMA].include?(payload.fetch(:name))
|
10
|
+
}
|
11
|
+
|
12
|
+
ActiveSupport::Notifications.subscribed(counter, "sql.active_record", &block)
|
13
|
+
|
14
|
+
queries
|
15
|
+
end
|
16
|
+
|
17
|
+
def assert_database_queries_count(expected, &block)
|
18
|
+
queries = sql_queries(&block)
|
19
|
+
assert_equal(
|
20
|
+
expected,
|
21
|
+
queries.count,
|
22
|
+
"Expected #{expected} queries, but found #{queries.count}:\n#{queries.join("\n")}"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|