closure_tree 4.6.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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?