closure_tree 4.0.1 → 4.1.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.
- 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
|