closure_tree 4.6.3 → 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.
@@ -3,15 +3,10 @@ module ClosureTree
3
3
  module SupportAttributes
4
4
 
5
5
  extend Forwardable
6
- def_delegators :model_class, :connection, :transaction, :table_name
6
+ def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
7
7
 
8
- # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
9
- def base_class
10
- options[:base_class]
11
- end
12
-
13
- def attribute_names
14
- @attribute_names ||= model_class.new.attributes.keys - model_class.protected_attributes.to_a
8
+ def advisory_lock_name
9
+ "ClosureTree::#{base_class.name}"
15
10
  end
16
11
 
17
12
  def quoted_table_name
@@ -27,13 +27,12 @@ module ClosureTree
27
27
  model_class != model_class.base_class
28
28
  end
29
29
 
30
- def has_type?
31
- attribute_names.include? 'type'
30
+ def has_inheritance_column?(hash = columns_hash)
31
+ hash.with_indifferent_access.include?(model_class.inheritance_column)
32
32
  end
33
33
 
34
34
  def has_name?
35
35
  model_class.new.attributes.include? options[:name_column]
36
36
  end
37
-
38
37
  end
39
38
  end
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new('4.6.3') unless defined?(::ClosureTree::VERSION)
2
+ VERSION = Gem::Version.new('5.0.0') unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -1,5 +1,5 @@
1
1
  common: &common
2
- database: closure_tree_test<%= ENV['TRAVIS_JOB_NUMBER'].to_s.gsub(/\W/, '_') %>
2
+ database: <%= db_name %>
3
3
  host: localhost
4
4
  pool: 50
5
5
  timeout: 5000
@@ -7,8 +7,8 @@ common: &common
7
7
  min_messages: ERROR
8
8
 
9
9
  sqlite:
10
+ <<: *common
10
11
  adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
11
- database: ':memory:'
12
12
 
13
13
  postgresql:
14
14
  <<: *common
@@ -98,10 +98,16 @@ end
98
98
 
99
99
  class Metal < ActiveRecord::Base
100
100
  self.table_name = "#{table_name_prefix}metal#{table_name_suffix}"
101
- acts_as_tree :order => 'sort_order'
101
+ acts_as_tree order: 'sort_order', name_column: 'value'
102
102
  self.inheritance_column = 'metal_type'
103
103
  end
104
104
 
105
+ class Adamantium < Metal
106
+ end
107
+
108
+ class Unobtanium < Metal
109
+ end
110
+
105
111
  class MenuItem < ActiveRecord::Base
106
112
  acts_as_tree(touch: true, with_advisory_lock: false)
107
113
  end
@@ -111,6 +111,7 @@ ActiveRecord::Schema.define(:version => 0) do
111
111
  t.integer "parent_id"
112
112
  t.string "metal_type"
113
113
  t.string "value"
114
+ t.string "description"
114
115
  t.integer "sort_order"
115
116
  end
116
117
 
@@ -7,6 +7,7 @@ describe ClosureTree::HierarchyMaintenance do
7
7
  Metal.create(:value => "Nitro-#{counter}", parent: Metal.all.sample)
8
8
  end
9
9
  hierarchy_count = MetalHierarchy.count
10
+ expect(hierarchy_count).to be > (20*2)-1 # shallowest-possible case, where all children use the first root
10
11
  MetalHierarchy.delete_all
11
12
  Metal.rebuild!
12
13
  expect(MetalHierarchy.count).to eq(hierarchy_count)
@@ -42,7 +42,6 @@ def create_preorder_tree(suffix = "", &block)
42
42
  end
43
43
 
44
44
  describe Label do
45
-
46
45
  context "destruction" do
47
46
  it "properly destroys descendents created with find_or_create_by_path" do
48
47
  c = Label.find_or_create_by_path %w(a b c)
@@ -54,10 +53,8 @@ describe Label do
54
53
 
55
54
  it "properly destroys descendents created with add_child" do
56
55
  a = Label.create(name: 'a')
