From c46c51353e3cc68a0ce64d68c257cd35e0958f3b Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Thu, 29 May 2025 18:44:56 +0100 Subject: [PATCH 1/3] ninit --- Appraisals | 51 - Gemfile | 10 +- bin/rails | 6 + closure_tree.gemspec | 1 - gemfiles/activerecord_7.1.gemfile | 21 - gemfiles/activerecord_7.2.gemfile | 15 - gemfiles/activerecord_8.0.gemfile | 15 - gemfiles/activerecord_edge.gemfile | 15 - test/closure_tree/tag_test.rb | 5 +- test/dummy/Rakefile | 8 + .../app/controllers/application_controller.rb | 7 + test/dummy/app/models/application_record.rb | 6 + test/dummy/app/models/label.rb | 4 + test/dummy/app/models/mysql_label.rb | 5 + test/dummy/app/models/mysql_record.rb | 6 + test/dummy/app/models/mysql_tag.rb | 10 + test/dummy/app/models/mysql_tag_audit.rb | 5 + test/dummy/app/models/tag.rb | 8 + test/dummy/app/models/tag_audit.rb | 4 + test/dummy/config.ru | 6 + test/dummy/config/application.rb | 17 + test/dummy/config/boot.rb | 7 + test/dummy/config/database.yml | 5 + test/dummy/config/environment.rb | 7 + test/dummy/config/environments/test.rb | 10 + test/dummy/config/routes.rb | 4 + test/dummy/db/schema.rb | 15 + test/dummy/db/secondary_schema.rb | 15 + test/dummy/lib/tasks/db.rake | 40 + test/support/tag_examples.rb | 1599 +++++++++-------- test/test_helper.rb | 6 + 31 files changed, 1036 insertions(+), 897 deletions(-) delete mode 100644 Appraisals create mode 100755 bin/rails delete mode 100644 gemfiles/activerecord_7.1.gemfile delete mode 100644 gemfiles/activerecord_7.2.gemfile delete mode 100644 gemfiles/activerecord_8.0.gemfile delete mode 100644 gemfiles/activerecord_edge.gemfile create mode 100644 test/dummy/Rakefile create mode 100644 test/dummy/app/controllers/application_controller.rb create mode 100644 test/dummy/app/models/application_record.rb create mode 100644 test/dummy/app/models/label.rb create mode 100644 test/dummy/app/models/mysql_label.rb create mode 100644 test/dummy/app/models/mysql_record.rb create mode 100644 test/dummy/app/models/mysql_tag.rb create mode 100644 test/dummy/app/models/mysql_tag_audit.rb create mode 100644 test/dummy/app/models/tag.rb create mode 100644 test/dummy/app/models/tag_audit.rb create mode 100644 test/dummy/config.ru create mode 100644 test/dummy/config/application.rb create mode 100644 test/dummy/config/boot.rb create mode 100644 test/dummy/config/database.yml create mode 100644 test/dummy/config/environment.rb create mode 100644 test/dummy/config/environments/test.rb create mode 100644 test/dummy/config/routes.rb create mode 100644 test/dummy/db/schema.rb create mode 100644 test/dummy/db/secondary_schema.rb create mode 100644 test/dummy/lib/tasks/db.rake diff --git a/Appraisals b/Appraisals deleted file mode 100644 index 0d4cb8e4..00000000 --- a/Appraisals +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -appraise 'activerecord-7.1' do - gem 'activerecord', '~> 7.1.0' - gem 'railties' - - platforms :ruby, :truffleruby do - gem 'mysql2' - gem 'pg' - gem 'sqlite3', '< 2.0' - end - - platforms :jruby do - gem 'activerecord-jdbcmysql-adapter' - gem 'activerecord-jdbcpostgresql-adapter' - gem 'activerecord-jdbcsqlite3-adapter' - end -end - -appraise 'activerecord-7.2' do - gem 'activerecord', '~> 7.2.0' - gem 'railties' - - platforms :ruby do - gem 'mysql2' - gem 'pg' - gem 'sqlite3' - end -end - -appraise 'activerecord-8.0' do - gem 'activerecord', '~> 8.0.0' - gem 'railties' - - platforms :ruby do - gem 'mysql2' - gem 'pg' - gem 'sqlite3' - end -end - -appraise 'activerecord-edge' do - gem 'activerecord', github: 'rails/rails' - gem 'railties', github: 'rails/rails' - - platforms :ruby do - gem 'mysql2' - gem 'pg' - gem 'sqlite3' - end -end diff --git a/Gemfile b/Gemfile index 8753f543..b9395046 100644 --- a/Gemfile +++ b/Gemfile @@ -4,4 +4,12 @@ source 'https://rubygems.org' gemspec -gem 'with_advisory_lock', github: 'closuretree/with_advisory_lock' \ No newline at end of file +gem 'with_advisory_lock', github: 'closuretree/with_advisory_lock' +gem 'railties' +# Test with ActiveRecord 7.1 directly +gem 'activerecord', '~> 7.1.0' + +# Database adapters +gem 'sqlite3' +gem 'pg' +gem 'mysql2' \ No newline at end of file diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..2614c65e --- /dev/null +++ b/bin/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) +require_relative '../test/dummy/config/boot' +require 'rails/commands' \ No newline at end of file diff --git a/closure_tree.gemspec b/closure_tree.gemspec index d014d301..33b73782 100644 --- a/closure_tree.gemspec +++ b/closure_tree.gemspec @@ -30,7 +30,6 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'activerecord', '>= 7.1.0' gem.add_runtime_dependency 'with_advisory_lock', '>= 6.0.0' - gem.add_development_dependency 'appraisal' gem.add_development_dependency 'database_cleaner' gem.add_development_dependency 'parallel' gem.add_development_dependency 'minitest' diff --git a/gemfiles/activerecord_7.1.gemfile b/gemfiles/activerecord_7.1.gemfile deleted file mode 100644 index 433f5e74..00000000 --- a/gemfiles/activerecord_7.1.gemfile +++ /dev/null @@ -1,21 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "with_advisory_lock", github: "closuretree/with_advisory_lock" -gem "activerecord", "~> 7.1.0" -gem "railties" - -platforms :ruby, :truffleruby do - gem "mysql2" - gem "pg" - gem "sqlite3", "< 2.0" -end - -platforms :jruby do - gem "activerecord-jdbcmysql-adapter" - gem "activerecord-jdbcpostgresql-adapter" - gem "activerecord-jdbcsqlite3-adapter" -end - -gemspec path: "../" diff --git a/gemfiles/activerecord_7.2.gemfile b/gemfiles/activerecord_7.2.gemfile deleted file mode 100644 index db97d653..00000000 --- a/gemfiles/activerecord_7.2.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "with_advisory_lock", github: "closuretree/with_advisory_lock" -gem "activerecord", "~> 7.2.0" -gem "railties" - -platforms :ruby do - gem "mysql2" - gem "pg" - gem "sqlite3" -end - -gemspec path: "../" diff --git a/gemfiles/activerecord_8.0.gemfile b/gemfiles/activerecord_8.0.gemfile deleted file mode 100644 index a2a4411a..00000000 --- a/gemfiles/activerecord_8.0.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "with_advisory_lock", github: "closuretree/with_advisory_lock" -gem "activerecord", "~> 8.0.0" -gem "railties" - -platforms :ruby do - gem "mysql2" - gem "pg" - gem "sqlite3" -end - -gemspec path: "../" diff --git a/gemfiles/activerecord_edge.gemfile b/gemfiles/activerecord_edge.gemfile deleted file mode 100644 index c30e4bfc..00000000 --- a/gemfiles/activerecord_edge.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "with_advisory_lock", github: "closuretree/with_advisory_lock" -gem "activerecord", github: "rails/rails" -gem "railties", github: "rails/rails" - -platforms :ruby do - gem "mysql2" - gem "pg" - gem "sqlite3" -end - -gemspec path: "../" diff --git a/test/closure_tree/tag_test.rb b/test/closure_tree/tag_test.rb index 62fde579..fc9a322e 100644 --- a/test/closure_tree/tag_test.rb +++ b/test/closure_tree/tag_test.rb @@ -3,6 +3,7 @@ require 'test_helper' require 'support/tag_examples' -describe Tag do +class TagTest < ActiveSupport::TestCase + TAG_CLASS = Tag include TagExamples -end +end \ No newline at end of file diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile new file mode 100644 index 00000000..488c551f --- /dev/null +++ b/test/dummy/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 00000000..1ff0944d --- /dev/null +++ b/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::Base + # Prevent CSRF attacks by raising an exception. + # For APIs, you may want to use :null_session instead. + protect_from_forgery with: :exception +end diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb new file mode 100644 index 00000000..848e8e7b --- /dev/null +++ b/test/dummy/app/models/application_record.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + establish_connection(:primary) +end diff --git a/test/dummy/app/models/label.rb b/test/dummy/app/models/label.rb new file mode 100644 index 00000000..dc5ac22c --- /dev/null +++ b/test/dummy/app/models/label.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Label < ApplicationRecord +end diff --git a/test/dummy/app/models/mysql_label.rb b/test/dummy/app/models/mysql_label.rb new file mode 100644 index 00000000..29c270e8 --- /dev/null +++ b/test/dummy/app/models/mysql_label.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MysqlLabel < MysqlRecord + self.table_name = 'mysql_labels' +end diff --git a/test/dummy/app/models/mysql_record.rb b/test/dummy/app/models/mysql_record.rb new file mode 100644 index 00000000..29823a04 --- /dev/null +++ b/test/dummy/app/models/mysql_record.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class MysqlRecord < ActiveRecord::Base + self.abstract_class = true + establish_connection :secondary +end diff --git a/test/dummy/app/models/mysql_tag.rb b/test/dummy/app/models/mysql_tag.rb new file mode 100644 index 00000000..49d34b1d --- /dev/null +++ b/test/dummy/app/models/mysql_tag.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class MysqlTag < MysqlRecord + self.table_name = 'mysql_tags' + + after_save do + MysqlTagAudit.create(tag_name: name) + MysqlLabel.create(name: name) + end +end diff --git a/test/dummy/app/models/mysql_tag_audit.rb b/test/dummy/app/models/mysql_tag_audit.rb new file mode 100644 index 00000000..83743709 --- /dev/null +++ b/test/dummy/app/models/mysql_tag_audit.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MysqlTagAudit < MysqlRecord + self.table_name = 'mysql_tag_audits' +end diff --git a/test/dummy/app/models/tag.rb b/test/dummy/app/models/tag.rb new file mode 100644 index 00000000..a58b419f --- /dev/null +++ b/test/dummy/app/models/tag.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Tag < ApplicationRecord + after_save do + TagAudit.create(tag_name: name) + Label.create(name: name) + end +end diff --git a/test/dummy/app/models/tag_audit.rb b/test/dummy/app/models/tag_audit.rb new file mode 100644 index 00000000..46bc1b3c --- /dev/null +++ b/test/dummy/app/models/tag_audit.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class TagAudit < ApplicationRecord +end diff --git a/test/dummy/config.ru b/test/dummy/config.ru new file mode 100644 index 00000000..5df2ee2f --- /dev/null +++ b/test/dummy/config.ru @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative 'config/environment' + +run Rails.application +Rails.application.load_server diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb new file mode 100644 index 00000000..ff2d96d0 --- /dev/null +++ b/test/dummy/config/application.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative 'boot' + +require 'rails' +require 'active_record/railtie' +require 'rails/test_unit/railtie' + +Bundler.require(*Rails.groups) +require 'closure_tree' + +module Dummy + class Application < Rails::Application + config.load_defaults Rails::VERSION::STRING.to_f + config.eager_load = false + end +end \ No newline at end of file diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb new file mode 100644 index 00000000..c9c87bbd --- /dev/null +++ b/test/dummy/config/boot.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) \ No newline at end of file diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml new file mode 100644 index 00000000..63123141 --- /dev/null +++ b/test/dummy/config/database.yml @@ -0,0 +1,5 @@ +test: + adapter: sqlite3 + database: ":memory:" + pool: 50 + timeout: 5000 \ No newline at end of file diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb new file mode 100644 index 00000000..d594e044 --- /dev/null +++ b/test/dummy/config/environment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! \ No newline at end of file diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb new file mode 100644 index 00000000..d0f8a20c --- /dev/null +++ b/test/dummy/config/environments/test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.cache_classes = true + config.eager_load = false + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.action_dispatch.show_exceptions = false + config.active_support.deprecation = :stderr +end \ No newline at end of file diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb new file mode 100644 index 00000000..edf04d2d --- /dev/null +++ b/test/dummy/config/routes.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb new file mode 100644 index 00000000..699044b1 --- /dev/null +++ b/test/dummy/db/schema.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define(version: 1) do + create_table 'tags', force: true do |t| + t.string 'name' + end + + create_table 'tag_audits', id: false, force: true do |t| + t.string 'tag_name' + end + + create_table 'labels', id: false, force: true do |t| + t.string 'name' + end +end diff --git a/test/dummy/db/secondary_schema.rb b/test/dummy/db/secondary_schema.rb new file mode 100644 index 00000000..a887b1a1 --- /dev/null +++ b/test/dummy/db/secondary_schema.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define(version: 1) do + create_table 'mysql_tags', force: true do |t| + t.string 'name' + end + + create_table 'mysql_tag_audits', id: false, force: true do |t| + t.string 'tag_name' + end + + create_table 'mysql_labels', id: false, force: true do |t| + t.string 'name' + end +end diff --git a/test/dummy/lib/tasks/db.rake b/test/dummy/lib/tasks/db.rake new file mode 100644 index 00000000..c5a76559 --- /dev/null +++ b/test/dummy/lib/tasks/db.rake @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +namespace :db do + namespace :test do + desc 'Load schema for all databases' + task prepare: :environment do + # Load schema for primary database + ActiveRecord::Base.establish_connection(:primary) + ActiveRecord::Schema.define(version: 1) do + create_table 'tags', force: true do |t| + t.string 'name' + end + + create_table 'tag_audits', id: false, force: true do |t| + t.string 'tag_name' + end + + create_table 'labels', id: false, force: true do |t| + t.string 'name' + end + end + + # Load schema for secondary database + ActiveRecord::Base.establish_connection(:secondary) + ActiveRecord::Schema.define(version: 1) do + create_table 'mysql_tags', force: true do |t| + t.string 'name' + end + + create_table 'mysql_tag_audits', id: false, force: true do |t| + t.string 'tag_name' + end + + create_table 'mysql_labels', id: false, force: true do |t| + t.string 'name' + end + end + end + end +end diff --git a/test/support/tag_examples.rb b/test/support/tag_examples.rb index 74a312bc..e90ae208 100644 --- a/test/support/tag_examples.rb +++ b/test/support/tag_examples.rb @@ -1,287 +1,271 @@ # frozen_string_literal: true 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 + def self.included(base) + base.class_eval do + def setup + super + @tag_class = self.class.const_get(:TAG_CLASS) || Tag + @tag_hierarchy_class = @tag_class.hierarchy_class + end - it 'should build hierarchy classname correctly' do + test "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 + test "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 + test "should return no entities when db is empty" do + assert_empty @tag_class.roots + assert_empty @tag_class.leaves 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 + test "#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 - 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 + test "#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 - it 'should return child_ids for root' do - assert_equal [@leaf.id], @root.child_ids - end + test "single tag should be a leaf and root" do + tag = @tag_class.create!(name: 'tag') + assert tag.leaf? + assert tag.root? + assert_nil tag.parent + assert_equal [tag], @tag_class.all + assert_equal [tag], @tag_class.roots + assert_equal [tag], @tag_class.leaves + end - it 'should return an empty array for leaves' do - assert_empty @leaf.child_ids - end + test "should not find tag with invalid path arguments" do + tag = @tag_class.create!(name: 'tag') + assert_nil @tag_class.find_by_path(['']) + assert_nil @tag_class.find_by_path([]) + assert_nil @tag_class.find_by_path(nil) + assert_nil @tag_class.find_by_path('') + assert_nil @tag_class.find_by_path([nil]) + assert_nil @tag_class.find_by_path([tag.name, '']) + assert_nil @tag_class.find_by_path([tag.name, nil]) 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 + test "should find tag by valid path" do + tag = @tag_class.create!(name: 'tag') + assert_equal tag, @tag_class.find_by_path([tag.name]) + assert_equal tag, @tag_class.find_by_path(tag.name) + end - it 'should create all tags' do - assert_equal [@root, @mid, @leaf].sort, @tag_class.all.to_a.sort - end + test "adds children through add_child" do + tag = @tag_class.create!(name: 'tag') + child = @tag_class.create!(name: 'tag 2') + tag.add_child child + + assert tag.root? + refute tag.leaf? + refute child.root? + assert child.leaf? + assert_equal tag, child.reload.parent + assert_equal [child], tag.reload.children.to_a + 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 + test "adds children through collection" do + tag = @tag_class.create!(name: 'tag') + child = @tag_class.create!(name: 'tag 2') + tag.children << child + + assert tag.root? + refute tag.leaf? + refute child.root? + assert child.leaf? + assert_equal tag, child.reload.parent + assert_equal [child], tag.reload.children.to_a + 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 + test "returns simple root and leaf with 2 tags" do + root = @tag_class.create!(name: 'root') + leaf = root.add_child(@tag_class.create!(name: 'leaf')) + + assert_equal [root], @tag_class.roots + assert_equal [leaf], @tag_class.leaves + assert_equal [leaf.id], root.child_ids + assert_empty leaf.child_ids + 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(&:name).sort - end + test "3 tag collection.create hierarchy" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + DestroyedTag.delete_all + + assert_equal [root, mid, leaf].sort, @tag_class.all.to_a.sort + assert_equal [root], @tag_class.roots + assert_equal [leaf], @tag_class.leaves + 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 + test "deletes leaves" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + DestroyedTag.delete_all + + @tag_class.leaves.destroy_all + assert_equal [root], @tag_class.roots + assert_equal [mid], @tag_class.leaves + end - it 'prevents ancestor loops' do - @leaf.add_child @root - refute @root.valid? - assert_includes @root.reload.descendants, @leaf - end + test "deletes everything when deleting roots" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + DestroyedTag.delete_all + + @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(&:name).sort + 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 + test "fixes self_and_ancestors properly on reparenting" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + + 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 '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 + test "prevents ancestor loops" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + + leaf.add_child root + refute root.valid? + assert_includes root.reload.descendants, leaf 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 + test "moves non-leaves" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + + 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 'should create all tags' do - assert_equal [@root, @mid, @leaf].sort, @tag_class.all.to_a.sort - end + test "moves leaves" do + root = @tag_class.create! name: 'root' + mid = root.children.create! name: 'mid' + leaf = mid.children.create! name: 'leaf' + + 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 - it 'should return a root and leaf without middle tag' do - assert_equal [@root], @tag_class.roots - assert_equal [@leaf], @tag_class.leaves - end + test "3 tag explicit_create hierarchy" 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')) + + assert_equal [root, mid, leaf].sort, @tag_class.all.to_a.sort + 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 + test "prevents parental loops from torso" 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')) + + 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 + test "prevents parental loops from toes" 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')) + + 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 + test "supports re-parenting" 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')) + + 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 + test "cleans up hierarchy references for leaves" 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')) + + 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 + test "cleans up hierarchy references" 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')) + + 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 + test "hierarchy models have different hash codes" 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')) + + 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 + test "equal hierarchy models have same hash code" do + root = @tag_class.create!(name: 'root') + root.add_child(@tag_class.create!(name: 'mid')) + + assert_equal @tag_hierarchy_class.first.hash, @tag_hierarchy_class.first.hash end - it 'performs as the readme says it does' do + test "performs as the readme says" do skip "JRuby has issues with ActiveRecord 7.1+ datetime handling in transactions" if defined?(JRUBY_VERSION) + grandparent = @tag_class.create(name: 'Grandparent') parent = grandparent.children.create(name: 'Parent') child1 = @tag_class.create(name: 'First Child', parent: parent) @@ -289,533 +273,627 @@ def assert_parent_and_children 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'], parent.ancestry_path) assert_equal(['Grandparent', 'Parent', 'First Child'], child1.ancestry_path) + assert_equal(['Grandparent', 'Parent', 'Second Child'], child2.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 + d.add_child(e) assert_equal %w[a b c d e f g h], h.ancestry_path end - it 'roots sort alphabetically' do + test "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(&: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 + test "finds global roots in simple tree" 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] + + assert_equal expected_roots.sort, @tag_class.roots.to_a.sort + end - it 'should not return root? for non-roots' do - [@b1, @b2, @c1a, @c1b].each { |ea| refute(ea.root?) } - end + test "returns root? for roots" do + @tag_class.find_or_create_by_path %w[a1 b1 c1a] + @tag_class.find_or_create_by_path %w[a2 b2] + @tag_class.find_or_create_by_path %w[a3] + + a1, a2, a3 = @tag_class.all.sort_by(&:name).select(&:root?) + [a1, a2, a3].each { |ea| assert(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 + test "does not return root? for non-roots" do + @tag_class.find_or_create_by_path %w[a1 b1 c1a] + @tag_class.find_or_create_by_path %w[a2 b2] + + a1, a2, b1, b2, c1a = @tag_class.all.sort_by(&:name) + [b1, b2, c1a].each { |ea| refute(ea.root?) } + end - it 'should assemble global leaves' do - assert_equal @expected_leaves.sort, @tag_class.leaves.to_a.sort + test "returns the correct root" 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[a2 b2] + + a1, a2, b1, b2, c1a, c1b = @tag_class.all.sort_by(&:name) + + { a1 => a1, a2 => a2, b1 => a1, b2 => a2, c1a => a1, c1b => a1 }.each do |node, root| + assert_equal(root, node.root) end + 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 + test "assembles global leaves" 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] + + _, _, a3, _, b1b, b2, c1a, c1b, c1c = @tag_class.all.sort_by(&:name) + expected_leaves = [c1a, c1b, c1c, b1b, b2, a3] + + assert_equal expected_leaves.sort, @tag_class.leaves.to_a.sort + 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 + test "assembles siblings properly" 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_siblings = [[a1, a2, a3], [b1, b1b], [c1a, c1b, c1c]] + expected_only_children = @tag_class.all - expected_siblings.flatten + + 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 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 + test "assembles before_siblings" 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, _, c1a, c1b, c1c = @tag_class.all.sort_by(&:name) + expected_siblings = [[a1, a2, a3], [b1, b1b], [c1a, c1b, c1c]] + + 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 '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 + test "assembles after_siblings" 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, _, c1a, c1b, c1c = @tag_class.all.sort_by(&:name) + expected_siblings = [[a1, a2, a3], [b1, b1b], [c1a, c1b, c1c]] + + 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 - - @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 - @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 + test "assembles instance leaves" 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_leaves = [c1a, c1b, c1c, b1b, b2, a3] + + { 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 - 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 + test "returns leaf? for leaves" do + @tag_class.find_or_create_by_path %w[a1 b1 c1a] + @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] + + _, _, a3, _, b1b, b2, c1a = @tag_class.all.sort_by(&:name) + expected_leaves = [c1a, b1b, b2, a3] + + expected_leaves.each { |ea| assert ea.leaf? } 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) + test "can move roots" do + @tag_class.find_or_create_by_path %w[a1 b1 c1a] + @tag_class.find_or_create_by_path %w[a2 b2] + @tag_class.find_or_create_by_path %w[a3] + + a1, a2, a3, b1, b2, c1a = @tag_class.all.sort_by(&:name) + + c1a.children << a2 + b2.reload.children << a3 + assert_equal %w[a1 b1 c1a a2 b2 a3], a3.reload.ancestry_path + end - 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]) + test "cascade-deletes from roots" 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 = @tag_class.all.sort_by(&:name).first + + 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 - 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 + test "with_ancestor works with no rows" do + assert_empty @tag_class.with_ancestor.to_a + 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 + test "with_ancestor finds only children" do + c = @tag_class.find_or_create_by_path %w[A B C] + a = c.parent.parent + b = c.parent + @tag_class.find_or_create_by_path %w[D E] + assert_equal [b, c], @tag_class.with_ancestor(a).to_a + 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 + test "with_ancestor 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] + 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 - 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) + test "with_descendant works with no rows" do + assert_empty @tag_class.with_descendant.to_a + end - assert_equal @t111, @tag_class.lowest_common_ancestor([@t111]) - assert_equal @t2, @tag_class.lowest_common_ancestor([@t2]) + test "with_descendant 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 - 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 + test "with_descendant 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 + + 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 - 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 + test "lowest_common_ancestor finds parent for siblings" 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) + + 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 '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 + test "lowest_common_ancestor finds grandparent for cousins" 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) + + 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 'assembles ancestors' do - assert_equal [@parent, @grandparent], @child.ancestors - assert_equal [@child, @parent, @grandparent], @child.self_and_ancestors - end + test "lowest_common_ancestor for aunt-uncle/niece-nephew" do + t1 = @tag_class.create!(name: 't1') + t11 = @tag_class.create!(name: 't11', parent: t1) + t112 = @tag_class.create!(name: 't112', parent: t11) + t12 = @tag_class.create!(name: 't12', parent: t1) + + 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 '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 + test "lowest_common_ancestor for parent/child" do + t1 = @tag_class.create!(name: 't1') + t12 = @tag_class.create!(name: 't12', parent: t1) + t121 = @tag_class.create!(name: 't121', parent: 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([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 '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 + test "lowest_common_ancestor for grandparent/grandchild" do + t1 = @tag_class.create!(name: 't1') + t11 = @tag_class.create!(name: 't11', parent: t1) + t111 = @tag_class.create!(name: 't111', parent: t11) + t2 = @tag_class.create!(name: 't2') + t21 = @tag_class.create!(name: 't21', parent: t2) + t211 = @tag_class.create!(name: 't211', parent: t21) + + 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 '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 + test "lowest_common_ancestor for whole extended family" 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) + t211 = @tag_class.create!(name: 't211', parent: t21) + + 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 '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 + test "lowest_common_ancestor 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 '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 + test "lowest_common_ancestor is nil for no common ancestors" do + t1 = @tag_class.create!(name: 't1') + t11 = @tag_class.create!(name: 't11', parent: t1) + t111 = @tag_class.create!(name: 't111', parent: t11) + t2 = @tag_class.create!(name: 't2') + t21 = @tag_class.create!(name: 't21', parent: t2) + t211 = @tag_class.create!(name: 't211', parent: t21) + + 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 '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 + test "lowest_common_ancestor is itself for single item" do + t1 = @tag_class.create!(name: 't1') + t11 = @tag_class.create!(name: 't11', parent: t1) + t111 = @tag_class.create!(name: 't111', parent: t11) + t2 = @tag_class.create!(name: '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([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 - 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 + test "builds ancestry path" 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 + + 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 '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 + test "assembles ancestors" 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 + + assert_equal [parent, grandparent], child.ancestors + assert_equal [child, parent, grandparent], child.self_and_ancestors + 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 + test "finds by path" 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 + + assert_equal child, @tag_class.find_by_path(%w[grandparent parent child]) + 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 '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 + test "respects attribute hashes with both selection and creation" do + grandparent = @tag_class.find_or_create_by_path([ + { name: 'grandparent', title: 'Nonnie' } + ]) + + 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 '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 + test "creates hierarchy with given attribute" do + expected_title = 'unicorn rainbows' + attrs = { title: expected_title } + child = @tag_class.find_or_create_by_path(%w[grandparent parent child], attrs) + + [child, child.parent, child.parent.parent].each do |ea| + assert_equal expected_title, ea.title 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 => {} - } - } + test "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 - @three_tree = { - @a => { - @b => { - @c1 => {} - }, - @b2 => {} - }, - @a2 => {}, - @a3 => { - @b3 => { - @c3 => {} - } - } - } + test "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 - @full_tree = { - @a => { - @b => { - @c1 => { - @d1 => {} - } - }, - @b2 => {} - }, - @a2 => {}, - @a3 => { - @b3 => { - @c3 => {} - } - } - } + test "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 - 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 + test "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 'limit_depth 4' do - assert_equal @full_tree, @tag_class.hash_tree(limit_depth: 4) - end + test "returns 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 - it 'no limit' do - assert_equal @full_tree, @tag_class.hash_tree - end - end + test "find_or_create_by_path 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 - describe '.hash_tree' do - it 'returns {} for depth 0' do - assert_equal({}, @b.hash_tree(limit_depth: 0)) - end + test "find_or_create_by_path 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 'limit_depth 1' do - assert_equal @two_tree[@a].slice(@b), @b.hash_tree(limit_depth: 1) - end + test "find_or_create_by_path 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 + assert_equal %w[TEST2 TEST1], subject.self_and_ancestors.map(&:title) + end - it 'limit_depth 2' do - assert_equal @three_tree[@a].slice(@b), @b.hash_tree(limit_depth: 2) - end + test "hash_tree returns {} for depth 0" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + assert_equal({}, @tag_class.hash_tree(limit_depth: 0)) + end - it 'limit_depth 3' do - assert_equal @full_tree[@a].slice(@b), @b.hash_tree(limit_depth: 3) - end + test "hash_tree limit_depth 1" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + a = d1.root + a2 = @tag_class.create(name: 'a2') + a3 = @tag_class.find_or_create_by_path(%w[a3 b3 c3]).root + + one_tree = { a => {}, a2 => {}, a3 => {} } + assert_equal one_tree, @tag_class.hash_tree(limit_depth: 1) + end - it 'no limit from subsubroot' do - assert_equal @full_tree[@a][@b].slice(@c1), @c1.hash_tree - end + test "hash_tree limit_depth 2" 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 + + two_tree = { + a => { b => {}, b2 => {} }, + a2 => {}, + a3 => { b3 => {} } + } + assert_equal two_tree, @tag_class.hash_tree(limit_depth: 2) + end - it 'no limit from subroot' do - assert_equal @full_tree[@a].slice(@b), @b.hash_tree - end + test "hash_tree limit_depth 3" 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 + + three_tree = { + a => { b => { c1 => {} }, b2 => {} }, + a2 => {}, + a3 => { b3 => { c3 => {} } } + } + assert_equal three_tree, @tag_class.hash_tree(limit_depth: 3) + end - it 'no limit from root' do - assert_equal @full_tree.slice(@a, @a2), @a.hash_tree.merge(@a2.hash_tree) - end - end + test "hash_tree limit_depth 4" 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 + + full_tree = { + a => { b => { c1 => { d1 => {} } }, b2 => {} }, + a2 => {}, + a3 => { b3 => { c3 => {} } } + } + assert_equal full_tree, @tag_class.hash_tree(limit_depth: 4) + 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 + test "hash_tree no limit" 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 + + full_tree = { + a => { b => { c1 => { d1 => {} } }, b2 => {} }, + a2 => {}, + a3 => { b3 => { c3 => {} } } + } + assert_equal full_tree, @tag_class.hash_tree + end - it 'no limit from chained activerecord association subroots' do - assert_equal @full_tree[@a], @a.children.hash_tree - end + test "instance hash_tree returns {} for depth 0" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + b = d1.parent.parent + assert_equal({}, b.hash_tree(limit_depth: 0)) + end - it 'limit_depth 3 from b.parent' do - assert_equal @three_tree.slice(@a), @b.parent.hash_tree(limit_depth: 3) - end + test "instance hash_tree limit_depth 1" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + c1 = d1.parent + b = c1.parent + a = b.parent + b2 = @tag_class.find_or_create_by_path %w[a b2] + + two_tree = { a => { b => {}, b2 => {} } } + assert_equal two_tree[a].slice(b), b.hash_tree(limit_depth: 1) + end - it 'no limit_depth from b.parent' do - assert_equal @full_tree.slice(@a), @b.parent.hash_tree - end + test "instance hash_tree no limit from subroot" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + c1 = d1.parent + b = c1.parent + a = b.parent + b2 = @tag_class.find_or_create_by_path %w[a b2] + + full_tree = { a => { b => { c1 => { d1 => {} } }, b2 => {} } } + assert_equal full_tree[a].slice(b), b.hash_tree + end - it 'no limit_depth from c.parent' do - assert_equal @full_tree[@a].slice(@b), @c1.parent.hash_tree - end - end + test "hash_tree from chained associations" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + c1 = d1.parent + b = c1.parent + a = b.parent + b2 = @tag_class.find_or_create_by_path %w[a b2] + + full_tree = { a => { b => { c1 => { d1 => {} } }, b2 => {} } } + assert_equal full_tree[a], a.children.hash_tree end - it 'finds_by_path for very deep trees' do + test "finds_by_path for very deep trees" do path = (1..20).to_a.map(&:to_s) subject = @tag_class.find_or_create_by_path(path) assert_equal path, subject.ancestry_path @@ -824,96 +902,65 @@ def assert_parent_and_children assert_equal subject, root.find_by_path(path[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 + test "DOT rendering for empty scope" do + assert_equal "digraph G {\n}\n", @tag_class.to_dot_digraph(@tag_class.where('0=1')) end - describe '.depth' do - 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 + test "DOT rendering for tree" 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 - describe '.depth' 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 - - - end - - it 'should return 0 for root' do - assert_equal 0, @a.depth - assert_equal 0, @a2.depth - assert_equal 0, @a3.depth - end + test "depth returns 0 for root" 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') + c3 = @tag_class.find_or_create_by_path %w[a3 b3 c3] + a3 = c3.parent.parent + + assert_equal 0, a.depth + assert_equal 0, a2.depth + assert_equal 0, a3.depth + end - it 'should return correct depth for nodes' do - assert_equal 1, @b.depth - assert_equal 2, @c1.depth - assert_equal 3, @d1.depth - assert_equal 1, @b2.depth - assert_equal 1, @b3.depth - assert_equal 2, @c3.depth - end + test "depth returns correct depth for nodes" do + d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] + c1 = d1.parent + b = c1.parent + 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 + + assert_equal 1, b.depth + assert_equal 2, c1.depth + assert_equal 3, d1.depth + assert_equal 1, b2.depth + assert_equal 1, b3.depth + assert_equal 2, c3.depth end end end -end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index f3ae3fc1..b5732ff2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -171,5 +171,11 @@ def callback(name, start, finish, message_id, values) ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex require 'closure_tree' + +# Helper method to skip tests on JRuby +def skip_on_jruby(message = "Skipping on JRuby") + skip message if defined?(JRUBY_VERSION) +end + require_relative 'support/schema' require_relative 'support/models' From ef939fd8f0a4e6aefb0ea641f364a9f14d015be7 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Fri, 30 May 2025 18:49:03 +0100 Subject: [PATCH 2/3] fix tests --- .github/workflows/ci.yml | 44 ++-- .github/workflows/ci_jruby.yml | 69 ------- .github/workflows/ci_truffleruby.yml | 72 ------- Gemfile | 23 ++- Rakefile | 5 + bin/rails | 13 +- test/closure_tree/label_order_value_test.rb | 56 ++++++ test/closure_tree/label_test.rb | 23 ++- test/closure_tree/matcher_test.rb | 4 +- test/closure_tree/user_test.rb | 9 +- test/closure_tree/uuid_tag_test.rb | 3 +- test/dummy/app/models/adamantium.rb | 2 + test/dummy/app/models/contract.rb | 6 + test/dummy/app/models/contract_type.rb | 5 + test/dummy/app/models/cuisine_type.rb | 5 + test/dummy/app/models/date_label.rb | 2 + test/dummy/app/models/destroyed_tag.rb | 4 + test/dummy/app/models/directory_label.rb | 2 + test/dummy/app/models/event_label.rb | 2 + test/dummy/app/models/group.rb | 5 + test/dummy/app/models/grouping.rb | 5 + test/dummy/app/models/label.rb | 9 + .../app/models/label_without_root_ordering.rb | 8 + test/dummy/app/models/menu_item.rb | 5 + test/dummy/app/models/metal.rb | 7 + test/dummy/app/models/mysql_label.rb | 5 - test/dummy/app/models/mysql_record.rb | 6 - test/dummy/app/models/mysql_tag.rb | 10 - test/dummy/app/models/mysql_tag_audit.rb | 5 - test/dummy/app/models/namespace.rb | 7 + test/dummy/app/models/namespace/type.rb | 7 + test/dummy/app/models/tag.rb | 13 +- test/dummy/app/models/tag_audit.rb | 4 - test/dummy/app/models/team.rb | 5 + test/dummy/app/models/unobtanium.rb | 2 + test/dummy/app/models/user.rb | 19 ++ test/dummy/app/models/user_set.rb | 5 + test/dummy/app/models/uuid_tag.rb | 21 ++ test/dummy/config/application.rb | 13 +- test/dummy/config/boot.rb | 6 +- test/dummy/config/database.yml | 17 +- test/dummy/config/environment.rb | 2 +- test/dummy/config/environments/test.rb | 4 +- test/dummy/config/routes.rb | 4 - test/dummy/db/schema.rb | 152 +++++++++++++- test/dummy/lib/tasks/db.rake | 40 ---- test/support/models.rb | 158 --------------- test/support/schema.rb | 157 --------------- test/support/tag_examples.rb | 189 +++++++++--------- test/test_helper.rb | 153 +++----------- 50 files changed, 583 insertions(+), 809 deletions(-) delete mode 100644 .github/workflows/ci_jruby.yml delete mode 100644 .github/workflows/ci_truffleruby.yml create mode 100644 test/closure_tree/label_order_value_test.rb create mode 100644 test/dummy/app/models/adamantium.rb create mode 100644 test/dummy/app/models/contract.rb create mode 100644 test/dummy/app/models/contract_type.rb create mode 100644 test/dummy/app/models/cuisine_type.rb create mode 100644 test/dummy/app/models/date_label.rb create mode 100644 test/dummy/app/models/destroyed_tag.rb create mode 100644 test/dummy/app/models/directory_label.rb create mode 100644 test/dummy/app/models/event_label.rb create mode 100644 test/dummy/app/models/group.rb create mode 100644 test/dummy/app/models/grouping.rb create mode 100644 test/dummy/app/models/label_without_root_ordering.rb create mode 100644 test/dummy/app/models/menu_item.rb create mode 100644 test/dummy/app/models/metal.rb delete mode 100644 test/dummy/app/models/mysql_label.rb delete mode 100644 test/dummy/app/models/mysql_record.rb delete mode 100644 test/dummy/app/models/mysql_tag.rb delete mode 100644 test/dummy/app/models/mysql_tag_audit.rb create mode 100644 test/dummy/app/models/namespace.rb create mode 100644 test/dummy/app/models/namespace/type.rb delete mode 100644 test/dummy/app/models/tag_audit.rb create mode 100644 test/dummy/app/models/team.rb create mode 100644 test/dummy/app/models/unobtanium.rb create mode 100644 test/dummy/app/models/user.rb create mode 100644 test/dummy/app/models/user_set.rb create mode 100644 test/dummy/app/models/uuid_tag.rb delete mode 100644 test/dummy/config/routes.rb delete mode 100644 test/dummy/lib/tasks/db.rake delete mode 100644 test/support/models.rb delete mode 100644 test/support/schema.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83926a4c..f336825b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,16 +38,22 @@ jobs: fail-fast: false matrix: ruby: - - '3.3' + - '3.4' + - 'jruby' + - 'truffleruby' rails: - - activerecord_8.0 - - activerecord_7.2 - - activerecord_7.1 - - activerecord_edge - adapter: - - 'sqlite3:///:memory:' - - mysql2://root:root@0/closure_tree_test - - postgres://closure_tree:closure_tree@0/closure_tree_test + - '8.0' + - '7.2' + - '7.1' + exclude: + # JRuby doesn't support Rails 8.0 yet + - ruby: 'jruby' + rails: '7.2' + - ruby: 'jruby' + rails: '8.0' + # TruffleRuby also has compatibility issues with Rails 8.0 + - ruby: 'truffleruby' + rails: '8.0' steps: - name: Checkout @@ -60,14 +66,26 @@ jobs: bundler-cache: true rubygems: latest env: - BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile + RAILS_VERSION: ${{ matrix.rails }} RAILS_ENV: test + - name: Setup databases + env: + RAILS_ENV: test + RAILS_VERSION: ${{ matrix.rails }} + DATABASE_URL_PG: postgres://postgres:postgres@127.0.0.1:5432/closure_tree_test + DATABASE_URL_MYSQL: mysql2://root:root@127.0.0.1:3306/closure_tree_test + DATABASE_URL_SQLITE3: 'sqlite3::memory:' + run: | + cd test/dummy + bundle exec rails db:schema:load + - name: Test env: RAILS_ENV: test RAILS_VERSION: ${{ matrix.rails }} - DB_ADAPTER: ${{ matrix.adapter }} - BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile + DATABASE_URL_PG: postgres://postgres:postgres@127.0.0.1:5432/closure_tree_test + DATABASE_URL_MYSQL: mysql2://root:root@127.0.0.1:3306/closure_tree_test + DATABASE_URL_SQLITE3: 'sqlite3::memory:' WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} - run: bin/rake + run: bin/rails test diff --git a/.github/workflows/ci_jruby.yml b/.github/workflows/ci_jruby.yml deleted file mode 100644 index b455b15c..00000000 --- a/.github/workflows/ci_jruby.yml +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: CI Jruby - -on: - push: - branches: - - master - pull_request: - branches: - - master -concurrency: - group: ci-${{ github.head_ref }}-jruby - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - services: - mysql: - image: mysql/mysql-server - ports: - - "3306:3306" - env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: closure_tree_test - MYSQL_ROOT_HOST: '%' - postgres: - image: 'postgres' - ports: ['5432:5432'] - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: closure_tree_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - strategy: - fail-fast: false - matrix: - rails: - - activerecord_7.1 - adapter: - - 'sqlite3:///:memory:' - - mysql2://root:root@0/closure_tree_test - - postgres://closure_tree:closure_tree@0/closure_tree_test - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: jruby - bundler-cache: true - rubygems: latest - env: - BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile - RAILS_ENV: test - - - name: Test - env: - RAILS_ENV: test - RAILS_VERSION: ${{ matrix.rails }} - DB_ADAPTER: ${{ matrix.adapter }} - BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile - WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} - run: bin/rake diff --git a/.github/workflows/ci_truffleruby.yml b/.github/workflows/ci_truffleruby.yml deleted file mode 100644 index 6bfaf2bf..00000000 --- a/.github/workflows/ci_truffleruby.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: CI Truffleruby - -on: - push: - branches: - - master - pull_request: - branches: - - master -concurrency: - group: ci-${{ github.head_ref }}-truffleruby - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-latest - services: - mysql: - image: mysql/mysql-server - ports: - - "3306:3306" - env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: closure_tree_test - MYSQL_ROOT_HOST: '%' - postgres: - image: 'postgres' - ports: ['5432:5432'] - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: closure_tree_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - strategy: - fail-fast: false - matrix: - ruby: - - truffleruby - rails: - - activerecord_7.1 - adapter: - - 'sqlite3:///:memory:' - - mysql2://root:root@0/closure_tree_test - - postgres://closure_tree:closure_tree@0/closure_tree_test - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: truffleruby - bundler-cache: true - rubygems: latest - env: - BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile - RAILS_ENV: test - - - name: Test - env: - RAILS_ENV: test - RAILS_VERSION: ${{ matrix.rails }} - DB_ADAPTER: ${{ matrix.adapter }} - BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile - WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} - run: bin/rake diff --git a/Gemfile b/Gemfile index b9395046..9229397b 100644 --- a/Gemfile +++ b/Gemfile @@ -4,12 +4,21 @@ source 'https://rubygems.org' gemspec -gem 'with_advisory_lock', github: 'closuretree/with_advisory_lock' gem 'railties' -# Test with ActiveRecord 7.1 directly -gem 'activerecord', '~> 7.1.0' +gem 'with_advisory_lock', github: 'closuretree/with_advisory_lock' + +gem 'activerecord', "~> #{ENV['RAILS_VERSION'] || '8.0'}.0" + +platforms :ruby, :truffleruby do + # Database adapters + gem 'mysql2' + gem 'pg' + gem 'sqlite3' +end -# Database adapters -gem 'sqlite3' -gem 'pg' -gem 'mysql2' \ No newline at end of file +platform :jruby do + # JRuby-specific gems + gem 'activerecord-jdbcmysql-adapter' + gem 'activerecord-jdbcpostgresql-adapter' + gem 'activerecord-jdbcsqlite3-adapter' +end diff --git a/Rakefile b/Rakefile index 2c9ce43c..8bccd833 100644 --- a/Rakefile +++ b/Rakefile @@ -23,3 +23,8 @@ namespace :test do end end end + + +require_relative 'test/dummy/config/application' + +Rails.application.load_tasks \ No newline at end of file diff --git a/bin/rails b/bin/rails index 2614c65e..69fb1dc2 100755 --- a/bin/rails +++ b/bin/rails @@ -1,6 +1,15 @@ #!/usr/bin/env ruby # frozen_string_literal: true +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + +ENGINE_ROOT = File.expand_path('..', __dir__) APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) -require_relative '../test/dummy/config/boot' -require 'rails/commands' \ No newline at end of file + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + +require 'rails/all' +require 'rails/engine/commands' \ No newline at end of file diff --git a/test/closure_tree/label_order_value_test.rb b/test/closure_tree/label_order_value_test.rb new file mode 100644 index 00000000..88d5cf2b --- /dev/null +++ b/test/closure_tree/label_order_value_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "test_helper" + +class LabelOrderValueTest < ActiveSupport::TestCase + def setup + Label.delete_all + LabelHierarchy.delete_all + end + + test "should set order_value on roots for Label" do + root = Label.create(name: "root") + assert_equal 0, root.order_value + end + + test "should set order_value with siblings for Label" do + root = Label.create(name: "root") + a = root.children.create(name: "a") + b = root.children.create(name: "b") + c = root.children.create(name: "c") + + assert_equal 0, a.order_value + assert_equal 1, b.order_value + assert_equal 2, c.order_value + end + + test "should reset order_value when a node is moved to another location for Label" do + root = Label.create(name: "root") + a = root.children.create(name: "a") + b = root.children.create(name: "b") + c = root.children.create(name: "c") + + root2 = Label.create(name: "root2") + root2.add_child b + + assert_equal 0, a.order_value + assert_equal 0, b.order_value + assert_equal 1, c.reload.order_value + end + + test "should set order_value on roots for LabelWithoutRootOrdering" do + root = LabelWithoutRootOrdering.create(name: "root") + assert_nil root.order_value + end + + test "should set order_value with siblings for LabelWithoutRootOrdering" do + root = LabelWithoutRootOrdering.create(name: "root") + a = root.children.create(name: "a") + b = root.children.create(name: "b") + c = root.children.create(name: "c") + + assert_equal 0, a.order_value + assert_equal 1, b.order_value + assert_equal 2, c.order_value + end +end \ No newline at end of file diff --git a/test/closure_tree/label_test.rb b/test/closure_tree/label_test.rb index 3f36dc3d..3b28150d 100644 --- a/test/closure_tree/label_test.rb +++ b/test/closure_tree/label_test.rb @@ -3,10 +3,14 @@ require "test_helper" module CorrectOrderValue - def self.shared_examples(&block) + def self.shared_examples(model, expected_root_order_value) describe "correct order_value" do before do - instance_exec(&block) + @model = model + @expected_root_order_value = expected_root_order_value + # Clean up any existing data + @model.delete_all + @model.hierarchy_class.delete_all @root = @model.create(name: "root") @a, @b, @c = %w[a b c].map { |n| @root.children.create(name: n) } end @@ -111,6 +115,11 @@ def create_preorder_tree(suffix = "") end describe "roots" do + before do + Label.delete_all + Label.hierarchy_class.delete_all + end + it "sorts alphabetically" do expected = (0..10).to_a expected.shuffle.each do |ea| @@ -471,17 +480,11 @@ def roots_name_and_order describe "order_value must be set" do describe "with normal model" do - CorrectOrderValue.shared_examples do - @model = Label - @expected_root_order_value = 0 - end + CorrectOrderValue.shared_examples(Label, 0) end describe "without root ordering" do - CorrectOrderValue.shared_examples do - @model = LabelWithoutRootOrdering - @expected_root_order_value = nil - end + CorrectOrderValue.shared_examples(LabelWithoutRootOrdering, nil) end end diff --git a/test/closure_tree/matcher_test.rb b/test/closure_tree/matcher_test.rb index ec11ea71..98ad681b 100644 --- a/test/closure_tree/matcher_test.rb +++ b/test/closure_tree/matcher_test.rb @@ -13,7 +13,7 @@ class MatcherTest < ActiveSupport::TestCase end test "be_a_closure_tree matcher" do - assert_closure_tree UUIDTag + assert_closure_tree UuidTag assert_closure_tree User assert_closure_tree Label, ordered: true assert_closure_tree Metal, ordered: :sort_order @@ -23,7 +23,7 @@ class MatcherTest < ActiveSupport::TestCase test "ordered option" do assert_closure_tree Label, ordered: true - assert_closure_tree UUIDTag, ordered: true + assert_closure_tree UuidTag, ordered: true assert_closure_tree Metal, ordered: :sort_order end diff --git a/test/closure_tree/user_test.rb b/test/closure_tree/user_test.rb index 5522cf17..842b8e43 100644 --- a/test/closure_tree/user_test.rb +++ b/test/closure_tree/user_test.rb @@ -3,6 +3,11 @@ require "test_helper" describe "empty db" do + before do + User.delete_all + User.hierarchy_class.delete_all + end + describe "empty db" do it "should return no entities" do assert User.roots.empty? @@ -79,8 +84,8 @@ end def assert_mid_and_leaf_remain - assert ReferralHierarchy.where(ancestor_id: @root_id).empty? - assert ReferralHierarchy.where(descendant_id: @root_id).empty? + assert User.hierarchy_class.where(ancestor_id: @root_id).empty? + assert User.hierarchy_class.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 diff --git a/test/closure_tree/uuid_tag_test.rb b/test/closure_tree/uuid_tag_test.rb index e2f2f276..d1c6b4bc 100644 --- a/test/closure_tree/uuid_tag_test.rb +++ b/test/closure_tree/uuid_tag_test.rb @@ -3,6 +3,7 @@ require 'test_helper' require 'support/tag_examples' -describe UUIDTag do +describe UuidTag do + TAG_CLASS = UuidTag include TagExamples end diff --git a/test/dummy/app/models/adamantium.rb b/test/dummy/app/models/adamantium.rb new file mode 100644 index 00000000..73c24b74 --- /dev/null +++ b/test/dummy/app/models/adamantium.rb @@ -0,0 +1,2 @@ +class Adamantium < Metal +end diff --git a/test/dummy/app/models/contract.rb b/test/dummy/app/models/contract.rb new file mode 100644 index 00000000..fa8f0b53 --- /dev/null +++ b/test/dummy/app/models/contract.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Contract < ApplicationRecord + belongs_to :user, inverse_of: :contracts + belongs_to :contract_type, inverse_of: :contracts, optional: true +end diff --git a/test/dummy/app/models/contract_type.rb b/test/dummy/app/models/contract_type.rb new file mode 100644 index 00000000..a33e93ac --- /dev/null +++ b/test/dummy/app/models/contract_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ContractType < ApplicationRecord + has_many :contracts, inverse_of: :contract_type +end diff --git a/test/dummy/app/models/cuisine_type.rb b/test/dummy/app/models/cuisine_type.rb new file mode 100644 index 00000000..bb055485 --- /dev/null +++ b/test/dummy/app/models/cuisine_type.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CuisineType < ApplicationRecord + acts_as_tree +end diff --git a/test/dummy/app/models/date_label.rb b/test/dummy/app/models/date_label.rb new file mode 100644 index 00000000..76c4d414 --- /dev/null +++ b/test/dummy/app/models/date_label.rb @@ -0,0 +1,2 @@ +class DateLabel < Label +end diff --git a/test/dummy/app/models/destroyed_tag.rb b/test/dummy/app/models/destroyed_tag.rb new file mode 100644 index 00000000..3388efa0 --- /dev/null +++ b/test/dummy/app/models/destroyed_tag.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class DestroyedTag < ApplicationRecord +end diff --git a/test/dummy/app/models/directory_label.rb b/test/dummy/app/models/directory_label.rb new file mode 100644 index 00000000..9b49c5e1 --- /dev/null +++ b/test/dummy/app/models/directory_label.rb @@ -0,0 +1,2 @@ +class DirectoryLabel < Label +end diff --git a/test/dummy/app/models/event_label.rb b/test/dummy/app/models/event_label.rb new file mode 100644 index 00000000..9eb243a9 --- /dev/null +++ b/test/dummy/app/models/event_label.rb @@ -0,0 +1,2 @@ +class EventLabel < Label +end diff --git a/test/dummy/app/models/group.rb b/test/dummy/app/models/group.rb new file mode 100644 index 00000000..ccf20c37 --- /dev/null +++ b/test/dummy/app/models/group.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Group < ApplicationRecord + has_closure_tree_root :root_user +end diff --git a/test/dummy/app/models/grouping.rb b/test/dummy/app/models/grouping.rb new file mode 100644 index 00000000..555f89de --- /dev/null +++ b/test/dummy/app/models/grouping.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Grouping < ApplicationRecord + has_closure_tree_root :root_person, class_name: 'User', foreign_key: :group_id +end diff --git a/test/dummy/app/models/label.rb b/test/dummy/app/models/label.rb index dc5ac22c..a59f0c12 100644 --- a/test/dummy/app/models/label.rb +++ b/test/dummy/app/models/label.rb @@ -1,4 +1,13 @@ # frozen_string_literal: true class Label < ApplicationRecord + # make sure order doesn't matter + acts_as_tree order: :column_whereby_ordering_is_inferred, # <- symbol, and not "sort_order" + numeric_order: true, + parent_column_name: 'mother_id', + dependent: :destroy + + def to_s + "#{self.class}: #{name}" + end end diff --git a/test/dummy/app/models/label_without_root_ordering.rb b/test/dummy/app/models/label_without_root_ordering.rb new file mode 100644 index 00000000..eeba5261 --- /dev/null +++ b/test/dummy/app/models/label_without_root_ordering.rb @@ -0,0 +1,8 @@ +class LabelWithoutRootOrdering < ActiveRecord::Base + self.table_name = 'labels' + has_closure_tree parent_column_name: 'mother_id', + name_column: 'name', + order: 'column_whereby_ordering_is_inferred', + numeric_order: true, + dont_order_roots: true +end diff --git a/test/dummy/app/models/menu_item.rb b/test/dummy/app/models/menu_item.rb new file mode 100644 index 00000000..8dac6448 --- /dev/null +++ b/test/dummy/app/models/menu_item.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MenuItem < ApplicationRecord + has_closure_tree touch: true, with_advisory_lock: false +end diff --git a/test/dummy/app/models/metal.rb b/test/dummy/app/models/metal.rb new file mode 100644 index 00000000..7d477f56 --- /dev/null +++ b/test/dummy/app/models/metal.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Metal < ApplicationRecord + self.table_name = "#{table_name_prefix}metal#{table_name_suffix}" + has_closure_tree order: 'sort_order', name_column: 'value' + self.inheritance_column = 'metal_type' +end diff --git a/test/dummy/app/models/mysql_label.rb b/test/dummy/app/models/mysql_label.rb deleted file mode 100644 index 29c270e8..00000000 --- a/test/dummy/app/models/mysql_label.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class MysqlLabel < MysqlRecord - self.table_name = 'mysql_labels' -end diff --git a/test/dummy/app/models/mysql_record.rb b/test/dummy/app/models/mysql_record.rb deleted file mode 100644 index 29823a04..00000000 --- a/test/dummy/app/models/mysql_record.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -class MysqlRecord < ActiveRecord::Base - self.abstract_class = true - establish_connection :secondary -end diff --git a/test/dummy/app/models/mysql_tag.rb b/test/dummy/app/models/mysql_tag.rb deleted file mode 100644 index 49d34b1d..00000000 --- a/test/dummy/app/models/mysql_tag.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class MysqlTag < MysqlRecord - self.table_name = 'mysql_tags' - - after_save do - MysqlTagAudit.create(tag_name: name) - MysqlLabel.create(name: name) - end -end diff --git a/test/dummy/app/models/mysql_tag_audit.rb b/test/dummy/app/models/mysql_tag_audit.rb deleted file mode 100644 index 83743709..00000000 --- a/test/dummy/app/models/mysql_tag_audit.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class MysqlTagAudit < MysqlRecord - self.table_name = 'mysql_tag_audits' -end diff --git a/test/dummy/app/models/namespace.rb b/test/dummy/app/models/namespace.rb new file mode 100644 index 00000000..b7c05d56 --- /dev/null +++ b/test/dummy/app/models/namespace.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Namespace + def self.table_name_prefix + 'namespace_' + end +end diff --git a/test/dummy/app/models/namespace/type.rb b/test/dummy/app/models/namespace/type.rb new file mode 100644 index 00000000..36b326a0 --- /dev/null +++ b/test/dummy/app/models/namespace/type.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Namespace + class Type < ApplicationRecord + has_closure_tree dependent: :destroy + end +end diff --git a/test/dummy/app/models/tag.rb b/test/dummy/app/models/tag.rb index a58b419f..c59dea92 100644 --- a/test/dummy/app/models/tag.rb +++ b/test/dummy/app/models/tag.rb @@ -1,8 +1,15 @@ # frozen_string_literal: true class Tag < ApplicationRecord - after_save do - TagAudit.create(tag_name: name) - Label.create(name: name) + has_closure_tree dependent: :destroy, order: :name + before_destroy :add_destroyed_tag + + def to_s + name + end + + def add_destroyed_tag + # Proof for the tests that the destroy rather than the delete method was called: + DestroyedTag.create(name: to_s) end end diff --git a/test/dummy/app/models/tag_audit.rb b/test/dummy/app/models/tag_audit.rb deleted file mode 100644 index 46bc1b3c..00000000 --- a/test/dummy/app/models/tag_audit.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -class TagAudit < ApplicationRecord -end diff --git a/test/dummy/app/models/team.rb b/test/dummy/app/models/team.rb new file mode 100644 index 00000000..0a336efd --- /dev/null +++ b/test/dummy/app/models/team.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Team < ApplicationRecord + has_closure_tree_root :root_user, class_name: 'User', foreign_key: :grp_id +end diff --git a/test/dummy/app/models/unobtanium.rb b/test/dummy/app/models/unobtanium.rb new file mode 100644 index 00000000..1c94a900 --- /dev/null +++ b/test/dummy/app/models/unobtanium.rb @@ -0,0 +1,2 @@ +class Unobtanium < Metal +end diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb new file mode 100644 index 00000000..6fda0b26 --- /dev/null +++ b/test/dummy/app/models/user.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class User < ApplicationRecord + acts_as_tree parent_column_name: 'referrer_id', + name_column: 'email', + hierarchy_class_name: 'ReferralHierarchy', + hierarchy_table_name: 'referral_hierarchies' + + has_many :contracts, inverse_of: :user + belongs_to :group, optional: true + + def indirect_contracts + Contract.where(user_id: descendant_ids) + end + + def to_s + email + end +end diff --git a/test/dummy/app/models/user_set.rb b/test/dummy/app/models/user_set.rb new file mode 100644 index 00000000..33720b8e --- /dev/null +++ b/test/dummy/app/models/user_set.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class UserSet < ApplicationRecord + has_closure_tree_root :root_user, class_name: 'User' +end diff --git a/test/dummy/app/models/uuid_tag.rb b/test/dummy/app/models/uuid_tag.rb new file mode 100644 index 00000000..246cfc58 --- /dev/null +++ b/test/dummy/app/models/uuid_tag.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class UuidTag < ApplicationRecord + self.primary_key = :uuid + before_create :set_uuid + has_closure_tree dependent: :destroy, order: 'name', parent_column_name: 'parent_uuid' + before_destroy :add_destroyed_tag + + def set_uuid + self.uuid = SecureRandom.uuid + end + + def to_s + name + end + + def add_destroyed_tag + # Proof for the tests that the destroy rather than the delete method was called: + DestroyedTag.create(name: to_s) + end +end diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb index ff2d96d0..c5b54a9e 100644 --- a/test/dummy/config/application.rb +++ b/test/dummy/config/application.rb @@ -1,17 +1,22 @@ # frozen_string_literal: true -require_relative 'boot' +require File.expand_path('boot', __dir__) require 'rails' +require 'active_model/railtie' require 'active_record/railtie' -require 'rails/test_unit/railtie' Bundler.require(*Rails.groups) require 'closure_tree' module Dummy class Application < Rails::Application - config.load_defaults Rails::VERSION::STRING.to_f + config.load_defaults [Rails::VERSION::MAJOR, Rails::VERSION::MINOR].join('.') config.eager_load = false + + # Test environment settings + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.action_dispatch.show_exceptions = false end -end \ No newline at end of file +end diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb index c9c87bbd..50c2bdf4 100644 --- a/test/dummy/config/boot.rb +++ b/test/dummy/config/boot.rb @@ -1,7 +1,3 @@ # frozen_string_literal: true -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) \ No newline at end of file +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml index 63123141..c769af29 100644 --- a/test/dummy/config/database.yml +++ b/test/dummy/config/database.yml @@ -1,5 +1,14 @@ -test: - adapter: sqlite3 - database: ":memory:" +default: &default pool: 50 - timeout: 5000 \ No newline at end of file + timeout: 5000 + +test: + primary: + <<: *default + url: "<%= ENV['DATABASE_URL_PG'] %>" + mysql: + <<: *default + url: "<%= ENV['DATABASE_URL_MYSQL'] %>" + sqlite: + <<: *default + url: "<%= ENV['DATABASE_URL_SQLITE3'] %>" \ No newline at end of file diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb index d594e044..d5abe558 100644 --- a/test/dummy/config/environment.rb +++ b/test/dummy/config/environment.rb @@ -4,4 +4,4 @@ require_relative 'application' # Initialize the Rails application. -Rails.application.initialize! \ No newline at end of file +Rails.application.initialize! diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb index d0f8a20c..7191cbdf 100644 --- a/test/dummy/config/environments/test.rb +++ b/test/dummy/config/environments/test.rb @@ -7,4 +7,6 @@ config.action_controller.perform_caching = false config.action_dispatch.show_exceptions = false config.active_support.deprecation = :stderr -end \ No newline at end of file + config.active_support.test_order = :random + config.active_record.maintain_test_schema = false +end diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb deleted file mode 100644 index edf04d2d..00000000 --- a/test/dummy/config/routes.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -Rails.application.routes.draw do -end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 699044b1..667ffee4 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -1,15 +1,159 @@ # frozen_string_literal: true +# frozen_string_literal: true + ActiveRecord::Schema.define(version: 1) do - create_table 'tags', force: true do |t| + create_table 'tags' do |t| + t.string 'name' + t.string 'title' + t.references 'parent' + t.integer 'sort_order' + t.timestamps null: false + end + + create_table 'tag_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false + end + + create_table 'uuid_tags', id: false do |t| + t.string 'uuid', primary_key: true + t.string 'name' + t.string 'title' + t.string 'parent_uuid' + t.integer 'sort_order' + t.timestamps null: false + end + + create_table 'uuid_tag_hierarchies', id: false do |t| + t.string 'ancestor_id', null: false + t.string 'descendant_id', null: false + t.integer 'generations', null: false + end + + create_table 'destroyed_tags' do |t| + t.string 'name' + end + + add_index 'tag_hierarchies', %i[ancestor_id descendant_id generations], unique: true, + name: 'tag_anc_desc_idx' + add_index 'tag_hierarchies', [:descendant_id], name: 'tag_desc_idx' + + create_table 'groups' do |t| + t.string 'name', null: false + end + + create_table 'groupings' do |t| + t.string 'name', null: false + end + + create_table 'user_sets' do |t| + t.string 'name', null: false + end + + create_table 'teams' do |t| + t.string 'name', null: false + end + + create_table 'users' do |t| + t.string 'email' + t.references 'referrer' + t.integer 'group_id' + t.timestamps null: false + end + + create_table 'contracts' do |t| + t.references 'user', null: false + t.references 'contract_type' + t.string 'title' + end + + create_table 'contract_types' do |t| + t.string 'name', null: false + end + + create_table 'referral_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false + end + + create_table 'labels' do |t| t.string 'name' + t.string 'type' + t.integer 'column_whereby_ordering_is_inferred' + t.references 'mother' end - create_table 'tag_audits', id: false, force: true do |t| - t.string 'tag_name' + create_table 'label_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false end - create_table 'labels', id: false, force: true do |t| + create_table 'cuisine_types' do |t| t.string 'name' + t.references 'parent' end + + create_table 'cuisine_type_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false + end + + create_table 'namespace_types' do |t| + t.string 'name' + t.references 'parent' + end + + create_table 'namespace_type_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false + end + + create_table 'metal' do |t| + t.references 'parent' + t.string 'metal_type' + t.string 'value' + t.string 'description' + t.integer 'sort_order' + end + + create_table 'metal_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false + end + + create_table 'menu_items' do |t| + t.string 'name' + t.references 'parent' + t.timestamps null: false + end + + create_table 'menu_item_hierarchies', id: false do |t| + t.references 'ancestor', null: false + t.references 'descendant', null: false + t.integer 'generations', null: false + end + + add_index 'label_hierarchies', %i[ancestor_id descendant_id generations], unique: true, + name: 'lh_anc_desc_idx' + add_index 'label_hierarchies', [:descendant_id], name: 'lh_desc_idx' + add_index 'referral_hierarchies', %i[ancestor_id descendant_id generations], unique: true, + name: 'ref_anc_desc_idx' + add_index 'referral_hierarchies', [:descendant_id], name: 'ref_desc_idx' + + add_foreign_key(:tags, :tags, column: 'parent_id', on_delete: :cascade) + add_foreign_key(:users, :users, column: 'referrer_id', on_delete: :cascade) + add_foreign_key(:labels, :labels, column: 'mother_id', on_delete: :cascade) + add_foreign_key(:metal, :metal, column: 'parent_id', on_delete: :cascade) + add_foreign_key(:menu_items, :menu_items, column: 'parent_id', on_delete: :cascade) + add_foreign_key(:menu_item_hierarchies, :menu_items, column: 'ancestor_id', on_delete: :cascade) + add_foreign_key(:menu_item_hierarchies, :menu_items, column: 'descendant_id', on_delete: :cascade) + add_foreign_key(:tag_hierarchies, :tags, column: 'ancestor_id', on_delete: :cascade) + add_foreign_key(:tag_hierarchies, :tags, column: 'descendant_id', on_delete: :cascade) end diff --git a/test/dummy/lib/tasks/db.rake b/test/dummy/lib/tasks/db.rake deleted file mode 100644 index c5a76559..00000000 --- a/test/dummy/lib/tasks/db.rake +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -namespace :db do - namespace :test do - desc 'Load schema for all databases' - task prepare: :environment do - # Load schema for primary database - ActiveRecord::Base.establish_connection(:primary) - ActiveRecord::Schema.define(version: 1) do - create_table 'tags', force: true do |t| - t.string 'name' - end - - create_table 'tag_audits', id: false, force: true do |t| - t.string 'tag_name' - end - - create_table 'labels', id: false, force: true do |t| - t.string 'name' - end - end - - # Load schema for secondary database - ActiveRecord::Base.establish_connection(:secondary) - ActiveRecord::Schema.define(version: 1) do - create_table 'mysql_tags', force: true do |t| - t.string 'name' - end - - create_table 'mysql_tag_audits', id: false, force: true do |t| - t.string 'tag_name' - end - - create_table 'mysql_labels', id: false, force: true do |t| - t.string 'name' - end - end - end - end -end diff --git a/test/support/models.rb b/test/support/models.rb deleted file mode 100644 index a49e0134..00000000 --- a/test/support/models.rb +++ /dev/null @@ -1,158 +0,0 @@ -# frozen_string_literal: true - -class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true - - # connects_to database: { writing: :primary, reading: :primary } -end - -class SecondDatabaseRecord < ActiveRecord::Base - self.abstract_class = true - - # connects_to database: { writing: :secondary, reading: :secondary } -end -class Tag < ApplicationRecord - has_closure_tree dependent: :destroy, order: :name - before_destroy :add_destroyed_tag - - def to_s - name - end - - def add_destroyed_tag - # Proof for the tests that the destroy rather than the delete method was called: - DestroyedTag.create(name: to_s) - end -end - -class UUIDTag < ApplicationRecord - self.primary_key = :uuid - before_create :set_uuid - has_closure_tree dependent: :destroy, order: 'name', parent_column_name: 'parent_uuid' - before_destroy :add_destroyed_tag - - def set_uuid - self.uuid = SecureRandom.uuid - end - - def to_s - name - end - - def add_destroyed_tag - # Proof for the tests that the destroy rather than the delete method was called: - DestroyedTag.create(name: to_s) - end -end - -class DestroyedTag < ApplicationRecord -end - -class Group < ApplicationRecord - has_closure_tree_root :root_user -end - -class Grouping < ApplicationRecord - has_closure_tree_root :root_person, class_name: 'User', foreign_key: :group_id -end - -class UserSet < ApplicationRecord - has_closure_tree_root :root_user, class_name: 'Useur' -end - -class Team < ApplicationRecord - has_closure_tree_root :root_user, class_name: 'User', foreign_key: :grp_id -end - -class User < ApplicationRecord - acts_as_tree parent_column_name: 'referrer_id', - name_column: 'email', - hierarchy_class_name: 'ReferralHierarchy', - hierarchy_table_name: 'referral_hierarchies' - - has_many :contracts, inverse_of: :user - belongs_to :group # Can't use and don't need inverse_of here when using has_closure_tree_root. - - def indirect_contracts - Contract.where(user_id: descendant_ids) - end - - def to_s - email - end -end - -class Contract < ApplicationRecord - belongs_to :user, inverse_of: :contracts - belongs_to :contract_type, inverse_of: :contracts -end - -class ContractType < ApplicationRecord - has_many :contracts, inverse_of: :contract_type -end - -class Label < ApplicationRecord - # make sure order doesn't matter - acts_as_tree order: :column_whereby_ordering_is_inferred, # <- symbol, and not "sort_order" - numeric_order: true, - parent_column_name: 'mother_id', - dependent: :destroy - - def to_s - "#{self.class}: #{name}" - end -end - -class EventLabel < Label -end - -class DateLabel < Label -end - -class DirectoryLabel < Label -end - -class LabelWithoutRootOrdering < ApplicationRecord - # make sure order doesn't matter - acts_as_tree order: :column_whereby_ordering_is_inferred, # <- symbol, and not "sort_order" - numeric_order: true, - dont_order_roots: true, - parent_column_name: 'mother_id', - hierarchy_table_name: 'label_hierarchies' - - self.table_name = "#{table_name_prefix}labels#{table_name_suffix}" - - def to_s - "#{self.class}: #{name}" - end -end - -class CuisineType < ApplicationRecord - acts_as_tree -end - -module Namespace - def self.table_name_prefix - 'namespace_' - end - - class Type < ApplicationRecord - has_closure_tree dependent: :destroy - end -end - -class Metal < ApplicationRecord - self.table_name = "#{table_name_prefix}metal#{table_name_suffix}" - has_closure_tree order: 'sort_order', name_column: 'value' - self.inheritance_column = 'metal_type' -end - -class Adamantium < Metal -end - -class Unobtanium < Metal -end - -class MenuItem < SecondDatabaseRecord - has_closure_tree touch: true, with_advisory_lock: false -end diff --git a/test/support/schema.rb b/test/support/schema.rb deleted file mode 100644 index cca75c50..00000000 --- a/test/support/schema.rb +++ /dev/null @@ -1,157 +0,0 @@ -# frozen_string_literal: true - -ActiveRecord::Schema.define(version: 1) do - create_table 'tags' do |t| - t.string 'name' - t.string 'title' - t.references 'parent' - t.integer 'sort_order' - t.timestamps null: false - end - - create_table 'tag_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - create_table 'uuid_tags', id: false do |t| - t.string 'uuid', primary_key: true - t.string 'name' - t.string 'title' - t.string 'parent_uuid' - t.integer 'sort_order' - t.timestamps null: false - end - - create_table 'uuid_tag_hierarchies', id: false do |t| - t.string 'ancestor_id', null: false - t.string 'descendant_id', null: false - t.integer 'generations', null: false - end - - create_table 'destroyed_tags' do |t| - t.string 'name' - end - - add_index 'tag_hierarchies', %i[ancestor_id descendant_id generations], unique: true, - name: 'tag_anc_desc_idx' - add_index 'tag_hierarchies', [:descendant_id], name: 'tag_desc_idx' - - create_table 'groups' do |t| - t.string 'name', null: false - end - - create_table 'groupings' do |t| - t.string 'name', null: false - end - - create_table 'user_sets' do |t| - t.string 'name', null: false - end - - create_table 'teams' do |t| - t.string 'name', null: false - end - - create_table 'users' do |t| - t.string 'email' - t.references 'referrer' - t.integer 'group_id' - t.timestamps null: false - end - - create_table 'contracts' do |t| - t.references 'user', null: false - t.references 'contract_type' - t.string 'title' - end - - create_table 'contract_types' do |t| - t.string 'name', null: false - end - - create_table 'referral_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - create_table 'labels' do |t| - t.string 'name' - t.string 'type' - t.integer 'column_whereby_ordering_is_inferred' - t.references 'mother' - end - - create_table 'label_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - create_table 'cuisine_types' do |t| - t.string 'name' - t.references 'parent' - end - - create_table 'cuisine_type_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - create_table 'namespace_types' do |t| - t.string 'name' - t.references 'parent' - end - - create_table 'namespace_type_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - create_table 'metal' do |t| - t.references 'parent' - t.string 'metal_type' - t.string 'value' - t.string 'description' - t.integer 'sort_order' - end - - create_table 'metal_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - create_table 'menu_items' do |t| - t.string 'name' - t.references 'parent' - t.timestamps null: false - end - - create_table 'menu_item_hierarchies', id: false do |t| - t.references 'ancestor', null: false - t.references 'descendant', null: false - t.integer 'generations', null: false - end - - add_index 'label_hierarchies', %i[ancestor_id descendant_id generations], unique: true, - name: 'lh_anc_desc_idx' - add_index 'label_hierarchies', [:descendant_id], name: 'lh_desc_idx' - add_index 'referral_hierarchies', %i[ancestor_id descendant_id generations], unique: true, - name: 'ref_anc_desc_idx' - add_index 'referral_hierarchies', [:descendant_id], name: 'ref_desc_idx' - - add_foreign_key(:tags, :tags, column: 'parent_id', on_delete: :cascade) - add_foreign_key(:users, :users, column: 'referrer_id', on_delete: :cascade) - add_foreign_key(:labels, :labels, column: 'mother_id', on_delete: :cascade) - add_foreign_key(:metal, :metal, column: 'parent_id', on_delete: :cascade) - add_foreign_key(:menu_items, :menu_items, column: 'parent_id', on_delete: :cascade) - add_foreign_key(:menu_item_hierarchies, :menu_items, column: 'ancestor_id', on_delete: :cascade) - add_foreign_key(:menu_item_hierarchies, :menu_items, column: 'descendant_id', on_delete: :cascade) - add_foreign_key(:tag_hierarchies, :tags, column: 'ancestor_id', on_delete: :cascade) - add_foreign_key(:tag_hierarchies, :tags, column: 'descendant_id', on_delete: :cascade) -end diff --git a/test/support/tag_examples.rb b/test/support/tag_examples.rb index e90ae208..7f578e39 100644 --- a/test/support/tag_examples.rb +++ b/test/support/tag_examples.rb @@ -1,36 +1,42 @@ # frozen_string_literal: true +require 'active_support/concern' + module TagExamples - def self.included(base) - base.class_eval do - def setup - super - @tag_class = self.class.const_get(:TAG_CLASS) || Tag - @tag_hierarchy_class = @tag_class.hierarchy_class - end + extend ActiveSupport::Concern + + included do + def setup + super + @tag_class = self.class.const_get(:TAG_CLASS) || Tag + @tag_hierarchy_class = @tag_class.hierarchy_class + # Clean up any existing data to ensure test isolation + @tag_class.delete_all + @tag_hierarchy_class.delete_all + end - test "should build hierarchy classname correctly" do + define_method "test_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 - test "should have a correct parent column name" do - expected_parent_column_name = @tag_class == UUIDTag ? 'parent_uuid' : 'parent_id' + define_method "test_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 - test "should return no entities when db is empty" do + define_method "test_should_return_no_entities_when_db_is_empty" do assert_empty @tag_class.roots assert_empty @tag_class.leaves end - test "#find_or_create_by_path with strings" do + define_method "test_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 - test "#find_or_create_by_path with hashes" do + define_method "test_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' }, @@ -40,7 +46,7 @@ def setup assert_equal(%w[C B A], subject.self_and_ancestors.map(&:title)) end - test "single tag should be a leaf and root" do + define_method "test_single_tag_should_be_a_leaf_and_root" do tag = @tag_class.create!(name: 'tag') assert tag.leaf? assert tag.root? @@ -50,7 +56,7 @@ def setup assert_equal [tag], @tag_class.leaves end - test "should not find tag with invalid path arguments" do + define_method "test_should_not_find_tag_with_invalid_path_arguments" do tag = @tag_class.create!(name: 'tag') assert_nil @tag_class.find_by_path(['']) assert_nil @tag_class.find_by_path([]) @@ -61,13 +67,13 @@ def setup assert_nil @tag_class.find_by_path([tag.name, nil]) end - test "should find tag by valid path" do + define_method "test_should find tag by valid path" do tag = @tag_class.create!(name: 'tag') assert_equal tag, @tag_class.find_by_path([tag.name]) assert_equal tag, @tag_class.find_by_path(tag.name) end - test "adds children through add_child" do + define_method "test_adds children through add_child" do tag = @tag_class.create!(name: 'tag') child = @tag_class.create!(name: 'tag 2') tag.add_child child @@ -80,7 +86,7 @@ def setup assert_equal [child], tag.reload.children.to_a end - test "adds children through collection" do + define_method "test_adds children through collection" do tag = @tag_class.create!(name: 'tag') child = @tag_class.create!(name: 'tag 2') tag.children << child @@ -93,7 +99,7 @@ def setup assert_equal [child], tag.reload.children.to_a end - test "returns simple root and leaf with 2 tags" do + define_method "test_returns simple root and leaf with 2 tags" do root = @tag_class.create!(name: 'root') leaf = root.add_child(@tag_class.create!(name: 'leaf')) @@ -103,7 +109,7 @@ def setup assert_empty leaf.child_ids end - test "3 tag collection.create hierarchy" do + define_method "test_3 tag collection.create hierarchy" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -114,7 +120,7 @@ def setup assert_equal [leaf], @tag_class.leaves end - test "deletes leaves" do + define_method "test_deletes leaves" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -125,7 +131,7 @@ def setup assert_equal [mid], @tag_class.leaves end - test "deletes everything when deleting roots" do + define_method "test_deletes everything when deleting roots" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -138,7 +144,7 @@ def setup assert_equal %w[root mid leaf].sort, DestroyedTag.all.map(&:name).sort end - test "fixes self_and_ancestors properly on reparenting" do + define_method "test_fixes self_and_ancestors properly on reparenting" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -149,7 +155,7 @@ def setup assert_equal [t, mid, root], t.self_and_ancestors.to_a end - test "prevents ancestor loops" do + define_method "test_prevents ancestor loops" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -159,7 +165,7 @@ def setup assert_includes root.reload.descendants, leaf end - test "moves non-leaves" do + define_method "test_moves non-leaves" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -171,7 +177,7 @@ def setup assert_equal %w[new_root mid leaf], leaf.reload.ancestry_path end - test "moves leaves" do + define_method "test_moves leaves" do root = @tag_class.create! name: 'root' mid = root.children.create! name: 'mid' leaf = mid.children.create! name: 'leaf' @@ -183,7 +189,7 @@ def setup assert_equal %w[new_root leaf], leaf.reload.ancestry_path end - test "3 tag explicit_create hierarchy" do + define_method "test_3 tag explicit_create hierarchy" 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')) @@ -193,7 +199,7 @@ def setup assert_equal [leaf], @tag_class.leaves end - test "prevents parental loops from torso" do + define_method "test_prevents parental loops from torso" 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')) @@ -203,7 +209,7 @@ def setup assert_equal [leaf], mid.reload.children end - test "prevents parental loops from toes" do + define_method "test_prevents parental loops from toes" 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')) @@ -213,7 +219,7 @@ def setup assert_empty leaf.reload.children end - test "supports re-parenting" do + define_method "test_supports re-parenting" 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')) @@ -222,7 +228,7 @@ def setup assert_equal [leaf, mid], @tag_class.leaves end - test "cleans up hierarchy references for leaves" do + define_method "test_cleans up hierarchy references for leaves" 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')) @@ -232,7 +238,7 @@ def setup assert_empty @tag_hierarchy_class.where(descendant_id: leaf.id) end - test "cleans up hierarchy references" do + define_method "test_cleans up hierarchy references" 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')) @@ -247,7 +253,7 @@ def setup assert_equal root_hiers, @tag_hierarchy_class.where(descendant_id: root.id) end - test "hierarchy models have different hash codes" do + define_method "test_hierarchy models have different hash codes" 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')) @@ -256,14 +262,14 @@ def setup assert_equal hashes.uniq.sort, hashes.sort end - test "equal hierarchy models have same hash code" do + define_method "test_equal hierarchy models have same hash code" do root = @tag_class.create!(name: 'root') root.add_child(@tag_class.create!(name: 'mid')) assert_equal @tag_hierarchy_class.first.hash, @tag_hierarchy_class.first.hash end - test "performs as the readme says" do + define_method "test_performs as the readme says" do skip "JRuby has issues with ActiveRecord 7.1+ datetime handling in transactions" if defined?(JRUBY_VERSION) grandparent = @tag_class.create(name: 'Grandparent') @@ -286,13 +292,13 @@ def setup assert_equal %w[a b c d e f g h], h.ancestry_path end - test "roots sort alphabetically" do + define_method "test_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(&:name) end - test "finds global roots in simple tree" do + define_method "test_finds global roots in simple tree" 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] @@ -306,7 +312,7 @@ def setup assert_equal expected_roots.sort, @tag_class.roots.to_a.sort end - test "returns root? for roots" do + define_method "test_returns root? for roots" do @tag_class.find_or_create_by_path %w[a1 b1 c1a] @tag_class.find_or_create_by_path %w[a2 b2] @tag_class.find_or_create_by_path %w[a3] @@ -315,7 +321,7 @@ def setup [a1, a2, a3].each { |ea| assert(ea.root?) } end - test "does not return root? for non-roots" do + define_method "test_does not return root? for non-roots" do @tag_class.find_or_create_by_path %w[a1 b1 c1a] @tag_class.find_or_create_by_path %w[a2 b2] @@ -323,7 +329,7 @@ def setup [b1, b2, c1a].each { |ea| refute(ea.root?) } end - test "returns the correct root" do + define_method "test_returns the correct root" 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[a2 b2] @@ -335,7 +341,7 @@ def setup end end - test "assembles global leaves" do + define_method "test_assembles global leaves" 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] @@ -349,7 +355,7 @@ def setup assert_equal expected_leaves.sort, @tag_class.leaves.to_a.sort end - test "assembles siblings properly" do + define_method "test_assembles siblings properly" 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] @@ -373,7 +379,7 @@ def setup end end - test "assembles before_siblings" do + define_method "test_assembles before_siblings" 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] @@ -393,7 +399,7 @@ def setup end end - test "assembles after_siblings" do + define_method "test_assembles after_siblings" 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] @@ -413,7 +419,7 @@ def setup end end - test "assembles instance leaves" do + define_method "test_assembles instance leaves" 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] @@ -431,7 +437,7 @@ def setup expected_leaves.each { |ea| assert_equal [ea], ea.leaves.to_a } end - test "returns leaf? for leaves" do + define_method "test_returns leaf? for leaves" do @tag_class.find_or_create_by_path %w[a1 b1 c1a] @tag_class.find_or_create_by_path %w[a1 b1b] @tag_class.find_or_create_by_path %w[a2 b2] @@ -443,7 +449,7 @@ def setup expected_leaves.each { |ea| assert ea.leaf? } end - test "can move roots" do + define_method "test_can move roots" do @tag_class.find_or_create_by_path %w[a1 b1 c1a] @tag_class.find_or_create_by_path %w[a2 b2] @tag_class.find_or_create_by_path %w[a3] @@ -455,7 +461,7 @@ def setup assert_equal %w[a1 b1 c1a a2 b2 a3], a3.reload.ancestry_path end - test "cascade-deletes from roots" do + define_method "test_cascade-deletes from roots" 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] @@ -471,11 +477,11 @@ def setup assert_equal survivor_names, @tag_class.all.map(&:name) end - test "with_ancestor works with no rows" do + define_method "test_with_ancestor works with no rows" do assert_empty @tag_class.with_ancestor.to_a end - test "with_ancestor finds only children" do + define_method "test_with_ancestor finds only children" do c = @tag_class.find_or_create_by_path %w[A B C] a = c.parent.parent b = c.parent @@ -483,7 +489,7 @@ def setup assert_equal [b, c], @tag_class.with_ancestor(a).to_a end - test "with_ancestor limits subsequent where clauses" do + define_method "test_with_ancestor 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] refute_equal a2c, a1c @@ -491,11 +497,11 @@ def setup assert_equal [a1c], @tag_class.with_ancestor(a1c.parent.parent).where(name: 'C').to_a.sort end - test "with_descendant works with no rows" do + define_method "test_with_descendant works with no rows" do assert_empty @tag_class.with_descendant.to_a end - test "with_descendant finds only parents" do + define_method "test_with_descendant finds only parents" do c = @tag_class.find_or_create_by_path %w[A B C] a = c.parent.parent b = c.parent @@ -503,7 +509,7 @@ def setup assert_equal [a, b], @tag_class.with_descendant(c).to_a end - test "with_descendant limits subsequent where clauses" do + define_method "test_with_descendant limits subsequent where clauses" do ac1 = @tag_class.create(name: 'A') ac2 = @tag_class.create(name: 'A') @@ -518,7 +524,7 @@ def setup assert_equal [ac1], @tag_class.with_descendant(c1).where(name: 'A').to_a end - test "lowest_common_ancestor finds parent for siblings" do + define_method "test_lowest_common_ancestor finds parent for siblings" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t111 = @tag_class.create!(name: 't111', parent: t11) @@ -533,7 +539,7 @@ def setup assert_equal t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t11])) end - test "lowest_common_ancestor finds grandparent for cousins" do + define_method "test_lowest_common_ancestor finds grandparent for cousins" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t111 = @tag_class.create!(name: 't111', parent: t11) @@ -546,7 +552,7 @@ def setup assert_equal t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t112 t111 t121])) end - test "lowest_common_ancestor for aunt-uncle/niece-nephew" do + define_method "test_lowest_common_ancestor for aunt-uncle/niece-nephew" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t112 = @tag_class.create!(name: 't112', parent: t11) @@ -557,7 +563,7 @@ def setup assert_equal t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t12 t112])) end - test "lowest_common_ancestor for parent/child" do + define_method "test_lowest_common_ancestor for parent/child" do t1 = @tag_class.create!(name: 't1') t12 = @tag_class.create!(name: 't12', parent: t1) t121 = @tag_class.create!(name: 't121', parent: t12) @@ -570,7 +576,7 @@ def setup assert_equal t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t1 t12])) end - test "lowest_common_ancestor for grandparent/grandchild" do + define_method "test_lowest_common_ancestor for grandparent/grandchild" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t111 = @tag_class.create!(name: 't111', parent: t11) @@ -586,7 +592,7 @@ def setup assert_equal t1, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t111 t1])) end - test "lowest_common_ancestor for whole extended family" do + define_method "test_lowest_common_ancestor for whole extended family" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t111 = @tag_class.create!(name: 't111', parent: t11) @@ -605,13 +611,13 @@ def setup assert_equal t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t2 t21 t211])) end - test "lowest_common_ancestor is nil for no items" do + define_method "test_lowest_common_ancestor 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 - test "lowest_common_ancestor is nil for no common ancestors" do + define_method "test_lowest_common_ancestor is nil for no common ancestors" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t111 = @tag_class.create!(name: 't111', parent: t11) @@ -624,7 +630,7 @@ def setup assert_nil @tag_class.lowest_common_ancestor(@tag_class.where(name: %w[t111 t211])) end - test "lowest_common_ancestor is itself for single item" do + define_method "test_lowest_common_ancestor is itself for single item" do t1 = @tag_class.create!(name: 't1') t11 = @tag_class.create!(name: 't11', parent: t1) t111 = @tag_class.create!(name: 't111', parent: t11) @@ -638,7 +644,7 @@ def setup assert_equal t2, @tag_class.lowest_common_ancestor(@tag_class.where(name: 't2')) end - test "builds ancestry path" do + define_method "test_builds ancestry path" do child = @tag_class.find_or_create_by_path([ { name: 'grandparent', title: 'Nonnie' }, { name: 'parent', title: 'Mom' }, @@ -652,7 +658,7 @@ def setup assert_equal %w[Nonnie Mom Kid], child.ancestry_path(:title) end - test "assembles ancestors" do + define_method "test_assembles ancestors" do child = @tag_class.find_or_create_by_path([ { name: 'grandparent', title: 'Nonnie' }, { name: 'parent', title: 'Mom' }, @@ -665,7 +671,7 @@ def setup assert_equal [child, parent, grandparent], child.self_and_ancestors end - test "finds by path" do + define_method "test_finds by path" do child = @tag_class.find_or_create_by_path([ { name: 'grandparent', title: 'Nonnie' }, { name: 'parent', title: 'Mom' }, @@ -680,7 +686,7 @@ def setup assert_nil parent.find_by_path(%w[child larvae]) end - test "respects attribute hashes with both selection and creation" do + define_method "test_respects attribute hashes with both selection and creation" do grandparent = @tag_class.find_or_create_by_path([ { name: 'grandparent', title: 'Nonnie' } ]) @@ -694,7 +700,7 @@ def setup assert_equal existing_title, grandparent.reload.title end - test "creates hierarchy with given attribute" do + define_method "test_creates hierarchy with given attribute" do expected_title = 'unicorn rainbows' attrs = { title: expected_title } child = @tag_class.find_or_create_by_path(%w[grandparent parent child], attrs) @@ -704,20 +710,20 @@ def setup end end - test "finds correctly rooted paths" do + define_method "test_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 - test "find_by_path for 1 node" do + define_method "test_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 - test "find_by_path for 2 nodes" do + define_method "test_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 @@ -728,33 +734,33 @@ def setup end end - test "find_by_path for 3 nodes" do + define_method "test_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 - test "returns nil for missing nodes" do + define_method "test_returns 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 - test "find_or_create_by_path uses existing records" do + define_method "test_find_or_create_by_path 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 - test "find_or_create_by_path creates 2-deep trees with strings" do + define_method "test_find_or_create_by_path 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 - test "find_or_create_by_path creates 2-deep trees with hashes" do + define_method "test_find_or_create_by_path creates 2-deep trees with hashes" do subject = @tag_class.find_or_create_by_path([ { name: 'test1', title: 'TEST1' }, { name: 'test2', title: 'TEST2' } @@ -763,12 +769,12 @@ def setup assert_equal %w[TEST2 TEST1], subject.self_and_ancestors.map(&:title) end - test "hash_tree returns {} for depth 0" do + define_method "test_hash_tree returns {} for depth 0" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] assert_equal({}, @tag_class.hash_tree(limit_depth: 0)) end - test "hash_tree limit_depth 1" do + define_method "test_hash_tree limit_depth 1" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] a = d1.root a2 = @tag_class.create(name: 'a2') @@ -778,7 +784,7 @@ def setup assert_equal one_tree, @tag_class.hash_tree(limit_depth: 1) end - test "hash_tree limit_depth 2" do + define_method "test_hash_tree limit_depth 2" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -797,7 +803,7 @@ def setup assert_equal two_tree, @tag_class.hash_tree(limit_depth: 2) end - test "hash_tree limit_depth 3" do + define_method "test_hash_tree limit_depth 3" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -816,7 +822,7 @@ def setup assert_equal three_tree, @tag_class.hash_tree(limit_depth: 3) end - test "hash_tree limit_depth 4" do + define_method "test_hash_tree limit_depth 4" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -835,7 +841,7 @@ def setup assert_equal full_tree, @tag_class.hash_tree(limit_depth: 4) end - test "hash_tree no limit" do + define_method "test_hash_tree no limit" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -854,13 +860,13 @@ def setup assert_equal full_tree, @tag_class.hash_tree end - test "instance hash_tree returns {} for depth 0" do + define_method "test_instance hash_tree returns {} for depth 0" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] b = d1.parent.parent assert_equal({}, b.hash_tree(limit_depth: 0)) end - test "instance hash_tree limit_depth 1" do + define_method "test_instance hash_tree limit_depth 1" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -871,7 +877,7 @@ def setup assert_equal two_tree[a].slice(b), b.hash_tree(limit_depth: 1) end - test "instance hash_tree no limit from subroot" do + define_method "test_instance hash_tree no limit from subroot" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -882,7 +888,7 @@ def setup assert_equal full_tree[a].slice(b), b.hash_tree end - test "hash_tree from chained associations" do + define_method "test_hash_tree from chained associations" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -893,7 +899,7 @@ def setup assert_equal full_tree[a], a.children.hash_tree end - test "finds_by_path for very deep trees" do + define_method "test_finds_by_path for very deep trees" do path = (1..20).to_a.map(&:to_s) subject = @tag_class.find_or_create_by_path(path) assert_equal path, subject.ancestry_path @@ -902,11 +908,11 @@ def setup assert_equal subject, root.find_by_path(path[1..]) end - test "DOT rendering for empty scope" do + define_method "test_DOT rendering for empty scope" do assert_equal "digraph G {\n}\n", @tag_class.to_dot_digraph(@tag_class.where('0=1')) end - test "DOT rendering for tree" do + define_method "test_DOT rendering for tree" 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]) @@ -932,7 +938,7 @@ def setup assert_equal(graph, dot) end - test "depth returns 0 for root" do + define_method "test_depth returns 0 for root" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -946,7 +952,7 @@ def setup assert_equal 0, a3.depth end - test "depth returns correct depth for nodes" do + define_method "test_depth returns correct depth for nodes" do d1 = @tag_class.find_or_create_by_path %w[a b c1 d1] c1 = d1.parent b = c1.parent @@ -961,6 +967,5 @@ def setup assert_equal 1, b3.depth assert_equal 2, c3.depth end - end end end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index b5732ff2..df5c09e5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,133 +1,32 @@ # frozen_string_literal: true -require 'logger' -require 'erb' -require 'active_record' -require 'with_advisory_lock' -require 'tmpdir' -require 'securerandom' -require 'minitest' +ENV['RAILS_ENV'] = 'test' +require_relative 'dummy/config/environment' +require 'rails/test_help' + require 'minitest/autorun' require 'database_cleaner' require 'support/query_counter' require 'parallel' require 'timecop' -# JRuby has issues with Timecop and ActiveRecord datetime casting -# Skip Timecop-dependent tests on JRuby -if defined?(JRUBY_VERSION) - puts "Warning: Timecop tests may fail on JRuby due to Time class incompatibilities" -end - -# Configure the database based on environment -database_url = ENV['DB_ADAPTER'] || ENV['DATABASE_URL'] || "sqlite3:///:memory:" +# Configure parallel tests +Thread.abort_on_exception = true +# Configure advisory_lock ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex -# Parse database URL and establish connection -connection_config = if database_url.start_with?('sqlite3://') - # SQLite needs special handling - if database_url == 'sqlite3:///:memory:' - { adapter: 'sqlite3', database: ':memory:' } - else - # Create a temporary database file - db_file = File.join(Dir.tmpdir, "closure_tree_test_#{SecureRandom.hex}.sqlite3") - { adapter: 'sqlite3', database: db_file } - end -elsif database_url.start_with?('mysql2://') - # Parse MySQL URL: mysql2://root:root@0/closure_tree_test - # The @0 means localhost in GitHub Actions - database_url.gsub('@0/', '@127.0.0.1/') -elsif database_url.start_with?('postgres://') - # Parse PostgreSQL URL: postgres://closure_tree:closure_tree@0/closure_tree_test - # The @0 means localhost in GitHub Actions - fixed_url = database_url.gsub('@0/', '@127.0.0.1/') - # PostgreSQL adapter expects 'postgresql://' not 'postgres://' - fixed_url.gsub('postgres://', 'postgresql://') -else - # For other database URLs, use directly - database_url -end - -# Set connection pool size for parallel tests -if connection_config.is_a?(Hash) - connection_config[:pool] = 50 - connection_config[:checkout_timeout] = 10 - # Add JRuby-specific properties if needed - if defined?(JRUBY_VERSION) - connection_config[:properties] ||= {} - connection_config[:properties][:allowPublicKeyRetrieval] = true - end - ActiveRecord::Base.establish_connection(connection_config) -else - # For URL-based configs, append pool parameters - separator = connection_config.include?('?') ? '&' : '?' - ActiveRecord::Base.establish_connection("#{connection_config}#{separator}pool=50&checkout_timeout=10") -end - -def env_db - @env_db ||= ActiveRecord::Base.connection_db_config.adapter.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 - -# Use in specs to skip some tests -def sqlite? - env_db == :sqlite3 -end - -# For PostgreSQL and MySQL, we need to create/reset the database structure -unless sqlite? - begin - if ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql') - # PostgreSQL requires disconnecting before dropping the database - ActiveRecord::Base.connection.disconnect! - # Connect to postgres database to drop/create closure_tree_test - if connection_config.is_a?(String) - # Parse the DATABASE_URL and change database to postgres - postgres_url = connection_config.gsub(/\/closure_tree_test/, '/postgres') - ActiveRecord::Base.establish_connection(postgres_url) - else - ActiveRecord::Base.establish_connection(connection_config.merge(database: 'postgres')) - end - ActiveRecord::Base.connection.drop_database('closure_tree_test') rescue nil - ActiveRecord::Base.connection.create_database('closure_tree_test') - ActiveRecord::Base.connection.disconnect! - ActiveRecord::Base.establish_connection(connection_config) - else - # MySQL can recreate directly - ActiveRecord::Base.connection.recreate_database('closure_tree_test') - end - rescue => e - puts "Warning: Could not recreate database: #{e.message}" - end -end -puts "Testing with #{env_db} database, ActiveRecord #{ActiveRecord.gem_version} and #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} as #{RUBY_VERSION}" - -DatabaseCleaner.strategy = :truncation -# Allow DatabaseCleaner to work with our test database -DatabaseCleaner.allow_remote_database_url = true - -module Minitest - class Spec - include QueryCounter - - 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 +# JRuby has issues with Timecop and ActiveRecord datetime casting +if defined?(JRUBY_VERSION) + puts "Warning: Timecop tests may fail on JRuby due to Time class incompatibilities" end class ActiveSupport::TestCase + # Configure DatabaseCleaner + self.use_transactional_tests = false + setup do + DatabaseCleaner.strategy = :truncation DatabaseCleaner.start end @@ -163,19 +62,17 @@ def callback(name, start, finish, message_id, values) end end -# Configure parallel tests -Thread.abort_on_exception = true - -# Configure advisory_lock -# See: https://github.com/ClosureTree/with_advisory_lock -ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex - -require 'closure_tree' +# Helper methods available globally +def env_db + @env_db ||= ActiveRecord::Base.connection.adapter_name.downcase.to_sym +end -# Helper method to skip tests on JRuby -def skip_on_jruby(message = "Skipping on JRuby") - skip message if defined?(JRUBY_VERSION) +def sqlite? + env_db == :sqlite3 end -require_relative 'support/schema' -require_relative 'support/models' +# Load support files +require_relative 'support/query_counter' + +# Include QueryCounter in Minitest +Minitest::Test.send(:include, QueryCounter) From adabb8ad29bc7f9bf9ae58431ed6aa4815729497 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Mon, 2 Jun 2025 19:35:39 +0100 Subject: [PATCH 3/3] rename schema --- test/dummy/db/{secondary_schema.rb => mysql_schema.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/dummy/db/{secondary_schema.rb => mysql_schema.rb} (100%) diff --git a/test/dummy/db/secondary_schema.rb b/test/dummy/db/mysql_schema.rb similarity index 100% rename from test/dummy/db/secondary_schema.rb rename to test/dummy/db/mysql_schema.rb