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.
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "4.0.1" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = Gem::Version.new("4.1.0") unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -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 "tags_uuid", :id => false, :force => true do |t|
30
- t.string "id", :unique => true
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 "parent_id"
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 "tag_hierarchies_uuid", :id => false, :force => true do |t|
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
@@ -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 == %w(b2 e2 d2 c1-six c1-seven c1-eight c1-nine)
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
 
@@ -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.find_all_by_name(ea).size.should == @iterations
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.find_all_by_name(@names).size.should == @iterations
52
+ Tag.where(:name => @names).size.should == @iterations
46
53
  %w(a b c).each do |ea|
47
- Tag.find_all_by_name(ea).size.should == @iterations
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.find_all_by_name(@names).size.should > @iterations
62
+ Tag.where(:name => @names).size.should > @iterations
56
63
  end
57
64
 
58
- # SQLite doesn't like parallelism, and Rails 3.0 and 3.1 have known threading issues. SKIP.
59
- end if ((ENV["DB"] != "sqlite") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
65
+ end unless parallelism_is_broken
@@ -2,12 +2,14 @@ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
2
  plugin_test_dir = File.dirname(__FILE__)
3
3
 
4
4
  require 'rubygems'
5
- require 'bundler/setup'
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 'logger'
9
-
10
- require 'action_controller' # rspec-rails needs this :(
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
- require 'rspec/rails' # TODO: clean this up-- I don't want to pull the elephant through the mouse hole just for fixture support
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
 
@@ -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
- attr_accessible :name # <- make sure order doesn't matter
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