57
- b = Label.new(name: 'b')
58
- a.add_child b
59
- c = Label.new(name: 'c')
60
- b.add_child c
56
+ b = a.add_child Label.new(name: 'b')
57
+ c = b.add_child Label.new(name: 'c')
61
58
  a.destroy
62
59
  expect(Label.exists?(a)).to be_falsey
63
60
  expect(Label.exists?(b)).to be_falsey
@@ -292,36 +289,51 @@ describe Label do
292
289
  end
293
290
  end
294
291
 
295
- it 'behaves like the readme' do
296
- root = Label.create(name: 'root')
297
- a = root.append_child(Label.new(name: 'a'))
298
- b = Label.create(name: 'b')
299
- c = Label.create(name: 'c')
300
-
301
- a.append_sibling(b)
302
- expect(a.self_and_siblings.collect(&:name)).to eq(%w(a b))
303
- expect(root.reload.children.collect(&:name)).to eq(%w(a b))
304
- expect(root.children.collect(&:order_value)).to eq([0, 1])
305
-
306
- a.prepend_sibling(b)
307
- expect(a.self_and_siblings.collect(&:name)).to eq(%w(b a))
308
- expect(root.reload.children.collect(&:name)).to eq(%w(b a))
309
- expect(root.children.collect(&:order_value)).to eq([0, 1])
310
-
311
- a.append_sibling(c)
312
- expect(a.self_and_siblings.collect(&:name)).to eq(%w(b a c))
313
- expect(root.reload.children.collect(&:name)).to eq(%w(b a c))
314
- expect(root.children.collect(&:order_value)).to eq([0, 1, 2])
315
-
316
- # We need to reload b because it was updated by a.append_sibling(c)
317
- b.reload.append_sibling(c)
318
- expect(root.reload.children.collect(&:name)).to eq(%w(b c a))
319
- expect(root.children.collect(&:order_value)).to eq([0, 1, 2])
320
-
321
- # We need to reload a because it was updated by b.append_sibling(c)
322
- d = a.reload.append_sibling(Label.new(:name => "d"))
323
- expect(d.self_and_siblings.collect(&:name)).to eq(%w(b c a d))
324
- expect(d.self_and_siblings.collect(&:order_value)).to eq([0, 1, 2, 3])
292
+ describe 'code in the readme' do
293
+ it 'creates STI label hierarchies' do
294
+ child = Label.find_or_create_by_path([
295
+ {type: 'DateLabel', name: '2014'},
296
+ {type: 'DateLabel', name: 'August'},
297
+ {type: 'DateLabel', name: '5'},
298
+ {type: 'EventLabel', name: 'Visit the Getty Center'}
299
+ ])
300
+ expect(child).to be_a(EventLabel)
301
+ expect(child.name).to eq('Visit the Getty Center')
302
+ expect(child.ancestors.map(&:name)).to eq(%w(5 August 2014))
303
+ expect(child.ancestors.map(&:class)).to eq([DateLabel, DateLabel, DateLabel])
304
+ end
305
+
306
+ it 'appends and prepends siblings' do
307
+ root = Label.create(name: 'root')
308
+ a = root.append_child(Label.new(name: 'a'))
309
+ b = Label.create(name: 'b')
310
+ c = Label.create(name: 'c')
311
+
312
+ a.append_sibling(b)
313
+ expect(a.self_and_siblings.collect(&:name)).to eq(%w(a b))
314
+ expect(root.reload.children.collect(&:name)).to eq(%w(a b))
315
+ expect(root.children.collect(&:order_value)).to eq([0, 1])
316
+
317
+ a.prepend_sibling(b)
318
+ expect(a.self_and_siblings.collect(&:name)).to eq(%w(b a))
319
+ expect(root.reload.children.collect(&:name)).to eq(%w(b a))
320
+ expect(root.children.collect(&:order_value)).to eq([0, 1])
321
+
322
+ a.append_sibling(c)
323
+ expect(a.self_and_siblings.collect(&:name)).to eq(%w(b a c))
324
+ expect(root.reload.children.collect(&:name)).to eq(%w(b a c))
325
+ expect(root.children.collect(&:order_value)).to eq([0, 1, 2])
326
+
327
+ # We need to reload b because it was updated by a.append_sibling(c)
328
+ b.reload.append_sibling(c)
329
+ expect(root.reload.children.collect(&:name)).to eq(%w(b c a))
330
+ expect(root.children.collect(&:order_value)).to eq([0, 1, 2])
331
+
332
+ # We need to reload a because it was updated by b.append_sibling(c)
333
+ d = a.reload.append_sibling(Label.new(:name => "d"))
334
+ expect(d.self_and_siblings.collect(&:name)).to eq(%w(b c a d))
335
+ expect(d.self_and_siblings.collect(&:order_value)).to eq([0, 1, 2, 3])
336
+ end
325
337
  end
