closure_tree 4.2.9 → 4.3.0

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