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 +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
|