326
338
 
327
339
  # https://github.com/mceachen/closure_tree/issues/84
@@ -511,5 +523,5 @@ describe Label do
511
523
  expected += ('a'..'r').collect { |ea| "#{ea}1" }
512
524
  expect(Label.roots_and_descendants_preordered.collect { |ea| ea.name }).to eq(expected)
513
525
  end
514
- end unless ENV['DB'] == 'sqlite' # sqlite doesn't have a power function.
526
+ end unless sqlite? # sqlite doesn't have a power function.
515
527
  end
@@ -1,9 +1,55 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Metal do
4
- it "creates" do
5
- s = Metal.create(:value => 'System')
6
- s.reload
7
- expect(s).not_to be_new_record
4
+ describe '#find_or_create_by_path' do
5
+ def assert_correctness(grandchild)
6
+ expect(grandchild).to be_a(Metal)
7
+ expect(grandchild.description).to eq('slag')
8
+ child = grandchild.parent
9
+ expect(child).to be_a(Unobtanium)
10
+ expect(child.description).to eq('frames')
11
+ expect(child.value).to eq('child')
12
+ parent = child.parent
13
+ expect(parent).to be_a(Adamantium)
14
+ expect(parent.description).to eq('claws')
15
+ expect(parent.value).to eq('parent')
16
+ end
17
+
18
+ let(:attr_path) do
19
+ [
20
+ {value: 'parent', description: 'claws', metal_type: 'Adamantium'},
21
+ {value: 'child', description: 'frames', metal_type: 'Unobtanium'},
22
+ {value: 'grandchild', description: 'slag', metal_type: 'Metal'}
23
+ ]
24
+ end
25
+
26
+ before do
27
+ # ensure the correct root is used in find_or_create_by_path:
28
+ [Metal, Adamantium, Unobtanium].each do |metal|
29
+ metal.find_or_create_by_path(%w(parent child grandchild))
30
+ end
31
+ end if false
32
+
33
+ it 'creates children from the proper root' do
34
+ assert_correctness(Metal.find_or_create_by_path(attr_path))
35
+ end
36
+
37
+ it 'supports STI from the base class' do
38
+ assert_correctness(Metal.find_or_create_by_path(attr_path))
39
+ end
40
+
41
+ it 'supports STI from a subclass' do
42
+ parent = Adamantium.create!(value: 'parent', description: 'claws')
43
+ assert_correctness(parent.find_or_create_by_path(attr_path.last(2)))
44
+ end
45
+
46
+ it 'maintains the current STI subclass if attributes are not specified' do
47
+ leaf = Unobtanium.find_or_create_by_path(%w(a b c d))
48
+ expect(leaf).to be_a(Unobtanium)
49
+ expect(leaf.ancestors.map(&:value)).to eq(%w(c b a))
50
+ leaf.ancestors.each do |anc|
51
+ expect(anc).to be_a(Unobtanium)
52
+ end
53
+ end
8
54
  end
9
55
  end
@@ -1,59 +1,75 @@
1
1
  require 'spec_helper'
