closure_tree 4.0.1 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +30 -4
- data/Rakefile +5 -1
- data/lib/closure_tree/acts_as_tree.rb +1 -1
- data/lib/closure_tree/model.rb +63 -33
- data/lib/closure_tree/numeric_deterministic_ordering.rb +2 -2
- data/lib/closure_tree/support.rb +52 -16
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/schema.rb +4 -4
- data/spec/label_spec.rb +3 -3
- data/spec/parallel_spec.rb +12 -6
- data/spec/spec_helper.rb +27 -6
- data/spec/support/models.rb +27 -5
- data/spec/tag_examples.rb +421 -0
- data/spec/tag_fixture_examples.rb +136 -0
- data/spec/tag_spec.rb +4 -480
- data/spec/user_spec.rb +8 -8
- data/spec/uuid_tag_spec.rb +6 -0
- metadata +173 -172
- checksums.yaml +0 -15
- data/spec/hash_tree_spec.rb +0 -91
data/lib/closure_tree/version.rb
CHANGED
data/spec/db/schema.rb
CHANGED
@@ -26,17 +26,17 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
26
26
|
t.integer "generations", :null => false
|
27
27
|
end
|
28
28
|
|
29
|
-
create_table "
|
30
|
-
t.string "
|
29
|
+
create_table "uuid_tags", :id => false, :force => true do |t|
|
30
|
+
t.string "uuid", :unique => true
|
31
31
|
t.string "name"
|
32
32
|
t.string "title"
|
33
|
-
t.string "
|
33
|
+
t.string "parent_uuid"
|
34
34
|
t.integer "sort_order"
|
35
35
|
t.datetime "created_at"
|
36
36
|
t.datetime "updated_at"
|
37
37
|
end
|
38
38
|
|
39
|
-
create_table "
|
39
|
+
create_table "uuid_tag_hierarchies", :id => false, :force => true do |t|
|
40
40
|
t.string "ancestor_id", :null => false
|
41
41
|
t.string "descendant_id", :null => false
|
42
42
|
t.integer "generations", :null => false
|
data/spec/label_spec.rb
CHANGED
@@ -123,10 +123,10 @@ describe Label do
|
|
123
123
|
end
|
124
124
|
roots = classes.first.roots
|
125
125
|
i = instances.shift
|
126
|
-
roots.should =~ [i]
|
126
|
+
roots.to_a.should =~ [i]
|
127
127
|
while (!instances.empty?) do
|
128
128
|
child = instances.shift
|
129
|
-
i.children.should =~ [child]
|
129
|
+
i.children.to_a.should =~ [child]
|
130
130
|
i = child
|
131
131
|
end
|
132
132
|
end
|
@@ -353,7 +353,7 @@ describe Label do
|
|
353
353
|
labels(:c2).append_sibling(labels(:e2))
|
354
354
|
labels(:e2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2), labels(:e2)]
|
355
355
|
labels(:a1).self_and_descendants.collect(&:name).should == %w(a1 b1 b2 c2 e2 d2 c1-six c1-seven c1-eight c1-nine)
|
356
|
-
labels(:a1).leaves.collect(&:name).should
|
356
|
+
labels(:a1).leaves.collect(&:name).should =~ %w(b2 e2 d2 c1-six c1-seven c1-eight c1-nine)
|
357
357
|
end
|
358
358
|
end
|
359
359
|
|
data/spec/parallel_spec.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
parallelism_is_broken = begin
|
4
|
+
# Rails < 3.2 has known bugs with parallelism
|
5
|
+
(ActiveRecord::VERSION::MAJOR <= 3 && ActiveRecord::VERSION::MINOR < 2) ||
|
6
|
+
# SQLite doesn't support parallel writes
|
7
|
+
ENV["DB"] =~ /sqlite/
|
8
|
+
end
|
9
|
+
|
3
10
|
describe "threadhot" do
|
4
11
|
|
5
12
|
before :each do
|
@@ -34,7 +41,7 @@ describe "threadhot" do
|
|
34
41
|
Tag.roots.collect { |ea| ea.name.to_i }.should =~ @times
|
35
42
|
# No dupe children:
|
36
43
|
%w(a b c).each do |ea|
|
37
|
-
Tag.
|
44
|
+
Tag.where(:name => ea).size.should == @iterations
|
38
45
|
end
|
39
46
|
end
|
40
47
|
|
@@ -42,9 +49,9 @@ describe "threadhot" do
|
|
42
49
|
@parent = Tag.create!(:name => "root")
|
43
50
|
run_workers
|
44
51
|
@parent.reload.children.collect { |ea| ea.name.to_i }.should =~ @times
|
45
|
-
Tag.
|
52
|
+
Tag.where(:name => @names).size.should == @iterations
|
46
53
|
%w(a b c).each do |ea|
|
47
|
-
Tag.
|
54
|
+
Tag.where(:name => ea).size.should == @iterations
|
48
55
|
end
|
49
56
|
end
|
50
57
|
|
@@ -52,8 +59,7 @@ describe "threadhot" do
|
|
52
59
|
# disable with_advisory_lock:
|
53
60
|
Tag.should_receive(:with_advisory_lock).any_number_of_times { |lock_name, &block| block.call }
|
54
61
|
run_workers
|
55
|
-
Tag.
|
62
|
+
Tag.where(:name => @names).size.should > @iterations
|
56
63
|
end
|
57
64
|
|
58
|
-
|
59
|
-
end if ((ENV["DB"] != "sqlite") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
|
65
|
+
end unless parallelism_is_broken
|
data/spec/spec_helper.rb
CHANGED
@@ -2,12 +2,14 @@ $:.unshift(File.dirname(__FILE__) + '/../lib')
|
|
2
2
|
plugin_test_dir = File.dirname(__FILE__)
|
3
3
|
|
4
4
|
require 'rubygems'
|
5
|
-
|
6
|
-
|
5
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
6
|
+
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
|
7
7
|
require 'rspec'
|
8
|
-
require '
|
9
|
-
|
10
|
-
require '
|
8
|
+
require 'rails'
|
9
|
+
require 'active_record'
|
10
|
+
require 'active_record/fixtures'
|
11
|
+
require 'rspec/rails/adapters'
|
12
|
+
require 'rspec/rails/fixture_support'
|
11
13
|
require 'closure_tree'
|
12
14
|
require 'tmpdir'
|
13
15
|
|
@@ -20,12 +22,31 @@ require 'erb'
|
|
20
22
|
ENV["DB"] ||= "mysql"
|
21
23
|
ActiveRecord::Base.table_name_prefix = ENV['DB_PREFIX'].to_s
|
22
24
|
ActiveRecord::Base.table_name_suffix = ENV['DB_SUFFIX'].to_s
|
25
|
+
|
26
|
+
if ENV['ATTR_ACCESSIBLE'] == '1'
|
27
|
+
# turn on whitelisted attributes:
|
28
|
+
ActiveRecord::Base.send(:include, ActiveModel::MassAssignmentSecurity)
|
29
|
+
end
|
30
|
+
|
23
31
|
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
|
24
32
|
ActiveRecord::Base.establish_connection(ENV["DB"])
|
25
33
|
ActiveRecord::Migration.verbose = false
|
26
34
|
require 'db/schema'
|
27
35
|
require 'support/models'
|
28
|
-
|
36
|
+
|
37
|
+
class Hash
|
38
|
+
def render_from_yield(&block)
|
39
|
+
inject({}) do |h, entry|
|
40
|
+
k, v = entry
|
41
|
+
h[block.call(k)] = if v.is_a?(Hash) then
|
42
|
+
v.render_from_yield(&block)
|
43
|
+
else
|
44
|
+
block.call(v)
|
45
|
+
end
|
46
|
+
h
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
29
50
|
|
30
51
|
DB_QUERIES = []
|
31
52
|
|
data/spec/support/models.rb
CHANGED
@@ -3,7 +3,26 @@ require 'uuidtools'
|
|
3
3
|
class Tag < ActiveRecord::Base
|
4
4
|
acts_as_tree :dependent => :destroy, :order => "name"
|
5
5
|
before_destroy :add_destroyed_tag
|
6
|
-
attr_accessible :name
|
6
|
+
attr_accessible :name if _ct.use_attr_accessible?
|
7
|
+
def to_s
|
8
|
+
name
|
9
|
+
end
|
10
|
+
def add_destroyed_tag
|
11
|
+
# Proof for the tests that the destroy rather than the delete method was called:
|
12
|
+
DestroyedTag.create(:name => name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class UUIDTag < ActiveRecord::Base
|
17
|
+
self.primary_key = :uuid
|
18
|
+
before_create :set_uuid
|
19
|
+
acts_as_tree :dependent => :destroy, :order => 'name', :parent_column_name => 'parent_uuid'
|
20
|
+
before_destroy :add_destroyed_tag
|
21
|
+
attr_accessible :name if _ct.use_attr_accessible?
|
22
|
+
|
23
|
+
def set_uuid
|
24
|
+
self.uuid = UUIDTools::UUID.timestamp_create.to_s
|
25
|
+
end
|
7
26
|
|
8
27
|
def to_s
|
9
28
|
name
|
@@ -15,8 +34,10 @@ class Tag < ActiveRecord::Base
|
|
15
34
|
end
|
16
35
|
end
|
17
36
|
|
37
|
+
USE_ATTR_ACCESSIBLE = Tag._ct.use_attr_accessible?
|
38
|
+
|
18
39
|
class DestroyedTag < ActiveRecord::Base
|
19
|
-
attr_accessible :name
|
40
|
+
attr_accessible :name if USE_ATTR_ACCESSIBLE
|
20
41
|
end
|
21
42
|
|
22
43
|
class User < ActiveRecord::Base
|
@@ -31,7 +52,7 @@ class User < ActiveRecord::Base
|
|
31
52
|
Contract.where(:user_id => descendant_ids)
|
32
53
|
end
|
33
54
|
|
34
|
-
attr_accessible :email, :referrer
|
55
|
+
attr_accessible :email, :referrer if USE_ATTR_ACCESSIBLE
|
35
56
|
|
36
57
|
def to_s
|
37
58
|
email
|
@@ -43,7 +64,8 @@ class Contract < ActiveRecord::Base
|
|
43
64
|
end
|
44
65
|
|
45
66
|
class Label < ActiveRecord::Base
|
46
|
-
|
67
|
+
# make sure order doesn't matter
|
68
|
+
attr_accessible :name if USE_ATTR_ACCESSIBLE
|
47
69
|
acts_as_tree :order => :sort_order, # <- LOOK IT IS A SYMBOL OMG
|
48
70
|
:parent_column_name => "mother_id",
|
49
71
|
:dependent => :destroy
|
@@ -69,6 +91,6 @@ end
|
|
69
91
|
module Namespace
|
70
92
|
class Type < ActiveRecord::Base
|
71
93
|
acts_as_tree :dependent => :destroy
|
72
|
-
attr_accessible :name
|
94
|
+
attr_accessible :name if _ct.use_attr_accessible?
|
73
95
|
end
|
74
96
|
end
|
@@ -0,0 +1,421 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples_for "Tag (without fixtures)" do
|
4
|
+
|
5
|
+
let (:tag_class) { described_class }
|
6
|
+
let (:tag_hierarchy_class) { described_class.hierarchy_class }
|
7
|
+
|
8
|
+
context 'class setup' do
|
9
|
+
|
10
|
+
it 'has correct accessible_attributes' do
|
11
|
+
if tag_class._ct.use_attr_accessible?
|
12
|
+
tag_class.accessible_attributes.to_a.should =~ %w(parent name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should build hierarchy classname correctly' do
|
17
|
+
tag_class.hierarchy_class.should == tag_hierarchy_class
|
18
|
+
tag_class._ct.hierarchy_class_name.should == tag_hierarchy_class.to_s
|
19
|
+
tag_class._ct.short_hierarchy_class_name.should == tag_hierarchy_class.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should have a correct parent column name' do
|
23
|
+
expected_parent_column_name = tag_class == UUIDTag ? "parent_uuid" : "parent_id"
|
24
|
+
tag_class._ct.parent_column_name.should == expected_parent_column_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
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
|
+
|
38
|
+
context "with no tags" do
|
39
|
+
it "should return no entities" do
|
40
|
+
tag_class.roots.should be_empty
|
41
|
+
tag_class.leaves.should be_empty
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "with 1 tag" do
|
46
|
+
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]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "with 2 tags" do
|
54
|
+
before :each do
|
55
|
+
@root = tag_class.create!(:name => "root")
|
56
|
+
@leaf = @root.add_child(tag_class.create!(:name => "leaf"))
|
57
|
+
end
|
58
|
+
it "should return a simple root and leaf" do
|
59
|
+
tag_class.roots.should == [@root]
|
60
|
+
tag_class.leaves.should == [@leaf]
|
61
|
+
end
|
62
|
+
it "should return child_ids for root" do
|
63
|
+
@root.child_ids.should == [@leaf.id]
|
64
|
+
end
|
65
|
+
it "should return an empty array for leaves" do
|
66
|
+
@leaf.child_ids.should be_empty
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "3 tag collection.create db" do
|
71
|
+
before :each do
|
72
|
+
@root = tag_class.create! :name => "root"
|
73
|
+
@mid = @root.children.create! :name => "mid"
|
74
|
+
@leaf = @mid.children.create! :name => "leaf"
|
75
|
+
DestroyedTag.delete_all
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should create all tags" do
|
79
|
+
tag_class.all.to_a.should =~ [@root, @mid, @leaf]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should return a root and leaf without middle tag" do
|
83
|
+
tag_class.roots.should == [@root]
|
84
|
+
tag_class.leaves.should == [@leaf]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should delete leaves" do
|
88
|
+
tag_class.leaves.destroy_all
|
89
|
+
tag_class.roots.should == [@root] # untouched
|
90
|
+
tag_class.leaves.should == [@mid]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should delete everything if you delete the roots" do
|
94
|
+
tag_class.roots.destroy_all
|
95
|
+
tag_class.all.should be_empty
|
96
|
+
tag_class.roots.should be_empty
|
97
|
+
tag_class.leaves.should be_empty
|
98
|
+
DestroyedTag.all.map { |t| t.name }.should =~ %w{root mid leaf}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "3 tag explicit_create db" do
|
103
|
+
before :each do
|
104
|
+
@root = tag_class.create!(:name => "root")
|
105
|
+
@mid = @root.add_child(tag_class.create!(:name => "mid"))
|
106
|
+
@leaf = @mid.add_child(tag_class.create!(:name => "leaf"))
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should create all tags" do
|
110
|
+
tag_class.all.to_a.should =~ [@root, @mid, @leaf]
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should return a root and leaf without middle tag" do
|
114
|
+
tag_class.roots.should == [@root]
|
115
|
+
tag_class.leaves.should == [@leaf]
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should prevent parental loops from torso" do
|
119
|
+
@mid.children << @root
|
120
|
+
@root.valid?.should be_false
|
121
|
+
@mid.reload.children.should == [@leaf]
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should prevent parental loops from toes" do
|
125
|
+
@leaf.children << @root
|
126
|
+
@root.valid?.should be_false
|
127
|
+
@leaf.reload.children.should be_empty
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should support re-parenting" do
|
131
|
+
@root.children << @leaf
|
132
|
+
tag_class.leaves.should == [@leaf, @mid]
|
133
|
+
end
|
134
|
+
|
135
|
+
it "cleans up hierarchy references for leaves" do
|
136
|
+
@leaf.destroy
|
137
|
+
tag_hierarchy_class.where(:ancestor_id => @leaf.id).should be_empty
|
138
|
+
tag_hierarchy_class.where(:descendant_id => @leaf.id).should be_empty
|
139
|
+
end
|
140
|
+
|
141
|
+
it "cleans up hierarchy references" do
|
142
|
+
@mid.destroy
|
143
|
+
tag_hierarchy_class.where(:ancestor_id => @mid.id).should be_empty
|
144
|
+
tag_hierarchy_class.where(:descendant_id => @mid.id).should be_empty
|
145
|
+
@root.reload.should be_root
|
146
|
+
root_hiers = @root.ancestor_hierarchies.to_a
|
147
|
+
root_hiers.size.should == 1
|
148
|
+
tag_hierarchy_class.where(:ancestor_id => @root.id).should == root_hiers
|
149
|
+
tag_hierarchy_class.where(:descendant_id => @root.id).should == root_hiers
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should have different hash codes for each hierarchy model" do
|
153
|
+
hashes = tag_hierarchy_class.all.map(&:hash)
|
154
|
+
hashes.should =~ hashes.uniq
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should return the same hash code for equal hierarchy models" do
|
158
|
+
tag_hierarchy_class.first.hash.should == tag_hierarchy_class.first.hash
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it "performs as the readme says it does" do
|
163
|
+
grandparent = tag_class.create(:name => 'Grandparent')
|
164
|
+
parent = grandparent.children.create(:name => 'Parent')
|
165
|
+
child1 = tag_class.create(:name => 'First Child', :parent => parent)
|
166
|
+
child2 = tag_class.new(:name => 'Second Child')
|
167
|
+
parent.children << child2
|
168
|
+
child3 = tag_class.new(:name => 'Third Child')
|
169
|
+
parent.add_child child3
|
170
|
+
grandparent.self_and_descendants.collect(&:name).should ==
|
171
|
+
["Grandparent", "Parent", "First Child", "Second Child", "Third Child"]
|
172
|
+
child1.ancestry_path.should ==
|
173
|
+
["Grandparent", "Parent", "First Child"]
|
174
|
+
child3.ancestry_path.should ==
|
175
|
+
["Grandparent", "Parent", "Third Child"]
|
176
|
+
d = tag_class.find_or_create_by_path %w(a b c d)
|
177
|
+
h = tag_class.find_or_create_by_path %w(e f g h)
|
178
|
+
e = h.root
|
179
|
+
d.add_child(e) # "d.children << e" would work too, of course
|
180
|
+
h.ancestry_path.should == %w(a b c d e f g h)
|
181
|
+
end
|
182
|
+
|
183
|
+
it "roots sort alphabetically" do
|
184
|
+
expected = ("a".."z").to_a
|
185
|
+
expected.shuffle.each { |ea| tag_class.create!(:name => ea) }
|
186
|
+
tag_class.roots.collect { |ea| ea.name }.should == expected
|
187
|
+
end
|
188
|
+
|
189
|
+
context "with simple tree" do
|
190
|
+
before :each do
|
191
|
+
tag_class.find_or_create_by_path %w(a1 b1 c1a)
|
192
|
+
tag_class.find_or_create_by_path %w(a1 b1 c1b)
|
193
|
+
tag_class.find_or_create_by_path %w(a2 b2)
|
194
|
+
tag_class.find_or_create_by_path %w(a3)
|
195
|
+
|
196
|
+
@a1, @a2, @a3, @b1, @b2, @c1a, @c1b = tag_class.where(:name => %w(a1 a2 a3 b1 b2 c1a c1b)).reorder(:name).to_a
|
197
|
+
@expected_roots = [@a1, @a2, @a3]
|
198
|
+
@expected_leaves = [@c1a, @c1b, @b2, @a3]
|
199
|
+
end
|
200
|
+
it 'should find global roots' do
|
201
|
+
tag_class.roots.to_a.should =~ @expected_roots
|
202
|
+
end
|
203
|
+
it 'should return root? for roots' do
|
204
|
+
@expected_roots.each { |ea| ea.should be_root }
|
205
|
+
end
|
206
|
+
it 'should not return root? for non-roots' do
|
207
|
+
[@b1, @b2, @c1a, @c1b].each { |ea| ea.should_not be_root }
|
208
|
+
end
|
209
|
+
it 'should return the correct root' do
|
210
|
+
{@a1 => @a1, @a2 => @a2, @a3 => @a3,
|
211
|
+
@b1 => @a1, @b2 => @a2, @c1a => @a1, @c1b => @a1}.each do |node, root|
|
212
|
+
node.root.should == root
|
213
|
+
end
|
214
|
+
end
|
215
|
+
it 'should assemble global leaves' do
|
216
|
+
tag_class.leaves.to_a.should =~ @expected_leaves
|
217
|
+
end
|
218
|
+
it 'should assemble instance leaves' do
|
219
|
+
{@a1 => [@c1a, @c1b], @b1 => [@c1a, @c1b], @a2 => [@b2]}.each do |node, leaves|
|
220
|
+
node.leaves.to_a.should == leaves
|
221
|
+
end
|
222
|
+
@expected_leaves.each { |ea| ea.leaves.to_a.should == [ea] }
|
223
|
+
end
|
224
|
+
it 'should return leaf? for leaves' do
|
225
|
+
@expected_leaves.each { |ea| ea.should be_leaf }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context "paths" do
|
230
|
+
before :each do
|
231
|
+
@child = tag_class.find_or_create_by_path(%w(grandparent parent child))
|
232
|
+
@child.title = "Kid"
|
233
|
+
@parent = @child.parent
|
234
|
+
@parent.title = "Mom"
|
235
|
+
@grandparent = @parent.parent
|
236
|
+
@grandparent.title = "Nonnie"
|
237
|
+
[@child, @parent, @grandparent].each { |ea| ea.save! }
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should build ancestry path" do
|
241
|
+
@child.ancestry_path.should == %w{grandparent parent child}
|
242
|
+
@child.ancestry_path(:name).should == %w{grandparent parent child}
|
243
|
+
@child.ancestry_path(:title).should == %w{Nonnie Mom Kid}
|
244
|
+
end
|
245
|
+
|
246
|
+
it "should find by path" do
|
247
|
+
# class method:
|
248
|
+
tag_class.find_by_path(%w{grandparent parent child}).should == @child
|
249
|
+
# instance method:
|
250
|
+
@parent.find_by_path(%w{child}).should == @child
|
251
|
+
@grandparent.find_by_path(%w{parent child}).should == @child
|
252
|
+
@parent.find_by_path(%w{child larvae}).should be_nil
|
253
|
+
end
|
254
|
+
|
255
|
+
it "finds correctly rooted paths" do
|
256
|
+
decoy = tag_class.find_or_create_by_path %w(a b c d)
|
257
|
+
b_d = tag_class.find_or_create_by_path %w(b c d)
|
258
|
+
tag_class.find_by_path(%w(b c d)).should == b_d
|
259
|
+
tag_class.find_by_path(%w(c d)).should be_nil
|
260
|
+
end
|
261
|
+
|
262
|
+
it "find_by_path for 1 node" do
|
263
|
+
b = tag_class.find_or_create_by_path %w(a b)
|
264
|
+
b2 = b.root.find_by_path(%w(b))
|
265
|
+
b2.should == b
|
266
|
+
end
|
267
|
+
|
268
|
+
it "find_by_path for 2 nodes" do
|
269
|
+
c = tag_class.find_or_create_by_path %w(a b c)
|
270
|
+
c.root.find_by_path(%w(b c)).should == c
|
271
|
+
c.root.find_by_path(%w(a c)).should be_nil
|
272
|
+
c.root.find_by_path(%w(c)).should be_nil
|
273
|
+
end
|
274
|
+
|
275
|
+
it "find_by_path for 3 nodes" do
|
276
|
+
d = tag_class.find_or_create_by_path %w(a b c d)
|
277
|
+
d.root.find_by_path(%w(b c d)).should == d
|
278
|
+
tag_class.find_by_path(%w(a b c d)).should == d
|
279
|
+
tag_class.find_by_path(%w(d)).should be_nil
|
280
|
+
end
|
281
|
+
|
282
|
+
it "should return nil for missing nodes" do
|
283
|
+
tag_class.find_by_path(%w{missing}).should be_nil
|
284
|
+
tag_class.find_by_path(%w{grandparent missing}).should be_nil
|
285
|
+
tag_class.find_by_path(%w{grandparent parent missing}).should be_nil
|
286
|
+
tag_class.find_by_path(%w{grandparent parent missing child}).should be_nil
|
287
|
+
end
|
288
|
+
|
289
|
+
it "should find or create by path" do
|
290
|
+
# class method:
|
291
|
+
grandparent = tag_class.find_or_create_by_path(%w{grandparent})
|
292
|
+
grandparent.should == @grandparent
|
293
|
+
child = tag_class.find_or_create_by_path(%w{grandparent parent child})
|
294
|
+
child.should == @child
|
295
|
+
tag_class.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
|
296
|
+
a = tag_class.find_or_create_by_path(%w{a})
|
297
|
+
a.ancestry_path.should == %w{a}
|
298
|
+
# instance method:
|
299
|
+
a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
context "hash_tree" do
|
304
|
+
|
305
|
+
before :each do
|
306
|
+
@b = tag_class.find_or_create_by_path %w(a b)
|
307
|
+
@a = @b.parent
|
308
|
+
@b2 = tag_class.find_or_create_by_path %w(a b2)
|
309
|
+
@d1 = @b.find_or_create_by_path %w(c1 d1)
|
310
|
+
@c1 = @d1.parent
|
311
|
+
@d2 = @b.find_or_create_by_path %w(c2 d2)
|
312
|
+
@c2 = @d2.parent
|
313
|
+
@full_tree = {@a => {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}, @b2 => {}}}
|
314
|
+
end
|
315
|
+
|
316
|
+
context "#hash_tree" do
|
317
|
+
it "returns {} for depth 0" do
|
318
|
+
tag_class.hash_tree(:limit_depth => 0).should == {}
|
319
|
+
end
|
320
|
+
it "limit_depth 1" do
|
321
|
+
tag_class.hash_tree(:limit_depth => 1).should == {@a => {}}
|
322
|
+
end
|
323
|
+
it "limit_depth 2" do
|
324
|
+
tag_class.hash_tree(:limit_depth => 2).should == {@a => {@b => {}, @b2 => {}}}
|
325
|
+
end
|
326
|
+
it "limit_depth 3" do
|
327
|
+
tag_class.hash_tree(:limit_depth => 3).should == {@a => {@b => {@c1 => {}, @c2 => {}}, @b2 => {}}}
|
328
|
+
end
|
329
|
+
it "limit_depth 4" do
|
330
|
+
tag_class.hash_tree(:limit_depth => 4).should == @full_tree
|
331
|
+
end
|
332
|
+
it "no limit holdum" do
|
333
|
+
tag_class.hash_tree.should == @full_tree
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def assert_no_dupes(scope)
|
338
|
+
# the named scope is complicated enough that an incorrect join could result in unnecessarily
|
339
|
+
# duplicated rows:
|
340
|
+
a = scope.collect { |ea| ea.id }
|
341
|
+
a.should == a.uniq
|
342
|
+
end
|
343
|
+
|
344
|
+
context "#hash_tree_scope" do
|
345
|
+
it "no dupes for any depth" do
|
346
|
+
(0..5).each do |ea|
|
347
|
+
assert_no_dupes(tag_class.hash_tree_scope(ea))
|
348
|
+
end
|
349
|
+
end
|
350
|
+
it "no limit holdum" do
|
351
|
+
assert_no_dupes(tag_class.hash_tree_scope)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
context ".hash_tree_scope" do
|
356
|
+
it "no dupes for any depth" do
|
357
|
+
(0..5).each do |ea|
|
358
|
+
assert_no_dupes(@a.hash_tree_scope(ea))
|
359
|
+
end
|
360
|
+
end
|
361
|
+
it "no limit holdum" do
|
362
|
+
assert_no_dupes(@a.hash_tree_scope)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
context ".hash_tree" do
|
367
|
+
before :each do
|
368
|
+
end
|
369
|
+
it "returns {} for depth 0" do
|
370
|
+
@b.hash_tree(:limit_depth => 0).should == {}
|
371
|
+
end
|
372
|
+
it "limit_depth 1" do
|
373
|
+
@b.hash_tree(:limit_depth => 1).should == {@b => {}}
|
374
|
+
end
|
375
|
+
it "limit_depth 2" do
|
376
|
+
@b.hash_tree(:limit_depth => 2).should == {@b => {@c1 => {}, @c2 => {}}}
|
377
|
+
end
|
378
|
+
it "limit_depth 3" do
|
379
|
+
@b.hash_tree(:limit_depth => 3).should == {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}}
|
380
|
+
end
|
381
|
+
it "no limit holdum from subsubroot" do
|
382
|
+
@c1.hash_tree.should == {@c1 => {@d1 => {}}}
|
383
|
+
end
|
384
|
+
it "no limit holdum from subroot" do
|
385
|
+
@b.hash_tree.should == {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}}
|
386
|
+
end
|
387
|
+
it "no limit holdum from root" do
|
388
|
+
@a.hash_tree.should == @full_tree
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe 'DOT rendering' do
|
394
|
+
it 'should render for an empty scope' do
|
395
|
+
tag_class.to_dot_digraph(tag_class.where("0=1")).should == "digraph G {\n}\n"
|
396
|
+
end
|
397
|
+
it 'should render for an empty scope' do
|
398
|
+
tag_class.find_or_create_by_path(%w(a b1 c1))
|
399
|
+
tag_class.find_or_create_by_path(%w(a b2 c2))
|
400
|
+
tag_class.find_or_create_by_path(%w(a b2 c3))
|
401
|
+
a, b1, b2, c1, c2, c3 = %w(a b1 b2 c1 c2 c3).map { |ea| tag_class.where(:name => ea).first.id }
|
402
|
+
dot = tag_class.roots.first.to_dot_digraph
|
403
|
+
dot.should == <<-DOT
|
404
|
+
digraph G {
|
405
|
+
#{a} [label="a"]
|
406
|
+
#{a} -> #{b1}
|
407
|
+
#{b1} [label="b1"]
|
408
|
+
#{a} -> #{b2}
|
409
|
+
#{b2} [label="b2"]
|
410
|
+
#{b1} -> #{c1}
|
411
|
+
#{c1} [label="c1"]
|
412
|
+
#{b2} -> #{c2}
|
413
|
+
#{c2} [label="c2"]
|
414
|
+
#{b2} -> #{c3}
|
415
|
+
#{c3} [label="c3"]
|
416
|
+
}
|
417
|
+
DOT
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|