diff --git a/spec/closure_tree/tag_spec.rb b/spec/closure_tree/tag_spec.rb deleted file mode 100644 index 2e5a39b..0000000 --- a/spec/closure_tree/tag_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -RSpec.describe Tag do - it_behaves_like Tag -end diff --git a/spec/closure_tree/uuid_tag_spec.rb b/spec/closure_tree/uuid_tag_spec.rb deleted file mode 100644 index 5c32ac1..0000000 --- a/spec/closure_tree/uuid_tag_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec_helper' - -RSpec.describe UUIDTag do - it_behaves_like Tag -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f06a10b..5f58ad4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -97,7 +97,6 @@ def sqlite? # Load test helpers require_relative 'support/schema' require_relative 'support/models' -require_relative 'support/tag_examples' require_relative 'support/helpers' require_relative 'support/exceed_query_limit' require_relative 'support/query_counter' diff --git a/spec/support/tag_examples.rb b/spec/support/tag_examples.rb deleted file mode 100644 index 6a21ee6..0000000 --- a/spec/support/tag_examples.rb +++ /dev/null @@ -1,821 +0,0 @@ -require 'spec_helper' - -RSpec.shared_examples_for Tag do - - let (:tag_class) { described_class } - let (:tag_hierarchy_class) { described_class.hierarchy_class } - - context 'class setup' do - - it 'has correct accessible_attributes' do - if tag_class._ct.use_attr_accessible? - expect(tag_class.accessible_attributes.to_a).to match_array(%w(parent name title)) - end - end - - it 'should build hierarchy classname correctly' do - expect(tag_class.hierarchy_class).to eq(tag_hierarchy_class) - expect(tag_class._ct.hierarchy_class_name).to eq(tag_hierarchy_class.to_s) - expect(tag_class._ct.short_hierarchy_class_name).to eq(tag_hierarchy_class.to_s) - end - - it 'should have a correct parent column name' do - expected_parent_column_name = tag_class == UUIDTag ? 'parent_uuid' : 'parent_id' - expect(tag_class._ct.parent_column_name).to eq(expected_parent_column_name) - end - end - - describe 'from empty db' do - - context 'with no tags' do - it 'should return no entities' do - expect(tag_class.roots).to be_empty - expect(tag_class.leaves).to be_empty - end - - it '#find_or_create_by_path with strings' do - a = tag_class.create!(name: 'a') - expect(a.find_or_create_by_path(%w{b c}).ancestry_path).to eq(%w{a b c}) - end - - it '#find_or_create_by_path with hashes' do - a = tag_class.create!(name: 'a', title: 'A') - subject = a.find_or_create_by_path([ - {name: 'b', title: 'B'}, - {name: 'c', title: 'C'} - ]) - expect(subject.ancestry_path).to eq(%w{a b c}) - expect(subject.self_and_ancestors.map(&:title)).to eq(%w{C B A}) - end - end - - context 'with 1 tag' do - before do - @tag = tag_class.create!(name: 'tag') - end - - it 'should be a leaf' do - expect(@tag.leaf?).to be_truthy - end - - it 'should be a root' do - expect(@tag.root?).to be_truthy - end - - it 'has no parent' do - expect(@tag.parent).to be_nil - end - - it 'should return the only entity as a root and leaf' do - expect(tag_class.all).to eq([@tag]) - expect(tag_class.roots).to eq([@tag]) - expect(tag_class.leaves).to eq([@tag]) - end - - it 'should not be found by passing find_by_path an array of blank strings' do - expect(tag_class.find_by_path([''])).to be_nil - end - - it 'should not be found by passing find_by_path an empty array' do - expect(tag_class.find_by_path([])).to be_nil - end - - it 'should not be found by passing find_by_path nil' do - expect(tag_class.find_by_path(nil)).to be_nil - end - - it 'should not be found by passing find_by_path an empty string' do - expect(tag_class.find_by_path('')).to be_nil - end - - it 'should not be found by passing find_by_path an array of nils' do - expect(tag_class.find_by_path([nil])).to be_nil - end - - it 'should not be found by passing find_by_path an array with an additional blank string' do - expect(tag_class.find_by_path([@tag.name, ''])).to be_nil - end - - it 'should not be found by passing find_by_path an array with an additional nil' do - expect(tag_class.find_by_path([@tag.name, nil])).to be_nil - end - - it 'should be found by passing find_by_path an array with its name' do - expect(tag_class.find_by_path([@tag.name])).to eq @tag - end - - it 'should be found by passing find_by_path its name' do - expect(tag_class.find_by_path(@tag.name)).to eq @tag - end - - context 'with child' do - before do - @child = tag_class.create!(name: 'tag 2') - end - - def assert_roots_and_leaves - expect(@tag.root?).to be_truthy - expect(@tag.leaf?).to be_falsey - - expect(@child.root?).to be_falsey - expect(@child.leaf?).to be_truthy - end - - def assert_parent_and_children - expect(@child.reload.parent).to eq(@tag) - expect(@tag.reload.children.to_a).to eq([@child]) - end - - it 'adds children through add_child' do - @tag.add_child @child - assert_roots_and_leaves - assert_parent_and_children - end - - it 'adds children through collection' do - @tag.children << @child - assert_roots_and_leaves - assert_parent_and_children - end - end - end - - context 'with 2 tags' do - before :each do - @root = tag_class.create!(name: 'root') - @leaf = @root.add_child(tag_class.create!(name: 'leaf')) - end - it 'should return a simple root and leaf' do - expect(tag_class.roots).to eq([@root]) - expect(tag_class.leaves).to eq([@leaf]) - end - it 'should return child_ids for root' do - expect(@root.child_ids).to eq([@leaf.id]) - end - - it 'should return an empty array for leaves' do - expect(@leaf.child_ids).to be_empty - end - end - - context '3 tag collection.create db' do - before :each do - @root = tag_class.create! name: 'root' - @mid = @root.children.create! name: 'mid' - @leaf = @mid.children.create! name: 'leaf' - DestroyedTag.delete_all - end - - it 'should create all tags' do - expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf]) - end - - it 'should return a root and leaf without middle tag' do - expect(tag_class.roots).to eq([@root]) - expect(tag_class.leaves).to eq([@leaf]) - end - - it 'should delete leaves' do - tag_class.leaves.destroy_all - expect(tag_class.roots).to eq([@root]) # untouched - expect(tag_class.leaves).to eq([@mid]) - end - - it 'should delete everything if you delete the roots' do - tag_class.roots.destroy_all - expect(tag_class.all).to be_empty - expect(tag_class.roots).to be_empty - expect(tag_class.leaves).to be_empty - expect(DestroyedTag.all.map { |t| t.name }).to match_array(%w{root mid leaf}) - end - - it 'fix self_and_ancestors properly on reparenting' do - t = tag_class.create! name: 'moar leaf' - expect(t.self_and_ancestors.to_a).to eq([t]) - @mid.children << t - expect(t.self_and_ancestors.to_a).to eq([t, @mid, @root]) - end - - it 'prevents ancestor loops' do - @leaf.add_child @root - expect(@root).not_to be_valid - expect(@root.reload.descendants).to include(@leaf) - end - - it 'moves non-leaves' do - new_root = tag_class.create! name: 'new_root' - new_root.children << @mid - expect(@root.reload.descendants).to be_empty - expect(new_root.descendants).to eq([@mid, @leaf]) - expect(@leaf.reload.ancestry_path).to eq(%w{new_root mid leaf}) - end - - it 'moves leaves' do - new_root = tag_class.create! name: 'new_root' - new_root.children << @leaf - expect(new_root.descendants).to eq([@leaf]) - expect(@root.reload.descendants).to eq([@mid]) - expect(@leaf.reload.ancestry_path).to eq(%w{new_root leaf}) - end - end - - context '3 tag explicit_create db' do - before :each do - @root = tag_class.create!(name: 'root') - @mid = @root.add_child(tag_class.create!(name: 'mid')) - @leaf = @mid.add_child(tag_class.create!(name: 'leaf')) - end - - it 'should create all tags' do - expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf]) - end - - it 'should return a root and leaf without middle tag' do - expect(tag_class.roots).to eq([@root]) - expect(tag_class.leaves).to eq([@leaf]) - end - - it 'should prevent parental loops from torso' do - @mid.children << @root - expect(@root.valid?).to be_falsey - expect(@mid.reload.children).to eq([@leaf]) - end - - it 'should prevent parental loops from toes' do - @leaf.children << @root - expect(@root.valid?).to be_falsey - expect(@leaf.reload.children).to be_empty - end - - it 'should support re-parenting' do - @root.children << @leaf - expect(tag_class.leaves).to eq([@leaf, @mid]) - end - - it 'cleans up hierarchy references for leaves' do - @leaf.destroy - expect(tag_hierarchy_class.where(ancestor_id: @leaf.id)).to be_empty - expect(tag_hierarchy_class.where(descendant_id: @leaf.id)).to be_empty - end - - it 'cleans up hierarchy references' do - @mid.destroy - expect(tag_hierarchy_class.where(ancestor_id: @mid.id)).to be_empty - expect(tag_hierarchy_class.where(descendant_id: @mid.id)).to be_empty - expect(@root.reload).to be_root - root_hiers = @root.ancestor_hierarchies.to_a - expect(root_hiers.size).to eq(1) - expect(tag_hierarchy_class.where(ancestor_id: @root.id)).to eq(root_hiers) - expect(tag_hierarchy_class.where(descendant_id: @root.id)).to eq(root_hiers) - end - - it 'should have different hash codes for each hierarchy model' do - hashes = tag_hierarchy_class.all.map(&:hash) - expect(hashes).to match_array(hashes.uniq) - end - - it 'should return the same hash code for equal hierarchy models' do - expect(tag_hierarchy_class.first.hash).to eq(tag_hierarchy_class.first.hash) - end - end - - it 'performs as the readme says it does' do - grandparent = tag_class.create(name: 'Grandparent') - parent = grandparent.children.create(name: 'Parent') - child1 = tag_class.create(name: 'First Child', parent: parent) - child2 = tag_class.new(name: 'Second Child') - parent.children << child2 - child3 = tag_class.new(name: 'Third Child') - parent.add_child child3 - expect(grandparent.self_and_descendants.collect(&:name)).to eq( - ['Grandparent', 'Parent', 'First Child', 'Second Child', 'Third Child'] - ) - expect(child1.ancestry_path).to eq( - ['Grandparent', 'Parent', 'First Child'] - ) - expect(child3.ancestry_path).to eq( - ['Grandparent', 'Parent', 'Third Child'] - ) - d = tag_class.find_or_create_by_path %w(a b c d) - h = tag_class.find_or_create_by_path %w(e f g h) - e = h.root - d.add_child(e) # "d.children << e" would work too, of course - expect(h.ancestry_path).to eq(%w(a b c d e f g h)) - end - - it 'roots sort alphabetically' do - expected = ('a'..'z').to_a - expected.shuffle.each { |ea| tag_class.create!(name: ea) } - expect(tag_class.roots.collect { |ea| ea.name }).to eq(expected) - end - - context 'with simple tree' do - before :each do - tag_class.find_or_create_by_path %w(a1 b1 c1a) - tag_class.find_or_create_by_path %w(a1 b1 c1b) - tag_class.find_or_create_by_path %w(a1 b1 c1c) - tag_class.find_or_create_by_path %w(a1 b1b) - tag_class.find_or_create_by_path %w(a2 b2) - tag_class.find_or_create_by_path %w(a3) - - @a1, @a2, @a3, @b1, @b1b, @b2, @c1a, @c1b, @c1c = - tag_class.all.sort_by(&:name) - @expected_roots = [@a1, @a2, @a3] - @expected_leaves = [@c1a, @c1b, @c1c, @b1b, @b2, @a3] - @expected_siblings = [[@a1, @a2, @a3], [@b1, @b1b], [@c1a, @c1b, @c1c]] - @expected_only_children = tag_class.all - @expected_siblings.flatten - end - - it 'should find global roots' do - expect(tag_class.roots.to_a).to match_array(@expected_roots) - end - it 'should return root? for roots' do - @expected_roots.each { |ea| expect(ea).to be_root } - end - it 'should not return root? for non-roots' do - [@b1, @b2, @c1a, @c1b].each { |ea| expect(ea).not_to be_root } - end - it 'should return the correct root' do - {@a1 => @a1, @a2 => @a2, @a3 => @a3, - @b1 => @a1, @b2 => @a2, @c1a => @a1, @c1b => @a1}.each do |node, root| - expect(node.root).to eq(root) - end - end - it 'should assemble global leaves' do - expect(tag_class.leaves.to_a).to match_array(@expected_leaves) - end - it 'assembles siblings properly' do - @expected_siblings.each do |siblings| - siblings.each do |ea| - expect(ea.self_and_siblings.to_a).to match_array(siblings) - expect(ea.siblings.to_a).to match_array(siblings - [ea]) - end - end - @expected_only_children.each do |ea| - expect(ea.siblings).to eq([]) - end - end - it 'assembles before_siblings' do - @expected_siblings.each do |siblings| - (siblings.size - 1).times do |i| - target = siblings[i] - expected_before = siblings.first(i) - expect(target.siblings_before.to_a).to eq(expected_before) - end - end - end - it 'assembles after_siblings' do - @expected_siblings.each do |siblings| - (siblings.size - 1).times do |i| - target = siblings[i] - expected_after = siblings.last(siblings.size - 1 - i) - expect(target.siblings_after.to_a).to eq(expected_after) - end - end - end - it 'should assemble instance leaves' do - {@a1 => [@b1b, @c1a, @c1b, @c1c], @b1 => [@c1a, @c1b, @c1c], @a2 => [@b2]}.each do |node, leaves| - expect(node.leaves.to_a).to eq(leaves) - end - @expected_leaves.each { |ea| expect(ea.leaves.to_a).to eq([ea]) } - end - it 'should return leaf? for leaves' do - @expected_leaves.each { |ea| expect(ea).to be_leaf } - end - - it 'can move roots' do - @c1a.children << @a2 - @b2.reload.children << @a3 - expect(@a3.reload.ancestry_path).to eq(%w(a1 b1 c1a a2 b2 a3)) - end - - it 'cascade-deletes from roots' do - victim_names = @a1.self_and_descendants.map(&:name) - survivor_names = tag_class.all.map(&:name) - victim_names - @a1.destroy - expect(tag_class.all.map(&:name)).to eq(survivor_names) - end - end - - context 'with_ancestor' do - it 'works with no rows' do - expect(tag_class.with_ancestor.to_a).to be_empty - end - it 'finds only children' do - c = tag_class.find_or_create_by_path %w(A B C) - a, b = c.parent.parent, c.parent - spurious_tags = tag_class.find_or_create_by_path %w(D E) - expect(tag_class.with_ancestor(a).to_a).to eq([b, c]) - end - it 'limits subsequent where clauses' do - a1c = tag_class.find_or_create_by_path %w(A1 B C) - a2c = tag_class.find_or_create_by_path %w(A2 B C) - # different paths! - expect(a1c).not_to eq(a2c) - expect(tag_class.where(:name => 'C').to_a).to match_array([a1c, a2c]) - expect(tag_class.with_ancestor(a1c.parent.parent).where(:name => 'C').to_a).to eq([a1c]) - end - end - - context 'with_descendant' do - it 'works with no rows' do - expect(tag_class.with_descendant.to_a).to be_empty - end - - it 'finds only parents' do - c = tag_class.find_or_create_by_path %w(A B C) - a, b = c.parent.parent, c.parent - spurious_tags = tag_class.find_or_create_by_path %w(D E) - expect(tag_class.with_descendant(c).to_a).to eq([a, b]) - end - - it 'limits subsequent where clauses' do - ac1 = tag_class.create(name: 'A') - ac2 = tag_class.create(name: 'A') - - c1 = tag_class.find_or_create_by_path %w(B C1) - ac1.children << c1.parent - - c2 = tag_class.find_or_create_by_path %w(B C2) - ac2.children << c2.parent - - # different paths! - expect(ac1).not_to eq(ac2) - expect(tag_class.where(:name => 'A').to_a).to match_array([ac1, ac2]) - expect(tag_class.with_descendant(c1).where(:name => 'A').to_a).to eq([ac1]) - end - end - - context 'lowest_common_ancestor' do - let!(:t1) { tag_class.create!(name: 't1') } - let!(:t11) { tag_class.create!(name: 't11', parent: t1) } - let!(:t111) { tag_class.create!(name: 't111', parent: t11) } - let!(:t112) { tag_class.create!(name: 't112', parent: t11) } - let!(:t12) { tag_class.create!(name: 't12', parent: t1) } - let!(:t121) { tag_class.create!(name: 't121', parent: t12) } - let!(:t2) { tag_class.create!(name: 't2') } - let!(:t21) { tag_class.create!(name: 't21', parent: t2) } - let!(:t211) { tag_class.create!(name: 't211', parent: t21) } - - it 'finds the parent for siblings' do - expect(tag_class.lowest_common_ancestor(t112, t111)).to eq t11 - expect(tag_class.lowest_common_ancestor(t12, t11)).to eq t1 - - expect(tag_class.lowest_common_ancestor([t112, t111])).to eq t11 - expect(tag_class.lowest_common_ancestor([t12, t11])).to eq t1 - - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t112', 't111']))).to eq t11 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't11']))).to eq t1 - end - - it 'finds the grandparent for cousins' do - expect(tag_class.lowest_common_ancestor(t112, t111, t121)).to eq t1 - expect(tag_class.lowest_common_ancestor([t112, t111, t121])).to eq t1 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t112', 't111', 't121']))).to eq t1 - end - - it 'finds the parent/grandparent for aunt-uncle/niece-nephew' do - expect(tag_class.lowest_common_ancestor(t12, t112)).to eq t1 - expect(tag_class.lowest_common_ancestor([t12, t112])).to eq t1 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't112']))).to eq t1 - end - - it 'finds the self/parent for parent/child' do - expect(tag_class.lowest_common_ancestor(t12, t121)).to eq t12 - expect(tag_class.lowest_common_ancestor(t1, t12)).to eq t1 - - expect(tag_class.lowest_common_ancestor([t12, t121])).to eq t12 - expect(tag_class.lowest_common_ancestor([t1, t12])).to eq t1 - - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't121']))).to eq t12 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t1', 't12']))).to eq t1 - end - - it 'finds the self/grandparent for grandparent/grandchild' do - expect(tag_class.lowest_common_ancestor(t211, t2)).to eq t2 - expect(tag_class.lowest_common_ancestor(t111, t1)).to eq t1 - - expect(tag_class.lowest_common_ancestor([t211, t2])).to eq t2 - expect(tag_class.lowest_common_ancestor([t111, t1])).to eq t1 - - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t211', 't2']))).to eq t2 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t111', 't1']))).to eq t1 - end - - it 'finds the grandparent for a whole extended family' do - expect(tag_class.lowest_common_ancestor(t1, t11, t111, t112, t12, t121)).to eq t1 - expect(tag_class.lowest_common_ancestor(t2, t21, t211)).to eq t2 - - expect(tag_class.lowest_common_ancestor([t1, t11, t111, t112, t12, t121])).to eq t1 - expect(tag_class.lowest_common_ancestor([t2, t21, t211])).to eq t2 - - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t1', 't11', 't111', 't112', 't12', 't121']))).to eq t1 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t2', 't21', 't211']))).to eq t2 - end - - it 'is nil for no items' do - expect(tag_class.lowest_common_ancestor).to be_nil - expect(tag_class.lowest_common_ancestor([])).to be_nil - expect(tag_class.lowest_common_ancestor(tag_class.none)).to be_nil - end - - it 'is nil if there are no common ancestors' do - expect(tag_class.lowest_common_ancestor(t111, t211)).to be_nil - expect(tag_class.lowest_common_ancestor([t111, t211])).to be_nil - expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t111', 't211']))).to be_nil - end - - it 'is itself for single item' do - expect(tag_class.lowest_common_ancestor(t111)).to eq t111 - expect(tag_class.lowest_common_ancestor(t2)).to eq t2 - - expect(tag_class.lowest_common_ancestor([t111])).to eq t111 - expect(tag_class.lowest_common_ancestor([t2])).to eq t2 - - expect(tag_class.lowest_common_ancestor(tag_class.where(name: 't111'))).to eq t111 - expect(tag_class.lowest_common_ancestor(tag_class.where(name: 't2'))).to eq t2 - end - end - - context 'paths' do - context 'with grandchild' do - before do - @child = tag_class.find_or_create_by_path([ - {name: 'grandparent', title: 'Nonnie'}, - {name: 'parent', title: 'Mom'}, - {name: 'child', title: 'Kid'}]) - @parent = @child.parent - @grandparent = @parent.parent - end - - it 'should build ancestry path' do - expect(@child.ancestry_path).to eq(%w{grandparent parent child}) - expect(@child.ancestry_path(:name)).to eq(%w{grandparent parent child}) - expect(@child.ancestry_path(:title)).to eq(%w{Nonnie Mom Kid}) - end - - it 'assembles ancestors' do - expect(@child.ancestors).to eq([@parent, @grandparent]) - expect(@child.self_and_ancestors).to eq([@child, @parent, @grandparent]) - end - - it 'should find by path' do - # class method: - expect(tag_class.find_by_path(%w{grandparent parent child})).to eq(@child) - # instance method: - expect(@parent.find_by_path(%w{child})).to eq(@child) - expect(@grandparent.find_by_path(%w{parent child})).to eq(@child) - expect(@parent.find_by_path(%w{child larvae})).to be_nil - end - - it 'should respect attribute hashes with both selection and creation' do - expected_title = 'something else' - attrs = {title: expected_title} - existing_title = @grandparent.title - new_grandparent = tag_class.find_or_create_by_path(%w{grandparent}, attrs) - expect(new_grandparent).not_to eq(@grandparent) - expect(new_grandparent.title).to eq(expected_title) - expect(@grandparent.reload.title).to eq(existing_title) - end - - it 'should create a hierarchy with a given attribute' do - expected_title = 'unicorn rainbows' - attrs = {title: expected_title} - child = tag_class.find_or_create_by_path(%w{grandparent parent child}, attrs) - expect(child).not_to eq(@child) - [child, child.parent, child.parent.parent].each do |ea| - expect(ea.title).to eq(expected_title) - end - end - end - - it 'finds correctly rooted paths' do - decoy = tag_class.find_or_create_by_path %w(a b c d) - b_d = tag_class.find_or_create_by_path %w(b c d) - expect(tag_class.find_by_path(%w(b c d))).to eq(b_d) - expect(tag_class.find_by_path(%w(c d))).to be_nil - end - - it 'find_by_path for 1 node' do - b = tag_class.find_or_create_by_path %w(a b) - b2 = b.root.find_by_path(%w(b)) - expect(b2).to eq(b) - end - - it 'find_by_path for 2 nodes' do - path = %w(a b c) - c = tag_class.find_or_create_by_path path - permutations = path.permutation.to_a - correct = %w(b c) - expect(c.root.find_by_path(correct)).to eq(c) - (permutations - correct).each do |bad_path| - expect(c.root.find_by_path(bad_path)).to be_nil - end - end - - it 'find_by_path for 3 nodes' do - d = tag_class.find_or_create_by_path %w(a b c d) - expect(d.root.find_by_path(%w(b c d))).to eq(d) - expect(tag_class.find_by_path(%w(a b c d))).to eq(d) - expect(tag_class.find_by_path(%w(d))).to be_nil - end - - it 'should return nil for missing nodes' do - expect(tag_class.find_by_path(%w{missing})).to be_nil - expect(tag_class.find_by_path(%w{grandparent missing})).to be_nil - expect(tag_class.find_by_path(%w{grandparent parent missing})).to be_nil - expect(tag_class.find_by_path(%w{grandparent parent missing child})).to be_nil - end - - describe '.find_or_create_by_path' do - it 'uses existing records' do - grandparent = tag_class.find_or_create_by_path(%w{grandparent}) - expect(grandparent).to eq(grandparent) - child = tag_class.find_or_create_by_path(%w{grandparent parent child}) - expect(child).to eq(child) - end - - it 'creates 2-deep trees with strings' do - subject = tag_class.find_or_create_by_path(%w{events anniversary}) - expect(subject.ancestry_path).to eq(%w{events anniversary}) - end - - it 'creates 2-deep trees with hashes' do - subject = tag_class.find_or_create_by_path([ - {name: 'test1', title: 'TEST1'}, - {name: 'test2', title: 'TEST2'} - ]) - expect(subject.ancestry_path).to eq(%w{test1 test2}) - # `self_and_ancestors` and `ancestors` is ordered parent-first. (!!) - expect(subject.self_and_ancestors.map(&:title)).to eq(%w{TEST2 TEST1}) - end - - end - end - - context 'hash_tree' do - before :each do - @d1 = tag_class.find_or_create_by_path %w(a b c1 d1) - @c1 = @d1.parent - @b = @c1.parent - @a = @b.parent - @a2 = tag_class.create(name: 'a2') - @b2 = tag_class.find_or_create_by_path %w(a b2) - @c3 = tag_class.find_or_create_by_path %w(a3 b3 c3) - @b3 = @c3.parent - @a3 = @b3.parent - @tree2 = { - @a => {@b => {}, @b2 => {}}, @a2 => {}, @a3 => {@b3 => {}} - } - - @one_tree = { - @a => {}, - @a2 => {}, - @a3 => {} - } - @two_tree = { - @a => { - @b => {}, - @b2 => {} - }, - @a2 => {}, - @a3 => { - @b3 => {} - } - } - @three_tree = { - @a => { - @b => { - @c1 => {}, - }, - @b2 => {} - }, - @a2 => {}, - @a3 => { - @b3 => { - @c3 => {} - } - } - } - @full_tree = { - @a => { - @b => { - @c1 => { - @d1 => {} - }, - }, - @b2 => {} - }, - @a2 => {}, - @a3 => { - @b3 => { - @c3 => {} - } - } - } - #File.open("example.dot", "w") { |f| f.write(tag_class.root.to_dot_digraph) } - end - - context '#hash_tree' do - it 'returns {} for depth 0' do - expect(tag_class.hash_tree(limit_depth: 0)).to eq({}) - end - it 'limit_depth 1' do - expect(tag_class.hash_tree(limit_depth: 1)).to eq(@one_tree) - end - it 'limit_depth 2' do - expect(tag_class.hash_tree(limit_depth: 2)).to eq(@two_tree) - end - it 'limit_depth 3' do - expect(tag_class.hash_tree(limit_depth: 3)).to eq(@three_tree) - end - it 'limit_depth 4' do - expect(tag_class.hash_tree(limit_depth: 4)).to eq(@full_tree) - end - it 'no limit' do - expect(tag_class.hash_tree).to eq(@full_tree) - end - end - - context '.hash_tree' do - it 'returns {} for depth 0' do - expect(@b.hash_tree(limit_depth: 0)).to eq({}) - end - it 'limit_depth 1' do - expect(@b.hash_tree(limit_depth: 1)).to eq(@two_tree[@a].slice(@b)) - end - it 'limit_depth 2' do - expect(@b.hash_tree(limit_depth: 2)).to eq(@three_tree[@a].slice(@b)) - end - it 'limit_depth 3' do - expect(@b.hash_tree(limit_depth: 3)).to eq(@full_tree[@a].slice(@b)) - end - it 'no limit from subsubroot' do - expect(@c1.hash_tree).to eq(@full_tree[@a][@b].slice(@c1)) - end - it 'no limit from subroot' do - expect(@b.hash_tree).to eq(@full_tree[@a].slice(@b)) - end - it 'no limit from root' do - expect(@a.hash_tree.merge(@a2.hash_tree)).to eq(@full_tree.slice(@a, @a2)) - end - end - - context '.hash_tree from relations' do - it 'limit_depth 2 from chained activerecord association subroots' do - expect(@a.children.hash_tree(limit_depth: 2)).to eq(@three_tree[@a]) - end - it 'no limit from chained activerecord association subroots' do - expect(@a.children.hash_tree).to eq(@full_tree[@a]) - end - it 'limit_depth 3 from b.parent' do - expect(@b.parent.hash_tree(limit_depth: 3)).to eq(@three_tree.slice(@a)) - end - it 'no limit_depth from b.parent' do - expect(@b.parent.hash_tree).to eq(@full_tree.slice(@a)) - end - it 'no limit_depth from c.parent' do - expect(@c1.parent.hash_tree).to eq(@full_tree[@a].slice(@b)) - end - end - end - - it 'finds_by_path for very deep trees' do - expect(tag_class._ct).to receive(:max_join_tables).at_least(1).and_return(3) - path = (1..20).to_a.map { |ea| ea.to_s } - subject = tag_class.find_or_create_by_path(path) - expect(subject.ancestry_path).to eq(path) - expect(tag_class.find_by_path(path)).to eq(subject) - root = subject.root - expect(root.find_by_path(path[1..-1])).to eq(subject) - end - - describe 'DOT rendering' do - it 'should render for an empty scope' do - expect(tag_class.to_dot_digraph(tag_class.where('0=1'))).to eq("digraph G {\n}\n") - end - it 'should render for an empty scope' do - tag_class.find_or_create_by_path(%w(a b1 c1)) - tag_class.find_or_create_by_path(%w(a b2 c2)) - tag_class.find_or_create_by_path(%w(a b2 c3)) - a, b1, b2, c1, c2, c3 = %w(a b1 b2 c1 c2 c3).map { |ea| tag_class.where(name: ea).first.id } - dot = tag_class.roots.first.to_dot_digraph - expect(dot).to eq <<-DOT -digraph G { - "#{a}" [label="a"] - "#{a}" -> "#{b1}" - "#{b1}" [label="b1"] - "#{a}" -> "#{b2}" - "#{b2}" [label="b2"] - "#{b1}" -> "#{c1}" - "#{c1}" [label="c1"] - "#{b2}" -> "#{c2}" - "#{c2}" [label="c2"] - "#{b2}" -> "#{c3}" - "#{c3}" [label="c3"] -} - DOT - end - end - end -end diff --git a/test/closure_tree/tag_test.rb b/test/closure_tree/tag_test.rb new file mode 100644 index 0000000..db99bbd --- /dev/null +++ b/test/closure_tree/tag_test.rb @@ -0,0 +1,5 @@ +require "test_helper" + +describe Tag do + include TagExamples +end diff --git a/test/closure_tree/uuid_tag_test.rb b/test/closure_tree/uuid_tag_test.rb new file mode 100644 index 0000000..1a2f797 --- /dev/null +++ b/test/closure_tree/uuid_tag_test.rb @@ -0,0 +1,5 @@ +require "test_helper" + +describe UUIDTag do + include TagExamples +end diff --git a/test/support/tag_examples.rb b/test/support/tag_examples.rb new file mode 100644 index 0000000..3986baa --- /dev/null +++ b/test/support/tag_examples.rb @@ -0,0 +1,862 @@ +module TagExamples + def self.included(mod) + @@described_class = mod.name.safe_constantize + end + + describe "TagExamples" do + before do + @tag_class = @@described_class + @tag_hierarchy_class = @@described_class.hierarchy_class + end + + describe "class setup" do + it "has correct accessible_attributes" do + if @tag_class._ct.use_attr_accessible? + assert_equal(%w[parent name title].sort, @tag_class.accessible_attributes.to_a.sort) + end + end + + it "should build hierarchy classname correctly" do + assert_equal @tag_hierarchy_class, @tag_class.hierarchy_class + assert_equal @tag_hierarchy_class.to_s, @tag_class._ct.hierarchy_class_name + assert_equal @tag_hierarchy_class.to_s, @tag_class._ct.short_hierarchy_class_name + end + + it "should have a correct parent column name" do + expected_parent_column_name = @tag_class == UUIDTag ? "parent_uuid" : "parent_id" + assert_equal expected_parent_column_name, @tag_class._ct.parent_column_name + end + end + + describe "from empty db" do + describe "with no tags" do + it "should return no entities" do + assert_empty @tag_class.roots + assert_empty @tag_class.leaves + end + + it "#find_or_create_by_path with strings" do + a = @tag_class.create!(name: "a") + assert_equal(%w[a b c], a.find_or_create_by_path(%w[b c]).ancestry_path) + end + + it "#find_or_create_by_path with hashes" do + a = @tag_class.create!(name: "a", title: "A") + subject = a.find_or_create_by_path([ + {name: "b", title: "B"}, + {name: "c", title: "C"} + ]) + assert_equal(%w[a b c], subject.ancestry_path) + assert_equal(%w[C B A], subject.self_and_ancestors.map(&:title)) + end + end + + describe "with 1 tag" do + before do + @tag = @tag_class.create!(name: "tag") + end + + it "should be a leaf" do + assert @tag.leaf? + end + + it "should be a root" do + assert @tag.root? + end + + it "has no parent" do + assert_nil @tag.parent + end + + it "should return the only entity as a root and leaf" do + assert_equal [@tag], @tag_class.all + assert_equal [@tag], @tag_class.roots + assert_equal [@tag], @tag_class.leaves + end + + it "should not be found by passing find_by_path an array of blank strings" do + assert_nil @tag_class.find_by_path([""]) + end + + it "should not be found by passing find_by_path an empty array" do + assert_nil @tag_class.find_by_path([]) + end + + it "should not be found by passing find_by_path nil" do + assert_nil @tag_class.find_by_path(nil) + end + + it "should not be found by passing find_by_path an empty string" do + assert_nil @tag_class.find_by_path("") + end + + it "should not be found by passing find_by_path an array of nils" do + assert_nil @tag_class.find_by_path([nil]) + end + + it "should not be found by passing find_by_path an array with an additional blank string" do + assert_nil @tag_class.find_by_path([@tag.name, ""]) + end + + it "should not be found by passing find_by_path an array with an additional nil" do + assert_nil @tag_class.find_by_path([@tag.name, nil]) + end + + it "should be found by passing find_by_path an array with its name" do + assert_equal @tag, @tag_class.find_by_path([@tag.name]) + end + + it "should be found by passing find_by_path its name" do + assert_equal @tag, @tag_class.find_by_path(@tag.name) + end + + describe "with child" do + before do + @child = @tag_class.create!(name: "tag 2") + end + + def assert_roots_and_leaves + assert @tag.root? + refute @tag.leaf? + + refute @child.root? + assert @child.leaf? + end + + def assert_parent_and_children + assert_equal @tag, @child.reload.parent + assert_equal [@child], @tag.reload.children.to_a + end + + it "adds children through add_child" do + @tag.add_child @child + assert_roots_and_leaves + assert_parent_and_children + end + + it "adds children through collection" do + @tag.children << @child + assert_roots_and_leaves + assert_parent_and_children + end + end + end + + describe "with 2 tags" do + before do + @root = @tag_class.create!(name: "root") + @leaf = @root.add_child(@tag_class.create!(name: "leaf")) + end + + it "should return a simple root and leaf" do + assert_equal [@root], @tag_class.roots + assert_equal [@leaf], @tag_class.leaves + end + + it "should return child_ids for root" do + assert_equal [@leaf.id], @root.child_ids + end + + it "should return an empty array for leaves" do + assert_empty @leaf.child_ids + end + end + + describe "3 tag collection.create db" do + before do + @root = @tag_class.create! name: "root" + @mid = @root.children.create! name: "mid" + @leaf = @mid.children.create! name: "leaf" + DestroyedTag.delete_all + end + + it "should create all tags" do + assert_equal [@root, @mid, @leaf].sort, @tag_class.all.to_a.sort + end + + it "should return a root and leaf without middle tag" do + assert_equal [@root], @tag_class.roots + assert_equal [@leaf], @tag_class.leaves + end + + it "should delete leaves" do + @tag_class.leaves.destroy_all + assert_equal [@root], @tag_class.roots # untouched + assert_equal [@mid], @tag_class.leaves + end + + it "should delete everything if you delete the roots" do + @tag_class.roots.destroy_all + assert_empty @tag_class.all + assert_empty @tag_class.roots + assert_empty @tag_class.leaves + assert_equal %w[root mid leaf].sort, DestroyedTag.all.map { |t| t.name }.sort + end + + it "fix self_and_ancestors properly on reparenting" do + t = @tag_class.create! name: "moar leaf" + assert_equal [t], t.self_and_ancestors.to_a + @mid.children << t + assert_equal [t, @mid, @root], t.self_and_ancestors.to_a + end + + it "prevents ancestor loops" do + @leaf.add_child @root + refute @root.valid? + assert_includes @root.reload.descendants, @leaf + end + + it "moves non-leaves" do + new_root = @tag_class.create! name: "new_root" + new_root.children << @mid + assert_empty @root.reload.descendants + assert_equal [@mid, @leaf], new_root.descendants + assert_equal %w[new_root mid leaf], @leaf.reload.ancestry_path + end + + it "moves leaves" do + new_root = @tag_class.create! name: "new_root" + new_root.children << @leaf + assert_equal [@leaf], new_root.descendants + assert_equal [@mid], @root.reload.descendants + assert_equal %w[new_root leaf], @leaf.reload.ancestry_path + end + end + + describe "3 tag explicit_create db" do + before do + @root = @tag_class.create!(name: "root") + @mid = @root.add_child(@tag_class.create!(name: "mid")) + @leaf = @mid.add_child(@tag_class.create!(name: "leaf")) + end + + it "should create all tags" do + assert_equal [@root, @mid, @leaf].sort, @tag_class.all.to_a.sort + end + + it "should return a root and leaf without middle tag" do + assert_equal [@root], @tag_class.roots + assert_equal [@leaf], @tag_class.leaves + end + + it "should prevent parental loops from torso" do + @mid.children << @root + refute @root.valid? + assert_equal [@leaf], @mid.reload.children + end + + it "should prevent parental loops from toes" do + @leaf.children << @root + refute @root.valid? + assert_empty @leaf.reload.children + end + + it "should support re-parenting" do + @root.children << @leaf + assert_equal [@leaf, @mid], @tag_class.leaves + end + + it "cleans up hierarchy references for leaves" do + @leaf.destroy + assert_empty @tag_hierarchy_class.where(ancestor_id: @leaf.id) + assert_empty @tag_hierarchy_class.where(descendant_id: @leaf.id) + end + + it "cleans up hierarchy references" do + @mid.destroy + assert_empty @tag_hierarchy_class.where(ancestor_id: @mid.id) + assert_empty @tag_hierarchy_class.where(descendant_id: @mid.id) + assert @root.reload.root? + root_hiers = @root.ancestor_hierarchies.to_a + assert_equal 1, root_hiers.size + assert_equal root_hiers, @tag_hierarchy_class.where(ancestor_id: @root.id) + assert_equal root_hiers, @tag_hierarchy_class.where(descendant_id: @root.id) + end + + it "should have different hash codes for each hierarchy model" do + hashes = @tag_hierarchy_class.all.map(&:hash) + assert_equal hashes.uniq.sort, hashes.sort + end + + it "should return the same hash code for equal hierarchy models" do + assert_equal @tag_hierarchy_class.first.hash, @tag_hierarchy_class.first.hash + end + end + + it "performs as the readme says it does" do + grandparent = @tag_class.create(name: "Grandparent") + parent = grandparent.children.create(name: "Parent") + child1 = @tag_class.create(name: "First Child", parent: parent) + child2 = @tag_class.new(name: "Second Child") + parent.children << child2 + child3 = @tag_class.new(name: "Third Child") + parent.add_child child3 + assert_equal( + ["Grandparent", "Parent", "First Child", "Second Child", "Third Child"], + grandparent.self_and_descendants.collect(&:name) + ) + assert_equal(["Grandparent", "Parent", "First Child"], child1.ancestry_path) + assert_equal(["Grandparent", "Parent", "Third Child"], child3.ancestry_path) + d = @tag_class.find_or_create_by_path %w[a b c d] + h = @tag_class.find_or_create_by_path %w[e f g h] + e = h.root + d.add_child(e) # "d.children << e" would work too, of course + assert_equal %w[a b c d e f g h], h.ancestry_path + end + + it "roots sort alphabetically" do + expected = ("a".."z").to_a + expected.shuffle.each { |ea| @tag_class.create!(name: ea) } + assert_equal expected, @tag_class.roots.collect { |ea| ea.name } + end + + describe "with simple tree" do + before do + @tag_class.find_or_create_by_path %w[a1 b1 c1a] + @tag_class.find_or_create_by_path %w[a1 b1 c1b] + @tag_class.find_or_create_by_path %w[a1 b1 c1c] + @tag_class.find_or_create_by_path %w[a1 b1b] + @tag_class.find_or_create_by_path %w[a2 b2] + @tag_class.find_or_create_by_path %w[a3] + + @a1, @a2, @a3, @b1, @b1b, @b2, @c1a, @c1b, @c1c = @tag_class.all.sort_by(&:name) + @expected_roots = [@a1, @a2, @a3] + @expected_leaves = [@c1a, @c1b, @c1c, @b1b, @b2, @a3] + @expected_siblings = [[@a1, @a2, @a3], [@b1, @b1b], [@c1a, @c1b, @c1c]] + @expected_only_children = @tag_class.all - @expected_siblings.flatten + end + + it "should find global roots" do + assert_equal @expected_roots.sort, @tag_class.roots.to_a.sort + end + + it "should return root? for roots" do + @expected_roots.each { |ea| assert(ea.root?) } + end + + it "should not return root? for non-roots" do + [@b1, @b2, @c1a, @c1b].each { |ea| refute(ea.root?) } + end + + it "should return the correct root" do + {@a1 => @a1, @a2 => @a2, @a3 => @a3, + @b1 => @a1, @b2 => @a2, @c1a => @a1, @c1b => @a1}.each do |node, root| + assert_equal(root, node.root) + end + end + + it "should assemble global leaves" do + assert_equal @expected_leaves.sort, @tag_class.leaves.to_a.sort + end + + it "assembles siblings properly" do + @expected_siblings.each do |siblings| + siblings.each do |ea| + assert_equal siblings.sort, ea.self_and_siblings.to_a.sort + assert_equal((siblings - [ea]).sort, ea.siblings.to_a.sort) + end + end + + @expected_only_children.each do |ea| + assert_equal [], ea.siblings + end + end + + it "assembles before_siblings" do + @expected_siblings.each do |siblings| + (siblings.size - 1).times do |i| + target = siblings[i] + expected_before = siblings.first(i) + assert_equal expected_before, target.siblings_before.to_a + end + end + end + + it "assembles after_siblings" do + @expected_siblings.each do |siblings| + (siblings.size - 1).times do |i| + target = siblings[i] + expected_after = siblings.last(siblings.size - 1 - i) + assert_equal expected_after, target.siblings_after.to_a + end + end + end + + it "should assemble instance leaves" do + {@a1 => [@b1b, @c1a, @c1b, @c1c], @b1 => [@c1a, @c1b, @c1c], @a2 => [@b2]}.each do |node, leaves| + assert_equal leaves, node.leaves.to_a + end + + @expected_leaves.each { |ea| assert_equal [ea], ea.leaves.to_a } + end + + it "should return leaf? for leaves" do + @expected_leaves.each { |ea| assert ea.leaf? } + end + + it "can move roots" do + @c1a.children << @a2 + @b2.reload.children << @a3 + assert_equal %w[a1 b1 c1a a2 b2 a3], @a3.reload.ancestry_path + end + + it "cascade-deletes from roots" do + victim_names = @a1.self_and_descendants.map(&:name) + survivor_names = @tag_class.all.map(&:name) - victim_names + @a1.destroy + assert_equal survivor_names, @tag_class.all.map(&:name) + end + end + + describe "with_ancestor" do + it "works with no rows" do + assert_empty @tag_class.with_ancestor.to_a + end + + it "finds only children" do + c = @tag_class.find_or_create_by_path %w[A B C] + a = c.parent.parent + b = c.parent + spurious_tags = @tag_class.find_or_create_by_path %w[D E] + assert_equal [b, c], @tag_class.with_ancestor(a).to_a + end + + it "limits subsequent where clauses" do + a1c = @tag_class.find_or_create_by_path %w[A1 B C] + a2c = @tag_class.find_or_create_by_path %w[A2 B C] + # different paths! + refute_equal a2c, a1c + assert_equal [a1c, a2c].sort, @tag_class.where(name: "C").to_a.sort + assert_equal [a1c], @tag_class.with_ancestor(a1c.parent.parent).where(name: "C").to_a.sort + end + end + + describe "with_descendant" do + it "works with no rows" do + assert_empty @tag_class.with_descendant.to_a + end + + it "finds only parents" do + c = @tag_class.find_or_create_by_path %w[A B C] + a = c.parent.parent + b = c.parent + spurious_tags = @tag_class.find_or_create_by_path %w[D E] + assert_equal [a, b], @tag_class.with_descendant(c).to_a + end + + it "limits subsequent where clauses" do + ac1 = @tag_class.create(name: "A") + ac2 = @tag_class.create(name: "A") + + c1 = @tag_class.find_or_create_by_path %w[B C1] + ac1.children << c1.parent + + c2 = @tag_class.find_or_create_by_path %w[B C2] + ac2.children << c2.parent + + # different paths! + refute_equal ac2, ac1 + assert_equal [ac1, ac2].sort, @tag_class.where(name: "A").to_a.sort + assert_equal [ac1], @tag_class.with_descendant(c1).where(name: "A").to_a + end + end + + describe "lowest_common_ancestor" do + before do + @t1 = @tag_class.create!(name: "t1") + @t11 = @tag_class.create!(name: "t11", parent: @t1) + @t111 = @tag_class.create!(name: "t111", parent: @t11) + @t112 = @tag_class.create!(name: "t112", parent: @t11) + @t12 = @tag_class.create!(name: "t12", parent: @t1) + @t121 = @tag_class.create!(name: "t121", parent: @t12) + @t2 = @tag_class.create!(name: "t2") + @t21 = @tag_class.create!(name: "t21", parent: @t2) + @t21 = @tag_class.create!(name: "t21", parent: @t2) + @t211 = @tag_class.create!(name: "t211", parent: @t21) + end + + it "finds the parent for siblings" do + assert_equal @t11, @tag_class.lowest_common_ancestor(@t112, @t111) + assert_equal @t1, @tag_class.lowest_common_ancestor(@t12, @t11) + + assert_equal @t11, @tag_class.lowest_common_ancestor([@t112, @t111]) + assert_equal @t1, @tag_class.lowest_common_ancestor([@t12, @t11]) + + assert_equal @t11, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t112 t111])) + assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t11])) + end + + it "finds the grandparent for cousins" do + assert_equal @t1, @tag_class.lowest_common_ancestor(@t112, @t111, @t121) + assert_equal @t1, @tag_class.lowest_common_ancestor([@t112, @t111, @t121]) + assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t112 t111 t121])) + end + + it "finds the parent/grandparent for aunt-uncle/niece-nephew" do + assert_equal @t1, @tag_class.lowest_common_ancestor(@t12, @t112) + assert_equal @t1, @tag_class.lowest_common_ancestor([@t12, @t112]) + assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t112])) + end + + it "finds the self/parent for parent/child" do + assert_equal @t12, @tag_class.lowest_common_ancestor(@t12, @t121) + assert_equal @t1, @tag_class.lowest_common_ancestor(@t1, @t12) + + assert_equal @t12, @tag_class.lowest_common_ancestor([@t12, @t121]) + assert_equal @t1, @tag_class.lowest_common_ancestor([@t1, @t12]) + + assert_equal @t12, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t121])) + assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t1 t12])) + end + + it "finds the self/grandparent for grandparent/grandchild" do + assert_equal @t2, @tag_class.lowest_common_ancestor(@t211, @t2) + assert_equal @t1, @tag_class.lowest_common_ancestor(@t111, @t1) + + assert_equal @t2, @tag_class.lowest_common_ancestor([@t211, @t2]) + assert_equal @t1, @tag_class.lowest_common_ancestor([@t111, @t1]) + + assert_equal @t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t211 t2])) + assert_equal @t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t111 t1])) + end + + it "finds the grandparent for a whole extended family" do + assert_equal @t1, @tag_class.lowest_common_ancestor(@t1, @t11, @t111, @t112, @t12, @t121) + assert_equal @t2, @tag_class.lowest_common_ancestor(@t2, @t21, @t211) + + assert_equal @t1, @tag_class.lowest_common_ancestor([@t1, @t11, @t111, @t112, @t12, @t121]) + assert_equal @t2, @tag_class.lowest_common_ancestor([@t2, @t21, @t211]) + + assert_equal @t1, + @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t1 t11 t111 t112 t12 t121])) + assert_equal @t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t2 t21 t211])) + end + + it "is nil for no items" do + assert_nil @tag_class.lowest_common_ancestor + assert_nil @tag_class.lowest_common_ancestor([]) + assert_nil @tag_class.lowest_common_ancestor(@tag_class.none) + end + + it "is nil if there are no common ancestors" do + assert_nil @tag_class.lowest_common_ancestor(@t111, @t211) + assert_nil @tag_class.lowest_common_ancestor([@t111, @t211]) + assert_nil @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t111 t211])) + end + + it "is itself for single item" do + assert_equal @t111, @tag_class.lowest_common_ancestor(@t111) + assert_equal @t2, @tag_class.lowest_common_ancestor(@t2) + + assert_equal @t111, @tag_class.lowest_common_ancestor([@t111]) + assert_equal @t2, @tag_class.lowest_common_ancestor([@t2]) + + assert_equal @t111, @tag_class.lowest_common_ancestor(@tag_class.where(name: "t111")) + assert_equal @t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: "t2")) + end + end + + describe "paths" do + describe "with grandchild " do + before do + @child = @tag_class.find_or_create_by_path([ + {name: "grandparent", title: "Nonnie"}, + {name: "parent", title: "Mom"}, + {name: "child", title: "Kid"} + ]) + @parent = @child.parent + @grandparent = @parent.parent + end + + it "should build ancestry path" do + assert_equal %w[grandparent parent child], @child.ancestry_path + assert_equal %w[grandparent parent child], @child.ancestry_path(:name) + assert_equal %w[Nonnie Mom Kid], @child.ancestry_path(:title) + end + + it "assembles ancestors" do + assert_equal [@parent, @grandparent], @child.ancestors + assert_equal [@child, @parent, @grandparent], @child.self_and_ancestors + end + + it "should find by path" do + # class method: + assert_equal @child, @tag_class.find_by_path(%w[grandparent parent child]) + # instance method: + assert_equal @child, @parent.find_by_path(%w[child]) + assert_equal @child, @grandparent.find_by_path(%w[parent child]) + assert_nil @parent.find_by_path(%w[child larvae]) + end + + it "should respect attribute hashes with both selection and creation" do + expected_title = "something else" + attrs = {title: expected_title} + existing_title = @grandparent.title + new_grandparent = @tag_class.find_or_create_by_path(%w[grandparent], attrs) + refute_equal @grandparent, new_grandparent + assert_equal expected_title, new_grandparent.title + assert_equal existing_title, @grandparent.reload.title + end + + it "should create a hierarchy with a given attribute" do + expected_title = "unicorn rainbows" + attrs = {title: expected_title} + child = @tag_class.find_or_create_by_path(%w[grandparent parent child], attrs) + refute_equal @child, child + [child, child.parent, child.parent.parent].each do |ea| + assert_equal expected_title, ea.title + end + end + end + + it "finds correctly rooted paths" do + decoy = @tag_class.find_or_create_by_path %w[a b c d] + b_d = @tag_class.find_or_create_by_path %w[b c d] + assert_equal b_d, @tag_class.find_by_path(%w[b c d]) + assert_nil @tag_class.find_by_path(%w[c d]) + end + + it "find_by_path for 1 node" do + b = @tag_class.find_or_create_by_path %w[a b] + b2 = b.root.find_by_path(%w[b]) + assert_equal b, b2 + end + + it "find_by_path for 2 nodes" do + path = %w[a b c] + c = @tag_class.find_or_create_by_path path + permutations = path.permutation.to_a + correct = %w[b c] + assert_equal c, c.root.find_by_path(correct) + (permutations - correct).each do |bad_path| + assert_nil c.root.find_by_path(bad_path) + end + end + + it "find_by_path for 3 nodes" do + d = @tag_class.find_or_create_by_path %w[a b c d] + assert_equal d, d.root.find_by_path(%w[b c d]) + assert_equal d, @tag_class.find_by_path(%w[a b c d]) + assert_nil @tag_class.find_by_path(%w[d]) + end + + it "should return nil for missing nodes" do + assert_nil @tag_class.find_by_path(%w[missing]) + assert_nil @tag_class.find_by_path(%w[grandparent missing]) + assert_nil @tag_class.find_by_path(%w[grandparent parent missing]) + assert_nil @tag_class.find_by_path(%w[grandparent parent missing child]) + end + + describe ".find_or_create_by_path" do + it "uses existing records" do + grandparent = @tag_class.find_or_create_by_path(%w[grandparent]) + assert_equal grandparent, grandparent + child = @tag_class.find_or_create_by_path(%w[grandparent parent child]) + assert_equal child, child + end + + it "creates 2-deep trees with strings" do + subject = @tag_class.find_or_create_by_path(%w[events anniversary]) + assert_equal %w[events anniversary], subject.ancestry_path + end + + it "creates 2-deep trees with hashes" do + subject = @tag_class.find_or_create_by_path([ + {name: "test1", title: "TEST1"}, + {name: "test2", title: "TEST2"} + ]) + assert_equal %w[test1 test2], subject.ancestry_path + # `self_and_ancestors` and `ancestors` is ordered parent-first. (!!) + assert_equal %w[TEST2 TEST1], subject.self_and_ancestors.map(&:title) + end + end + end + + describe "hash_tree" do + before do + @d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + @c1 = @d1.parent + @b = @c1.parent + @a = @b.parent + @a2 = @tag_class.create(name: "a2") + @b2 = @tag_class.find_or_create_by_path %w[a b2] + @c3 = @tag_class.find_or_create_by_path %w[a3 b3 c3] + @b3 = @c3.parent + @a3 = @b3.parent + + @tree2 = { + @a => {@b => {}, @b2 => {}}, @a2 => {}, @a3 => {@b3 => {}} + } + + @one_tree = { + @a => {}, + @a2 => {}, + @a3 => {} + } + + @two_tree = { + @a => { + @b => {}, + @b2 => {} + }, + @a2 => {}, + @a3 => { + @b3 => {} + } + } + + @three_tree = { + @a => { + @b => { + @c1 => {} + }, + @b2 => {} + }, + @a2 => {}, + @a3 => { + @b3 => { + @c3 => {} + } + } + } + + @full_tree = { + @a => { + @b => { + @c1 => { + @d1 => {} + } + }, + @b2 => {} + }, + @a2 => {}, + @a3 => { + @b3 => { + @c3 => {} + } + } + } + end + + describe "#hash_tree" do + it "returns {} for depth 0" do + assert_equal({}, @tag_class.hash_tree(limit_depth: 0)) + end + + it "limit_depth 1" do + assert_equal @one_tree, @tag_class.hash_tree(limit_depth: 1) + end + + it "limit_depth 2" do + assert_equal @two_tree, @tag_class.hash_tree(limit_depth: 2) + end + + it "limit_depth 3" do + assert_equal @three_tree, @tag_class.hash_tree(limit_depth: 3) + end + + it "limit_depth 4" do + assert_equal @full_tree, @tag_class.hash_tree(limit_depth: 4) + end + + it "no limit" do + assert_equal @full_tree, @tag_class.hash_tree + end + end + + describe ".hash_tree" do + it "returns {} for depth 0" do + assert_equal({}, @b.hash_tree(limit_depth: 0)) + end + + it "limit_depth 1" do + assert_equal @two_tree[@a].slice(@b), @b.hash_tree(limit_depth: 1) + end + + it "limit_depth 2" do + assert_equal @three_tree[@a].slice(@b), @b.hash_tree(limit_depth: 2) + end + + it "limit_depth 3" do + assert_equal @full_tree[@a].slice(@b), @b.hash_tree(limit_depth: 3) + end + + it "no limit from subsubroot" do + assert_equal @full_tree[@a][@b].slice(@c1), @c1.hash_tree + end + + it "no limit from subroot" do + assert_equal @full_tree[@a].slice(@b), @b.hash_tree + end + + it "no limit from root" do + assert_equal @full_tree.slice(@a, @a2), @a.hash_tree.merge(@a2.hash_tree) + end + end + + describe ".hash_tree from relations" do + it "limit_depth 2 from chained activerecord association subroots" do + assert_equal @three_tree[@a], @a.children.hash_tree(limit_depth: 2) + end + + it "no limit from chained activerecord association subroots" do + assert_equal @full_tree[@a], @a.children.hash_tree + end + + it "limit_depth 3 from b.parent" do + assert_equal @three_tree.slice(@a), @b.parent.hash_tree(limit_depth: 3) + end + + it "no limit_depth from b.parent" do + assert_equal @full_tree.slice(@a), @b.parent.hash_tree + end + + it "no limit_depth from c.parent" do + assert_equal @full_tree[@a].slice(@b), @c1.parent.hash_tree + end + end + end + + it "finds_by_path for very deep trees" do + path = (1..20).to_a.map { |ea| ea.to_s } + subject = @tag_class.find_or_create_by_path(path) + assert_equal path, subject.ancestry_path + assert_equal subject, @tag_class.find_by_path(path) + root = subject.root + assert_equal subject, root.find_by_path(path[1..-1]) + end + + describe "DOT rendering" do + it "should render for an empty scope" do + assert_equal "digraph G {\n}\n", @tag_class.to_dot_digraph(@tag_class.where("0=1")) + end + + it "should render for an empty scope" do + @tag_class.find_or_create_by_path(%w[a b1 c1]) + @tag_class.find_or_create_by_path(%w[a b2 c2]) + @tag_class.find_or_create_by_path(%w[a b2 c3]) + a, b1, b2, c1, c2, c3 = %w[a b1 b2 c1 c2 c3].map { |ea| @tag_class.where(name: ea).first.id } + dot = @tag_class.roots.first.to_dot_digraph + + graph = <<~DOT + digraph G { + "#{a}" [label="a"] + "#{a}" -> "#{b1}" + "#{b1}" [label="b1"] + "#{a}" -> "#{b2}" + "#{b2}" [label="b2"] + "#{b1}" -> "#{c1}" + "#{c1}" [label="c1"] + "#{b2}" -> "#{c2}" + "#{c2}" [label="c2"] + "#{b2}" -> "#{c3}" + "#{c3}" [label="c3"] + } + DOT + + assert_equal(graph, dot) + end + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 7aff03c..c70dd25 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,53 +1,61 @@ -require 'erb' -require 'active_record' -require 'with_advisory_lock' -require 'tmpdir' -require 'securerandom' -require 'minitest' -require 'minitest/autorun' +require "erb" +require "active_record" +require "with_advisory_lock" +require "tmpdir" +require "securerandom" +require "minitest" +require "minitest/autorun" +require "database_cleaner" ActiveRecord::Base.configurations = { default_env: { - url: ENV.fetch('DATABASE_URL', "sqlite3://#{Dir.tmpdir}/#{SecureRandom.hex}.sqlite3"), - properties: { allowPublicKeyRetrieval: true } # for JRuby madness + url: ENV.fetch("DATABASE_URL", "sqlite3://#{Dir.tmpdir}/#{SecureRandom.hex}.sqlite3"), + properties: {allowPublicKeyRetrieval: true} # for JRuby madness } } -ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex +ENV["WITH_ADVISORY_LOCK_PREFIX"] ||= SecureRandom.hex ActiveRecord::Base.establish_connection def env_db @env_db ||= if ActiveRecord::Base.respond_to?(:connection_db_config) - ActiveRecord::Base.connection_db_config.adapter - else - ActiveRecord::Base.connection_config[:adapter] - end.to_sym + ActiveRecord::Base.connection_db_config.adapter + else + ActiveRecord::Base.connection_config[:adapter] + end.to_sym end ActiveRecord::Migration.verbose = false -ActiveRecord::Base.table_name_prefix = ENV['DB_PREFIX'].to_s -ActiveRecord::Base.table_name_suffix = ENV['DB_SUFFIX'].to_s +ActiveRecord::Base.table_name_prefix = ENV["DB_PREFIX"].to_s +ActiveRecord::Base.table_name_suffix = ENV["DB_SUFFIX"].to_s ActiveRecord::Base.establish_connection -def env_db - @env_db ||= if ActiveRecord::Base.respond_to?(:connection_db_config) - ActiveRecord::Base.connection_db_config.adapter - else - ActiveRecord::Base.connection_config[:adapter] - end.to_sym -end - # Use in specs to skip some tests def sqlite? env_db == :sqlite3 end -ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex +ENV["WITH_ADVISORY_LOCK_PREFIX"] ||= SecureRandom.hex ActiveRecord::Base.connection.recreate_database("closure_tree_test") unless sqlite? puts "Testing with #{env_db} database, ActiveRecord #{ActiveRecord.gem_version} and #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} as #{RUBY_VERSION}" -require 'closure_tree' -require_relative '../spec/support/schema' -require_relative '../spec/support/models' \ No newline at end of file +DatabaseCleaner.strategy = :transaction + +class MiniTest::Spec + before :each do + ENV["FLOCK_DIR"] = Dir.mktmpdir + DatabaseCleaner.start + end + + after :each do + FileUtils.remove_entry_secure ENV["FLOCK_DIR"] + DatabaseCleaner.clean + end +end + +require "closure_tree" +require_relative "../spec/support/schema" +require_relative "../spec/support/models" +require "support/tag_examples"