2
- require 'securerandom'
2
+
3
+ # We don't need to run the expensive parallel tests for every combination of prefix/suffix.
4
+ # Those affect SQL generation, not parallelism
5
+ def run_parallel_tests?
6
+ ActiveRecord::Base.table_name_prefix.empty? &&
7
+ ActiveRecord::Base.table_name_suffix.empty?
8
+ end
9
+
10
+ def max_threads
11
+ 5
12
+ end
3
13
 
4
14
  class WorkerBase
5
- def initialize(target, run_at, name)
15
+ extend Forwardable
16
+ attr_reader :name
17
+ def_delegators :@thread, :join, :wakeup, :status, :to_s
18
+
19
+ def initialize(target, name)
6
20
  @target = target
21
+ @name = name
7
22
  @thread = Thread.new do
8
- ActiveRecord::Base.connection_pool.with_connection do
9
- before_work
10
- sleep((run_at - Time.now).to_f)
11
- do_work(name)
12
- end
23
+ ActiveRecord::Base.connection_pool.with_connection { before_work } if respond_to? :before_work
24
+ puts "#{Thread.current} going to sleep..."
25
+ sleep
26
+ puts "#{Thread.current} woke up..."
27
+ ActiveRecord::Base.connection_pool.with_connection { work }
28
+ puts "#{Thread.current} done..."
13
29
  end
14
30
  end
15
-
16
- def before_work
17
- end
18
-
19
- def work(name)
20
- raise
21
- end
22
-
23
- def join
24
- @thread.join
25
- end
26
31
  end
27
32
 
28
33
  class FindOrCreateWorker < WorkerBase
29
- def do_work(name)
30
- (@target || Tag).find_or_create_by_path([name.to_s, :a, :b, :c])
34
+ def work
35
+ path = [name, :a, :b, :c]
36
+ puts "#{Thread.current} making #{path}..."
37
+ t = (@target || Tag).find_or_create_by_path(path)
38
+ puts "#{Thread.current} made #{t.id}, #{t.ancestry_path}"
31
39
  end
32
40
  end
33
41
 
34
- describe 'Concurrent creation', if: support_concurrency do
35
-
42
+ describe 'Concurrent creation' do
36
43
  before :each do
37
44
  @target = nil
38
45
  @iterations = 5
39
- @threads = 10
40
46
  end
41
47
 
42
48
  def run_workers(worker_class = FindOrCreateWorker)
43
- all_workers = []
44
49
  @names = @iterations.times.map { |iter| "iteration ##{iter}" }
45
50
  @names.each do |name|
46
- wake_time = 1.second.from_now
47
- workers = @threads.times.map do
48
- worker_class.new(@target, wake_time, name)
51
+ workers = max_threads.times.map { worker_class.new(@target, name) }
52
+ # Wait for all the threads to get ready:
53
+ while true
54
+ unready_workers = workers.select { |ea| ea.status != 'sleep' }
55
+ if unready_workers.empty?
56
+ break
57
+ else
58
+ puts "Not ready to wakeup: #{unready_workers.map { |ea| [ea.to_s, ea.status] }}"
59
+ sleep(0.1)
60
+ end
49
61
  end
62
+ sleep(0.25)
63
+ # OK, GO!
64
+ puts 'Calling .wakeup on all workers...'
65
+ workers.each(&:wakeup)
66
+ sleep(0.25)
67
+ # Then wait for them to finish:
68
+ puts 'Calling .join on all workers...'
50
69
  workers.each(&:join)
51
- all_workers += workers
52
- puts name
53
70
  end
54
71
  # Ensure we're still connected:
55
72
  ActiveRecord::Base.connection_pool.connection
56
- all_workers
57
73
  end
58
74
 
59
75
  it 'will not create dupes from class methods' do
@@ -79,8 +95,9 @@ describe 'Concurrent creation', if: support_concurrency do
79
95
  # disable with_advisory_lock:
80
96
  allow(Tag).to receive(:with_advisory_lock) { |_lock_name, &block| block.call }
81
97
  run_workers
98
+ # duplication from at least one iteration:
82
99
  expect(Tag.where(name: @names).size).to be > @iterations
83
- end
100
+ end unless sqlite? # sqlite throws errors from concurrent access
84
101
 
