closure_tree 4.2.9 → 4.3.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- OTg4Mjc4NzJlYTM4ZTU4MTE5NzVhNzJlOTc1ZWMyNjAwYjk5NTU2Zg==
4
+ Y2I0ZTRhYzcwYzQ4YmM1MmIxMjUxZTdkZDdlMzY4NzkxNGEwMzM5OA==
5
5
  data.tar.gz: !binary |-
6
- ZjBiMGUzZWUxZGI3MTI3N2Y4YTYwNGQ1ODJlYzQzNDVkYWViZGY3Nw==
6
+ MDQ3YzAyNjA4NzI4YjgwM2JkYzgzYzlhYThhYTRkNmMxNjdhZDYxYg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- OTYyMDY4MDA0MmUzNTUwOGJmNTdjODVkMzU1MTMwMjNkZDhhNDA1MWUyYmQ4
10
- NzEzOWY2OGE2MDdjYzNlZWJmYjFiZmQ0N2I0ZmI5MjU1ZjA2YzExYjI5NmM2
11
- NjVjNjE0MjQ3N2U3M2I2NDU3ZGI5ZDRhZGNhM2U4ZmRjNmE0NDU=
9
+ OTVlZGNkNzlhN2Q3ODkxYzJjMTQ2YzNkZjA1MGI4ZDhhNDM4NTU2ZTRlZDEw
10
+ NjYxOWZjMDViNmJlNTE4NjM3ZDQ2MTM5Yjg1ZTkxNjAyNTBhMTU2N2FmNzRi
11
+ ZTQ5NmRlNjZlYzRjZGViYzU2OTBiZGY5YzQ4MTYxNmZkMTEwYmE=
12
12
  data.tar.gz: !binary |-
