closure_tree 7.4.0 → 8.0.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -56
  3. data/.github/workflows/ci_jruby.yml +68 -0
  4. data/.github/workflows/ci_truffleruby.yml +71 -0
  5. data/.github/workflows/release.yml +17 -0
  6. data/.gitignore +1 -1
  7. data/.release-please-manifest.json +1 -0
  8. data/.tool-versions +1 -0
  9. data/Appraisals +9 -53
  10. data/CHANGELOG.md +5 -0
  11. data/Gemfile +2 -3
  12. data/README.md +21 -9
  13. data/Rakefile +11 -16
  14. data/closure_tree.gemspec +16 -9
  15. data/lib/closure_tree/active_record_support.rb +3 -14
  16. data/lib/closure_tree/digraphs.rb +1 -1
  17. data/lib/closure_tree/finders.rb +1 -1
  18. data/lib/closure_tree/hash_tree.rb +1 -1
  19. data/lib/closure_tree/hierarchy_maintenance.rb +3 -6
  20. data/lib/closure_tree/model.rb +3 -3
  21. data/lib/closure_tree/numeric_deterministic_ordering.rb +3 -8
  22. data/lib/closure_tree/support.rb +3 -7
  23. data/lib/closure_tree/version.rb +1 -1
  24. data/lib/generators/closure_tree/migration_generator.rb +1 -4
  25. data/release-please-config.json +4 -0
  26. data/test/closure_tree/cache_invalidation_test.rb +36 -0
  27. data/test/closure_tree/cuisine_type_test.rb +42 -0
  28. data/test/closure_tree/generator_test.rb +49 -0
  29. data/test/closure_tree/has_closure_tree_root_test.rb +80 -0
  30. data/test/closure_tree/hierarchy_maintenance_test.rb +56 -0
  31. data/test/closure_tree/label_test.rb +674 -0
  32. data/test/closure_tree/metal_test.rb +59 -0
  33. data/test/closure_tree/model_test.rb +9 -0
  34. data/test/closure_tree/namespace_type_test.rb +13 -0
  35. data/test/closure_tree/parallel_test.rb +162 -0
  36. data/test/closure_tree/pool_test.rb +33 -0
  37. data/test/closure_tree/support_test.rb +18 -0
  38. data/test/closure_tree/tag_test.rb +8 -0
  39. data/test/closure_tree/user_test.rb +175 -0
  40. data/test/closure_tree/uuid_tag_test.rb +8 -0
  41. data/test/support/query_counter.rb +25 -0
  42. data/test/support/tag_examples.rb +923 -0
  43. data/test/test_helper.rb +99 -0
  44. metadata +52 -21
  45. data/_config.yml +0 -1
  46. data/tests.sh +0 -11