85
102
  class SiblingPrependerWorker < WorkerBase
86
103
  def before_work
@@ -88,11 +105,12 @@ describe 'Concurrent creation', if: support_concurrency do
88
105
  @sibling = Label.new(name: SecureRandom.hex(10))
89
106
  end
90
107
 
91
- def do_work(name)
108
+ def work
92
109
  @target.prepend_sibling @sibling
93
110
  end
94
111
  end
95
112
 
113
+ # TODO: this test should be rewritten to be proper producer-consumer code
96
114
  xit 'fails to deadlock from parallel sibling churn' do
97
115
  # target should be non-trivially long to maximize time spent in hierarchy maintenance
98
116
  target = Tag.find_or_create_by_path(('a'..'z').to_a + ('A'..'Z').to_a)
@@ -102,7 +120,7 @@ describe 'Concurrent creation', if: support_concurrency do
102
120
  children_to_delete = []
103
121
  deleted_children = []
104
122
  creator_threads = @workers.times.map do
105
- DbThread.new do
123
+ Thread.new do
106
124
  while children_to_add.present?
107
125
  name = children_to_add.shift
108
126
  unless name.nil?
@@ -115,7 +133,7 @@ describe 'Concurrent creation', if: support_concurrency do
115
133
  end
116
134
  run_destruction = true
117
135
  destroyer_threads = @workers.times.map do
118
- DbThread.new do
136
+ Thread.new do
119
137
  begin
120
138
  victim_name = children_to_delete.shift
121
139
  if victim_name
@@ -130,27 +148,26 @@ describe 'Concurrent creation', if: support_concurrency do
130
148
  end while run_destruction || !children_to_delete.empty?
131
149
  end
132
150
  end
133
- creator_threads.each { |ea| ea.join }
134
- run_destruction = false
135
- destroyer_threads.each { |ea| ea.join }
151
+ creator_threads.each(&:join)
152
+ destroyer_threads.each(&:join)
136
153
  expect(added_children).to match(expected_children)
137
154
  expect(deleted_children).to match(expected_children)
138
155
  end
139
156
 
140
- xit 'fails to deadlock while simultaneously deleting items from the same hierarchy' do
157
+ it 'fails to deadlock while simultaneously deleting items from the same hierarchy' do
141
158
  target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s })
142
- to_delete = target.self_and_ancestors.to_a.shuffle.map(&:email)
143
- destroyer_threads = @workers.times.map do
144
- DbThread.new do
145
- until to_delete.empty?
146
- email = to_delete.shift
147
- User.transaction { User.where(email: email).first.destroy } if email
159
+ emails = target.self_and_ancestors.to_a.map(&:email).shuffle
160
+ Parallel.map(emails, :in_threads => max_threads) do |email|
161
+ ActiveRecord::Base.connection_pool.with_connection do
162
+ User.transaction do
163
+ puts "Destroying #{email}..."
164
+ User.where(email: email).destroy_all
148
165
  end
149
166
  end
150
167
  end
151
- destroyer_threads.each { |ea| ea.join }
168
+ User.connection.reconnect!
152
169
  expect(User.all).to be_empty
153
- end
170
+ end unless sqlite? # sqlite throws errors from concurrent access
154
171
 
155
172
  class SiblingPrependerWorker < WorkerBase
156
173
  def before_work
@@ -158,7 +175,7 @@ describe 'Concurrent creation', if: support_concurrency do
158
175
  @sibling = Label.new(name: SecureRandom.hex(10))
159
176
  end
160
177
 
161
- def do_work(name)
178
+ def work
162
179
  @target.prepend_sibling @sibling
163
180
  end
164
181
  end
@@ -172,6 +189,6 @@ describe 'Concurrent creation', if: support_concurrency do
172
189
 
173
190
  # The only non-root node should be "root":
174
191
  expect(Label.all.select { |ea| ea.root? }).to eq([@target.parent])
175
- end
192
+ end unless sqlite? # sqlite throws errors from concurrent access
176
193
 
177
- end
194
+ end if run_parallel_tests?