13
- MWUxMjMxN2MyZDEzNGZjYzY5OTE5ZjJkYTNlNTQ3MzFhNWI2MTU3OTA3ZWUz
14
- YjVmM2UxY2Q4NjhlOGUyMjFiMjFhNGYzYjY5MzIwZTk2NWI2ZDQ5ZmY4NWI0
15
- MjIwZmRmMTgxNWNmMTIzYzkzNWY1ZmRlMTIzMGY4NmUxYzRlMjI=
13
+ MmQ3OTBkNDZhNDkyZjZmY2E2NDRiZWRmYzc4YzA4Y2FjMTRmNTQwZTFmZjBl
14
+ Y2UwNGRiNDk2MTVhMGY5ZGNmNjZkMGY1MGI5NGYzNmNkYjRjMjJjZGM0NWNj
15
+ YmEyN2FkYjlkZDAxNWU3MWM4NGY0ZTk4MTQ1ZmFiM2UzNjVhNjE=
data/README.md CHANGED
@@ -387,7 +387,7 @@ If your ```order``` column is an integer attribute, you'll also have these:
387
387
  ```ruby
388
388
 
389
389
  root = OrderedTag.create(:name => "root")
390
- a = OrderedTag.create(:name => "a", :parent => "root")
390
+ a = OrderedTag.create(:name => "a", :parent => root)
391
391
  b = OrderedTag.create(:name => "b")
392
392
  c = OrderedTag.create(:name => "c")
393
393
 
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new('4.2.9') unless defined?(::ClosureTree::VERSION)
2
+ VERSION = Gem::Version.new('4.3.0') unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -1,19 +1,21 @@
1
+ common: &common
2
+ database: closure_tree_test
3
+ host: localhost
4
+ pool: 50
5
+ timeout: 5000
6
+ reaping_frequency: 1000
7
+ min_messages: ERROR
8
+
1
9
  sqlite:
2
10
  adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
3
11
  database: spec/sqlite3.db
4
- pool: 50
5
- timeout: 5000
6
12
 
7
13
  postgresql:
14
+ <<: *common
8
15
  adapter: postgresql
9
16
  username: postgres
10
- database: closure_tree_test
11
- min_messages: ERROR
12
- pool: 50
13
17
 
14
18
  mysql:
19
+ <<: *common
15
20
  adapter: mysql2
16
- host: localhost
17
21
  username: root
18
- database: closure_tree_test
19
- pool: 50
@@ -1,17 +1,8 @@
1
1
  # encoding: UTF-8
2
- class ActiveRecord::ConnectionAdapters::AbstractAdapter
3
- def force_add_index(table_name, columns, options = {})
4
- begin
5
- remove_index!(table_name, options[:name])
6
- rescue ActiveRecord::StatementInvalid, ArgumentError
7
- end
8
- add_index table_name, columns, options
9
- end
10
- end
11
2
 
12
3
  ActiveRecord::Schema.define(:version => 0) do
13
4
 
14
- create_table "tags", :force => true do |t|
5
+ create_table "tags" do |t|
15
6
  t.string "name"
16
7
  t.string "title"
17
8
  t.integer "parent_id"
@@ -20,13 +11,18 @@ ActiveRecord::Schema.define(:version => 0) do
20
11
  t.datetime "updated_at"
21
12
  end
22
13
 
23
- create_table "tag_hierarchies", :id => false, :force => true do |t|
14
+ add_foreign_key(:tags, :tags, :column => 'parent_id')
15
+
16
+ create_table "tag_hierarchies", :id => false do |t|
24
17
  t.integer "ancestor_id", :null => false
25
18
  t.integer "descendant_id", :null => false
26
19
  t.integer "generations", :null => false
27
20
  end
28
21
 
29
- create_table "uuid_tags", :id => false, :force => true do |t|
22
+ add_foreign_key(:tag_hierarchies, :tags, :column => 'ancestor_id')
23
+ add_foreign_key(:tag_hierarchies, :tags, :column => 'descendant_id')
24
+
25
+ create_table "uuid_tags", :id => false do |t|
30
26
  t.string "uuid", :unique => true
31
27
  t.string "name"
32
28
  t.string "title"
@@ -36,87 +32,96 @@ ActiveRecord::Schema.define(:version => 0) do
36
32
  t.datetime "updated_at"
37
33
  end
38
34
 
39
- create_table "uuid_tag_hierarchies", :id => false, :force => true do |t|
35
+ create_table "uuid_tag_hierarchies", :id => false do |t|
40
36
  t.string "ancestor_id", :null => false
41
37
  t.string "descendant_id", :null => false
42
38
  t.integer "generations", :null => false
43
39
  end
44
40
 
45
- create_table "destroyed_tags", :force => true do |t|
41
+ create_table "destroyed_tags" do |t|
46
42
  t.string "name"
47
43
  end
48
44
 
49
- force_add_index "tag_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "tag_anc_desc_idx"
50
- force_add_index "tag_hierarchies", [:descendant_id], :name => "tag_desc_idx"
45
+ add_index "tag_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "tag_anc_desc_idx"
46
+ add_index "tag_hierarchies", [:descendant_id], :name => "tag_desc_idx"
51
47
 
52
- create_table "users", :force => true do |t|
48
+ create_table "users" do |t|
53
49
  t.string "email"
54
50
  t.integer "referrer_id"
55
51
  t.datetime "created_at"
56
52
  t.datetime "updated_at"
57
53
  end
58
54
 
59
- create_table "contracts", :force => true do |t|
55
+ add_foreign_key(:users, :users, :column => 'referrer_id')
56
+
57
+ create_table "contracts" do |t|
60
58
  t.integer "user_id", :null => false
61
59
  end
62
60
 
63
- create_table "referral_hierarchies", :id => false, :force => true do |t|
61
+ create_table "referral_hierarchies", :id => false do |t|
64
62
  t.integer "ancestor_id", :null => false
65
63
  t.integer "descendant_id", :null => false
66
64
  t.integer "generations", :null => false
67
65
  end
68
66
 
69
- force_add_index "referral_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "ref_anc_desc_idx"
70
- force_add_index "referral_hierarchies", [:descendant_id], :name => "ref_desc_idx"
67
+ add_index "referral_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "ref_anc_desc_idx"
68
+ add_index "referral_hierarchies", [:descendant_id], :name => "ref_desc_idx"
71
69
 
72
- create_table "labels", :force => true do |t|
70
+ create_table "labels" do |t|
73
71
  t.string "name"
74
72
  t.string "type"
75
73
  t.integer "sort_order"
76
74
  t.integer "mother_id"
77
75
  end
78
76
 
79
- create_table "label_hierarchies", :id => false, :force => true do |t|
77
+ add_foreign_key(:labels, :labels, :column => 'mother_id')
78
+
79
+ create_table "label_hierarchies", :id => false do |t|
80
80
  t.integer "ancestor_id", :null => false
81
81
  t.integer "descendant_id", :null => false
82
82
  t.integer "generations", :null => false
83
83
  end
84
84
 
85
- force_add_index "label_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "lh_anc_desc_idx"
86
- force_add_index "label_hierarchies", [:descendant_id], :name => "lh_desc_idx"
85
+ add_index "label_hierarchies", [:ancestor_id, :descendant_id, :generations], :unique => true, :name => "lh_anc_desc_idx"
86
+ add_index "label_hierarchies", [:descendant_id], :name => "lh_desc_idx"
87
87
 
88
- create_table "cuisine_types", :force => true do |t|
88
+ create_table "cuisine_types" do |t|
89
89
  t.string "name"
90
90
  t.integer "parent_id"
91
91
  end
92
92
 
93
- create_table "cuisine_type_hierarchies", :id => false, :force => true do |t|
93
+ create_table "cuisine_type_hierarchies", :id => false do |t|
94
94
  t.integer "ancestor_id", :null => false
95
95
  t.integer "descendant_id", :null => false
96
96
  t.integer "generations", :null => false
97
97
  end
98
98
 
99
- create_table "namespace_types", :force => true do |t|
99
+ create_table "namespace_types" do |t|
100
100
  t.string "name"
101
101
  t.integer "parent_id"
102
102
  end
103
103
 
104
- create_table "namespace_type_hierarchies", :id => false, :force => true do |t|
104
+ create_table "namespace_type_hierarchies", :id => false do |t|
105
105
  t.integer "ancestor_id", :null => false
106
106
  t.integer "descendant_id", :null => false
107
107
  t.integer "generations", :null => false
108
108
  end
109
109
 
110
- create_table "metal", :force => true do |t|
110
+ create_table "metal" do |t|
111
111
  t.integer "parent_id"
112
112
  t.string "metal_type"
113
113
  t.string "value"
114
114
  t.integer "sort_order"
115
115
  end
116
116
 
117
- create_table "metal_hierarchies", :id => false, :force => true do |t|
117
+ add_foreign_key(:metal, :metal, :column => 'parent_id')
118
+
119
+ create_table "metal_hierarchies", :id => false do |t|
118
120
  t.integer "ancestor_id", :null => false
119
121
  t.integer "descendant_id", :null => false
120
122
  t.integer "generations", :null => false
121
123
  end
124
+
125
+ add_foreign_key(:metal_hierarchies, :metal, :column => 'ancestor_id')
126
+ add_foreign_key(:metal_hierarchies, :metal, :column => 'descendant_id')
122
127
  end
@@ -1,10 +1,5 @@
1
1
  require 'spec_helper'
2
2
 
3
- def delete_all_labels
4
- LabelHierarchy.delete_all
5
- Label.delete_all
6
- end
7
-
8
3
  def create_label_tree
9
4
  @d1 = Label.find_or_create_by_path %w(a1 b1 c1 d1)
10
5
  @c1 = @d1.parent
@@ -19,7 +14,7 @@ def create_label_tree
19
14
  Label.update_all("sort_order = id")
20
15
  end
21
16
 
22
- def create_preorder_tree(suffix = "")
17
+ def create_preorder_tree(suffix = "", &block)
23
18
  %w(
24
19
  a/l/n/r
25
20
  a/l/n/q
@@ -34,10 +29,12 @@ def create_preorder_tree(suffix = "")
34
29
 
35
30
  Label.roots.each_with_index do |root, root_idx|
36
31
  root.order_value = root_idx
32
+ yield(root) if block_given?
37
33
  root.save!
38
34
  root.self_and_descendants.each do |ea|
39
35
  ea.children.to_a.sort_by(&:name).each_with_index do |ea, idx|
40
36
  ea.order_value = idx
37
+ yield(ea) if block_given?
41
38
  ea.save!
42
39
  end
43
40
  end
@@ -59,9 +56,6 @@ describe Label do
59
56
  end
60
57
 
61
58
  context "roots" do
62
- before :each do
63
- delete_all_labels
64
- end
65
59
  it "sorts alphabetically" do
66
60
  expected = (0..10).to_a
67
61
  expected.shuffle.each do |ea|
@@ -119,27 +113,29 @@ describe Label do
119
113
  end
120
114
 
121
115
  context "Mixed class tree" do
122
- it "should support mixed type ancestors" do
123
- [Label, DateLabel, DirectoryLabel, EventLabel].permutation do |classes|
124
- delete_all_labels
125
- classes.each { |c| c.all.should(be_empty, "class #{c} wasn't cleaned out") }
126
- names = ('A'..'Z').to_a.first(classes.size)
127
- instances = classes.collect { |clazz| clazz.new(:name => names.shift) }
128
- a = instances.first
129
- a.save!
130
- a.name.should == "A"
131
- instances[1..-1].each_with_index do |ea, idx|
132
- instances[idx].children << ea
133
- end
134
- roots = classes.first.roots
135
- i = instances.shift
136
- roots.to_a.should =~ [i]
137
- while (!instances.empty?) do
138
- child = instances.shift
139
- i.children.to_a.should =~ [child]
140
- i = child
116
+ context "preorder tree" do
117
+ before do
118
+ classes = [Label, DateLabel, DirectoryLabel, EventLabel]
119
+ create_preorder_tree do |ea|
120
+ ea.type = classes[ea.sort_order % 4].to_s
141
121
  end
142
122
  end
123
+ it "finds roots with specific classes" do
124
+ Label.roots.should == Label.where(:name => 'a').to_a
125
+ DirectoryLabel.roots.should be_empty
126
+ EventLabel.roots.should be_empty
127
+ end
128
+
129
+ it "all is limited to subclasses" do
130
+ DateLabel.all.map(&:name).should =~ %w(f h l n p)
131
+ DirectoryLabel.all.map(&:name).should =~ %w(g q)
132
+ EventLabel.all.map(&:name).should == %w(r)
133
+ end
134
+
135
+ it "returns descendents regardless of subclass" do
136
+ Label.root.descendants.map{|ea|ea.class.to_s}.uniq.should =~
137
+ %w(Label DateLabel DirectoryLabel EventLabel)
138
+ end
143
139
  end
144
140
 
145
141
  it "supports children << and add_child" do
@@ -160,8 +156,7 @@ describe Label do
160
156
  end
161
157
 
162
158
  context "find_all_by_generation" do
163
- before :all do
164
- delete_all_labels
159
+ before :each do
165
160
  create_label_tree
166
161
  end
167
162
 
@@ -201,8 +196,7 @@ describe Label do
201
196
  end
202
197
 
203
198
  context "loading through self_and_ scopes" do
204
- before :all do
205
- delete_all_labels
199
+ before :each do
206
200
  create_label_tree
207
201
  end
208
202
 
@@ -286,10 +280,6 @@ describe Label do
286
280
  end
287
281
 
288
282
  context "#add_sibling" do
289
- before :each do
290
- Label.delete_all
291
- end
292
-
293
283
  it "should move a node before another node which has an uninitialized sort_order" do
294
284
  f = Label.find_or_create_by_path %w(a b c d e fa)
295
285
  f0 = f.prepend_sibling(Label.new(:name => "fb")) # < not alpha sort, so name shouldn't matter
@@ -325,7 +315,6 @@ describe Label do
325
315
 
326
316
  context "destructive reordering" do
327
317
  before :each do
328
- Label.delete_all
329
318
  # to make sure sort_order isn't affected by additional nodes:
330
319
  create_preorder_tree
331
320
  @root = Label.create(:name => "root")
@@ -359,7 +348,6 @@ describe Label do
359
348
 
360
349
  context "preorder" do
361
350
  it "returns descendants in proper order" do
362
- delete_all_labels
363
351
  create_preorder_tree
364
352
  a = Label.root
365
353
  a.name.should == "a"
@@ -4,8 +4,6 @@ require 'securerandom'
4
4
  describe "threadhot" do
5
5
 
6
6
  before :each do
7
- LabelHierarchy.delete_all
8
- Label.delete_all
9
7
  @iterations = 5
10
8
  @workers = 8
11
9
  end
@@ -7,41 +7,62 @@ parallelism_is_broken = begin
7
7
  ENV["DB"] =~ /sqlite/
8
8
  end
9
9
 
10
+ class DbThread
11
+ def initialize(&block)
12
+ @thread = Thread.new do
13
+ ActiveRecord::Base.connection_pool.with_connection(&block)
14
+ end
15
+ end
16
+
17
+ def join
18
+ @thread.join
19
+ end
20
+ end
21
+
10
22
  describe "threadhot" do
11
23
 
12
24
  before :each do
13
- ActiveRecord::Base.connection.reconnect!
14
- TagHierarchy.delete_all
15
- Tag.delete_all
16
25
  @parent = nil
17
- # These values seem to allow Travis to reliably pass:
18
- @iterations = 5
19
- @workers = 6
20
- @time_between_runs = 3
26
+ @iterations = 3
27
+ @workers = 10
28
+ @min_sleep_time = 0.3
29
+ @lock = Mutex.new
30
+ @wake_times = []
31
+ DatabaseCleaner.clean
32
+ end
33
+
34
+ after :each do
35
+ DatabaseCleaner.clean
21
36
  end
22
37
 
23
- def find_or_create_at_even_second(run_at)
24
- sleep(run_at - Time.now.to_f)
25
- ActiveRecord::Base.connection.reconnect!
26
- (@parent || Tag).find_or_create_by_path([run_at.to_s, :a, :b, :c])
38
+ def find_or_create_at_same_time(name)
39
+ @lock.synchronize { @wake_times << Time.now.to_f + @min_sleep_time }
40
+ while @wake_times.size < @workers
41
+ sleep(0.1)
42
+ end
43
+ max_wait_time = @lock.synchronize { @wake_times.max }
44
+ sleep_time = max_wait_time - Time.now.to_f
45
+ $stderr.puts "sleeping for #{sleep_time}"
46
+ sleep(sleep_time)
47
+ (@parent || Tag).find_or_create_by_path([name.to_s, :a, :b, :c])
27
48
  end
28
49
 
29
50
  def run_workers
30
- expected_thread_setup_time = 4
31
- start_time = Time.now.to_i + expected_thread_setup_time
32
- @times = @iterations.times.collect { |ea| start_time + (ea * @time_between_runs) }
33
- @names = @times.collect { |ea| ea.to_s }
34
- @threads = @workers.times.collect do
35
- Thread.new do
36
- @times.each { |ea| find_or_create_at_even_second(ea) }
51
+ @names = []
52
+ @iterations.times.each do |iter|
53
+ name = "iteration ##{iter}"
54
+ @names << name
55
+ threads = @workers.times.map do
56
+ DbThread.new { find_or_create_at_same_time(name) }
37
57
  end
58
+ threads.each { |ea| ea.join }
59
+ @wake_times.clear
38
60
  end
39
- @threads.each { |ea| ea.join }
40
61
  end
41
62
 
42
63
  it "class method will not create dupes" do
43
64
  run_workers
44
- Tag.roots.collect { |ea| ea.name.to_i }.should =~ @times
65
+ Tag.roots.collect { |ea| ea.name }.should =~ @names
45
66
  # No dupe children:
46
67
  %w(a b c).each do |ea|
47
68
  Tag.where(:name => ea).size.should == @iterations
@@ -51,7 +72,7 @@ describe "threadhot" do
51
72
  it "instance method will not create dupes" do
52
73
  @parent = Tag.create!(:name => "root")
53
74
  run_workers
54
- @parent.reload.children.collect { |ea| ea.name.to_i }.should =~ @times
75
+ @parent.reload.children.collect { |ea| ea.name }.should =~ @names
55
76
  Tag.where(:name => @names).size.should == @iterations
56
77
  %w(a b c).each do |ea|
57
78
  Tag.where(:name => ea).size.should == @iterations
@@ -65,7 +86,7 @@ describe "threadhot" do
65
86
  Tag.where(:name => @names).size.should > @iterations
66
87
  end
67
88
 
68
- it "fails to deadlock from parallel sibling churn" do
89
+ it 'fails to deadlock from parallel sibling churn' do
69
90
  # target should be non-trivially long to maximize time spent in hierarchy maintenance
70
91
  target = Tag.find_or_create_by_path(('a'..'z').to_a + ('A'..'Z').to_a)
71
92
  expected_children = (1..100).to_a.map { |ea| "root ##{ea}" }
@@ -74,29 +95,30 @@ describe "threadhot" do
74
95
  children_to_delete = []
75
96
  deleted_children = []
76
97
  creator_threads = @workers.times.map do
77
- Thread.new do
78
- ActiveRecord::Base.connection.reconnect!
79
- begin
98
+ DbThread.new do
99
+ while children_to_add.present?
80
100
  name = children_to_add.shift
81
101
  unless name.nil?
82
- target.find_or_create_by_path(name)
102
+ Tag.transaction { target.find_or_create_by_path(name) }
83
103
  children_to_delete << name
84
104
  added_children << name
85
105
  end
86
- end while !children_to_add.empty?
106
+ end
87
107
  end
88
108
  end
89
109
  run_destruction = true
90
110
  destroyer_threads = @workers.times.map do
91
- Thread.new do
92
- ActiveRecord::Base.connection.reconnect!
111
+ DbThread.new do
93
112
  begin
94
- victim = children_to_delete.shift
95
- if victim
96
- target.children.where(:name => victim).first.destroy
97
- deleted_children << victim
113
+ victim_name = children_to_delete.shift
114
+ if victim_name
115
+ Tag.transaction do
116
+ victim = target.children.where(:name => victim_name).first
117
+ victim.destroy
118
+ deleted_children << victim_name
119
+ end
98
120
  else
99
- sleep rand # wait for moar victims
121
+ sleep rand # wait for more victims
100
122
  end
101
123
  end while run_destruction || !children_to_delete.empty?
102
124
  end
@@ -104,33 +126,19 @@ describe "threadhot" do
104
126
  creator_threads.each { |ea| ea.join }
105
127
  run_destruction = false
106
128
  destroyer_threads.each { |ea| ea.join }
107
-
108
129
  added_children.should =~ expected_children
109
130
  deleted_children.should =~ expected_children
110
131
  end
111
132
 
112
- # Oh, yeah, I'm totes monkey patching in a bad shuffle. I AM A NAUGHTY MONKAY
113
- class Array
114
- def bad_shuffle!(shuffle_count = nil)
115
- shuffle_count ||= size / 10
116
- pairs = Hash[*(0..(size)).to_a.shuffle.first(shuffle_count)]
117
- pairs.each do |from, to|
118
- self[from], self[to] = self[to], self[from]
119
- end
120
- self
121
- end
122
- end
123
-
124
133
  it "fails to deadlock while simultaneously deleting items from the same hierarchy" do
125
134
  target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s })
126
- nodes_to_delete = target.self_and_ancestors.to_a.bad_shuffle!
135
+ to_delete = target.self_and_ancestors.to_a.shuffle.map(&:email)
127
136
  destroyer_threads = @workers.times.map do
128
- Thread.new do
129
- ActiveRecord::Base.connection.reconnect!
130
- begin
131
- victim = nodes_to_delete.shift
132
- victim.destroy if victim
133
- end while !nodes_to_delete.empty?
137
+ DbThread.new do
138
+ until to_delete.empty?
139
+ email = to_delete.shift
140
+ User.transaction { User.where(:email => email).first.destroy } if email
141
+ end
134
142
  end
135
143
  end
136
144
  destroyer_threads.each { |ea| ea.join }
@@ -5,17 +5,17 @@ require 'rubygems'
5
5
  ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
6
6
  require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7
7
  require 'rspec'
8
- require 'rails'
9
8
  require 'active_record'
10
- require 'active_record/fixtures'
11
- require 'rspec/rails/adapters'
12
- require 'rspec/rails/fixture_support'
9
+ require 'foreigner'
10
+ require 'database_cleaner'
13
11
  require 'closure_tree'
14
12
  require 'tmpdir'
15
13
 
16
- #log = Logger.new(STDOUT)
17
- #log.sev_threshold = Logger::DEBUG
18
- #ActiveRecord::Base.logger = log
14
+ if ENV['STDOUT_LOGGING']
15
+ log = Logger.new(STDOUT)
16
+ log.sev_threshold = Logger::DEBUG
17
+ ActiveRecord::Base.logger = log
18
+ end
19
19
 
20
20
  require 'yaml'
21
21
  require 'erb'
@@ -29,11 +29,30 @@ if ENV['ATTR_ACCESSIBLE'] == '1'
29
29
  end
30
30
 
31
31
  ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
32
+
33
+ def recreate_db
34
+ db_name = ActiveRecord::Base.configurations[ENV["DB"]]["database"]
35
+ case ENV['DB'] || 'mysql'
36
+ when 'sqlite'
37
+ File.delete 'spec/sqlite3.db' if File.exist? 'spec/sqlite3.db'
38
+ when 'postgresql'
39
+ `psql -c 'DROP DATABASE #{db_name}' -U postgres`
40
+ `psql -c 'CREATE DATABASE #{db_name}' -U postgres`
41
+ when 'mysql'
42
+ `mysql -e 'DROP DATABASE IF EXISTS #{db_name}'`
43
+ `mysql -e 'CREATE DATABASE #{db_name}'`
44
+ end
45
+ ActiveRecord::Base.connection.reconnect!
46
+ end
47
+
32
48
  ActiveRecord::Base.establish_connection(ENV["DB"])
49
+
33
50
  ActiveRecord::Migration.verbose = false
34
51
  if ENV['NONUKES']
35
52
  puts 'skipping database creation'
36
53
  else
54
+ Foreigner.load
55
+ recreate_db
37
56
  require 'db/schema'
38
57
  end
39
58
  require 'support/models'
@@ -65,12 +84,14 @@ end
65
84
 
66
85
  Thread.abort_on_exception = true
67
86
 
87
+ DatabaseCleaner.strategy = :truncation
88
+
68
89
  RSpec.configure do |config|
69
- config.fixture_path = "#{plugin_test_dir}/fixtures"
70
- # true runs the tests 1 second faster, but then you can't
71
- # see what's going on while debuggering with different db connections.
72
- config.use_transactional_fixtures = false
90
+ config.before(:each) do
91
+ DatabaseCleaner.start
92
+ end
73
93
  config.after(:each) do
94
+ DatabaseCleaner.clean
74
95
  DB_QUERIES.clear
75
96
  end
76
97
  config.before(:all) do
@@ -95,7 +95,7 @@ module Namespace
95
95
  end
96
96
 
97
97
  class Metal < ActiveRecord::Base
98
- self.table_name = 'metal'
98
+ self.table_name = "#{table_name_prefix}metal#{table_name_suffix}"
99
99
  acts_as_tree :order => 'sort_order'
100
100
  self.inheritance_column = 'metal_type'
101
101
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- shared_examples_for "Tag (without fixtures)" do
3
+ shared_examples_for Tag do
4
4
 
5
5
  let (:tag_class) { described_class }
6
6
  let (:tag_hierarchy_class) { described_class.hierarchy_class }
@@ -26,27 +26,71 @@ shared_examples_for "Tag (without fixtures)" do
26
26
  end
27
27
 
28
28
  describe "from empty db" do
29
- def nuke_db
30
- tag_hierarchy_class.delete_all
31
- tag_class.delete_all
32
- end
33
-
34
- before :each do
35
- nuke_db
36
- end
37
29
 
38
30
  context "with no tags" do
39
31
  it "should return no entities" do
40
32
  tag_class.roots.should be_empty
41
33
  tag_class.leaves.should be_empty
42
34
  end
35
+
36
+ it "#find_or_create_by_path" do
37
+ a = tag_class.create!(:name => 'a')
38
+ a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
39
+ end
43
40
  end
44
41
 
45
42
  context "with 1 tag" do
43
+ before do
44
+ @tag = tag_class.create!(:name => "tag")
45
+ end
46
+
47
+ it "should be a leaf" do
48
+ @tag.leaf?.should be_true
49
+ end
50
+
51
+ it "should be a root" do
52
+ @tag.root?.should be_true
53
+ end
54
+
55
+ it 'has no parent' do
56
+ @tag.parent.should be_nil
57
+ end
58
+
46
59
  it "should return the only entity as a root and leaf" do
47
- a = tag_class.create!(:name => "a")
48
- tag_class.roots.should == [a]
49
- tag_class.leaves.should == [a]
60
+ tag_class.all.should == [@tag]
61
+ tag_class.roots.should == [@tag]
62
+ tag_class.leaves.should == [@tag]
63
+ end
64
+
65
+ context "with child" do
66
+ before do
67
+ @child = tag_class.create!(:name => 'tag 2')
68
+ end
69
+
70
+ def assert_roots_and_leaves
71
+ @tag.root?.should be_true
72
+ @tag.leaf?.should be_false
73
+
74
+ @child.root?.should be_false
75
+ @child.leaf?.should be_true
76
+ end
77
+
78
+ def assert_parent_and_children
79
+ @child.reload.parent.should == @tag
80
+ @tag.reload.children.to_a.should == [@child]
81
+ end
82
+
83
+ it "adds children through add_child" do
84
+ @tag.add_child @child
85
+ assert_roots_and_leaves
86
+ assert_parent_and_children
87
+ end
88
+
89
+ it "adds children through collection" do
90
+ @tag.children << @child
91
+ assert_roots_and_leaves
92
+ assert_parent_and_children
93
+ end
50
94
  end
51
95
  end
52
96
 
@@ -62,6 +106,7 @@ shared_examples_for "Tag (without fixtures)" do
62
106
  it "should return child_ids for root" do
63
107
  @root.child_ids.should == [@leaf.id]
64
108
  end
109
+
65
110
  it "should return an empty array for leaves" do
66
111
  @leaf.child_ids.should be_empty
67
112
  end
@@ -104,6 +149,28 @@ shared_examples_for "Tag (without fixtures)" do
104
149
  @mid.children << t
105
150
  t.self_and_ancestors.to_a.should == [t, @mid, @root]
106
151
  end
152
+
153
+ it 'prevents ancestor loops' do
154
+ @leaf.add_child @root
155
+ @root.should_not be_valid
156
+ @root.reload.descendants.should include(@leaf)
157
+ end
158
+
159
+ it 'moves non-leaves' do
160
+ new_root = tag_class.create! :name => "new_root"
161
+ new_root.children << @mid
162
+ @root.reload.descendants.should be_empty
163
+ new_root.descendants.should == [@mid, @leaf]
164
+ @leaf.reload.ancestry_path.should == %w{new_root mid leaf}
165
+ end
166
+
167
+ it 'moves leaves' do
168
+ new_root = tag_class.create! :name => "new_root"
169
+ new_root.children << @leaf
170
+ new_root.descendants.should == [@leaf]
171
+ @root.reload.descendants.should == [@mid]
172
+ @leaf.reload.ancestry_path.should == %w{new_root leaf}
173
+ end
107
174
  end
108
175
 
109
176
  context "3 tag explicit_create db" do
@@ -197,13 +264,20 @@ shared_examples_for "Tag (without fixtures)" do
197
264
  before :each do
198
265
  tag_class.find_or_create_by_path %w(a1 b1 c1a)
199
266
  tag_class.find_or_create_by_path %w(a1 b1 c1b)
267
+ tag_class.find_or_create_by_path %w(a1 b1 c1c)
268
+ tag_class.find_or_create_by_path %w(a1 b1b)
200
269
  tag_class.find_or_create_by_path %w(a2 b2)
201
270
  tag_class.find_or_create_by_path %w(a3)
202
271
 
203
- @a1, @a2, @a3, @b1, @b2, @c1a, @c1b = tag_class.where(:name => %w(a1 a2 a3 b1 b2 c1a c1b)).reorder(:name).to_a
272
+ @a1, @a2, @a3, @b1, @b1b, @b2, @c1a, @c1b, @c1c = tag_class.
273
+ where(:name => %w(a1 a2 a3 b1 b1b b2 c1a c1b c1c)).
274
+ reorder(:name).to_a
204
275
  @expected_roots = [@a1, @a2, @a3]
205
- @expected_leaves = [@c1a, @c1b, @b2, @a3]
276
+ @expected_leaves = [@c1a, @c1b, @c1c, @b1b, @b2, @a3]
277
+ @expected_siblings = [[@a1, @a2, @a3], [@b1, @b1b], [@c1a, @c1b, @c1c]]
278
+ @expected_only_children = tag_class.all - @expected_siblings.flatten
206
279
  end
280
+
207
281
  it 'should find global roots' do
208
282
  tag_class.roots.to_a.should =~ @expected_roots
209
283
  end
@@ -222,8 +296,37 @@ shared_examples_for "Tag (without fixtures)" do
222
296
  it 'should assemble global leaves' do
223
297
  tag_class.leaves.to_a.should =~ @expected_leaves
224
298
  end
299
+ it 'assembles siblings properly' do
300
+ @expected_siblings.each do |siblings|
301
+ siblings.each do |ea|
302
+ ea.self_and_siblings.to_a.should =~ siblings
303
+ ea.siblings.to_a.should =~ siblings - [ea]
304
+ end
305
+ end
306
+ @expected_only_children.each do |ea|
307
+ ea.siblings.should == []
308
+ end
309
+ end
310
+ it 'assembles before_siblings' do
311
+ @expected_siblings.each do |siblings|
312
+ (siblings.size - 1).times do |i|
313
+ target = siblings[i]
314
+ expected_before = siblings.first(i)
315
+ target.siblings_before.to_a.should == expected_before
316
+ end
317
+ end
318
+ end
319
+ it 'assembles after_siblings' do
320
+ @expected_siblings.each do |siblings|
321
+ (siblings.size - 1).times do |i|
322
+ target = siblings[i]
323
+ expected_after = siblings.last(siblings.size - 1 - i)
324
+ target.siblings_after.to_a.should == expected_after
325
+ end
326
+ end
327
+ end
225
328
  it 'should assemble instance leaves' do
226
- {@a1 => [@c1a, @c1b], @b1 => [@c1a, @c1b], @a2 => [@b2]}.each do |node, leaves|
329
+ {@a1 => [@b1b, @c1a, @c1b, @c1c], @b1 => [@c1a, @c1b, @c1c], @a2 => [@b2]}.each do |node, leaves|
227
330
  node.leaves.to_a.should == leaves
228
331
  end
229
332
  @expected_leaves.each { |ea| ea.leaves.to_a.should == [ea] }
@@ -231,16 +334,29 @@ shared_examples_for "Tag (without fixtures)" do
231
334
  it 'should return leaf? for leaves' do
232
335
  @expected_leaves.each { |ea| ea.should be_leaf }
233
336
  end
337
+
338
+ it 'can move roots' do
339
+ @c1a.children << @a2
340
+ @b2.reload.children << @a3
341
+ @a3.reload.ancestry_path.should ==%w(a1 b1 c1a a2 b2 a3)
342
+ end
343
+
344
+ it 'cascade-deletes from roots' do
345
+ victim_names = @a1.self_and_descendants.map(&:name)
346
+ survivor_names = tag_class.all.map(&:name) - victim_names
347
+ @a1.destroy
348
+ tag_class.all.map(&:name).should == survivor_names
349
+ end
234
350
  end
235
351
 
236
352
  context 'with_ancestor' do
237
353
  it 'works with no rows' do
238
- tag_class.with_ancestor().to_a.should be_empty
354
+ tag_class.with_ancestor.to_a.should be_empty
239
355
  end
240
356
  it 'finds only children' do
241
357
  c = tag_class.find_or_create_by_path %w(A B C)
242
358
  a, b = c.parent.parent, c.parent
243
- e = tag_class.find_or_create_by_path %w(D E)
359
+ spurious_tags = tag_class.find_or_create_by_path %w(D E)
244
360
  tag_class.with_ancestor(a).to_a.should == [b, c]
245
361
  end
246
362
  it 'limits subsequent where clauses' do
@@ -268,6 +384,11 @@ shared_examples_for "Tag (without fixtures)" do
268
384
  @child.ancestry_path(:title).should == %w{Nonnie Mom Kid}
269
385
  end
270
386
 
387
+ it 'assembles ancestors' do
388
+ @child.ancestors.should == [@parent, @grandparent]
389
+ @child.self_and_ancestors.should == [@child, @parent, @grandparent]
390
+ end
391
+
271
392
  it "should find by path" do
272
393
  # class method:
273
394
  tag_class.find_by_path(%w{grandparent parent child}).should == @child
@@ -311,17 +432,12 @@ shared_examples_for "Tag (without fixtures)" do
311
432
  tag_class.find_by_path(%w{grandparent parent missing child}).should be_nil
312
433
  end
313
434
 
314
- it "should find or create by path" do
315
- # class method:
435
+ it ".find_or_create_by_path" do
316
436
  grandparent = tag_class.find_or_create_by_path(%w{grandparent})
317
437
  grandparent.should == @grandparent
318
438
  child = tag_class.find_or_create_by_path(%w{grandparent parent child})
319
439
  child.should == @child
320
440
  tag_class.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
321
- a = tag_class.find_or_create_by_path(%w{a})
322
- a.ancestry_path.should == %w{a}
323
- # instance method:
324
- a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
325
441
  end
326
442
 
327
443
  it "should respect attribute hashes with both selection and creation" do
@@ -1,8 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'tag_examples'
3
- require 'tag_fixture_examples'
4
3
 
5
4
  describe Tag do
6
- it_behaves_like 'Tag (without fixtures)'
7
- it_behaves_like 'Tag (with fixtures)'
5
+ it_behaves_like Tag
8
6
  end
@@ -2,11 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  describe "empty db" do
4
4
 
5
- before :each do
6
- ReferralHierarchy.delete_all
7
- User.delete_all
8
- end
9
-
10
5
  context "empty db" do
11
6
  it "should return no entities" do
12
7
  User.roots.should be_empty
@@ -2,5 +2,5 @@ require 'spec_helper'
2
2
  require 'tag_examples'
3
3
 
4
4
  describe UUIDTag do
5
- it_behaves_like "Tag (without fixtures)"
5
+ it_behaves_like Tag
6
6
  end unless ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 1
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.9
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-10 00:00:00.000000000 Z
11
+ date: 2013-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -178,6 +178,34 @@ dependencies:
178
178
  - - ! '>='
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: foreigner
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ! '>='
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ! '>='
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: database_cleaner
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ! '>='
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ! '>='
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
181
209
  description: Easily and efficiently make your ActiveRecord model support hierarchies
182
210
  email:
183
211
  - matthew-github@mceachen.org
@@ -214,7 +242,6 @@ files:
214
242
  - spec/spec_helper.rb
215
243
  - spec/support/models.rb
216
244
  - spec/tag_examples.rb
217
- - spec/tag_fixture_examples.rb
218
245
  - spec/tag_spec.rb
219
246
  - spec/user_spec.rb
220
247
  - spec/uuid_tag_spec.rb
@@ -238,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
265
  version: '0'
239
266
  requirements: []
240
267
  rubyforge_project:
241
- rubygems_version: 2.1.5
268
+ rubygems_version: 2.1.10
242
269
  signing_key:
243
270
  specification_version: 4
244
271
  summary: Easily and efficiently make your ActiveRecord model support hierarchies
@@ -255,7 +282,6 @@ test_files:
255
282
  - spec/spec_helper.rb
256
283
  - spec/support/models.rb
257
284
  - spec/tag_examples.rb
258
- - spec/tag_fixture_examples.rb
259
285
  - spec/tag_spec.rb
260
286
  - spec/user_spec.rb
261
287
  - spec/uuid_tag_spec.rb
@@ -1,136 +0,0 @@
1
- shared_examples_for 'Tag (with fixtures)' do
2
-
3
- let (:tag_class) { described_class }
4
- let (:tag_hierarchy_class) { described_class.hierarchy_class }
5
-
6
- describe 'Tag (with fixtures)' do
7
- fixtures :tags
8
- before :each do
9
- tag_class.rebuild!
10
- DestroyedTag.delete_all
11
- end
12
-
13
- context 'adding children' do
14
- it 'should work explicitly' do
15
- sb = Tag.create!(:name => 'Santa Barbara')
16
- sb.leaf?.should_not be_nil
17
- tags(:california).add_child sb
18
- sb.leaf?.should_not be_nil
19
- validate_city_tag sb
20
- end
21
-
22
- it 'should work implicitly through the collection' do
23
- eg = Tag.create!(:name => 'El Granada')
24
- eg.leaf?.should_not be_nil
25
- tags(:california).children << eg
26
- eg.leaf?.should_not be_nil
27
- validate_city_tag eg
28
- end
29
-
30
- it 'should fail to create ancestor loops' do
31
- child = tags(:child)
32
- parent = child.parent
33
- child.add_child(parent) # this should fail
34
- parent.valid?.should be_false
35
- child.reload.children.should be_empty
36
- parent.reload.children.should == [child]
37
- end
38
-
39
- it 'should move non-leaves' do
40
- # This is what the fixture should encode:
41
- tags(:d2).ancestry_path.should == %w{a1 b2 c2 d2}
42
- tags(:b1).add_child(tags(:c2))
43
- tags(:b2).leaf?.should_not be_nil
44
- tags(:b1).children.include?(tags(:c2)).should be_true
45
- tags(:d2).reload.ancestry_path.should == %w{a1 b1 c2 d2}
46
- end
47
-
48
- it 'should move leaves' do
49
- l = Tag.find_or_create_by_path(%w{leaftest branch1 leaf})
50
- b2 = Tag.find_or_create_by_path(%w{leaftest branch2})
51
- b2.children << l
52
- l.ancestry_path.should == %w{leaftest branch2 leaf}
53
- end
54
-
55
- it 'should move roots' do
56
- l1 = Tag.find_or_create_by_path(%w{roottest1 branch1 leaf1})
57
- l2 = Tag.find_or_create_by_path(%w{roottest2 branch2 leaf2})
58
- l1.children << l2.root
59
- l1.reload.ancestry_path.should == %w{roottest1 branch1 leaf1}
60
- l2.reload.ancestry_path.should == %w{roottest1 branch1 leaf1 roottest2 branch2 leaf2}
61
- end
62
-
63
- it 'should cascade delete all children' do
64
- b2 = tags(:b2)
65
- entities = b2.self_and_descendants.to_a
66
- names = b2.self_and_descendants.collect { |t| t.name }
67
- b2.destroy
68
- entities.each { |e| Tag.find_by_id(e.id).should be_nil }
69
- DestroyedTag.all.collect { |t| t.name }.should =~ names
70
- end
71
- end
72
-
73
- context 'injected attributes' do
74
- it 'should compute level correctly' do
75
- tags(:grandparent).level.should == 0
76
- tags(:parent).level.should == 1
77
- tags(:child).level.should == 2
78
- end
79
-
80
- it 'should determine parent correctly' do
81
- tags(:grandparent).parent.should == nil
82
- tags(:parent).parent.should == tags(:grandparent)
83
- tags(:child).parent.should == tags(:parent)
84
- end
85
-
86
- it 'should have a sane children collection' do
87
- tags(:grandparent).children.include? tags(:parent).should be_true
88
- tags(:parent).children.include? tags(:child).should be_true
89
- tags(:child).children.should be_empty
90
- end
91
-
92
- it 'assembles siblings correctly' do
93
- tags(:b1).siblings.to_a.should =~ [tags(:b2)]
94
- tags(:a1).siblings.to_a.should =~ (Tag.roots.to_a - [tags(:a1)])
95
- tags(:a1).self_and_siblings.to_a.should =~ Tag.roots.to_a
96
-
97
- # must be ordered
98
- tags(:indoor).siblings.to_a.should == [tags(:home), tags(:museum), tags(:outdoor), tags(:united_states)]
99
- tags(:indoor).self_and_siblings.to_a.should == [tags(:home), tags(:indoor), tags(:museum), tags(:outdoor), tags(:united_states)]
100
- end
101
-
102
- it 'assembles siblings before correctly' do
103
- tags(:home).siblings_before.to_a.should == []
104
- tags(:indoor).siblings_before.to_a.should == [tags(:home)]
105
- tags(:outdoor).siblings_before.to_a.should == [tags(:home), tags(:indoor), tags(:museum)]
106
- tags(:united_states).siblings_before.to_a.should == [tags(:home), tags(:indoor), tags(:museum), tags(:outdoor)]
107
- end
108
-
109
- it 'assembles siblings after correctly' do
110
- tags(:indoor).siblings_after.to_a.should == [tags(:museum), tags(:outdoor), tags(:united_states)]
111
- tags(:outdoor).siblings_after.to_a.should == [tags(:united_states)]
112
- tags(:united_states).siblings_after.to_a.should == []
113
- end
114
-
115
- it 'assembles ancestors' do
116
- tags(:child).ancestors.should == [tags(:parent), tags(:grandparent)]
117
- tags(:child).self_and_ancestors.should == [tags(:child), tags(:parent), tags(:grandparent)]
118
- end
119
-
120
- it 'assembles descendants' do
121
- tags(:parent).descendants.should == [tags(:child)]
122
- tags(:parent).self_and_descendants.should == [tags(:parent), tags(:child)]
123
- tags(:grandparent).descendants.should == [tags(:parent), tags(:child)]
124
- tags(:grandparent).self_and_descendants.should == [tags(:grandparent), tags(:parent), tags(:child)]
125
- tags(:grandparent).self_and_descendants.collect { |t| t.name }.join(" > ").should == 'grandparent > parent > child'
126
- end
127
- end
128
-
129
- def validate_city_tag city
130
- tags(:california).children.include?(city).should_not be_nil
131
- city.ancestors.should == [tags(:california), tags(:united_states), tags(:places)]
132
- city.self_and_ancestors.should == [city, tags(:california), tags(:united_states), tags(:places)]
133
- end
134
-
135
- end
136
- end