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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +30 -56
- data/.github/workflows/ci_jruby.yml +68 -0
- data/.github/workflows/ci_truffleruby.yml +71 -0
- data/.github/workflows/release.yml +17 -0
- data/.gitignore +1 -1
- data/.release-please-manifest.json +1 -0
- data/.tool-versions +1 -0
- data/Appraisals +9 -53
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -3
- data/README.md +21 -9
- data/Rakefile +11 -16
- data/closure_tree.gemspec +16 -9
- data/lib/closure_tree/active_record_support.rb +3 -14
- data/lib/closure_tree/digraphs.rb +1 -1
- data/lib/closure_tree/finders.rb +1 -1
- data/lib/closure_tree/hash_tree.rb +1 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +3 -6
- data/lib/closure_tree/model.rb +3 -3
- data/lib/closure_tree/numeric_deterministic_ordering.rb +3 -8
- data/lib/closure_tree/support.rb +3 -7
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/migration_generator.rb +1 -4
- data/release-please-config.json +4 -0
- data/test/closure_tree/cache_invalidation_test.rb +36 -0
- data/test/closure_tree/cuisine_type_test.rb +42 -0
- data/test/closure_tree/generator_test.rb +49 -0
- data/test/closure_tree/has_closure_tree_root_test.rb +80 -0
- data/test/closure_tree/hierarchy_maintenance_test.rb +56 -0
- data/test/closure_tree/label_test.rb +674 -0
- data/test/closure_tree/metal_test.rb +59 -0
- data/test/closure_tree/model_test.rb +9 -0
- data/test/closure_tree/namespace_type_test.rb +13 -0
- data/test/closure_tree/parallel_test.rb +162 -0
- data/test/closure_tree/pool_test.rb +33 -0
- data/test/closure_tree/support_test.rb +18 -0
- data/test/closure_tree/tag_test.rb +8 -0
- data/test/closure_tree/user_test.rb +175 -0
- data/test/closure_tree/uuid_tag_test.rb +8 -0
- data/test/support/query_counter.rb +25 -0
- data/test/support/tag_examples.rb +923 -0
- data/test/test_helper.rb +99 -0
- metadata +52 -21
- data/_config.yml +0 -1
- data/tests.sh +0 -11
@@ -0,0 +1,674 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
module CorrectOrderValue
|
6
|
+
def self.shared_examples(&block)
|
7
|
+
describe "correct order_value" do
|
8
|
+
before do
|
9
|
+
instance_exec(&block)
|
10
|
+
@root = @model.create(name: "root")
|
11
|
+
@a, @b, @c = %w[a b c].map { |n| @root.children.create(name: n) }
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set order_value on roots" do
|
15
|
+
if @expected_root_order_value.nil?
|
16
|
+
assert_nil @root.order_value
|
17
|
+
else
|
18
|
+
assert_equal @expected_root_order_value, @root.order_value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should set order_value with siblings" do
|
23
|
+
assert_equal 0, @a.order_value
|
24
|
+
assert_equal 1, @b.order_value
|
25
|
+
assert_equal 2, @c.order_value
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should reset order_value when a node is moved to another location" do
|
29
|
+
root2 = @model.create(name: "root2")
|
30
|
+
root2.add_child @b
|
31
|
+
assert_equal 0, @a.order_value
|
32
|
+
assert_equal 0, @b.order_value
|
33
|
+
assert_equal 1, @c.reload.order_value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_label_tree
|
40
|
+
@d1 = Label.find_or_create_by_path %w[a1 b1 c1 d1]
|
41
|
+
@c1 = @d1.parent
|
42
|
+
@b1 = @c1.parent
|
43
|
+
@a1 = @b1.parent
|
44
|
+
@d2 = Label.find_or_create_by_path %w[a1 b1 c2 d2]
|
45
|
+
@c2 = @d2.parent
|
46
|
+
@d3 = Label.find_or_create_by_path %w[a2 b2 c3 d3]
|
47
|
+
@c3 = @d3.parent
|
48
|
+
@b2 = @c3.parent
|
49
|
+
@a2 = @b2.parent
|
50
|
+
Label.update_all("#{Label._ct.order_column} = id")
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_preorder_tree(suffix = "")
|
54
|
+
%w[
|
55
|
+
a/l/n/r
|
56
|
+
a/l/n/q
|
57
|
+
a/l/n/p
|
58
|
+
a/l/n/o
|
59
|
+
a/l/m
|
60
|
+
a/b/h/i/j/k
|
61
|
+
a/b/c/d/g
|
62
|
+
a/b/c/d/f
|
63
|
+
a/b/c/d/e
|
64
|
+
].shuffle.each { |ea| Label.find_or_create_by_path(ea.split("/").collect { |ea| "#{ea}#{suffix}" }) }
|
65
|
+
|
66
|
+
Label.roots.each_with_index do |root, root_idx|
|
67
|
+
root.order_value = root_idx
|
68
|
+
yield(root) if block_given?
|
69
|
+
root.save!
|
70
|
+
root.self_and_descendants.each do |ea|
|
71
|
+
ea.children.to_a.sort_by(&:name).each_with_index do |ea, idx|
|
72
|
+
ea.order_value = idx
|
73
|
+
yield(ea) if block_given?
|
74
|
+
ea.save!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe Label do
|
81
|
+
describe "destruction" do
|
82
|
+
it "properly destroys descendents created with find_or_create_by_path" do
|
83
|
+
c = Label.find_or_create_by_path %w[a b c]
|
84
|
+
b = c.parent
|
85
|
+
a = c.root
|
86
|
+
a.destroy
|
87
|
+
refute Label.exists?(id: [a.id, b.id, c.id])
|
88
|
+
end
|
89
|
+
|
90
|
+
it "properly destroys descendents created with add_child" do
|
91
|
+
a = Label.create(name: "a")
|
92
|
+
b = a.add_child Label.new(name: "b")
|
93
|
+
c = b.add_child Label.new(name: "c")
|
94
|
+
a.destroy
|
95
|
+
refute Label.exists?(a.id)
|
96
|
+
refute Label.exists?(b.id)
|
97
|
+
refute Label.exists?(c.id)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "properly destroys descendents created with <<" do
|
101
|
+
a = Label.create(name: "a")
|
102
|
+
b = Label.new(name: "b")
|
103
|
+
a.children << b
|
104
|
+
c = Label.new(name: "c")
|
105
|
+
b.children << c
|
106
|
+
a.destroy
|
107
|
+
refute Label.exists?(a.id)
|
108
|
+
refute Label.exists?(b.id)
|
109
|
+
refute Label.exists?(c.id)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "roots" do
|
114
|
+
it "sorts alphabetically" do
|
115
|
+
expected = (0..10).to_a
|
116
|
+
expected.shuffle.each do |ea|
|
117
|
+
Label.create! do |l|
|
118
|
+
l.name = "root #{ea}"
|
119
|
+
l.order_value = ea
|
120
|
+
end
|
121
|
+
end
|
122
|
+
assert_equal expected, Label.roots.collect { |ea| ea.order_value }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "Base Label class" do
|
127
|
+
it "should find or create by path" do
|
128
|
+
# class method:
|
129
|
+
c = Label.find_or_create_by_path(%w[grandparent parent child])
|
130
|
+
assert_equal %w[grandparent parent child], c.ancestry_path
|
131
|
+
assert_equal "child", c.name
|
132
|
+
assert_equal "parent", c.parent.name
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "Parent/child inverse relationships" do
|
137
|
+
it "should associate both sides of the parent and child relationships" do
|
138
|
+
parent = Label.new(name: "parent")
|
139
|
+
child = parent.children.build(name: "child")
|
140
|
+
assert parent.root?
|
141
|
+
refute parent.leaf?
|
142
|
+
refute child.root?
|
143
|
+
assert child.leaf?
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "DateLabel" do
|
148
|
+
it "should find or create by path" do
|
149
|
+
date = DateLabel.find_or_create_by_path(%w[2011 November 23])
|
150
|
+
assert_equal %w[2011 November 23], date.ancestry_path
|
151
|
+
date.self_and_ancestors.each { |ea| assert_equal DateLabel, ea.class }
|
152
|
+
assert_equal "23", date.name
|
153
|
+
assert_equal "November", date.parent.name
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "DirectoryLabel" do
|
158
|
+
it "should find or create by path" do
|
159
|
+
dir = DirectoryLabel.find_or_create_by_path(%w[grandparent parent child])
|
160
|
+
assert_equal %w[grandparent parent child], dir.ancestry_path
|
161
|
+
assert_equal "child", dir.name
|
162
|
+
assert_equal "parent", dir.parent.name
|
163
|
+
assert_equal "grandparent", dir.parent.parent.name
|
164
|
+
assert_equal "grandparent", dir.root.name
|
165
|
+
refute_equal Label.find_or_create_by_path(%w[grandparent parent child]), dir.id
|
166
|
+
dir.self_and_ancestors.each { |ea| assert_equal DirectoryLabel, ea.class }
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "Mixed class tree" do
|
171
|
+
describe "preorder tree" do
|
172
|
+
before do
|
173
|
+
classes = [Label, DateLabel, DirectoryLabel, EventLabel]
|
174
|
+
create_preorder_tree do |ea|
|
175
|
+
ea.type = classes[ea.order_value % 4].to_s
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
it "finds roots with specific classes" do
|
180
|
+
assert_equal Label.where(name: "a").to_a, Label.roots
|
181
|
+
assert DirectoryLabel.roots.empty?
|
182
|
+
assert EventLabel.roots.empty?
|
183
|
+
end
|
184
|
+
|
185
|
+
it "all is limited to subclasses" do
|
186
|
+
assert_equal %w[f h l n p].sort, DateLabel.all.map(&:name).sort
|
187
|
+
assert_equal %w[g q].sort, DirectoryLabel.all.map(&:name).sort
|
188
|
+
assert_equal %w[r], EventLabel.all.map(&:name)
|
189
|
+
end
|
190
|
+
|
191
|
+
it "returns descendents regardless of subclass" do
|
192
|
+
assert_equal %w[Label DateLabel DirectoryLabel EventLabel].sort, Label.root.descendants.map { |ea|
|
193
|
+
ea.class.to_s
|
194
|
+
}.uniq.sort
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it "supports children << and add_child" do
|
199
|
+
a = EventLabel.create!(name: "a")
|
200
|
+
b = DateLabel.new(name: "b")
|
201
|
+
a.children << b
|
202
|
+
c = Label.new(name: "c")
|
203
|
+
b.add_child(c)
|
204
|
+
|
205
|
+
assert_equal [EventLabel, DateLabel, Label], a.self_and_descendants.collect { |ea| ea.class }
|
206
|
+
assert_equal %w[a b c], a.self_and_descendants.collect { |ea| ea.name }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "find_all_by_generation" do
|
211
|
+
before do
|
212
|
+
create_label_tree
|
213
|
+
end
|
214
|
+
|
215
|
+
it "finds roots from the class method" do
|
216
|
+
assert_equal [@a1, @a2], Label.find_all_by_generation(0).to_a
|
217
|
+
end
|
218
|
+
|
219
|
+
it "finds roots from themselves" do
|
220
|
+
assert_equal [@a1], @a1.find_all_by_generation(0).to_a
|
221
|
+
end
|
222
|
+
|
223
|
+
it "finds itself for non-roots" do
|
224
|
+
assert_equal [@b1], @b1.find_all_by_generation(0).to_a
|
225
|
+
end
|
226
|
+
|
227
|
+
it "finds children for roots" do
|
228
|
+
assert_equal [@b1, @b2], Label.find_all_by_generation(1).to_a
|
229
|
+
end
|
230
|
+
|
231
|
+
it "finds children" do
|
232
|
+
assert_equal [@b1], @a1.find_all_by_generation(1).to_a
|
233
|
+
assert_equal [@c1, @c2], @b1.find_all_by_generation(1).to_a
|
234
|
+
end
|
235
|
+
|
236
|
+
it "finds grandchildren for roots" do
|
237
|
+
assert_equal [@c1, @c2, @c3], Label.find_all_by_generation(2).to_a
|
238
|
+
end
|
239
|
+
|
240
|
+
it "finds grandchildren" do
|
241
|
+
assert_equal [@c1, @c2], @a1.find_all_by_generation(2).to_a
|
242
|
+
assert_equal [@d1, @d2], @b1.find_all_by_generation(2).to_a
|
243
|
+
end
|
244
|
+
|
245
|
+
it "finds great-grandchildren for roots" do
|
246
|
+
assert_equal [@d1, @d2, @d3], Label.find_all_by_generation(3).to_a
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
describe "loading through self_and_ scopes" do
|
251
|
+
before do
|
252
|
+
create_label_tree
|
253
|
+
end
|
254
|
+
|
255
|
+
it "self_and_descendants should result in one select" do
|
256
|
+
assert_database_queries_count(1) do
|
257
|
+
a1_array = @a1.self_and_descendants
|
258
|
+
assert_equal(%w[a1 b1 c1 c2 d1 d2], a1_array.collect { |ea| ea.name })
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
it "self_and_ancestors should result in one select" do
|
263
|
+
assert_database_queries_count(1) do
|
264
|
+
d1_array = @d1.self_and_ancestors
|
265
|
+
assert_equal(%w[d1 c1 b1 a1], d1_array.collect { |ea| ea.name })
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "deterministically orders with polymorphic siblings" do
|
271
|
+
before do
|
272
|
+
@parent = Label.create!(name: "parent")
|
273
|
+
@a, @b, @c, @d, @e, @f = ("a".."f").map { |ea| EventLabel.new(name: ea) }
|
274
|
+
@parent.children << @a
|
275
|
+
@a.append_sibling(@b)
|
276
|
+
@b.append_sibling(@c)
|
277
|
+
@c.append_sibling(@d)
|
278
|
+
@parent.append_sibling(@e)
|
279
|
+
@e.append_sibling(@f)
|
280
|
+
end
|
281
|
+
|
282
|
+
def name_and_order(enum)
|
283
|
+
enum.map { |ea| [ea.name, ea.order_value] }
|
284
|
+
end
|
285
|
+
|
286
|
+
def children_name_and_order
|
287
|
+
name_and_order(@parent.children.reload)
|
288
|
+
end
|
289
|
+
|
290
|
+
def roots_name_and_order
|
291
|
+
name_and_order(Label.roots)
|
292
|
+
end
|
293
|
+
|
294
|
+
it "order_values properly" do
|
295
|
+
assert_equal [["a", 0], ["b", 1], ["c", 2], ["d", 3]], children_name_and_order
|
296
|
+
end
|
297
|
+
|
298
|
+
it "when inserted before" do
|
299
|
+
@b.append_sibling(@a)
|
300
|
+
assert_equal [["b", 0], ["a", 1], ["c", 2], ["d", 3]], children_name_and_order
|
301
|
+
end
|
302
|
+
|
303
|
+
it "when inserted after" do
|
304
|
+
@a.append_sibling(@c)
|
305
|
+
assert_equal [["a", 0], ["c", 1], ["b", 2], ["d", 3]], children_name_and_order
|
306
|
+
end
|
307
|
+
|
308
|
+
it "when inserted before the first" do
|
309
|
+
@a.prepend_sibling(@d)
|
310
|
+
assert_equal [["d", 0], ["a", 1], ["b", 2], ["c", 3]], children_name_and_order
|
311
|
+
end
|
312
|
+
|
313
|
+
it "when inserted after the last" do
|
314
|
+
@d.append_sibling(@b)
|
315
|
+
assert_equal [["a", 0], ["c", 1], ["d", 2], ["b", 3]], children_name_and_order
|
316
|
+
end
|
317
|
+
|
318
|
+
it "prepends to root nodes" do
|
319
|
+
@parent.prepend_sibling(@f)
|
320
|
+
assert_equal [["f", 0], ["parent", 1], ["e", 2]], roots_name_and_order
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe "doesn't order roots when requested" do
|
325
|
+
before do
|
326
|
+
@root1 = LabelWithoutRootOrdering.create!(name: "root1")
|
327
|
+
@root2 = LabelWithoutRootOrdering.create!(name: "root2")
|
328
|
+
@a, @b, @c, @d, @e = ("a".."e").map { |ea| LabelWithoutRootOrdering.new(name: ea) }
|
329
|
+
@root1.children << @a
|
330
|
+
@root1.append_child(@c)
|
331
|
+
@root1.prepend_child(@d)
|
332
|
+
|
333
|
+
# Reload is needed here and below because order values may have been adjusted in the DB during
|
334
|
+
# prepend_child, append_sibling, etc.
|
335
|
+
[@a, @c, @d].each(&:reload)
|
336
|
+
|
337
|
+
@a.append_sibling(@b)
|
338
|
+
[@a, @c, @d, @b].each(&:reload)
|
339
|
+
@d.prepend_sibling(@e)
|
340
|
+
end
|
341
|
+
|
342
|
+
it "order_values properly" do
|
343
|
+
assert @root1.reload.order_value.nil?
|
344
|
+
orders_and_names = @root1.children.reload.map { |ea| [ea.name, ea.order_value] }
|
345
|
+
assert_equal [["e", 0], ["d", 1], ["a", 2], ["b", 3], ["c", 4]], orders_and_names
|
346
|
+
end
|
347
|
+
|
348
|
+
it "raises on prepending and appending to root" do
|
349
|
+
assert_raises ClosureTree::RootOrderingDisabledError do
|
350
|
+
@root1.prepend_sibling(@f)
|
351
|
+
end
|
352
|
+
|
353
|
+
assert_raises ClosureTree::RootOrderingDisabledError do
|
354
|
+
@root1.append_sibling(@f)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
it "returns empty array for siblings_before and after" do
|
359
|
+
assert_equal [], @root1.siblings_before
|
360
|
+
assert_equal [], @root1.siblings_after
|
361
|
+
end
|
362
|
+
|
363
|
+
unless sqlite?
|
364
|
+
it "returns expected result for self_and_descendants_preordered" do
|
365
|
+
assert_equal [@root1, @e, @d, @a, @b, @c], @root1.self_and_descendants_preordered.to_a
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
it "raises on roots_and_descendants_preordered" do
|
370
|
+
assert_raises ClosureTree::RootOrderingDisabledError do
|
371
|
+
LabelWithoutRootOrdering.roots_and_descendants_preordered
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
describe "code in the readme" do
|
377
|
+
it "creates STI label hierarchies" do
|
378
|
+
child = Label.find_or_create_by_path([
|
379
|
+
{type: "DateLabel", name: "2014"},
|
380
|
+
{type: "DateLabel", name: "August"},
|
381
|
+
{type: "DateLabel", name: "5"},
|
382
|
+
{type: "EventLabel", name: "Visit the Getty Center"}
|
383
|
+
])
|
384
|
+
assert child.is_a?(EventLabel)
|
385
|
+
assert_equal "Visit the Getty Center", child.name
|
386
|
+
assert_equal %w[5 August 2014], child.ancestors.map(&:name)
|
387
|
+
assert_equal [DateLabel, DateLabel, DateLabel], child.ancestors.map(&:class)
|
388
|
+
end
|
389
|
+
|
390
|
+
it "appends and prepends siblings" do
|
391
|
+
root = Label.create(name: "root")
|
392
|
+
a = root.append_child(Label.new(name: "a"))
|
393
|
+
b = Label.create(name: "b")
|
394
|
+
c = Label.create(name: "c")
|
395
|
+
|
396
|
+
a.append_sibling(b)
|
397
|
+
assert_equal %w[a b], a.self_and_siblings.collect(&:name)
|
398
|
+
assert_equal %w[a b], root.reload.children.collect(&:name)
|
399
|
+
assert_equal [0, 1], root.children.collect(&:order_value)
|
400
|
+
|
401
|
+
a.prepend_sibling(b)
|
402
|
+
assert_equal %w[b a], a.self_and_siblings.collect(&:name)
|
403
|
+
assert_equal %w[b a], root.reload.children.collect(&:name)
|
404
|
+
assert_equal [0, 1], root.children.collect(&:order_value)
|
405
|
+
|
406
|
+
a.append_sibling(c)
|
407
|
+
assert_equal %w[b a c], a.self_and_siblings.collect(&:name)
|
408
|
+
assert_equal %w[b a c], root.reload.children.collect(&:name)
|
409
|
+
assert_equal [0, 1, 2], root.children.collect(&:order_value)
|
410
|
+
|
411
|
+
# We need to reload b because it was updated by a.append_sibling(c)
|
412
|
+
b.reload.append_sibling(c)
|
413
|
+
assert_equal %w[b c a], root.reload.children.collect(&:name)
|
414
|
+
assert_equal [0, 1, 2], root.children.collect(&:order_value)
|
415
|
+
|
416
|
+
d = a.reload.append_sibling(Label.new(name: "d"))
|
417
|
+
assert_equal %w[b c a d], d.self_and_siblings.collect(&:name)
|
418
|
+
assert_equal [0, 1, 2, 3], d.self_and_siblings.collect(&:order_value)
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# https://github.com/mceachen/closure_tree/issues/84
|
423
|
+
it "properly appends children with <<" do
|
424
|
+
root = Label.create(name: "root")
|
425
|
+
a = Label.create(name: "a", parent: root)
|
426
|
+
b = Label.create(name: "b", parent: root)
|
427
|
+
|
428
|
+
# Add a child to root at end of children.
|
429
|
+
root.children << b
|
430
|
+
assert_equal root, b.parent
|
431
|
+
assert_equal %w[a b], a.self_and_siblings.collect(&:name)
|
432
|
+
assert_equal %w[a b], root.reload.children.collect(&:name)
|
433
|
+
assert_equal [0, 1], root.children.collect(&:order_value)
|
434
|
+
end
|
435
|
+
|
436
|
+
describe "#add_sibling" do
|
437
|
+
it "should move a node before another node which has an uninitialized order_value" do
|
438
|
+
f = Label.find_or_create_by_path %w[a b c d e fa]
|
439
|
+
f0 = f.prepend_sibling(Label.new(name: "fb")) # < not alpha sort, so name shouldn't matter
|
440
|
+
assert_equal %w[a b c d e fb], f0.ancestry_path
|
441
|
+
assert_equal [f0], f.siblings_before.to_a
|
442
|
+
assert f0.siblings_before.empty?
|
443
|
+
assert_equal [f], f0.siblings_after
|
444
|
+
assert f.siblings_after.empty?
|
445
|
+
assert_equal [f0, f], f0.self_and_siblings
|
446
|
+
assert_equal [f0, f], f.self_and_siblings
|
447
|
+
end
|
448
|
+
|
449
|
+
before do
|
450
|
+
@f1 = Label.find_or_create_by_path %w[a1 b1 c1 d1 e1 f1]
|
451
|
+
end
|
452
|
+
|
453
|
+
it "should move a node to another tree" do
|
454
|
+
f2 = Label.find_or_create_by_path %w[a2 b2 c2 d2 e2 f2]
|
455
|
+
@f1.add_sibling(f2)
|
456
|
+
assert_equal %w[a1 b1 c1 d1 e1 f2], f2.ancestry_path
|
457
|
+
assert_equal [@f1, f2], @f1.parent.reload.children
|
458
|
+
end
|
459
|
+
|
460
|
+
it "should reorder old-parent siblings when a node moves to another tree" do
|
461
|
+
f2 = Label.find_or_create_by_path %w[a2 b2 c2 d2 e2 f2]
|
462
|
+
f3 = f2.prepend_sibling(Label.new(name: "f3"))
|
463
|
+
_f4 = f2.append_sibling(Label.new(name: "f4"))
|
464
|
+
@f1.add_sibling(f2)
|
465
|
+
assert_equal [0, 1], @f1.self_and_siblings.collect(&:order_value)
|
466
|
+
assert_equal [0, 1], f3.self_and_siblings.collect(&:order_value)
|
467
|
+
assert_equal %w[f1 f2], @f1.self_and_siblings.collect(&:name)
|
468
|
+
assert_equal %w[f3 f4], f3.self_and_siblings.collect(&:name)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
describe "order_value must be set" do
|
473
|
+
describe "with normal model" do
|
474
|
+
CorrectOrderValue.shared_examples do
|
475
|
+
@model = Label
|
476
|
+
@expected_root_order_value = 0
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
describe "without root ordering" do
|
481
|
+
CorrectOrderValue.shared_examples do
|
482
|
+
@model = LabelWithoutRootOrdering
|
483
|
+
@expected_root_order_value = nil
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
describe "destructive reordering" do
|
489
|
+
before do
|
490
|
+
# to make sure order_value isn't affected by additional nodes:
|
491
|
+
create_preorder_tree
|
492
|
+
@root = Label.create(name: "root")
|
493
|
+
@a = @root.children.create!(name: "a")
|
494
|
+
@b = @a.append_sibling(Label.new(name: "b"))
|
495
|
+
@c = @b.append_sibling(Label.new(name: "c"))
|
496
|
+
end
|
497
|
+
|
498
|
+
describe "doesn't create sort order gaps" do
|
499
|
+
it "from head" do
|
500
|
+
@a.destroy
|
501
|
+
assert_equal [@b, @c], @root.reload.children
|
502
|
+
assert_equal([0, 1], @root.children.map { |ea| ea.order_value })
|
503
|
+
end
|
504
|
+
|
505
|
+
it "from mid" do
|
506
|
+
@b.destroy
|
507
|
+
assert_equal [@a, @c], @root.reload.children
|
508
|
+
assert_equal([0, 1], @root.children.map { |ea| ea.order_value })
|
509
|
+
end
|
510
|
+
|
511
|
+
it "from tail" do
|
512
|
+
@c.destroy
|
513
|
+
assert_equal [@a, @b], @root.reload.children
|
514
|
+
assert_equal([0, 1], @root.children.map { |ea| ea.order_value })
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
describe "add_sibling moves descendant nodes" do
|
519
|
+
before do
|
520
|
+
@roots = (0..10).map { |ea| Label.create(name: ea) }
|
521
|
+
@first_root = @roots.first
|
522
|
+
@last_root = @roots.last
|
523
|
+
end
|
524
|
+
|
525
|
+
it "should retain sort orders of descendants when moving to a new parent" do
|
526
|
+
expected_order = ("a".."z").to_a.shuffle
|
527
|
+
expected_order.map { |ea| @first_root.add_child(Label.new(name: ea)) }
|
528
|
+
actual_order = @first_root.children.reload.pluck(:name)
|
529
|
+
assert_equal expected_order, actual_order
|
530
|
+
@last_root.append_child(@first_root)
|
531
|
+
assert_equal(%w[10 0] + expected_order, @last_root.self_and_descendants.pluck(:name))
|
532
|
+
end
|
533
|
+
|
534
|
+
it "should retain sort orders of descendants when moving within the same new parent" do
|
535
|
+
path = ("a".."z").to_a
|
536
|
+
z = @first_root.find_or_create_by_path(path)
|
537
|
+
z_children_names = (100..150).to_a.shuffle.map { |ea| ea.to_s }
|
538
|
+
z_children_names.reverse_each { |ea| z.prepend_child(Label.new(name: ea)) }
|
539
|
+
assert_equal z_children_names, z.children.reload.pluck(:name)
|
540
|
+
a = @first_root.find_by_path(["a"])
|
541
|
+
# move b up to a's level:
|
542
|
+
b = a.children.first
|
543
|
+
a.add_sibling(b)
|
544
|
+
assert_equal @first_root, b.parent
|
545
|
+
assert_equal z_children_names, z.children.reload.pluck(:name)
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
it "shouldn't fail if all children are destroyed" do
|
550
|
+
roots = Label.roots.to_a
|
551
|
+
roots.each { |ea| ea.children.destroy_all }
|
552
|
+
assert_equal roots.sort, Label.all.to_a.sort
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
describe "descendent destruction" do
|
557
|
+
it "properly destroys descendents created with add_child" do
|
558
|
+
a = Label.create(name: "a")
|
559
|
+
b = Label.new(name: "b")
|
560
|
+
a.add_child b
|
561
|
+
c = Label.new(name: "c")
|
562
|
+
b.add_child c
|
563
|
+
a.destroy
|
564
|
+
refute Label.exists?(id: [a.id, b.id, c.id])
|
565
|
+
end
|
566
|
+
|
567
|
+
it "properly destroys descendents created with <<" do
|
568
|
+
a = Label.create(name: "a")
|
569
|
+
b = Label.new(name: "b")
|
570
|
+
a.children << b
|
571
|
+
c = Label.new(name: "c")
|
572
|
+
b.children << c
|
573
|
+
a.destroy
|
574
|
+
refute Label.exists?(id: [a.id, b.id, c.id])
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
unless sqlite?
|
579
|
+
describe "preorder" do
|
580
|
+
it "returns descendants in proper order" do
|
581
|
+
create_preorder_tree
|
582
|
+
a = Label.root
|
583
|
+
assert_equal "a", a.name
|
584
|
+
expected = ("a".."r").to_a
|
585
|
+
assert_equal expected, a.self_and_descendants_preordered.collect { |ea| ea.name }
|
586
|
+
assert_equal expected, Label.roots_and_descendants_preordered.collect { |ea| ea.name }
|
587
|
+
# Let's create the second root by hand so we can explicitly set the sort order
|
588
|
+
Label.create! do |l|
|
589
|
+
l.name = "a1"
|
590
|
+
l.order_value = a.order_value + 1
|
591
|
+
end
|
592
|
+
create_preorder_tree("1")
|
593
|
+
# Should be no change:
|
594
|
+
assert_equal expected, a.reload.self_and_descendants_preordered.collect { |ea| ea.name }
|
595
|
+
expected += ("a".."r").collect { |ea| "#{ea}1" }
|
596
|
+
assert_equal expected, Label.roots_and_descendants_preordered.collect { |ea| ea.name }
|
597
|
+
end
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
describe "hash_tree" do
|
602
|
+
before do
|
603
|
+
@a = EventLabel.create(name: "a")
|
604
|
+
@b = DateLabel.create(name: "b")
|
605
|
+
@c = DirectoryLabel.create(name: "c")
|
606
|
+
(1..3).each { |i| DirectoryLabel.create!(name: "c#{i}", mother_id: @c.id) }
|
607
|
+
end
|
608
|
+
|
609
|
+
it "should return tree with correct scope when called on class" do
|
610
|
+
tree = DirectoryLabel.hash_tree
|
611
|
+
assert_equal 1, tree.keys.size
|
612
|
+
assert_equal @c, tree.keys.first
|
613
|
+
assert_equal 3, tree[@c].keys.size
|
614
|
+
end
|
615
|
+
|
616
|
+
it "should return tree with correct scope when called on all" do
|
617
|
+
tree = DirectoryLabel.all.hash_tree
|
618
|
+
assert_equal 1, tree.keys.size
|
619
|
+
assert_equal @c, tree.keys.first
|
620
|
+
assert_equal 3, tree[@c].keys.size
|
621
|
+
end
|
622
|
+
|
623
|
+
it "should return tree with correct scope when called on scope chain" do
|
624
|
+
tree = Label.where(name: "b").hash_tree
|
625
|
+
assert_equal 1, tree.keys.size
|
626
|
+
assert_equal @b, tree.keys.first
|
627
|
+
assert_equal({}, tree[@b])
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
describe "relationship between nodes" do
|
632
|
+
before do
|
633
|
+
create_label_tree
|
634
|
+
end
|
635
|
+
|
636
|
+
it "checks parent of node" do
|
637
|
+
assert @a1.parent_of?(@b1)
|
638
|
+
assert @c2.parent_of?(@d2)
|
639
|
+
refute @c1.parent_of?(@b1)
|
640
|
+
end
|
641
|
+
|
642
|
+
it "checks children of node" do
|
643
|
+
assert @d1.child_of?(@c1)
|
644
|
+
assert @c2.child_of?(@b1)
|
645
|
+
refute @c3.child_of?(@b1)
|
646
|
+
end
|
647
|
+
|
648
|
+
it "checks root of node" do
|
649
|
+
assert @a1.root_of?(@d1)
|
650
|
+
assert @a1.root_of?(@c2)
|
651
|
+
refute @a2.root_of?(@c2)
|
652
|
+
end
|
653
|
+
|
654
|
+
it "checks ancestor of node" do
|
655
|
+
assert @a1.ancestor_of?(@d1)
|
656
|
+
assert @b1.ancestor_of?(@d1)
|
657
|
+
refute @b1.ancestor_of?(@c3)
|
658
|
+
end
|
659
|
+
|
660
|
+
it "checks descendant of node" do
|
661
|
+
assert @c1.descendant_of?(@a1)
|
662
|
+
assert @d2.descendant_of?(@a1)
|
663
|
+
refute @b1.descendant_of?(@a2)
|
664
|
+
end
|
665
|
+
|
666
|
+
it "checks descendant of node" do
|
667
|
+
assert @b1.family_of?(@b1)
|
668
|
+
assert @a1.family_of?(@c1)
|
669
|
+
assert @d3.family_of?(@a2)
|
670
|
+
assert @c1.family_of?(@d2)
|
671
|
+
refute @c3.family_of?(@a1)
|
672
|
+
end
|
673
|
+
end
|
674
|
+
end
|