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 +8 -8
- data/README.md +1 -1
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/database.yml +10 -8
- data/spec/db/schema.rb +36 -31
- data/spec/label_spec.rb +26 -38
- data/spec/parallel_prepend_sibling_spec.rb +0 -2
- data/spec/parallel_spec.rb +62 -54
- data/spec/spec_helper.rb +32 -11
- data/spec/support/models.rb +1 -1
- data/spec/tag_examples.rb +139 -23
- data/spec/tag_spec.rb +1 -3
- data/spec/user_spec.rb +0 -5
- data/spec/uuid_tag_spec.rb +1 -1
- metadata +31 -5
- data/spec/tag_fixture_examples.rb +0 -136
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
Y2I0ZTRhYzcwYzQ4YmM1MmIxMjUxZTdkZDdlMzY4NzkxNGEwMzM5OA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MDQ3YzAyNjA4NzI4YjgwM2JkYzgzYzlhYThhYTRkNmMxNjdhZDYxYg==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
OTVlZGNkNzlhN2Q3ODkxYzJjMTQ2YzNkZjA1MGI4ZDhhNDM4NTU2ZTRlZDEw
|
10
|
+
NjYxOWZjMDViNmJlNTE4NjM3ZDQ2MTM5Yjg1ZTkxNjAyNTBhMTU2N2FmNzRi
|
11
|
+
ZTQ5NmRlNjZlYzRjZGViYzU2OTBiZGY5YzQ4MTYxNmZkMTEwYmE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
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 =>
|
390
|
+
a = OrderedTag.create(:name => "a", :parent => root)
|
391
391
|
b = OrderedTag.create(:name => "b")
|
392
392
|
c = OrderedTag.create(:name => "c")
|
393
393
|
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/db/database.yml
CHANGED
@@ -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
|
data/spec/db/schema.rb
CHANGED
@@ -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"
|
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
|
-
|
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
|
-
|
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
|
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"
|
41
|
+
create_table "destroyed_tags" do |t|
|
46
42
|
t.string "name"
|
47
43
|
end
|
48
44
|
|
49
|
-
|
50
|
-
|
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"
|
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
|
-
|
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
|
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
|
-
|
70
|
-
|
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"
|
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
|
-
|
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
|
-
|
86
|
-
|
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"
|
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
|
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"
|
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
|
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"
|
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
|
-
|
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
|
data/spec/label_spec.rb
CHANGED
@@ -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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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 :
|
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 :
|
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"
|
data/spec/parallel_spec.rb
CHANGED
@@ -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
|
-
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
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
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
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
|
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
|
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
|
-
|
78
|
-
|
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
|
106
|
+
end
|
87
107
|
end
|
88
108
|
end
|
89
109
|
run_destruction = true
|
90
110
|
destroyer_threads = @workers.times.map do
|
91
|
-
|
92
|
-
ActiveRecord::Base.connection.reconnect!
|
111
|
+
DbThread.new do
|
93
112
|
begin
|
94
|
-
|
95
|
-
if
|
96
|
-
|
97
|
-
|
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
|
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
|
-
|
135
|
+
to_delete = target.self_and_ancestors.to_a.shuffle.map(&:email)
|
127
136
|
destroyer_threads = @workers.times.map do
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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 }
|
data/spec/spec_helper.rb
CHANGED
@@ -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 '
|
11
|
-
require '
|
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
|
-
|
17
|
-
|
18
|
-
|
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.
|
70
|
-
|
71
|
-
|
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
|
data/spec/support/models.rb
CHANGED
data/spec/tag_examples.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
shared_examples_for
|
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
|
-
|
48
|
-
tag_class.roots.should == [
|
49
|
-
tag_class.leaves.should == [
|
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.
|
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
|
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
|
-
|
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 "
|
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
|
data/spec/tag_spec.rb
CHANGED
data/spec/user_spec.rb
CHANGED
data/spec/uuid_tag_spec.rb
CHANGED
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.
|
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-
|
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.
|
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
|