diff --git a/spec/closure_tree/user_spec.rb b/spec/closure_tree/user_spec.rb deleted file mode 100644 index 327224a..0000000 --- a/spec/closure_tree/user_spec.rb +++ /dev/null @@ -1,174 +0,0 @@ -require 'spec_helper' - -RSpec.describe "empty db" do - - context "empty db" do - it "should return no entities" do - expect(User.roots).to be_empty - expect(User.leaves).to be_empty - end - end - - context "1 user db" do - it "should return the only entity as a root and leaf" do - a = User.create!(:email => "me@domain.com") - expect(User.roots).to eq([a]) - expect(User.leaves).to eq([a]) - end - end - - context "2 user db" do - it "should return a simple root and leaf" do - root = User.create!(:email => "first@t.co") - leaf = root.children.create!(:email => "second@t.co") - expect(User.roots).to eq([root]) - expect(User.leaves).to eq([leaf]) - end - end - - context "3 User collection.create db" do - before :each do - @root = User.create! :email => "poppy@t.co" - @mid = @root.children.create! :email => "matt@t.co" - @leaf = @mid.children.create! :email => "james@t.co" - @root_id = @root.id - end - - it "should create all Users" do - expect(User.all.to_a).to match_array([@root, @mid, @leaf]) - end - - it 'orders self_and_ancestor_ids nearest generation first' do - expect(@leaf.self_and_ancestor_ids).to eq([@leaf.id, @mid.id, @root.id]) - end - - it 'orders self_and_descendant_ids nearest generation first' do - expect(@root.self_and_descendant_ids).to eq([@root.id, @mid.id, @leaf.id]) - end - - it "should have children" do - expect(@root.children.to_a).to eq([@mid]) - expect(@mid.children.to_a).to eq([@leaf]) - expect(@leaf.children.to_a).to eq([]) - end - - it "roots should have children" do - expect(User.roots.first.children.to_a).to match_array([@mid]) - end - - it "should return a root and leaf without middle User" do - expect(User.roots.to_a).to eq([@root]) - expect(User.leaves.to_a).to eq([@leaf]) - end - - it "should delete leaves" do - User.leaves.destroy_all - expect(User.roots.to_a).to eq([@root]) # untouched - expect(User.leaves.to_a).to eq([@mid]) - end - - it "should delete roots and maintain hierarchies" do - User.roots.destroy_all - assert_mid_and_leaf_remain - end - - it "should root all children" do - @root.destroy - assert_mid_and_leaf_remain - end - - def assert_mid_and_leaf_remain - expect(ReferralHierarchy.where(:ancestor_id => @root_id)).to be_empty - expect(ReferralHierarchy.where(:descendant_id => @root_id)).to be_empty - expect(@mid.ancestry_path).to eq(%w{matt@t.co}) - expect(@leaf.ancestry_path).to eq(%w{matt@t.co james@t.co}) - expect(@mid.self_and_descendants.to_a).to match_array([@mid, @leaf]) - expect(User.roots).to eq([@mid]) - expect(User.leaves).to eq([@leaf]) - end - end - - it "supports users with contracts" do - u = User.find_or_create_by_path(%w(a@t.co b@t.co c@t.co)) - expect(u.descendant_ids).to eq([]) - expect(u.ancestor_ids).to eq([u.parent.id, u.root.id]) - expect(u.self_and_ancestor_ids).to eq([u.id, u.parent.id, u.root.id]) - expect(u.root.descendant_ids).to eq([u.parent.id, u.id]) - expect(u.root.ancestor_ids).to eq([]) - expect(u.root.self_and_ancestor_ids).to eq([u.root.id]) - c1 = u.contracts.create! - c2 = u.parent.contracts.create! - expect(u.root.indirect_contracts.to_a).to match_array([c1, c2]) - end - - it "supports << on shallow unsaved hierarchies" do - a = User.new(:email => "a") - b = User.new(:email => "b") - a.children << b - a.save - expect(User.roots).to eq([a]) - expect(User.leaves).to eq([b]) - expect(b.ancestry_path).to eq(%w(a b)) - end - - it "supports << on deep unsaved hierarchies" do - a = User.new(:email => "a") - b1 = User.new(:email => "b1") - a.children << b1 - b2 = User.new(:email => "b2") - a.children << b2 - c1 = User.new(:email => "c1") - b2.children << c1 - c2 = User.new(:email => "c2") - b2.children << c2 - d = User.new(:email => "d") - c2.children << d - - a.save - expect(User.roots.to_a).to eq([a]) - expect(User.leaves.to_a).to match_array([b1, c1, d]) - expect(d.ancestry_path).to eq(%w(a b2 c2 d)) - end - - it "supports siblings" do - expect(User._ct.order_option?).to be_falsey - a = User.create(:email => "a") - b1 = a.children.create(:email => "b1") - b2 = a.children.create(:email => "b2") - b3 = a.children.create(:email => "b3") - expect(a.siblings).to be_empty - expect(b1.siblings.to_a).to match_array([b2, b3]) - end - - context "when a user is not yet saved" do - it "supports siblings" do - expect(User._ct.order_option?).to be_falsey - a = User.create(:email => "a") - b1 = a.children.new(:email => "b1") - b2 = a.children.create(:email => "b2") - b3 = a.children.create(:email => "b3") - expect(a.siblings).to be_empty - expect(b1.siblings.to_a).to match_array([b2, b3]) - end - end - - it "properly nullifies descendents" do - c = User.find_or_create_by_path %w(a b c) - b = c.parent - c.root.destroy - expect(b.reload).to be_root - expect(b.child_ids).to eq([c.id]) - end - - context "roots" do - it "works on models without ordering" do - expected = ("a".."z").to_a - expected.shuffle.each do |ea| - User.create! do |u| - u.email = ea - end - end - expect(User.roots.collect { |ea| ea.email }.sort).to eq(expected) - end - end -end diff --git a/spec/closure_tree/parallel_spec.rb b/test/closure_tree/parallel_test.rb similarity index 55% rename from spec/closure_tree/parallel_spec.rb rename to test/closure_tree/parallel_test.rb index 4ec3a61..3d6202b 100644 --- a/spec/closure_tree/parallel_spec.rb +++ b/test/closure_tree/parallel_test.rb @@ -1,4 +1,6 @@ -require 'spec_helper' +# frozen_string_literal: true + +require "test_helper" # We don't need to run the expensive parallel tests for every combination of prefix/suffix. # Those affect SQL generation, not parallelism. @@ -16,10 +18,11 @@ def max_threads class WorkerBase extend Forwardable attr_reader :name + def_delegators :@thread, :join, :wakeup, :status, :to_s def log(msg) - puts("#{Thread.current}: #{msg}") if ENV['VERBOSE'] + puts("#{Thread.current}: #{msg}") if ENV["VERBOSE"] end def initialize(target, name) @@ -27,11 +30,11 @@ def initialize(target, name) @name = name @thread = Thread.new do ActiveRecord::Base.connection_pool.with_connection { before_work } if respond_to? :before_work - log 'going to sleep...' + log "going to sleep..." sleep - log 'woke up...' + log "woke up..." ActiveRecord::Base.connection_pool.with_connection { work } - log 'done.' + log "done." end end end @@ -45,14 +48,25 @@ def work end end -RSpec.describe 'Concurrent creation' do - before :each do +class SiblingPrependerWorker < WorkerBase + def before_work + @target.reload + @sibling = Label.new(name: SecureRandom.hex(10)) + end + + def work + @target.prepend_sibling @sibling + end +end + +describe "Concurrent creation" do + before do @target = nil @iterations = 5 end def log(msg) - puts(msg) if ENV['VERBOSE'] + puts(msg) if ENV["VERBOSE"] end def run_workers(worker_class = FindOrCreateWorker) @@ -61,7 +75,7 @@ def run_workers(worker_class = FindOrCreateWorker) workers = max_threads.times.map { worker_class.new(@target, name) } # Wait for all the threads to get ready: while true - unready_workers = workers.select { |ea| ea.status != 'sleep' } + unready_workers = workers.select { |ea| ea.status != "sleep" } if unready_workers.empty? break else @@ -71,59 +85,57 @@ def run_workers(worker_class = FindOrCreateWorker) end sleep(0.25) # OK, GO! - log 'Calling .wakeup on all workers...' + log "Calling .wakeup on all workers..." workers.each(&:wakeup) sleep(0.25) # Then wait for them to finish: - log 'Calling .join on all workers...' + log "Calling .join on all workers..." workers.each(&:join) end # Ensure we're still connected: ActiveRecord::Base.connection_pool.connection end - it 'will not create dupes from class methods' do + it "will not create dupes from class methods" do + skip("unsupported") unless run_parallel_tests? + run_workers - expect(Tag.roots.collect { |ea| ea.name }).to match_array(@names) + assert_equal @names.sort, Tag.roots.collect { |ea| ea.name }.sort # No dupe children: - %w(a b c).each do |ea| - expect(Tag.where(name: ea).size).to eq(@iterations) + %w[a b c].each do |ea| + assert_equal @iterations, Tag.where(name: ea).size end end - it 'will not create dupes from instance methods' do - @target = Tag.create!(name: 'root') - run_workers - expect(@target.reload.children.collect { |ea| ea.name }).to match_array(@names) - expect(Tag.where(name: @names).size).to eq(@iterations) - %w(a b c).each do |ea| - expect(Tag.where(name: ea).size).to eq(@iterations) - end - end + it "will not create dupes from instance methods" do + skip("unsupported") unless run_parallel_tests? - it 'creates dupe roots without advisory locks' do - # disable with_advisory_lock: - allow(Tag).to receive(:with_advisory_lock) { |_lock_name, &block| block.call } + @target = Tag.create!(name: "root") run_workers - # duplication from at least one iteration: - expect(Tag.where(name: @names).size).to be > @iterations + assert_equal @names.sort, @target.reload.children.collect { |ea| ea.name }.sort + assert_equal @iterations, Tag.where(name: @names).size + %w[a b c].each do |ea| + assert_equal @iterations, Tag.where(name: ea).size + end end - class SiblingPrependerWorker < WorkerBase - def before_work - @target.reload - @sibling = Label.new(name: SecureRandom.hex(10)) - end + it "creates dupe roots without advisory locks" do + skip("unsupported") unless run_parallel_tests? - def work - @target.prepend_sibling @sibling + # disable with_advisory_lock: + Tag.stub(:with_advisory_lock, ->(_lock_name, &block) { block.call }) do + run_workers + # duplication from at least one iteration: + assert Tag.where(name: @names).size > @iterations end end - it 'fails to deadlock while simultaneously deleting items from the same hierarchy' do + it "fails to deadlock while simultaneously deleting items from the same hierarchy" do + skip("unsupported") unless run_parallel_tests? + target = User.find_or_create_by_path((1..200).to_a.map { |ea| ea.to_s }) emails = target.self_and_ancestors.to_a.map(&:email).shuffle - Parallel.map(emails, :in_threads => max_threads) do |email| + Parallel.map(emails, in_threads: max_threads) do |email| ActiveRecord::Base.connection_pool.with_connection do User.transaction do log "Destroying #{email}..." @@ -132,28 +144,19 @@ def work end end User.connection.reconnect! - expect(User.all).to be_empty + assert User.all.empty? end - class SiblingPrependerWorker < WorkerBase - def before_work - @target.reload - @sibling = Label.new(name: SecureRandom.hex(10)) - end + it "fails to deadlock from prepending siblings" do + skip("unsupported") unless run_parallel_tests? - def work - @target.prepend_sibling @sibling - end - end - - it 'fails to deadlock from prepending siblings' do - @target = Label.find_or_create_by_path %w(root parent) + @target = Label.find_or_create_by_path %w[root parent] run_workers(SiblingPrependerWorker) children = Label.roots uniq_order_values = children.collect { |ea| ea.order_value }.uniq - expect(children.size).to eq(uniq_order_values.size) + assert_equal uniq_order_values.size, children.size # The only non-root node should be "root": - expect(Label.all.select { |ea| ea.root? }).to eq([@target.parent]) + assert_equal([@target.parent], Label.all.select { |ea| ea.root? }) end -end if run_parallel_tests? +end diff --git a/test/closure_tree/user_test.rb b/test/closure_tree/user_test.rb new file mode 100644 index 0000000..5522cf1 --- /dev/null +++ b/test/closure_tree/user_test.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require "test_helper" + +describe "empty db" do + describe "empty db" do + it "should return no entities" do + assert User.roots.empty? + assert User.leaves.empty? + end + end + + describe "1 user db" do + it "should return the only entity as a root and leaf" do + a = User.create!(email: "me@domain.com") + assert_equal [a], User.roots + assert_equal [a], User.leaves + end + end + + describe "2 user db" do + it "should return a simple root and leaf" do + root = User.create!(email: "first@t.co") + leaf = root.children.create!(email: "second@t.co") + assert_equal [root], User.roots + assert_equal [leaf], User.leaves + end + end + + describe "3 User collection.create db" do + before do + @root = User.create! email: "poppy@t.co" + @mid = @root.children.create! email: "matt@t.co" + @leaf = @mid.children.create! email: "james@t.co" + @root_id = @root.id + end + + it "should create all Users" do + assert_equal [@root, @mid, @leaf], User.all.to_a.sort + end + + it "orders self_and_ancestor_ids nearest generation first" do + assert_equal [@leaf.id, @mid.id, @root.id], @leaf.self_and_ancestor_ids + end + + it "orders self_and_descendant_ids nearest generation first" do + assert_equal [@root.id, @mid.id, @leaf.id], @root.self_and_descendant_ids + end + + it "should have children" do + assert_equal [@mid], @root.children.to_a + assert_equal [@leaf], @mid.children.to_a + assert_equal [], @leaf.children.to_a + end + + it "roots should have children" do + assert_equal [@mid], User.roots.first.children.to_a + end + + it "should return a root and leaf without middle User" do + assert_equal [@root], User.roots.to_a + assert_equal [@leaf], User.leaves.to_a + end + + it "should delete leaves" do + User.leaves.destroy_all + assert_equal [@root], User.roots.to_a # untouched + assert_equal [@mid], User.leaves.to_a + end + + it "should delete roots and maintain hierarchies" do + User.roots.destroy_all + assert_mid_and_leaf_remain + end + + it "should root all children" do + @root.destroy + assert_mid_and_leaf_remain + end + + def assert_mid_and_leaf_remain + assert ReferralHierarchy.where(ancestor_id: @root_id).empty? + assert ReferralHierarchy.where(descendant_id: @root_id).empty? + assert_equal %w[matt@t.co], @mid.ancestry_path + assert_equal %w[matt@t.co james@t.co], @leaf.ancestry_path + assert_equal [@mid, @leaf].sort, @mid.self_and_descendants.to_a.sort + assert_equal [@mid], User.roots + assert_equal [@leaf], User.leaves + end + end + + it "supports users with contracts" do + u = User.find_or_create_by_path(%w[a@t.co b@t.co c@t.co]) + assert_equal [], u.descendant_ids + assert_equal [u.parent.id, u.root.id], u.ancestor_ids + assert_equal [u.id, u.parent.id, u.root.id], u.self_and_ancestor_ids + assert_equal [u.parent.id, u.id], u.root.descendant_ids + assert_equal [], u.root.ancestor_ids + assert_equal [u.root.id], u.root.self_and_ancestor_ids + c1 = u.contracts.create! + c2 = u.parent.contracts.create! + assert_equal [c1, c2].sort, u.root.indirect_contracts.to_a.sort + end + + it "supports << on shallow unsaved hierarchies" do + a = User.new(email: "a") + b = User.new(email: "b") + a.children << b + a.save + assert_equal [a], User.roots + assert_equal [b], User.leaves + assert_equal %w[a b], b.ancestry_path + end + + it "supports << on deep unsaved hierarchies" do + a = User.new(email: "a") + b1 = User.new(email: "b1") + a.children << b1 + b2 = User.new(email: "b2") + a.children << b2 + c1 = User.new(email: "c1") + b2.children << c1 + c2 = User.new(email: "c2") + b2.children << c2 + d = User.new(email: "d") + c2.children << d + + a.save + assert_equal [a], User.roots.to_a + assert_equal [b1, c1, d].sort, User.leaves.to_a.sort + assert_equal %w[a b2 c2 d], d.ancestry_path + end + + it "supports siblings" do + refute User._ct.order_option? + a = User.create(email: "a") + b1 = a.children.create(email: "b1") + b2 = a.children.create(email: "b2") + b3 = a.children.create(email: "b3") + assert a.siblings.empty? + assert_equal [b2, b3].sort, b1.siblings.to_a.sort + end + + describe "when a user is not yet saved" do + it "supports siblings" do + refute User._ct.order_option? + a = User.create(email: "a") + b1 = a.children.new(email: "b1") + b2 = a.children.create(email: "b2") + b3 = a.children.create(email: "b3") + assert a.siblings.empty? + assert_equal [b2, b3].sort, b1.siblings.to_a.sort + end + end + + it "properly nullifies descendents" do + c = User.find_or_create_by_path %w[a b c] + b = c.parent + c.root.destroy + assert b.reload.root? + assert_equal [c.id], b.child_ids + end + + describe "roots" do + it "works on models without ordering" do + expected = ("a".."z").to_a + expected.shuffle.each do |ea| + User.create! do |u| + u.email = ea + end + end + assert_equal(expected, User.roots.collect { |ea| ea.email }.sort) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 969e376..94fcfaa 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,6 +9,7 @@ require 'minitest/autorun' require 'database_cleaner' require 'support/query_counter' +require 'parallel' ActiveRecord::Base.configurations = { default_env: { @@ -39,8 +40,6 @@ def sqlite? env_db == :sqlite3 end -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}" @@ -62,6 +61,9 @@ class Spec end end +# Configure parallel tests +Thread.abort_on_exception = true + require 'closure_tree' require_relative '../spec/support/schema' require_relative '../spec/support/models'