@@ -0,0 +1,923 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TagExamples
4
+ def self.included(mod)
5
+ @@described_class = mod.name.safe_constantize
6
+ end
7
+
8
+ describe 'TagExamples' do
9
+ before do
10
+ @tag_class = @@described_class
11
+ @tag_hierarchy_class = @@described_class.hierarchy_class
12
+ end
13
+
14
+ describe 'class setup' do
15
+ it 'has correct accessible_attributes' do
16
+ if @tag_class._ct.use_attr_accessible?
17
+ assert_equal(%w[parent name title].sort, @tag_class.accessible_attributes.to_a.sort)
18
+ end
19
+ end
20
+
21
+ it 'should build hierarchy classname correctly' do
22
+ assert_equal @tag_hierarchy_class, @tag_class.hierarchy_class
23
+ assert_equal @tag_hierarchy_class.to_s, @tag_class._ct.hierarchy_class_name
24
+ assert_equal @tag_hierarchy_class.to_s, @tag_class._ct.short_hierarchy_class_name
25
+ end
26
+
27
+ it 'should have a correct parent column name' do
28
+ expected_parent_column_name = @tag_class == UUIDTag ? 'parent_uuid' : 'parent_id'
29
+ assert_equal expected_parent_column_name, @tag_class._ct.parent_column_name
30
+ end
31
+ end
32
+
33
+ describe 'from empty db' do
34
+ describe 'with no tags' do
35
+ it 'should return no entities' do
36
+ assert_empty @tag_class.roots
37
+ assert_empty @tag_class.leaves
38
+ end
39
+
40
+ it '#find_or_create_by_path with strings' do
41
+ a = @tag_class.create!(name: 'a')
42
+ assert_equal(%w[a b c], a.find_or_create_by_path(%w[b c]).ancestry_path)
43
+ end
44
+
45
+ it '#find_or_create_by_path with hashes' do
46
+ a = @tag_class.create!(name: 'a', title: 'A')
47
+ subject = a.find_or_create_by_path([
48
+ { name: 'b', title: 'B' },
49
+ { name: 'c', title: 'C' }
50
+ ])
51
+ assert_equal(%w[a b c], subject.ancestry_path)
52
+ assert_equal(%w[C B A], subject.self_and_ancestors.map(&:title))
53
+ end
54
+ end
55
+
56
+ describe 'with 1 tag' do
57
+ before do
58
+ @tag = @tag_class.create!(name: 'tag')
59
+ end
60
+
61
+ it 'should be a leaf' do
62
+ assert @tag.leaf?
63
+ end
64
+
65
+ it 'should be a root' do
66
+ assert @tag.root?
67
+ end
68
+
69
+ it 'has no parent' do
70
+ assert_nil @tag.parent
71
+ end
72
+
73
+ it 'should return the only entity as a root and leaf' do
74
+ assert_equal [@tag], @tag_class.all
75
+ assert_equal [@tag], @tag_class.roots
76
+ assert_equal [@tag], @tag_class.leaves
77
+ end
78
+
79
+ it 'should not be found by passing find_by_path an array of blank strings' do
80
+ assert_nil @tag_class.find_by_path([''])
81
+ end
82
+
83
+ it 'should not be found by passing find_by_path an empty array' do
84
+ assert_nil @tag_class.find_by_path([])
85
+ end
86
+
87
+ it 'should not be found by passing find_by_path nil' do
88
+ assert_nil @tag_class.find_by_path(nil)
89
+ end
90
+
91
+ it 'should not be found by passing find_by_path an empty string' do
92
+ assert_nil @tag_class.find_by_path('')
93
+ end
94
+
95
+ it 'should not be found by passing find_by_path an array of nils' do
96
+ assert_nil @tag_class.find_by_path([nil])
97
+ end
98
+
99
+ it 'should not be found by passing find_by_path an array with an additional blank string' do
100
+ assert_nil @tag_class.find_by_path([@tag.name, ''])
101
+ end
102
+
103
+ it 'should not be found by passing find_by_path an array with an additional nil' do
104
+ assert_nil @tag_class.find_by_path([@tag.name, nil])
105
+ end
106
+
107
+ it 'should be found by passing find_by_path an array with its name' do
108
+ assert_equal @tag, @tag_class.find_by_path([@tag.name])
109
+ end
110
+
111
+ it 'should be found by passing find_by_path its name' do
112
+ assert_equal @tag, @tag_class.find_by_path(@tag.name)
113
+ end
114
+
115
+ describe 'with child' do
116
+ before do
117
+ @child = @tag_class.create!(name: 'tag 2')
118
+ end
119
+
120
+ def assert_roots_and_leaves
121
+ assert @tag.root?
122
+ refute @tag.leaf?
123
+
124
+ refute @child.root?
125
+ assert @child.leaf?
126
+ end
127
+
128
+ def assert_parent_and_children
129
+ assert_equal @tag, @child.reload.parent
130
+ assert_equal [@child], @tag.reload.children.to_a
131
+ end
132
+
133
+ it 'adds children through add_child' do
134
+ @tag.add_child @child
135
+ assert_roots_and_leaves
136
+ assert_parent_and_children
137
+ end
138
+
139
+ it 'adds children through collection' do
140
+ @tag.children << @child
141
+ assert_roots_and_leaves
142
+ assert_parent_and_children
143
+ end
144
+ end
145
+ end
146
+
147
+ describe 'with 2 tags' do
148
+ before do
149
+ @root = @tag_class.create!(name: 'root')
150
+ @leaf = @root.add_child(@tag_class.create!(name: 'leaf'))
151
+ end
152
+
153
+ it 'should return a simple root and leaf' do
154
+ assert_equal [@root], @tag_class.roots
155
+ assert_equal [@leaf], @tag_class.leaves
156
+ end
157
+
158
+ it 'should return child_ids for root' do
159
+ assert_equal [@leaf.id], @root.child_ids
160
+ end
161
+
162
+ it 'should return an empty array for leaves' do
163
+ assert_empty @leaf.child_ids
164
+ end
165
+ end
166
+
167
+ describe '3 tag collection.create db' do
168
+ before do
169
+ @root = @tag_class.create! name: 'root'
170
+ @mid = @root.children.create! name: 'mid'
171
+ @leaf = @mid.children.create! name: 'leaf'
172
+ DestroyedTag.delete_all
173
+ end
174
+
175
+ it 'should create all tags' do
176
+ assert_equal [@root, @mid, @leaf].sort, @tag_class.all.to_a.sort
177
+ end
178
+
179
+ it 'should return a root and leaf without middle tag' do
180
+ assert_equal [@root], @tag_class.roots
181
+ assert_equal [@leaf], @tag_class.leaves
182
+ end
183
+
184
+ it 'should delete leaves' do
185
+ @tag_class.leaves.destroy_all
186
+ assert_equal [@root], @tag_class.roots # untouched
187
+ assert_equal [@mid], @tag_class.leaves
188
+ end
189
+
190
+ it 'should delete everything if you delete the roots' do
191
+ @tag_class.roots.destroy_all
192
+ assert_empty @tag_class.all
193
+ assert_empty @tag_class.roots
194
+ assert_empty @tag_class.leaves
195
+ assert_equal %w[root mid leaf].sort, DestroyedTag.all.map(&:name).sort
196
+ end
197
+
198
+ it 'fix self_and_ancestors properly on reparenting' do
199
+ t = @tag_class.create! name: 'moar leaf'
200
+ assert_equal [t], t.self_and_ancestors.to_a
201
+ @mid.children << t
202
+ assert_equal [t, @mid, @root], t.self_and_ancestors.to_a
203
+ end
204
+
205
+ it 'prevents ancestor loops' do
206
+ @leaf.add_child @root
207
+ refute @root.valid?
208
+ assert_includes @root.reload.descendants, @leaf
209
+ end
210
+
211
+ it 'moves non-leaves' do
212
+ new_root = @tag_class.create! name: 'new_root'
213
+ new_root.children << @mid
214
+ assert_empty @root.reload.descendants
215
+ assert_equal [@mid, @leaf], new_root.descendants
216
+ assert_equal %w[new_root mid leaf], @leaf.reload.ancestry_path
217
+ end
218
+
219
+ it 'moves leaves' do
220
+ new_root = @tag_class.create! name: 'new_root'
221
+ new_root.children << @leaf
222
+ assert_equal [@leaf], new_root.descendants
223
+ assert_equal [@mid], @root.reload.descendants
224
+ assert_equal %w[new_root leaf], @leaf.reload.ancestry_path
225
+ end
226
+ end
227
+
228
+ describe '3 tag explicit_create db' do
229
+ before do
230
+ @root = @tag_class.create!(name: 'root')
231
+ @mid = @root.add_child(@tag_class.create!(name: 'mid'))
232
+ @leaf = @mid.add_child(@tag_class.create!(name: 'leaf'))
233
+ end
234
+
235
+ it 'should create all tags' do
236
+ assert_equal [@root, @mid, @leaf].sort, @tag_class.all.to_a.sort
237
+ end
238
+
239
+ it 'should return a root and leaf without middle tag' do
240
+ assert_equal [@root], @tag_class.roots
241
+ assert_equal [@leaf], @tag_class.leaves
242
+ end
243
+
244
+ it 'should prevent parental loops from torso' do
245
+ @mid.children << @root
246
+ refute @root.valid?
247
+ assert_equal [@leaf], @mid.reload.children
248
+ end
249
+
250
+ it 'should prevent parental loops from toes' do
251
+ @leaf.children << @root
252
+ refute @root.valid?
253
+ assert_empty @leaf.reload.children
254
+ end
255
+
256
+ it 'should support re-parenting' do
257
+ @root.children << @leaf
258
+ assert_equal [@leaf, @mid], @tag_class.leaves
259
+ end
260
+
261
+ it 'cleans up hierarchy references for leaves' do
262
+ @leaf.destroy
263
+ assert_empty @tag_hierarchy_class.where(ancestor_id: @leaf.id)
264
+ assert_empty @tag_hierarchy_class.where(descendant_id: @leaf.id)
265
+ end
266
+
267
+ it 'cleans up hierarchy references' do
268
+ @mid.destroy
269
+ assert_empty @tag_hierarchy_class.where(ancestor_id: @mid.id)
270
+ assert_empty @tag_hierarchy_class.where(descendant_id: @mid.id)
271
+ assert @root.reload.root?
272
+ root_hiers = @root.ancestor_hierarchies.to_a
273
+ assert_equal 1, root_hiers.size
274
+ assert_equal root_hiers, @tag_hierarchy_class.where(ancestor_id: @root.id)
275
+ assert_equal root_hiers, @tag_hierarchy_class.where(descendant_id: @root.id)
276
+ end
277
+
278
+ it 'should have different hash codes for each hierarchy model' do
279
+ hashes = @tag_hierarchy_class.all.map(&:hash)
280
+ assert_equal hashes.uniq.sort, hashes.sort
281
+ end
282
+
283
+ it 'should return the same hash code for equal hierarchy models' do
284
+ assert_equal @tag_hierarchy_class.first.hash, @tag_hierarchy_class.first.hash
285
+ end
286
+ end
287
+
288
+ it 'performs as the readme says it does' do
289
+ grandparent = @tag_class.create(name: 'Grandparent')
290
+ parent = grandparent.children.create(name: 'Parent')
291
+ child1 = @tag_class.create(name: 'First Child', parent: parent)
292
+ child2 = @tag_class.new(name: 'Second Child')
293
+ parent.children << child2
294
+ child3 = @tag_class.new(name: 'Third Child')
295
+ parent.add_child child3
296
+ assert_equal(
297
+ ['Grandparent', 'Parent', 'First Child', 'Second Child', 'Third Child'],
298
+ grandparent.self_and_descendants.collect(&:name)
299
+ )
300
+ assert_equal(['Grandparent', 'Parent', 'First Child'], child1.ancestry_path)
301
+ assert_equal(['Grandparent', 'Parent', 'Third Child'], child3.ancestry_path)
302
+ d = @tag_class.find_or_create_by_path %w[a b c d]
303
+ h = @tag_class.find_or_create_by_path %w[e f g h]
304
+ e = h.root
305
+ d.add_child(e) # "d.children << e" would work too, of course
306
+ assert_equal %w[a b c d e f g h], h.ancestry_path
307
+ end
308
+
309
+ it 'roots sort alphabetically' do
310
+ expected = ('a'..'z').to_a
311
+ expected.shuffle.each { |ea| @tag_class.create!(name: ea) }
312
+ assert_equal expected, @tag_class.roots.collect(&:name)
313
+ end
314
+
315
+ describe 'with simple tree' do
316
+ before do
317
+ @tag_class.find_or_create_by_path %w[a1 b1 c1a]
318
+ @tag_class.find_or_create_by_path %w[a1 b1 c1b]
319
+ @tag_class.find_or_create_by_path %w[a1 b1 c1c]
320
+ @tag_class.find_or_create_by_path %w[a1 b1b]
321
+ @tag_class.find_or_create_by_path %w[a2 b2]
322
+ @tag_class.find_or_create_by_path %w[a3]
323
+
324
+ @a1, @a2, @a3, @b1, @b1b, @b2, @c1a, @c1b, @c1c = @tag_class.all.sort_by(&:name)
325
+ @expected_roots = [@a1, @a2, @a3]
326
+ @expected_leaves = [@c1a, @c1b, @c1c, @b1b, @b2, @a3]
327
+ @expected_siblings = [[@a1, @a2, @a3], [@b1, @b1b], [@c1a, @c1b, @c1c]]
328
+ @expected_only_children = @tag_class.all - @expected_siblings.flatten
329
+ end
330
+
331
+ it 'should find global roots' do
332
+ assert_equal @expected_roots.sort, @tag_class.roots.to_a.sort
333
+ end
334
+
335
+ it 'should return root? for roots' do
336
+ @expected_roots.each { |ea| assert(ea.root?) }
337
+ end
338
+
339
+ it 'should not return root? for non-roots' do
340
+ [@b1, @b2, @c1a, @c1b].each { |ea| refute(ea.root?) }
341
+ end
342
+
343
+ it 'should return the correct root' do
344
+ { @a1 => @a1, @a2 => @a2, @a3 => @a3,
345
+ @b1 => @a1, @b2 => @a2, @c1a => @a1, @c1b => @a1 }.each do |node, root|
346
+ assert_equal(root, node.root)
347
+ end
348
+ end
349
+
350
+ it 'should assemble global leaves' do
351
+ assert_equal @expected_leaves.sort, @tag_class.leaves.to_a.sort
352
+ end
353
+
354
+ it 'assembles siblings properly' do
355
+ @expected_siblings.each do |siblings|
356
+ siblings.each do |ea|
357
+ assert_equal siblings.sort, ea.self_and_siblings.to_a.sort
358
+ assert_equal((siblings - [ea]).sort, ea.siblings.to_a.sort)
359
+ end
360
+ end
361
+
362
+ @expected_only_children.each do |ea|
363
+ assert_equal [], ea.siblings
364
+ end
365
+ end
366
+
367
+ it 'assembles before_siblings' do
368
+ @expected_siblings.each do |siblings|
369
+ (siblings.size - 1).times do |i|
370
+ target = siblings[i]
371
+ expected_before = siblings.first(i)
372
+ assert_equal expected_before, target.siblings_before.to_a
373
+ end
374
+ end
375
+ end
376
+
377
+ it 'assembles after_siblings' do
378
+ @expected_siblings.each do |siblings|
379
+ (siblings.size - 1).times do |i|
380
+ target = siblings[i]
381
+ expected_after = siblings.last(siblings.size - 1 - i)
382
+ assert_equal expected_after, target.siblings_after.to_a
383
+ end
384
+ end
385
+ end
386
+
387
+ it 'should assemble instance leaves' do
388
+ { @a1 => [@b1b, @c1a, @c1b, @c1c], @b1 => [@c1a, @c1b, @c1c], @a2 => [@b2] }.each do |node, leaves|
389
+ assert_equal leaves, node.leaves.to_a
390
+ end
391
+
392
+ @expected_leaves.each { |ea| assert_equal [ea], ea.leaves.to_a }
393
+ end
394
+
395
+ it 'should return leaf? for leaves' do
396
+ @expected_leaves.each { |ea| assert ea.leaf? }
397
+ end
398
+
399
+ it 'can move roots' do
400
+ @c1a.children << @a2
401
+ @b2.reload.children << @a3
402
+ assert_equal %w[a1 b1 c1a a2 b2 a3], @a3.reload.ancestry_path
403
+ end
404
+
405
+ it 'cascade-deletes from roots' do
406
+ victim_names = @a1.self_and_descendants.map(&:name)
407
+ survivor_names = @tag_class.all.map(&:name) - victim_names
408
+ @a1.destroy
409
+ assert_equal survivor_names, @tag_class.all.map(&:name)
410
+ end
411
+ end
412
+
413
+ describe 'with_ancestor' do
414
+ it 'works with no rows' do
415
+ assert_empty @tag_class.with_ancestor.to_a
416
+ end
417
+
418
+ it 'finds only children' do
419
+ c = @tag_class.find_or_create_by_path %w[A B C]
420
+ a = c.parent.parent
421
+ b = c.parent
422
+ @tag_class.find_or_create_by_path %w[D E]
423
+ assert_equal [b, c], @tag_class.with_ancestor(a).to_a
424
+ end
425
+
426
+ it 'limits subsequent where clauses' do
427
+ a1c = @tag_class.find_or_create_by_path %w[A1 B C]
428
+ a2c = @tag_class.find_or_create_by_path %w[A2 B C]
429
+ # different paths!
430
+ refute_equal a2c, a1c
431
+ assert_equal [a1c, a2c].sort, @tag_class.where(name: 'C').to_a.sort
432
+ assert_equal [a1c], @tag_class.with_ancestor(a1c.parent.parent).where(name: 'C').to_a.sort
433
+ end
434
+ end
435
+
436
+ describe 'with_descendant' do
437
+ it 'works with no rows' do
438
+ assert_empty @tag_class.with_descendant.to_a
439
+ end
440
+
441
+ it 'finds only parents' do
442
+ c = @tag_class.find_or_create_by_path %w[A B C]
443
+ a = c.parent.parent
444
+ b = c.parent
445
+ _spurious_tags = @tag_class.find_or_create_by_path %w[D E]
446
+ assert_equal [a, b], @tag_class.with_descendant(c).to_a
447
+ end
448
+
449
+ it 'limits subsequent where clauses' do
450
+ ac1 = @tag_class.create(name: 'A')
451
+ ac2 = @tag_class.create(name: 'A')
452
+
453
+ c1 = @tag_class.find_or_create_by_path %w[B C1]
454
+ ac1.children << c1.parent
455
+
456
+ c2 = @tag_class.find_or_create_by_path %w[B C2]
457
+ ac2.children << c2.parent
458
+
459
+ # different paths!
460
+ refute_equal ac2, ac1
461
+ assert_equal [ac1, ac2].sort, @tag_class.where(name: 'A').to_a.sort
462
+ assert_equal [ac1], @tag_class.with_descendant(c1).where(name: 'A').to_a
463
+ end
464
+ end
465
+
466
+ describe 'lowest_common_ancestor' do
467
+ before do
468
+ @t1 = @tag_class.create!(name: 't1')
469
+ @t11 = @tag_class.create!(name: 't11', parent: @t1)
470
+ @t111 = @tag_class.create!(name: 't111', parent: @t11)
471
+ @t112 = @tag_class.create!(name: 't112', parent: @t11)
472
+ @t12 = @tag_class.create!(name: 't12', parent: @t1)
473
+ @t121 = @tag_class.create!(name: 't121', parent: @t12)
474
+ @t2 = @tag_class.create!(name: 't2')
475
+ @t21 = @tag_class.create!(name: 't21', parent: @t2)
476
+ @t21 = @tag_class.create!(name: 't21', parent: @t2)
477
+ @t211 = @tag_class.create!(name: 't211', parent: @t21)
478
+ end
479
+
480
+ it 'finds the parent for siblings' do
481
+ assert_equal @t11, @tag_class.lowest_common_ancestor(@t112, @t111)
482
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@t12, @t11)
483
+
484
+ assert_equal @t11, @tag_class.lowest_common_ancestor([@t112, @t111])
485
+ assert_equal @t1, @tag_class.lowest_common_ancestor([@t12, @t11])
486
+
487
+ assert_equal @t11, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t112 t111]))
488
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t11]))
489
+ end
490
+
491
+ it 'finds the grandparent for cousins' do
492
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@t112, @t111, @t121)
493
+ assert_equal @t1, @tag_class.lowest_common_ancestor([@t112, @t111, @t121])
494
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t112 t111 t121]))
495
+ end
496
+
497
+ it 'finds the parent/grandparent for aunt-uncle/niece-nephew' do
498
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@t12, @t112)
499
+ assert_equal @t1, @tag_class.lowest_common_ancestor([@t12, @t112])
500
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t112]))
501
+ end
502
+
503
+ it 'finds the self/parent for parent/child' do
504
+ assert_equal @t12, @tag_class.lowest_common_ancestor(@t12, @t121)
505
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@t1, @t12)
506
+
507
+ assert_equal @t12, @tag_class.lowest_common_ancestor([@t12, @t121])
508
+ assert_equal @t1, @tag_class.lowest_common_ancestor([@t1, @t12])
509
+
510
+ assert_equal @t12, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t121]))
511
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t1 t12]))
512
+ end
513
+
514
+ it 'finds the self/grandparent for grandparent/grandchild' do
515
+ assert_equal @t2, @tag_class.lowest_common_ancestor(@t211, @t2)
516
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@t111, @t1)
517
+
518
+ assert_equal @t2, @tag_class.lowest_common_ancestor([@t211, @t2])
519
+ assert_equal @t1, @tag_class.lowest_common_ancestor([@t111, @t1])
520
+
521
+ assert_equal @t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t211 t2]))
522
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t111 t1]))
523
+ end
524
+
525
+ it 'finds the grandparent for a whole extended family' do
526
+ assert_equal @t1, @tag_class.lowest_common_ancestor(@t1, @t11, @t111, @t112, @t12, @t121)
527
+ assert_equal @t2, @tag_class.lowest_common_ancestor(@t2, @t21, @t211)
528
+
529
+ assert_equal @t1, @tag_class.lowest_common_ancestor([@t1, @t11, @t111, @t112, @t12, @t121])
530
+ assert_equal @t2, @tag_class.lowest_common_ancestor([@t2, @t21, @t211])
531
+
532
+ assert_equal @t1,
533
+ @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t1 t11 t111 t112 t12 t121]))
534
+ assert_equal @t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t2 t21 t211]))
535
+ end
536
+
537
+ it 'is nil for no items' do
538
+ assert_nil @tag_class.lowest_common_ancestor
539
+ assert_nil @tag_class.lowest_common_ancestor([])
540
+ assert_nil @tag_class.lowest_common_ancestor(@tag_class.none)
541
+ end
542
+
543
+ it 'is nil if there are no common ancestors' do
544
+ assert_nil @tag_class.lowest_common_ancestor(@t111, @t211)
545
+ assert_nil @tag_class.lowest_common_ancestor([@t111, @t211])
546
+ assert_nil @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t111 t211]))
547
+ end
548
+
549
+ it 'is itself for single item' do
550
+ assert_equal @t111, @tag_class.lowest_common_ancestor(@t111)
551
+ assert_equal @t2, @tag_class.lowest_common_ancestor(@t2)
552
+
553
+ assert_equal @t111, @tag_class.lowest_common_ancestor([@t111])
554
+ assert_equal @t2, @tag_class.lowest_common_ancestor([@t2])
555
+
556
+ assert_equal @t111, @tag_class.lowest_common_ancestor(@tag_class.where(name: 't111'))
557
+ assert_equal @t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: 't2'))
558
+ end
559
+ end
560
+
561
+ describe 'paths' do
562
+ describe 'with grandchild ' do
563
+ before do
564
+ @child = @tag_class.find_or_create_by_path([
565
+ { name: 'grandparent', title: 'Nonnie' },
566
+ { name: 'parent', title: 'Mom' },
567
+ { name: 'child', title: 'Kid' }
568
+ ])
569
+ @parent = @child.parent
570
+ @grandparent = @parent.parent
571
+ end
572
+
573
+ it 'should build ancestry path' do
574
+ assert_equal %w[grandparent parent child], @child.ancestry_path
575
+ assert_equal %w[grandparent parent child], @child.ancestry_path(:name)
576
+ assert_equal %w[Nonnie Mom Kid], @child.ancestry_path(:title)
577
+ end
578
+
579
+ it 'assembles ancestors' do
580
+ assert_equal [@parent, @grandparent], @child.ancestors
581
+ assert_equal [@child, @parent, @grandparent], @child.self_and_ancestors
582
+ end
583
+
584
+ it 'should find by path' do
585
+ # class method:
586
+ assert_equal @child, @tag_class.find_by_path(%w[grandparent parent child])
587
+ # instance method:
588
+ assert_equal @child, @parent.find_by_path(%w[child])
589
+ assert_equal @child, @grandparent.find_by_path(%w[parent child])
590
+ assert_nil @parent.find_by_path(%w[child larvae])
591
+ end
592
+
593
+ it 'should respect attribute hashes with both selection and creation' do
594
+ expected_title = 'something else'
595
+ attrs = { title: expected_title }
596
+ existing_title = @grandparent.title
597
+ new_grandparent = @tag_class.find_or_create_by_path(%w[grandparent], attrs)
598
+ refute_equal @grandparent, new_grandparent
599
+ assert_equal expected_title, new_grandparent.title
600
+ assert_equal existing_title, @grandparent.reload.title
601
+ end
602
+
603
+ it 'should create a hierarchy with a given attribute' do
604
+ expected_title = 'unicorn rainbows'
605
+ attrs = { title: expected_title }
606
+ child = @tag_class.find_or_create_by_path(%w[grandparent parent child], attrs)
607
+ refute_equal @child, child
608
+ [child, child.parent, child.parent.parent].each do |ea|
609
+ assert_equal expected_title, ea.title
610
+ end
611
+ end
612
+ end
613
+
614
+ it 'finds correctly rooted paths' do
615
+ _decoy = @tag_class.find_or_create_by_path %w[a b c d]
616
+ b_d = @tag_class.find_or_create_by_path %w[b c d]
617
+ assert_equal b_d, @tag_class.find_by_path(%w[b c d])
618
+ assert_nil @tag_class.find_by_path(%w[c d])
619
+ end
620
+
621
+ it 'find_by_path for 1 node' do
622
+ b = @tag_class.find_or_create_by_path %w[a b]
623
+ b2 = b.root.find_by_path(%w[b])
624
+ assert_equal b, b2
625
+ end
626
+
627
+ it 'find_by_path for 2 nodes' do
628
+ path = %w[a b c]
629
+ c = @tag_class.find_or_create_by_path path
630
+ permutations = path.permutation.to_a
631
+ correct = %w[b c]
632
+ assert_equal c, c.root.find_by_path(correct)
633
+ (permutations - correct).each do |bad_path|
634
+ assert_nil c.root.find_by_path(bad_path)
635
+ end
636
+ end
637
+
638
+ it 'find_by_path for 3 nodes' do
639
+ d = @tag_class.find_or_create_by_path %w[a b c d]
640
+ assert_equal d, d.root.find_by_path(%w[b c d])
641
+ assert_equal d, @tag_class.find_by_path(%w[a b c d])
642
+ assert_nil @tag_class.find_by_path(%w[d])
643
+ end
644
+
645
+ it 'should return nil for missing nodes' do
646
+ assert_nil @tag_class.find_by_path(%w[missing])
647
+ assert_nil @tag_class.find_by_path(%w[grandparent missing])
648
+ assert_nil @tag_class.find_by_path(%w[grandparent parent missing])
649
+ assert_nil @tag_class.find_by_path(%w[grandparent parent missing child])
650
+ end
651
+
652
+ describe '.find_or_create_by_path' do
653
+ it 'uses existing records' do
654
+ grandparent = @tag_class.find_or_create_by_path(%w[grandparent])
655
+ assert_equal grandparent, grandparent
656
+ child = @tag_class.find_or_create_by_path(%w[grandparent parent child])
657
+ assert_equal child, child
658
+ end
659
+
660
+ it 'creates 2-deep trees with strings' do
661
+ subject = @tag_class.find_or_create_by_path(%w[events anniversary])
662
+ assert_equal %w[events anniversary], subject.ancestry_path
663
+ end
664
+
665
+ it 'creates 2-deep trees with hashes' do
666
+ subject = @tag_class.find_or_create_by_path([
667
+ { name: 'test1', title: 'TEST1' },
668
+ { name: 'test2', title: 'TEST2' }
669
+ ])
670
+ assert_equal %w[test1 test2], subject.ancestry_path
671
+ # `self_and_ancestors` and `ancestors` is ordered parent-first. (!!)
672
+ assert_equal %w[TEST2 TEST1], subject.self_and_ancestors.map(&:title)
673
+ end
674
+ end
675
+ end
676
+
677
+ describe 'hash_tree' do
678
+ before do
679
+ @d1 = @tag_class.find_or_create_by_path %w[a b c1 d1]
680
+ @c1 = @d1.parent
681
+ @b = @c1.parent
682
+ @a = @b.parent
683
+ @a2 = @tag_class.create(name: 'a2')
684
+ @b2 = @tag_class.find_or_create_by_path %w[a b2]
685
+ @c3 = @tag_class.find_or_create_by_path %w[a3 b3 c3]
686
+ @b3 = @c3.parent
687
+ @a3 = @b3.parent
688
+
689
+ @tree2 = {
690
+ @a => { @b => {}, @b2 => {} }, @a2 => {}, @a3 => { @b3 => {} }
691
+ }
692
+
693
+ @one_tree = {
694
+ @a => {},
695
+ @a2 => {},
696
+ @a3 => {}
697
+ }
698
+
699
+ @two_tree = {
700
+ @a => {
701
+ @b => {},
702
+ @b2 => {}
703
+ },
704
+ @a2 => {},
705
+ @a3 => {
706
+ @b3 => {}
707
+ }
708
+ }
709
+
710
+ @three_tree = {
711
+ @a => {
712
+ @b => {
713
+ @c1 => {}
714
+ },
715
+ @b2 => {}
716
+ },
717
+ @a2 => {},
718
+ @a3 => {
719
+ @b3 => {
720
+ @c3 => {}
721
+ }
722
+ }
723
+ }
724
+
725
+ @full_tree = {
726
+ @a => {
727
+ @b => {
728
+ @c1 => {
729
+ @d1 => {}
730
+ }
731
+ },
732
+ @b2 => {}
733
+ },
734
+ @a2 => {},
735
+ @a3 => {
736
+ @b3 => {
737
+ @c3 => {}
738
+ }
739
+ }
740
+ }
741
+ end
742
+
743
+ describe '#hash_tree' do
744
+ it 'returns {} for depth 0' do
745
+ assert_equal({}, @tag_class.hash_tree(limit_depth: 0))
746
+ end
747
+
748
+ it 'limit_depth 1' do
749
+ assert_equal @one_tree, @tag_class.hash_tree(limit_depth: 1)
750
+ end
751
+
752
+ it 'limit_depth 2' do
753
+ assert_equal @two_tree, @tag_class.hash_tree(limit_depth: 2)
754
+ end
755
+
756
+ it 'limit_depth 3' do
757
+ assert_equal @three_tree, @tag_class.hash_tree(limit_depth: 3)
758
+ end
759
+
760
+ it 'limit_depth 4' do
761
+ assert_equal @full_tree, @tag_class.hash_tree(limit_depth: 4)
762
+ end
763
+
764
+ it 'no limit' do
765
+ assert_equal @full_tree, @tag_class.hash_tree
766
+ end
767
+ end
768
+
769
+ describe '.hash_tree' do
770
+ it 'returns {} for depth 0' do
771
+ assert_equal({}, @b.hash_tree(limit_depth: 0))
772
+ end
773
+
774
+ it 'limit_depth 1' do
775
+ assert_equal @two_tree[@a].slice(@b), @b.hash_tree(limit_depth: 1)
776
+ end
777
+
778
+ it 'limit_depth 2' do
779
+ assert_equal @three_tree[@a].slice(@b), @b.hash_tree(limit_depth: 2)
780
+ end
781
+
782
+ it 'limit_depth 3' do
783
+ assert_equal @full_tree[@a].slice(@b), @b.hash_tree(limit_depth: 3)
784
+ end
785
+
786
+ it 'no limit from subsubroot' do
787
+ assert_equal @full_tree[@a][@b].slice(@c1), @c1.hash_tree
788
+ end
789
+
790
+ it 'no limit from subroot' do
791
+ assert_equal @full_tree[@a].slice(@b), @b.hash_tree
792
+ end
793
+
794
+ it 'no limit from root' do
795
+ assert_equal @full_tree.slice(@a, @a2), @a.hash_tree.merge(@a2.hash_tree)
796
+ end
797
+ end
798
+
799
+ describe '.hash_tree from relations' do
800
+ it 'limit_depth 2 from chained activerecord association subroots' do
801
+ assert_equal @three_tree[@a], @a.children.hash_tree(limit_depth: 2)
802
+ end
803
+
804
+ it 'no limit from chained activerecord association subroots' do
805
+ assert_equal @full_tree[@a], @a.children.hash_tree
806
+ end
807
+
808
+ it 'limit_depth 3 from b.parent' do
809
+ assert_equal @three_tree.slice(@a), @b.parent.hash_tree(limit_depth: 3)
810
+ end
811
+
812
+ it 'no limit_depth from b.parent' do
813
+ assert_equal @full_tree.slice(@a), @b.parent.hash_tree
814
+ end
815
+
816
+ it 'no limit_depth from c.parent' do
817
+ assert_equal @full_tree[@a].slice(@b), @c1.parent.hash_tree
818
+ end
819
+ end
820
+ end
821
+
822
+ it 'finds_by_path for very deep trees' do
823
+ path = (1..20).to_a.map(&:to_s)
824
+ subject = @tag_class.find_or_create_by_path(path)
825
+ assert_equal path, subject.ancestry_path
826
+ assert_equal subject, @tag_class.find_by_path(path)
827
+ root = subject.root
828
+ assert_equal subject, root.find_by_path(path[1..])
829
+ end
830
+
831
+ describe 'DOT rendering' do
832
+ it 'should render for an empty scope' do
833
+ assert_equal "digraph G {\n}\n", @tag_class.to_dot_digraph(@tag_class.where('0=1'))
834
+ end
835
+
836
+ it 'should render for an empty scope' do
837
+ @tag_class.find_or_create_by_path(%w[a b1 c1])
838
+ @tag_class.find_or_create_by_path(%w[a b2 c2])
839
+ @tag_class.find_or_create_by_path(%w[a b2 c3])
840
+ a, b1, b2, c1, c2, c3 = %w[a b1 b2 c1 c2 c3].map { |ea| @tag_class.where(name: ea).first.id }
841
+ dot = @tag_class.roots.first.to_dot_digraph
842
+
843
+ graph = <<~DOT
844
+ digraph G {
845
+ "#{a}" [label="a"]
846
+ "#{a}" -> "#{b1}"
847
+ "#{b1}" [label="b1"]
848
+ "#{a}" -> "#{b2}"
849
+ "#{b2}" [label="b2"]
850
+ "#{b1}" -> "#{c1}"
851
+ "#{c1}" [label="c1"]
852
+ "#{b2}" -> "#{c2}"
853
+ "#{c2}" [label="c2"]
854
+ "#{b2}" -> "#{c3}"
855
+ "#{c3}" [label="c3"]
856
+ }
857
+ DOT
858
+
859
+ assert_equal(graph, dot)
860
+ end
861
+ end
862
+
863
+ describe '.depth' do
864
+ it 'should render for an empty scope' do
865
+ @tag_class.find_or_create_by_path(%w[a b1 c1])
866
+ @tag_class.find_or_create_by_path(%w[a b2 c2])
867
+ @tag_class.find_or_create_by_path(%w[a b2 c3])
868
+ a, b1, b2, c1, c2, c3 = %w[a b1 b2 c1 c2 c3].map { |ea| @tag_class.where(name: ea).first.id }
869
+ dot = @tag_class.roots.first.to_dot_digraph
870
+
871
+ graph = <<~DOT
872
+ digraph G {
873
+ "#{a}" [label="a"]
874
+ "#{a}" -> "#{b1}"
875
+ "#{b1}" [label="b1"]
876
+ "#{a}" -> "#{b2}"
877
+ "#{b2}" [label="b2"]
878
+ "#{b1}" -> "#{c1}"
879
+ "#{c1}" [label="c1"]
880
+ "#{b2}" -> "#{c2}"
881
+ "#{c2}" [label="c2"]
882
+ "#{b2}" -> "#{c3}"
883
+ "#{c3}" [label="c3"]
884
+ }
885
+ DOT
886
+
887
+ assert_equal(graph, dot)
888
+ end
889
+ end
890
+
891
+ describe '.depth' do
892
+ before do
893
+ @d1 = @tag_class.find_or_create_by_path %w[a b c1 d1]
894
+ @c1 = @d1.parent
895
+ @b = @c1.parent
896
+ @a = @b.parent
897
+ @a2 = @tag_class.create(name: 'a2')
898
+ @b2 = @tag_class.find_or_create_by_path %w[a b2]
899
+ @c3 = @tag_class.find_or_create_by_path %w[a3 b3 c3]
900
+ @b3 = @c3.parent
901
+ @a3 = @b3.parent
902
+
903
+
904
+ end
905
+
906
+ it 'should return 0 for root' do
907
+ assert_equal 0, @a.depth
908
+ assert_equal 0, @a2.depth
909
+ assert_equal 0, @a3.depth
910
+ end
911
+
912
+ it 'should return correct depth for nodes' do
913
+ assert_equal 1, @b.depth
914
+ assert_equal 2, @c1.depth
915
+ assert_equal 3, @d1.depth
916
+ assert_equal 1, @b2.depth
917
+ assert_equal 1, @b3.depth
918
+ assert_equal 2, @c3.depth
919
+ end
920
+ end
921
+ end
922
+ end
923
+ end