diff --git a/.github/workflows/legacy_ruby.yml b/.github/workflows/legacy_ruby.yml index ae91dcb1..b7c4881c 100644 --- a/.github/workflows/legacy_ruby.yml +++ b/.github/workflows/legacy_ruby.yml @@ -59,7 +59,7 @@ jobs: TEST_CONFIG: ./spec/config.github.yml steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Database run: | psql -c "CREATE ROLE runner SUPERUSER LOGIN CREATEDB;" -U postgres -h localhost diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 00000000..17891526 --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,29 @@ +name: RuboCop + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +permissions: + contents: read + +jobs: + test: + name: RuboCop + runs-on: ubuntu-latest + + env: + BUNDLE_GEMFILE: gemfiles/rubocop.gemfile + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + rubygems: latest + bundler-cache: true + - name: RuboCop + run: bundle exec rubocop diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 5d35a548..4b7f87ca 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -72,7 +72,7 @@ jobs: continue-on-error: ${{ matrix.channel != 'stable' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Database run: | psql -c "CREATE ROLE runner SUPERUSER LOGIN CREATEDB;" -U postgres -h localhost diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..743e656d --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,82 @@ +require: + - rubocop-packaging + - rubocop-performance + - rubocop-rails + - rubocop-rake + - rubocop-rspec + +AllCops: + NewCops: enable + TargetRailsVersion: 5.0 + TargetRubyVersion: 2.2 + Exclude: + - 'gemfiles/**/*' + - 'lib/**/*' + - 'vendor/bundle/**/*' + - 'tmp/**/*' + +Gemspec/DevelopmentDependencies: + Enabled: false + +Layout/LineLength: + Max: 140 + Exclude: + - 'spec/**/*' + +Lint/MissingSuper: + Exclude: + - 'spec/**/*' + +Metrics/AbcSize: + Exclude: + - 'spec/**/*' + +Metrics/BlockLength: + Exclude: + - 'chrono_model.gemspec' + - 'spec/**/*' + +Metrics/MethodLength: + Exclude: + - 'spec/**/*' + +Rails/ApplicationRecord: + Enabled: false + +Rails/Date: + Enabled: false + +Rails/RakeEnvironment: + Enabled: false + +Rails/SkipsModelValidations: + AllowedMethods: ['touch', 'touch_all', 'update_column', 'update_columns'] + +Rails/TimeZone: + Enabled: false + +RSpec/BeforeAfterAll: + Exclude: + - 'spec/support/adapter/helpers.rb' + +RSpec/ExampleLength: + Enabled: false + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/NestedGroups: + Enabled: false + +RSpec/SpecFilePathFormat: + Enabled: false + +Style/GlobalVars: + Exclude: + - 'spec/**/*' + +# NOTE: This cop is enabled by RuboCop Rails, because active support adds this +# method to hash, but `except` is not available in specs +Style/HashExcept: + Exclude: + - 'spec/**/*' diff --git a/Gemfile b/Gemfile index ba27789a..131037b8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' # Specify your gem's dependencies in chrono_model.gemspec diff --git a/Rakefile b/Rakefile index bbec1d58..b0b33cba 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,13 @@ -require "bundler/gem_tasks" +# frozen_string_literal: true + +require 'bundler/gem_tasks' # RSpec require 'rspec/core/rake_task' RSpec::Core::RakeTask.new do |spec| spec.rspec_opts = '-f doc' end -task :default => ['testapp:create', :spec] +task default: ['testapp:create', :spec] # Create a test Rails app in tmp/railsapp for testing the rake # tasks and overall Rails integration with Aruba. diff --git a/chrono_model.gemspec b/chrono_model.gemspec index 8ad3ca00..d437cb65 100644 --- a/chrono_model.gemspec +++ b/chrono_model.gemspec @@ -9,9 +9,7 @@ Gem::Specification.new do |gem| gem.summary = 'Temporal extensions (SCD Type II) for Active Record' gem.homepage = 'https://github.com/ifad/chronomodel' - gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } - gem.files = `git ls-files`.split("\n") - gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.files = Dir.glob('{LICENSE,README.md,lib/**/*.rb}', File::FNM_DOTMATCH) gem.name = 'chrono_model' gem.license = 'MIT' gem.require_paths = ['lib'] @@ -20,7 +18,8 @@ Gem::Specification.new do |gem| gem.metadata = { 'bug_tracker_uri' => 'https://github.com/ifad/chronomodel/issues', 'homepage_uri' => 'https://github.com/ifad/chronomodel', - 'source_code_uri' => 'https://github.com/ifad/chronomodel' + 'source_code_uri' => 'https://github.com/ifad/chronomodel', + 'rubygems_mfa_required' => 'true' } gem.required_ruby_version = '>= 2.2.2' @@ -30,6 +29,7 @@ Gem::Specification.new do |gem| gem.add_dependency 'pg', '> 1.1' gem.add_development_dependency 'aruba' + gem.add_development_dependency 'bundler' gem.add_development_dependency 'byebug' gem.add_development_dependency 'fuubar' gem.add_development_dependency 'hirb' diff --git a/gemfiles/rubocop.gemfile b/gemfiles/rubocop.gemfile new file mode 100644 index 00000000..bcc0f3ee --- /dev/null +++ b/gemfiles/rubocop.gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem 'rubocop', require: false +gem 'rubocop-packaging', require: false +gem 'rubocop-performance', require: false +gem 'rubocop-rails', require: false +gem 'rubocop-rake', require: false +gem 'rubocop-rspec', require: false diff --git a/spec/aruba/dbconsole_spec.rb b/spec/aruba/dbconsole_spec.rb index 3501843e..1029ba6a 100644 --- a/spec/aruba/dbconsole_spec.rb +++ b/spec/aruba/dbconsole_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # The db consle does not work on Rails 5.0 # unless Bundler.default_gemfile.to_s =~ /rails_5.0/ diff --git a/spec/aruba/migrations_spec.rb b/spec/aruba/migrations_spec.rb index 21383aa4..b2e0ce7a 100644 --- a/spec/aruba/migrations_spec.rb +++ b/spec/aruba/migrations_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'database migrations', type: :aruba do before { copy_db_config } - context 'after a migration was generated' do + context 'when a migration is generated' do before { run_command_and_stop('bundle exec rails g migration CreateModels name:string') } describe 'bundle exec rake db:migrate' do @@ -17,19 +19,21 @@ describe 'rerun bundle exec rake db:drop db:create db:migrate', issue: 56 do let(:command) { 'bundle exec rake db:drop db:create db:migrate' } - before { copy('%/migrations/56/', 'db/migrate') } - let(:action) { run_command(command) } let(:regex) { /-- change_table\(:impressions, {:temporal=>true, :copy_data=>true}\)/ } + before { copy('%/migrations/56/', 'db/migrate') } + describe 'once' do let(:last_command) { action && last_command_started } + it { expect(last_command).to be_successfully_executed } it { expect(last_command).to have_output(regex) } end describe 'twice' do let(:last_command) { run_command_and_stop(command) && action && last_command_started } + it { expect(last_command).to be_successfully_executed } it { expect(last_command).to have_output(regex) } end diff --git a/spec/aruba/rake_task_spec.rb b/spec/aruba/rake_task_spec.rb index 5108425f..8cf63519 100644 --- a/spec/aruba/rake_task_spec.rb +++ b/spec/aruba/rake_task_spec.rb @@ -1,34 +1,43 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake' -include ChronoTest::Aruba + +# TODO: Understand why this is needed at root level and fix the RuboCop offense +include ChronoTest::Aruba # rubocop:disable Style/MixinUsage # add :announce_stdout, :announce_stderr, before the type: aruba tag in order # to see the commmands' stdout and stderr output. # RSpec.describe 'rake tasks', type: :aruba do describe 'bundle exec rake -T' do - before { run_command_and_stop('bundle exec rake -T') } subject { last_command_started } + before { run_command_and_stop('bundle exec rake -T') } + it { is_expected.to have_output(load_schema_task(as_regexp: true)) } end - describe "#{dump_schema_task}" do - before { copy_db_config } + describe dump_schema_task.to_s do + let(:db_file) { 'db/test.sql' } - before { run_command_and_stop("bundle exec rake #{dump_schema_task} SCHEMA=db/test.sql") } + before do + copy_db_config + run_command_and_stop("bundle exec rake #{dump_schema_task} SCHEMA=db/test.sql") + end it { expect(last_command_started).to be_successfully_executed } - it { expect('db/test.sql').to be_an_existing_file } - it { expect('db/test.sql').not_to have_file_content(/\A--/) } + it { expect(db_file).to be_an_existing_file } + it { expect(db_file).not_to have_file_content(/\A--/) } context 'with schema_search_path option' do - before { copy_db_config('database_with_schema_search_path.yml') } - - before { run_command_and_stop("bundle exec rake #{dump_schema_task} SCHEMA=db/test.sql") } + before do + copy_db_config('database_with_schema_search_path.yml') + run_command_and_stop("bundle exec rake #{dump_schema_task} SCHEMA=db/test.sql") + end it 'includes chronomodel schemas' do - expect('db/test.sql').to have_file_content(/^CREATE SCHEMA IF NOT EXISTS history;$/) + expect(db_file).to have_file_content(/^CREATE SCHEMA IF NOT EXISTS history;$/) .and have_file_content(/^CREATE SCHEMA IF NOT EXISTS temporal;$/) .and have_file_content(/^CREATE SCHEMA IF NOT EXISTS public;$/) end @@ -39,28 +48,27 @@ before do copy_db_config copy('%/set_config.sql', 'db/test.sql') + run_command_and_stop('bundle exec rake db:schema:load SCHEMA=db/test.sql') end - before { run_command_and_stop('bundle exec rake db:schema:load SCHEMA=db/test.sql') } - it { expect(last_command_started).to be_successfully_executed } end - describe "#{load_schema_task}" do + describe load_schema_task.to_s do let(:action) { run_command("bundle exec rake #{load_schema_task}") } let(:last_command) { action && last_command_started } - context 'given a file db/structure.sql' do + context 'with db/structure.sql' do before do copy('%/empty_structure.sql', 'db/structure.sql') end context 'with default username and password', issue: 55 do - before { copy_db_config('database_with_default_username_and_password.yml') } - - # Handle Homebrew on MacOS, whose database superuser name is - # equal to the name of the current user. before do + copy_db_config('database_with_default_username_and_password.yml') + + # Handle Homebrew on MacOS, whose database superuser name is + # equal to the name of the current user. if which 'brew' file_mangle!('config/database.yml') do |contents| contents.sub('username: postgres', "username: #{Etc.getlogin}") @@ -83,10 +91,9 @@ before do copy_db_config copy('%/set_config.sql', 'db/test.sql') + run_command_and_stop('bundle exec rake db:data:dump DUMP=db/test.sql') end - before { run_command_and_stop('bundle exec rake db:data:dump DUMP=db/test.sql') } - it { expect(last_command_started).to be_successfully_executed } it { expect('db/test.sql').to be_an_existing_file } end @@ -95,10 +102,9 @@ before do copy_db_config copy('%/empty_structure.sql', 'db/test.sql') + run_command_and_stop('bundle exec rake db:data:load DUMP=db/test.sql') end - before { run_command_and_stop('bundle exec rake db:data:load DUMP=db/test.sql') } - it { expect(last_command_started).to be_successfully_executed } end end diff --git a/spec/aruba/subclasses_spec.rb b/spec/aruba/subclasses_spec.rb index 51fd5ef7..5ccaf893 100644 --- a/spec/aruba/subclasses_spec.rb +++ b/spec/aruba/subclasses_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe 'subclasses spec', type: :aruba do @@ -10,7 +12,7 @@ copy_db_config write_file( 'app/models/foo.rb', - "class Foo < ApplicationRecord; include ChronoModel::TimeMachine; end; " + 'class Foo < ApplicationRecord; include ChronoModel::TimeMachine; end;' ) end diff --git a/spec/chrono_model/adapter/base_spec.rb b/spec/chrono_model/adapter/base_spec.rb index 6ec1f561..428d9209 100644 --- a/spec/chrono_model/adapter/base_spec.rb +++ b/spec/chrono_model/adapter/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/adapter/structure' @@ -6,91 +8,66 @@ include ChronoTest::Adapter::Structure subject { adapter } - it { is_expected.to be_a_kind_of(ChronoModel::Adapter) } - context do - subject { adapter.adapter_name } - it { is_expected.to eq 'PostgreSQL' } - end + it { is_expected.to be_a(described_class) } - context do - before { expect(adapter).to receive(:postgresql_version).and_return(90_300) } - it { is_expected.to be_chrono_supported } - end + describe '.adapter_name' do + subject { adapter.adapter_name } - context do - before { expect(adapter).to receive(:postgresql_version).and_return(90_000) } - it { is_expected.to_not be_chrono_supported } + it { is_expected.to eq 'PostgreSQL' } end - describe '.primary_key' do - subject { adapter.primary_key(table) } - - assert = proc do - it { is_expected.to eq 'id' } - end - - with_temporal_table(&assert) - with_plain_table(&assert) - end + describe '.chrono_supported?' do + subject { adapter.chrono_supported? } - describe '.indexes' do - subject { adapter.indexes(table) } + before { allow(adapter).to receive(:postgresql_version).and_return(postgres_version) } - assert = proc do - before(:all) do - adapter.add_index table, :foo, name: 'foo_index' - adapter.add_index table, %i[bar baz], name: 'bar_index' - end + context 'with Postgres 9.3' do + let(:postgres_version) { 90_300 } - it { expect(subject.map(&:name)).to match_array %w[foo_index bar_index] } - it { expect(subject.map(&:columns)).to match_array [['foo'], %w[bar baz]] } + it { is_expected.to be true } end - with_temporal_table(&assert) - with_plain_table(&assert) - end - - describe '.column_definitions' do - subject { adapter.column_definitions(table).map { |d| d.take(2) } } + context 'with Postgres 9.0' do + let(:postgres_version) { 90_000 } - assert = proc do - it { expect(subject & columns).to eq columns } - it { is_expected.to include(['id', pk_type]) } + it { is_expected.to be false } end - - with_temporal_table(&assert) - with_plain_table(&assert) end describe '.on_schema' do - before(:all) do + subject(:on_schema) { adapter } + + before do adapter.execute 'BEGIN' 5.times { |i| adapter.execute "CREATE SCHEMA test_#{i}" } end - after(:all) do + after do adapter.execute 'ROLLBACK' end - context 'by default' do + context 'with default settings' do it 'saves the schema at each recursion' do - is_expected.to be_in_schema(:default) - - adapter.on_schema('test_1') { is_expected.to be_in_schema('test_1') - adapter.on_schema('test_2') { is_expected.to be_in_schema('test_2') - adapter.on_schema('test_3') { is_expected.to be_in_schema('test_3') - } - is_expected.to be_in_schema('test_2') - } - is_expected.to be_in_schema('test_1') - } + expect(on_schema).to be_in_schema(:default) + + adapter.on_schema('test_1') do + expect(on_schema).to be_in_schema('test_1') + adapter.on_schema('test_2') do + expect(on_schema).to be_in_schema('test_2') + adapter.on_schema('test_3') do + expect(on_schema).to be_in_schema('test_3') + end + expect(on_schema).to be_in_schema('test_2') + end + expect(on_schema).to be_in_schema('test_1') + end - is_expected.to be_in_schema(:default) + expect(on_schema).to be_in_schema(:default) end context 'when errors occur' do - subject do + subject(:on_schema) do adapter.on_schema('test_1') do adapter.on_schema('test_2') do adapter.execute 'BEGIN' @@ -99,56 +76,69 @@ end end - it { - expect { subject }. - to raise_error(/current transaction is aborted/). - and change { adapter.instance_variable_get(:@schema_search_path) } - } - after do adapter.execute 'ROLLBACK' end + + it { + expect { on_schema } + .to raise_error(/current transaction is aborted/) + .and(change { adapter.instance_variable_get(:@schema_search_path) }) + } end end context 'with recurse: :ignore' do it 'ignores recursive calls' do - is_expected.to be_in_schema(:default) - - adapter.on_schema('test_1', recurse: :ignore) { is_expected.to be_in_schema('test_1') - adapter.on_schema('test_2', recurse: :ignore) { is_expected.to be_in_schema('test_1') - adapter.on_schema('test_3', recurse: :ignore) { is_expected.to be_in_schema('test_1') - } } } + expect(on_schema).to be_in_schema(:default) + + adapter.on_schema('test_1', recurse: :ignore) do + expect(on_schema).to be_in_schema('test_1') + adapter.on_schema('test_2', + recurse: :ignore) do + expect(on_schema).to be_in_schema('test_1') + adapter.on_schema('test_3', + recurse: :ignore) do + expect(on_schema).to be_in_schema('test_1') + end + end + end - is_expected.to be_in_schema(:default) + expect(on_schema).to be_in_schema(:default) end end end describe '.is_chrono?' do + subject(:is_chrono?) { adapter.is_chrono?(table) } + with_temporal_table do - it { expect(adapter.is_chrono?(table)).to be(true) } + it { is_expected.to be true } end with_plain_table do - it { expect(adapter.is_chrono?(table)).to be(false) } + it { is_expected.to be false } end context 'when schemas are not there yet' do - before(:all) do + before do adapter.execute 'BEGIN' adapter.execute 'DROP SCHEMA temporal CASCADE' adapter.execute 'DROP SCHEMA history CASCADE' adapter.execute 'CREATE TABLE test_table (id integer)' end - after(:all) do + after do adapter.execute 'ROLLBACK' end - it { expect { adapter.is_chrono?(table) }.to_not raise_error } + it 'does not raise errors' do + expect do + is_chrono? + end.not_to raise_error + end - it { expect(adapter.is_chrono?(table)).to be(false) } + it { is_expected.to be false } end end end diff --git a/spec/chrono_model/adapter/ddl_spec.rb b/spec/chrono_model/adapter/ddl_spec.rb index 6c8f2175..e446eadc 100644 --- a/spec/chrono_model/adapter/ddl_spec.rb +++ b/spec/chrono_model/adapter/ddl_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/adapter/structure' @@ -16,124 +18,105 @@ def ids(table) adapter.select_values("SELECT id FROM ONLY #{table} ORDER BY id") end - context 'INSERT multiple values' do - before :all do + context 'when inserting multiple values' do + before do adapter.create_table table, temporal: true, &columns end - after :all do + after do adapter.drop_table table end - context 'when succeeding' do - def insert - adapter.execute <<-SQL - INSERT INTO #{table} (test, foo) VALUES - ('test1', 1), - ('test2', 2); - SQL - end + it 'supports a sequence of INSERT commands' do + adapter.execute <<-SQL.squish + INSERT INTO #{table} (test, foo) VALUES + ('test1', 1), + ('test2', 2); + SQL - it { expect { insert }.to_not raise_error } - it { expect(count(current)).to eq 2 } - it { expect(count(history)).to eq 2 } - end + expect(count(current)).to eq 2 + expect(count(history)).to eq 2 - context 'when failing' do - def insert - adapter.execute <<-SQL + expect do + adapter.execute <<-SQL.squish INSERT INTO #{table} (test, foo) VALUES ('test3', 3), (NULL, 0); SQL - end - - it { expect { insert }.to raise_error(ActiveRecord::StatementInvalid) } - it { expect(count(current)).to eq 2 } # Because the previous - it { expect(count(history)).to eq 2 } # records are preserved - end - - context 'after a failure' do - def insert - adapter.execute <<-SQL - INSERT INTO #{table} (test, foo) VALUES - ('test4', 3), - ('test5', 4); - SQL - end + end.to raise_error(ActiveRecord::StatementInvalid) - it { expect { insert }.to_not raise_error } + expect(count(current)).to eq 2 + expect(count(history)).to eq 2 - it { expect(count(current)).to eq 4 } - it { expect(count(history)).to eq 4 } + adapter.execute <<-SQL.squish + INSERT INTO #{table} (test, foo) VALUES + ('test4', 3), + ('test5', 4); + SQL - it { expect(ids(current)).to eq ids(history) } + expect(count(current)).to eq 4 + expect(count(history)).to eq 4 + expect(ids(current)).to eq ids(history) end end - context 'INSERT on NOT NULL columns but with a DEFAULT value' do - before :all do + describe 'INSERT on NOT NULL columns but with a DEFAULT value' do + before do adapter.create_table table, temporal: true, &columns - end - - after :all do - adapter.drop_table table - end - def insert - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} DEFAULT VALUES SQL end - def select - adapter.select_values <<-SQL + after do + adapter.drop_table table + end + + let(:select) do + adapter.select_values <<-SQL.squish SELECT test FROM #{table} SQL end - it { expect { insert }.to_not raise_error } - it { insert; expect(select.uniq).to eq ['default-value'] } + it { expect(select.uniq).to eq ['default-value'] } end - context 'INSERT with string IDs' do - before :all do + describe 'INSERT with string IDs' do + before do adapter.create_table table, temporal: true, id: :string, &columns - end - - after :all do - adapter.drop_table table - end - def insert - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} (test, id) VALUES ('test1', 'hello'); SQL end - it { expect { insert }.to_not raise_error } + after do + adapter.drop_table table + end + it { expect(count(current)).to eq 1 } it { expect(count(history)).to eq 1 } end - context 'redundant UPDATEs' do - before :all do + describe 'redundant UPDATEs' do + before do adapter.create_table table, temporal: true, &columns - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} (test, foo) VALUES ('test1', 1); SQL - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET test = 'test2'; SQL - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET test = 'test2'; SQL end - after :all do + after do adapter.drop_table table end @@ -141,33 +124,33 @@ def insert it { expect(count(history)).to eq 2 } end - context 'updates on non-journaled fields' do - before :all do + describe 'UPDATEs on non-journaled fields' do + before do adapter.create_table table, temporal: true do |t| t.string 'test' t.timestamps null: false end - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} (test, created_at, updated_at) VALUES ('test', now(), now()); SQL - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET test = 'test2', updated_at = now(); SQL 2.times do - adapter.execute <<-SQL # Redundant update with only updated_at change + adapter.execute <<-SQL.squish # Redundant update with only updated_at change UPDATE #{table} SET test = 'test2', updated_at = now(); SQL - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET updated_at = now(); SQL end end - after :all do + after do adapter.drop_table table end @@ -175,24 +158,24 @@ def insert it { expect(count(history)).to eq 2 } end - context 'selective journaled fields' do + context 'with selective journaled fields' do describe 'basic behaviour' do - specify do + it 'does not record history when ignored fields change' do adapter.create_table table, temporal: true, journal: %w[foo] do |t| t.string 'foo' t.string 'bar' end - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar'); SQL - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET foo = 'test foo', bar = 'no history'; SQL 2.times do - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET bar = 'really no history'; SQL end @@ -222,14 +205,14 @@ def insert it 'preserves options upon column change' do adapter.change_table table, temporal: true, journal: %w[foo bar] - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar'); SQL expect(count(current)).to eq 1 expect(count(history)).to eq 1 - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET foo = 'test foo', bar = 'chronomodel'; SQL @@ -240,7 +223,7 @@ def insert it 'changes option upon table change' do adapter.change_table table, temporal: true, journal: %w[bar] - adapter.execute <<-SQL + adapter.execute <<-SQL.squish INSERT INTO #{table} (foo, bar) VALUES ('test foo', 'test bar'); UPDATE #{table} SET foo = 'test foo', bar = 'no history'; SQL @@ -248,7 +231,7 @@ def insert expect(count(current)).to eq 1 expect(count(history)).to eq 1 - adapter.execute <<-SQL + adapter.execute <<-SQL.squish UPDATE #{table} SET foo = 'test foo again', bar = 'no history'; SQL diff --git a/spec/chrono_model/adapter/indexes_spec.rb b/spec/chrono_model/adapter/indexes_spec.rb index eef4880f..44379d27 100644 --- a/spec/chrono_model/adapter/indexes_spec.rb +++ b/spec/chrono_model/adapter/indexes_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/adapter/structure' @@ -5,14 +7,14 @@ include ChronoTest::Adapter::Helpers include ChronoTest::Adapter::Structure - before :all do + before do adapter.create_table :meetings do |t| t.string :name t.tsrange :interval end end - after :all do + after do adapter.drop_table :meetings end @@ -21,23 +23,22 @@ adapter.add_temporal_indexes :meetings, :interval end - it { expect(adapter.indexes(:meetings).map(&:name)).to eq %w[ - index_meetings_temporal_on_interval - index_meetings_temporal_on_lower_interval - index_meetings_temporal_on_upper_interval - ] } - after do adapter.remove_temporal_indexes :meetings, :interval end + + it { + expect(adapter.indexes(:meetings).map(&:name)).to eq %w[ + index_meetings_temporal_on_interval + index_meetings_temporal_on_lower_interval + index_meetings_temporal_on_upper_interval + ] + } end describe '.remove_temporal_indexes' do - before :all do - adapter.add_temporal_indexes :meetings, :interval - end - before do + adapter.add_temporal_indexes :meetings, :interval adapter.remove_temporal_indexes :meetings, :interval end @@ -49,21 +50,20 @@ adapter.add_timeline_consistency_constraint(:meetings, :interval) end - it { expect(adapter.indexes(:meetings).map(&:name)).to eq [ - 'meetings_timeline_consistency' - ] } - after do adapter.remove_timeline_consistency_constraint(:meetings) end + + it { + expect(adapter.indexes(:meetings).map(&:name)).to eq [ + 'meetings_timeline_consistency' + ] + } end describe '.remove_timeline_consistency_constraint' do - before :all do - adapter.add_timeline_consistency_constraint :meetings, :interval - end - before do + adapter.add_timeline_consistency_constraint :meetings, :interval adapter.remove_timeline_consistency_constraint(:meetings) end diff --git a/spec/chrono_model/adapter/migrations_spec.rb b/spec/chrono_model/adapter/migrations_spec.rb index 7111c30a..afe30321 100644 --- a/spec/chrono_model/adapter/migrations_spec.rb +++ b/spec/chrono_model/adapter/migrations_spec.rb @@ -1,12 +1,18 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/adapter/structure' # For the structure of these tables, please see spec/support/adabters/structure.rb. # + +# TODO: `with_temporal_table` and `with_plain_table` are confusing Rubocop. +# They create a context and run examples inside that context +# rubocop:disable RSpec/RepeatedExample,RSpec/ScatteredSetup RSpec.shared_examples_for 'temporal table' do it { expect(adapter.is_chrono?(subject)).to be(true) } - it { is_expected.to_not have_public_backing } + it { is_expected.not_to have_public_backing } it { is_expected.to have_temporal_backing } it { is_expected.to have_history_backing } @@ -24,10 +30,10 @@ it { is_expected.to have_public_backing } - it { is_expected.to_not have_temporal_backing } - it { is_expected.to_not have_history_backing } - it { is_expected.to_not have_history_functions } - it { is_expected.to_not have_public_interface } + it { is_expected.not_to have_temporal_backing } + it { is_expected.not_to have_history_backing } + it { is_expected.not_to have_history_functions } + it { is_expected.not_to have_public_interface } it { is_expected.to have_columns(columns) } end @@ -38,11 +44,11 @@ describe '.create_table' do with_temporal_table do - it_should_behave_like 'temporal table' + it_behaves_like 'temporal table' end with_plain_table do - it_should_behave_like 'plain table' + it_behaves_like 'plain table' end end @@ -50,17 +56,18 @@ renamed = 'foo_table' subject { renamed } - context 'temporal: true' do - before :all do + context 'with temporal tables' do + before do adapter.create_table table, temporal: true, &columns adapter.add_index table, :test adapter.add_index table, %i[foo bar] adapter.rename_table table, renamed end - after(:all) { adapter.drop_table(renamed) } - it_should_behave_like 'temporal table' + after { adapter.drop_table(renamed) } + + it_behaves_like 'temporal table' it 'renames indexes' do new_index_names = adapter.indexes(renamed).map(&:name) @@ -71,29 +78,30 @@ end end - context 'temporal: false' do - before :all do + context 'with plain tables' do + before do adapter.create_table table, temporal: false, &columns adapter.rename_table table, renamed end - after(:all) { adapter.drop_table(renamed) } - it_should_behave_like 'plain table' + after { adapter.drop_table(renamed) } + + it_behaves_like 'plain table' end end describe '.change_table' do with_temporal_table do - before :all do + before do adapter.change_table table, temporal: false end - it_should_behave_like 'plain table' + it_behaves_like 'plain table' end with_plain_table do - before :all do + before do adapter.add_index table, :foo adapter.add_index table, :bar, unique: true adapter.add_index table, '(lower(baz))' @@ -102,7 +110,7 @@ adapter.change_table table, temporal: true end - it_should_behave_like 'temporal table' + it_behaves_like 'temporal table' let(:temporal_indexes) do adapter.indexes(table) @@ -114,11 +122,11 @@ end end - it "copies plain index to history" do + it 'copies plain index to history' do expect(history_indexes.find { |i| i.columns == ['foo'] }).to be_present end - it "copies unique index to history without uniqueness constraint" do + it 'copies unique index to history without uniqueness constraint' do expect(history_indexes.find { |i| i.columns == ['bar'] && i.unique == false }).to be_present end @@ -137,37 +145,37 @@ end with_plain_table do - before :all do - adapter.change_table table do |t| + before do + adapter.change_table table do |_t| adapter.add_column table, :frupper, :string end end - it_should_behave_like 'plain table' + it_behaves_like 'plain table' it { is_expected.to have_columns([['frupper', 'character varying']]) } end # https://github.com/ifad/chronomodel/issues/91 - context 'given a table using a sequence not owned by a column' do - before :all do + context 'with a table using a sequence not owned by a column' do + before do adapter.execute 'create sequence temporal.foobar owned by none' adapter.execute "create table #{table} (id integer primary key default nextval('temporal.foobar'::regclass), label character varying)" end - after :all do + after do adapter.execute "drop table if exists #{table}" - adapter.execute "drop sequence temporal.foobar" + adapter.execute 'drop sequence temporal.foobar' end it { is_expected.to have_columns([%w[id integer], ['label', 'character varying']]) } context 'when moving to temporal' do - before :all do + before do adapter.change_table table, temporal: true end - after :all do + after do adapter.drop_table table end @@ -182,36 +190,36 @@ describe '.drop_table' do context 'with temporal tables' do - before :all do + before do adapter.create_table table, temporal: true, &columns adapter.drop_table table end - it { is_expected.to_not have_public_backing } - it { is_expected.to_not have_temporal_backing } - it { is_expected.to_not have_history_backing } - it { is_expected.to_not have_history_functions } - it { is_expected.to_not have_public_interface } + it { is_expected.not_to have_public_backing } + it { is_expected.not_to have_temporal_backing } + it { is_expected.not_to have_history_backing } + it { is_expected.not_to have_history_functions } + it { is_expected.not_to have_public_interface } end - context 'without temporal tables' do - before :all do + context 'with plain tables' do + before do adapter.create_table table, temporal: false, &columns adapter.drop_table table, temporal: false end - it { is_expected.to_not have_public_backing } - it { is_expected.to_not have_public_interface } + it { is_expected.not_to have_public_backing } + it { is_expected.not_to have_public_interface } end end describe '.add_index' do with_temporal_table do - before :all do + before do adapter.add_index table, %i[foo bar], name: 'foobar_index' - adapter.add_index table, [:test], name: 'test_index' + adapter.add_index table, [:test], name: 'test_index' adapter.add_index table, :baz end @@ -222,24 +230,24 @@ it { is_expected.to have_temporal_index 'index_test_table_on_baz', %w[baz] } it { is_expected.to have_history_index 'index_test_table_on_baz', %w[baz] } - it { is_expected.to_not have_index 'foobar_index', %w[foo bar] } - it { is_expected.to_not have_index 'test_index', %w[test] } - it { is_expected.to_not have_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_index 'foobar_index', %w[foo bar] } + it { is_expected.not_to have_index 'test_index', %w[test] } + it { is_expected.not_to have_index 'index_test_table_on_baz', %w[baz] } end with_plain_table do - before :all do + before do adapter.add_index table, %i[foo bar], name: 'foobar_index' - adapter.add_index table, [:test], name: 'test_index' + adapter.add_index table, [:test], name: 'test_index' adapter.add_index table, :baz end - it { is_expected.to_not have_temporal_index 'foobar_index', %w[foo bar] } - it { is_expected.to_not have_history_index 'foobar_index', %w[foo bar] } - it { is_expected.to_not have_temporal_index 'test_index', %w[test] } - it { is_expected.to_not have_history_index 'test_index', %w[test] } - it { is_expected.to_not have_temporal_index 'index_test_table_on_baz', %w[baz] } - it { is_expected.to_not have_history_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_temporal_index 'foobar_index', %w[foo bar] } + it { is_expected.not_to have_history_index 'foobar_index', %w[foo bar] } + it { is_expected.not_to have_temporal_index 'test_index', %w[test] } + it { is_expected.not_to have_history_index 'test_index', %w[test] } + it { is_expected.not_to have_temporal_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_history_index 'index_test_table_on_baz', %w[baz] } it { is_expected.to have_index 'foobar_index', %w[foo bar] } it { is_expected.to have_index 'test_index', %w[test] } @@ -249,41 +257,41 @@ describe '.remove_index' do with_temporal_table do - before :all do + before do adapter.add_index table, %i[foo bar], name: 'foobar_index' - adapter.add_index table, [:test], name: 'test_index' + adapter.add_index table, [:test], name: 'test_index' adapter.add_index table, :baz adapter.remove_index table, name: 'test_index' adapter.remove_index table, :baz end - it { is_expected.to_not have_temporal_index 'test_index', %w[test] } - it { is_expected.to_not have_history_index 'test_index', %w[test] } - it { is_expected.to_not have_index 'test_index', %w[test] } + it { is_expected.not_to have_temporal_index 'test_index', %w[test] } + it { is_expected.not_to have_history_index 'test_index', %w[test] } + it { is_expected.not_to have_index 'test_index', %w[test] } - it { is_expected.to_not have_temporal_index 'index_test_table_on_baz', %w[baz] } - it { is_expected.to_not have_history_index 'index_test_table_on_baz', %w[baz] } - it { is_expected.to_not have_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_temporal_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_history_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_index 'index_test_table_on_baz', %w[baz] } end with_plain_table do - before :all do + before do adapter.add_index table, %i[foo bar], name: 'foobar_index' - adapter.add_index table, [:test], name: 'test_index' + adapter.add_index table, [:test], name: 'test_index' adapter.add_index table, :baz adapter.remove_index table, name: 'test_index' adapter.remove_index table, :baz end - it { is_expected.to_not have_temporal_index 'test_index', %w[test] } - it { is_expected.to_not have_history_index 'test_index', %w[test] } - it { is_expected.to_not have_index 'test_index', %w[test] } + it { is_expected.not_to have_temporal_index 'test_index', %w[test] } + it { is_expected.not_to have_history_index 'test_index', %w[test] } + it { is_expected.not_to have_index 'test_index', %w[test] } - it { is_expected.to_not have_temporal_index 'index_test_table_on_baz', %w[baz] } - it { is_expected.to_not have_history_index 'index_test_table_on_baz', %w[baz] } - it { is_expected.to_not have_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_temporal_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_history_index 'index_test_table_on_baz', %w[baz] } + it { is_expected.not_to have_index 'index_test_table_on_baz', %w[baz] } end end @@ -291,7 +299,7 @@ let(:extra_columns) { [%w[foobarbaz integer]] } with_temporal_table do - before :all do + before do adapter.add_column table, :foobarbaz, :integer end @@ -301,7 +309,7 @@ end with_plain_table do - before :all do + before do adapter.add_column table, :foobarbaz, :integer end @@ -309,7 +317,7 @@ end with_temporal_table do - before :all do + before do adapter.add_column table, :foobarbaz, :integer, default: 0 end @@ -319,7 +327,7 @@ end with_plain_table do - before :all do + before do adapter.add_column table, :foobarbaz, :integer, default: 0 end @@ -328,10 +336,10 @@ end describe '.remove_column' do - let(:resulting_columns) { columns.reject { |c,_| c == 'foo' } } + let(:resulting_columns) { columns.reject { |c, _| c == 'foo' } } with_temporal_table do - before :all do + before do adapter.remove_column table, :foo, :integer, default: 0 end @@ -339,30 +347,48 @@ it { is_expected.to have_temporal_columns(resulting_columns) } it { is_expected.to have_history_columns(resulting_columns) } - it { is_expected.to_not have_columns([%w[foo integer]]) } - it { is_expected.to_not have_temporal_columns([%w[foo integer]]) } - it { is_expected.to_not have_history_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } + it { is_expected.not_to have_temporal_columns([%w[foo integer]]) } + it { is_expected.not_to have_history_columns([%w[foo integer]]) } end with_plain_table do - before :all do + before do adapter.remove_column table, :foo, :integer, default: 0 end it { is_expected.to have_columns(resulting_columns) } - it { is_expected.to_not have_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } + end + + with_temporal_table do + before do + adapter.remove_column table, :foo + end + + it { is_expected.not_to have_columns([%w[foo integer]]) } + it { is_expected.not_to have_temporal_columns([%w[foo integer]]) } + it { is_expected.not_to have_history_columns([%w[foo integer]]) } + end + + with_plain_table do + before do + adapter.remove_column table, :foo + end + + it { is_expected.not_to have_columns([%w[foo integer]]) } end end describe '.rename_column' do with_temporal_table do - before :all do + before do adapter.rename_column table, :foo, :taratapiatapioca end - it { is_expected.to_not have_columns([%w[foo integer]]) } - it { is_expected.to_not have_temporal_columns([%w[foo integer]]) } - it { is_expected.to_not have_history_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } + it { is_expected.not_to have_temporal_columns([%w[foo integer]]) } + it { is_expected.not_to have_history_columns([%w[foo integer]]) } it { is_expected.to have_columns([%w[taratapiatapioca integer]]) } it { is_expected.to have_temporal_columns([%w[taratapiatapioca integer]]) } @@ -370,65 +396,46 @@ end with_plain_table do - before :all do + before do adapter.rename_column table, :foo, :taratapiatapioca end - it { is_expected.to_not have_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } it { is_expected.to have_columns([%w[taratapiatapioca integer]]) } end end describe '.change_column' do with_temporal_table do - before :all do + before do adapter.change_column table, :foo, :float end - it { is_expected.to_not have_columns([%w[foo integer]]) } - it { is_expected.to_not have_temporal_columns([%w[foo integer]]) } - it { is_expected.to_not have_history_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } + it { is_expected.not_to have_temporal_columns([%w[foo integer]]) } + it { is_expected.not_to have_history_columns([%w[foo integer]]) } it { is_expected.to have_columns([['foo', 'double precision']]) } it { is_expected.to have_temporal_columns([['foo', 'double precision']]) } it { is_expected.to have_history_columns([['foo', 'double precision']]) } context 'with options' do - before :all do + before do adapter.change_column table, :foo, :float, default: 0 end - it { is_expected.to_not have_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } end end with_plain_table do - before(:all) do + before do adapter.change_column table, :foo, :float end - it { is_expected.to_not have_columns([%w[foo integer]]) } + it { is_expected.not_to have_columns([%w[foo integer]]) } it { is_expected.to have_columns([['foo', 'double precision']]) } end end - - describe '.remove_column' do - with_temporal_table do - before :all do - adapter.remove_column table, :foo - end - - it { is_expected.to_not have_columns([%w[foo integer]]) } - it { is_expected.to_not have_temporal_columns([%w[foo integer]]) } - it { is_expected.to_not have_history_columns([%w[foo integer]]) } - end - - with_plain_table do - before :all do - adapter.remove_column table, :foo - end - - it { is_expected.to_not have_columns([%w[foo integer]]) } - end - end end +# rubocop:enable RSpec/RepeatedExample,RSpec/ScatteredSetup diff --git a/spec/chrono_model/conversions_spec.rb b/spec/chrono_model/conversions_spec.rb index 0ecc4da8..c7289efe 100644 --- a/spec/chrono_model/conversions_spec.rb +++ b/spec/chrono_model/conversions_spec.rb @@ -1,37 +1,39 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe ChronoModel::Conversions do - describe 'string_to_utc_time' do - subject { described_class.string_to_utc_time(string) } + describe '.string_to_utc_time' do + subject(:string_to_utc_time) { described_class.string_to_utc_time(string) } - context 'given a valid UTC time string' do + context 'with a valid UTC time string' do let(:string) { '2017-02-06 09:46:31.129626' } it { is_expected.to be_a(Time) } - it { expect(subject.year).to eq 2017 } - it { expect(subject.month).to eq 2 } - it { expect(subject.day).to eq 6 } - it { expect(subject.hour).to eq 9 } - it { expect(subject.min).to eq 46 } - it { expect(subject.sec).to eq 31 } - it { expect(subject.usec).to eq 129_626 } # Ref Issue #32 + it { expect(string_to_utc_time.year).to eq 2017 } + it { expect(string_to_utc_time.month).to eq 2 } + it { expect(string_to_utc_time.day).to eq 6 } + it { expect(string_to_utc_time.hour).to eq 9 } + it { expect(string_to_utc_time.min).to eq 46 } + it { expect(string_to_utc_time.sec).to eq 31 } + it { expect(string_to_utc_time.usec).to eq 129_626 } # Ref Issue #32 end - context 'given a valid UTC string without least significant zeros' do + context 'with a valid UTC string without least significant zeros' do let(:string) { '2017-02-06 09:46:31.129' } it { is_expected.to be_a(Time) } - it { expect(subject.usec).to eq 129_000 } # Ref Issue #32 + it { expect(string_to_utc_time.usec).to eq 129_000 } # Ref Issue #32 end - context 'given an invalid UTC time string' do + context 'with an invalid UTC time string' do let(:string) { 'foobar' } - it { is_expected.to be(nil) } + it { is_expected.to be_nil } end end - describe 'time_to_utc_string' do + describe '.time_to_utc_string' do subject { described_class.time_to_utc_string(time) } let(:time) { Time.utc(1981, 4, 11, 2, 42, 10, 123_456) } diff --git a/spec/chrono_model/history_models_spec.rb b/spec/chrono_model/history_models_spec.rb index 56799219..d35b9ee9 100644 --- a/spec/chrono_model/history_models_spec.rb +++ b/spec/chrono_model/history_models_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' RSpec.describe ChronoModel do describe '.history_models' do - subject { ChronoModel.history_models } + subject(:history_models) { described_class.history_models } it 'tracks recorded history models' do expected = {} @@ -16,7 +18,7 @@ expected['noos'] = Noo::History if defined?(Noo::History) expected['sub_bars'] = SubBar::History if defined?(SubBar::History) - expected['sub_sub_bars'] = SubSubBar::History if defined?(SubSubBar::History) + expected['sub_sub_bars'] = SubSubBar::History if defined?(SubSubBar::History) # default_scope_spec expected['defoos'] = Defoo::History if defined?(Defoo::History) @@ -29,9 +31,9 @@ expected['animals'] = Animal::History if defined?(Animal::History) expected['elements'] = Element::History if defined?(Element::History) - is_expected.to eq(expected) + expect(history_models).to eq(expected) end - it { expect(subject.size).to be > 0 } + it { is_expected.to be_present } end end diff --git a/spec/chrono_model/json_ops_spec.rb b/spec/chrono_model/json_ops_spec.rb index 2df6ccc4..7d66573c 100644 --- a/spec/chrono_model/json_ops_spec.rb +++ b/spec/chrono_model/json_ops_spec.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + ########################################################## ### DEPRECATED: JSON operators are an hack and there is no ### reason not to use jsonb other than migrating your data ########################################################## if ENV['HAVE_PLPYTHON'] == '1' - require 'spec_helper' require 'support/adapter/helpers' @@ -14,43 +15,53 @@ table 'json_test' - before :all do + before do ChronoModel::Json.create end - after :all do + after do ChronoModel::Json.drop end - it { expect(adapter.select_value(%( SELECT '{"a":1}'::json = '{"a":1}'::json ))).to eq true } - it { expect(adapter.select_value(%( SELECT '{"a":1}'::json = '{"a" : 1}'::json ))).to eq true } - it { expect(adapter.select_value(%( SELECT '{"a":1}'::json = '{"a":2}'::json ))).to eq false } - it { expect(adapter.select_value(%( SELECT '{"a":1,"b":2}'::json = '{"b":2,"a":1}'::json ))).to eq true } - it { expect(adapter.select_value(%( SELECT '{"a":1,"b":2,"x":{"c":4,"d":5}}'::json = '{"b":2, "x": { "d": 5, "c": 4}, "a":1}'::json ))).to eq true } + it { expect(adapter.select_value(%(SELECT '{"a":1}'::json = '{"a":1}'::json))).to be true } + it { expect(adapter.select_value(%(SELECT '{"a":1}'::json = '{"a" : 1}'::json))).to be true } + it { expect(adapter.select_value(%(SELECT '{"a":1}'::json = '{"a":2}'::json))).to be false } + it { expect(adapter.select_value(%(SELECT '{"a":1,"b":2}'::json = '{"b":2,"a":1}'::json))).to be true } - context 'on a temporal table' do - before :all do + it { + expect(adapter.select_value(%(SELECT '{"a":1,"b":2,"x":{"c":4,"d":5}}'::json = '{"b":2, "x": { "d": 5, "c": 4}, "a":1}'::json))).to be true + } + + context 'with a temporal table' do + before do adapter.create_table table, temporal: true do |t| t.json 'data' end - adapter.execute %[ - INSERT INTO #{table} ( data ) VALUES ( '{"a":1,"b":2}' ) - ] + adapter.execute <<-SQL.squish + INSERT INTO #{table} ( data ) VALUES ( '{"a":1,"b":2}' ) + SQL end - after :all do + after do adapter.drop_table table end - it { expect { - adapter.execute "UPDATE #{table} SET data = NULL" - }.to_not raise_error } + it { + expect do + adapter.execute <<-SQL.squish + UPDATE #{table} SET data = NULL + SQL + end.not_to raise_error + } - it { expect { - adapter.execute %(UPDATE #{table} SET data = '{"x":1,"y":2}') - }.to_not raise_error } + it { + expect do + adapter.execute <<-SQL.squish + UPDATE #{table} SET data = '{"x":1,"y":2}' + SQL + end.not_to raise_error + } end end - end diff --git a/spec/chrono_model/time_machine/as_of_spec.rb b/spec/chrono_model/time_machine/as_of_spec.rb index e8501c2d..0d00099f 100644 --- a/spec/chrono_model/time_machine/as_of_spec.rb +++ b/spec/chrono_model/time_machine/as_of_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -20,28 +22,27 @@ it { expect(Foo.as_of($t.foos[0].ts[0]).first).to be_a(Foo) } it { expect(Bar.as_of($t.foos[0].ts[0]).first).to be_a(Bar) } - # Associations - context do - subject { $t.foos[0].id } + context 'with associations' do + let(:id) { $t.foos[0].id } - it { expect(Foo.as_of($t.foos[0].ts[0]).find(subject).bars).to eq [] } - it { expect(Foo.as_of($t.foos[1].ts[0]).find(subject).bars).to eq [] } - it { expect(Foo.as_of($t.bars[0].ts[0]).find(subject).bars).to eq [$t.bars[0]] } - it { expect(Foo.as_of($t.bars[1].ts[0]).find(subject).bars).to eq [$t.bars[0]] } - it { expect(Foo.as_of(Time.now).find(subject).bars).to eq [$t.bars[0]] } + it { expect(Foo.as_of($t.foos[0].ts[0]).find(id).bars).to eq [] } + it { expect(Foo.as_of($t.foos[1].ts[0]).find(id).bars).to eq [] } + it { expect(Foo.as_of($t.bars[0].ts[0]).find(id).bars).to eq [$t.bars[0]] } + it { expect(Foo.as_of($t.bars[1].ts[0]).find(id).bars).to eq [$t.bars[0]] } + it { expect(Foo.as_of(Time.now).find(id).bars).to eq [$t.bars[0]] } - it { expect(Foo.as_of($t.bars[0].ts[0]).find(subject).bars.first).to be_a(Bar) } + it { expect(Foo.as_of($t.bars[0].ts[0]).find(id).bars.first).to be_a(Bar) } end - context do - subject { $t.foos[1].id } + context 'with association at a different timestamp' do + let(:id) { $t.foos[1].id } - it { expect { Foo.as_of($t.foos[0].ts[0]).find(subject) }.to raise_error(ActiveRecord::RecordNotFound) } - it { expect { Foo.as_of($t.foos[1].ts[0]).find(subject) }.to_not raise_error } + it { expect { Foo.as_of($t.foos[0].ts[0]).find(id) }.to raise_error(ActiveRecord::RecordNotFound) } + it { expect { Foo.as_of($t.foos[1].ts[0]).find(id) }.not_to raise_error } - it { expect(Foo.as_of($t.bars[0].ts[0]).find(subject).bars).to eq [] } - it { expect(Foo.as_of($t.bars[1].ts[0]).find(subject).bars).to eq [$t.bars[1]] } - it { expect(Foo.as_of(Time.now).find(subject).bars).to eq [$t.bars[1]] } + it { expect(Foo.as_of($t.bars[0].ts[0]).find(id).bars).to eq [] } + it { expect(Foo.as_of($t.bars[1].ts[0]).find(id).bars).to eq [$t.bars[1]] } + it { expect(Foo.as_of(Time.now).find(id).bars).to eq [$t.bars[1]] } end end @@ -140,10 +141,10 @@ it { expect(Foo.as_of($t.foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 0 } it { expect(Foo.as_of($t.foo.ts[2]).includes(:bars, :sub_bars).first.sub_bars.count).to eq 1 } - it { expect(Foo.as_of($t.foo.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first).to be nil } - it { expect(Foo.as_of($t.foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.first).to be nil } + it { expect(Foo.as_of($t.foo.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first).to be_nil } + it { expect(Foo.as_of($t.foo.ts[1]).includes(:bars, :sub_bars).first.sub_bars.first).to be_nil } - it { expect(Foo.as_of($t.bar.ts[0]).includes(:sub_bars).first.bars.first.sub_bars.first).to be nil } + it { expect(Foo.as_of($t.bar.ts[0]).includes(:sub_bars).first.bars.first.sub_bars.first).to be_nil } it { expect(Foo.as_of($t.subbar.ts[0]).includes(:sub_bars).first.bars.first.sub_bars.first.name).to eq 'sub-bar' } it { expect(Foo.as_of($t.subbar.ts[0]).includes(:bars, :sub_bars).first.sub_bars.first.name).to eq 'sub-bar' } @@ -155,9 +156,9 @@ end it 'does not raise RecordNotFound when no history records are found' do - expect { $t.foo.as_of(5.minutes.ago) }.to_not raise_error + expect { $t.foo.as_of(5.minutes.ago) }.not_to raise_error - expect($t.foo.as_of(5.minutes.ago)).to be(nil) + expect($t.foo.as_of(5.minutes.ago)).to be_nil end it 'raises ActiveRecord::RecordNotFound in the bang variant' do diff --git a/spec/chrono_model/time_machine/batches_spec.rb b/spec/chrono_model/time_machine/batches_spec.rb index 2efcbad6..b44ceb43 100644 --- a/spec/chrono_model/time_machine/batches_spec.rb +++ b/spec/chrono_model/time_machine/batches_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -5,20 +7,20 @@ include ChronoTest::TimeMachine::Helpers describe '.in_batches' do - let(:foo_in_batches_of_two) { + let(:foo_in_batches_of_two) do [ ['new foo', 'foo 0'], ['foo 1'] ] - } + end - let(:foo_history_in_batches_of_two) { + let(:foo_history_in_batches_of_two) do [ ['foo', 'foo bar'], ['new foo', 'foo 0'], ['foo 1'] ] - } + end it { expect(Foo.in_batches(of: 2).map { |g| g.map(&:name) }).to eq foo_in_batches_of_two } it { expect(Foo.history.in_batches(of: 2).map { |g| g.map(&:name) }).to eq foo_history_in_batches_of_two } diff --git a/spec/chrono_model/time_machine/changes_spec.rb b/spec/chrono_model/time_machine/changes_spec.rb index f6d91d46..92bff247 100644 --- a/spec/chrono_model/time_machine/changes_spec.rb +++ b/spec/chrono_model/time_machine/changes_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -5,45 +7,51 @@ include ChronoTest::TimeMachine::Helpers describe '#last_changes' do - context 'on plain records' do - context 'having history' do + context 'with plain records' do + context 'with history' do subject { $t.bar.last_changes } + it { is_expected.to eq('name' => ['bar bar', 'new bar']) } end context 'without history' do - let(:record) { Bar.create!(name: 'foreveralone') } subject { record.last_changes } - it { is_expected.to be_nil } + + let(:record) { Bar.create!(name: 'foreveralone') } + after { record.destroy.history.delete_all } # UGLY + + it { is_expected.to be_nil } end end - context 'on history records' do - context 'at the beginning of the timeline' do + context 'with historical records' do + context 'when at the beginning of the timeline' do subject { $t.bar.history.first.last_changes } + it { is_expected.to be_nil } end - context 'in the middle of the timeline' do + context 'when in the middle of the timeline' do subject { $t.bar.history.second.last_changes } + it { is_expected.to eq('name' => ['bar', 'foo bar']) } end end end describe '#changes_against' do - context 'can compare records against history' do + context 'when comparing records against history' do it { expect($t.bar.changes_against($t.bar.history.first)).to eq('name' => ['bar', 'new bar']) } it { expect($t.bar.changes_against($t.bar.history.second)).to eq('name' => ['foo bar', 'new bar']) } it { expect($t.bar.changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'new bar']) } it { expect($t.bar.changes_against($t.bar.history.last)).to eq({}) } end - context 'can compare history against history' do - it { expect($t.bar.history.first. changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'bar']) } + context 'when comparing history against history' do + it { expect($t.bar.history.first.changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'bar']) } it { expect($t.bar.history.second.changes_against($t.bar.history.third)).to eq('name' => ['bar bar', 'foo bar']) } - it { expect($t.bar.history.third. changes_against($t.bar.history.third)).to eq({}) } + it { expect($t.bar.history.third.changes_against($t.bar.history.third)).to eq({}) } end end end diff --git a/spec/chrono_model/time_machine/counter_cache_race_spec.rb b/spec/chrono_model/time_machine/counter_cache_race_spec.rb index 0a7e899a..37c725ed 100644 --- a/spec/chrono_model/time_machine/counter_cache_race_spec.rb +++ b/spec/chrono_model/time_machine/counter_cache_race_spec.rb @@ -1,33 +1,35 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' -RSpec.describe 'models with counter cache' do - include ChronoTest::TimeMachine::Helpers +class Section < ActiveRecord::Base + include ChronoModel::TimeMachine - adapter.create_table 'sections', temporal: true, no_journal: %w[articles_count] do |t| - t.string :name - t.integer :articles_count, default: 0 - end + has_many :articles +end - adapter.create_table 'articles', temporal: true do |t| - t.string :title - t.references :section - end +class Article < ActiveRecord::Base + include ChronoModel::TimeMachine - class ::Section < ActiveRecord::Base - include ChronoModel::TimeMachine + belongs_to :section, counter_cache: true +end - has_many :articles - end +RSpec.describe ChronoModel::TimeMachine do + include ChronoTest::TimeMachine::Helpers - class ::Article < ActiveRecord::Base - include ChronoModel::TimeMachine + context 'when models have a counter cache' do + adapter.create_table 'sections', temporal: true, no_journal: %w[articles_count] do |t| + t.string :name + t.integer :articles_count, default: 0 + end - belongs_to :section, counter_cache: true - end + adapter.create_table 'articles', temporal: true do |t| + t.string :title + t.references :section + end - describe 'are not subject to race condition if no_journal is set on the counter cache column' do - specify do + it 'is not subject to race condition if no_journal is set on the counter cache column' do section = Section.create! expect(section.articles_count).to eq(0) @@ -36,11 +38,11 @@ class ::Article < ActiveRecord::Base num_threads = 10 - expect { + expect do Array.new(num_threads).map do Thread.new { Article.create!(section_id: section.id) } end.each(&:join) - }.to_not raise_error + end.not_to raise_error end end end diff --git a/spec/chrono_model/time_machine/default_scope_spec.rb b/spec/chrono_model/time_machine/default_scope_spec.rb index 8e7bb60f..9fd57dba 100644 --- a/spec/chrono_model/time_machine/default_scope_spec.rb +++ b/spec/chrono_model/time_machine/default_scope_spec.rb @@ -1,6 +1,14 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' +class Defoo < ActiveRecord::Base + include ChronoModel::TimeMachine + + default_scope proc { where(active: true) } +end + RSpec.describe ChronoModel::TimeMachine do include ChronoTest::TimeMachine::Helpers @@ -11,12 +19,6 @@ t.boolean :active end - class ::Defoo < ActiveRecord::Base - include ChronoModel::TimeMachine - - default_scope proc { where(active: true) } - end - active = ts_eval { Defoo.create! name: 'active 1', active: true } ts_eval(active) { update! name: 'active 2' } diff --git a/spec/chrono_model/time_machine/delegate_missing_to_spec.rb b/spec/chrono_model/time_machine/delegate_missing_to_spec.rb index 18560718..8946e8de 100644 --- a/spec/chrono_model/time_machine/delegate_missing_to_spec.rb +++ b/spec/chrono_model/time_machine/delegate_missing_to_spec.rb @@ -1,7 +1,18 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' if Rails.version >= '5.1' + class Attachment < ActiveRecord::Base + belongs_to :blob + delegate_missing_to :blob + end + + class Blob < ActiveRecord::Base + has_many :attachments + end + RSpec.describe 'delegate_missing_to' do include ChronoTest::TimeMachine::Helpers @@ -13,21 +24,12 @@ t.string :name end - class ::Attachment < ActiveRecord::Base - belongs_to :blob - delegate_missing_to :blob - end - - class ::Blob < ActiveRecord::Base - has_many :attachments - end - let(:attachment) { Attachment.create!(blob: Blob.create!(name: 'test')).reload } it 'does not raise errors' do - expect { + expect do attachment.blob - }.not_to raise_error + end.not_to raise_error end it 'allows delegation to associated models' do diff --git a/spec/chrono_model/time_machine/history_spec.rb b/spec/chrono_model/time_machine/history_spec.rb index 6016e9d3..63de4b4a 100644 --- a/spec/chrono_model/time_machine/history_spec.rb +++ b/spec/chrono_model/time_machine/history_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -5,13 +7,13 @@ include ChronoTest::TimeMachine::Helpers describe '.history' do - let(:foo_history) { + let(:foo_history) do ['foo', 'foo bar', 'new foo', 'foo 0', 'foo 1'] - } + end - let(:bar_history) { + let(:bar_history) do ['bar', 'foo bar', 'bar bar', 'new bar', 'bar 0', 'bar 1'] - } + end it { expect(Foo.history.all.map(&:name)).to eq foo_history } it { expect(Bar.history.all.map(&:name)).to eq bar_history } @@ -48,12 +50,13 @@ describe 'takes care of associated records' do subject { $t.foo.history.map { |f| f.bars.first.try(:name) } } + it { is_expected.to eq [nil, 'foo bar', 'new bar'] } end describe 'does not return read only associated records' do - it { expect($t.foo.history[2].bars.all?(&:readonly?)).to_not be(true) } - it { expect($t.bar.history.all? { |b| b.foo.readonly? }).to_not be(true) } + it { expect($t.foo.history[2].bars.all?(&:readonly?)).not_to be(true) } + it { expect($t.bar.history.all? { |b| b.foo.readonly? }).not_to be(true) } end describe 'allows a custom select list' do @@ -62,9 +65,12 @@ end describe 'does not add as_of_time when there are aggregates' do - it { expect($t.foo.history.select('max(id)').to_sql).to_not match(/as_of_time/) } + it { expect($t.foo.history.select('max(id)').to_sql).not_to match(/as_of_time/) } - it { expect($t.foo.history.reorder('id').select('max(id) as foo, min(id) as bar').group('id').first.attributes.keys).to match_array %w[hid foo bar] } + it { + expect($t.foo.history.reorder('id').select('max(id) as foo, min(id) as bar').group('id').first.attributes.keys).to match_array %w[hid foo + bar] + } end context 'when finding historical elements by hid' do @@ -88,7 +94,7 @@ end end - context '.sorted' do + describe '.sorted' do describe 'orders by recorded_at, hid' do it { expect($t.foo.history.sorted.to_sql).to match(/order by .+"recorded_at" ASC, .+"hid" ASC/i) } end @@ -98,16 +104,19 @@ describe '#current_version' do describe 'on plain records' do subject { $t.foo.current_version } + it { is_expected.to eq $t.foo } end describe 'from #as_of' do subject { $t.foo.as_of(Time.now) } + it { is_expected.to eq $t.foo } end describe 'on historical records' do subject { $t.foo.history.sample.current_version } + it { is_expected.to eq $t.foo } end end @@ -117,17 +126,20 @@ describe 'on plain records' do let(:record) { $t.foo } + it { is_expected.to be(false) } end describe 'on historical records' do describe 'from #history' do let(:record) { $t.foo.history.first } + it { is_expected.to be(true) } end describe 'from #as_of' do let(:record) { $t.foo.as_of(Time.now) } + it { is_expected.to be(true) } end end diff --git a/spec/chrono_model/time_machine/keep_cool_spec.rb b/spec/chrono_model/time_machine/keep_cool_spec.rb index a7d0412b..dbe24cb5 100644 --- a/spec/chrono_model/time_machine/keep_cool_spec.rb +++ b/spec/chrono_model/time_machine/keep_cool_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -13,7 +15,7 @@ it { expect(Foo.includes(bars: :sub_bars)).to eq all_foos } it { expect(Foo.includes(:bars).preload(bars: :sub_bars)).to eq all_foos } - it { expect(Foo.includes(:bars).preload(bars: :sub_bars).as_of(Time.new)).to eq all_foos } + it { expect(Foo.includes(:bars).preload(bars: :sub_bars).as_of(Time.now)).to eq all_foos } it { expect(Foo.includes(:bars).first.name).to eq 'new foo' } it { expect(Foo.includes(:bars).as_of($t.foo.ts[0]).first.name).to eq 'foo' } @@ -26,6 +28,6 @@ it { expect(Foo.first.bars.includes(:sub_bars)).to eq [$t.bar] } it { expect(Moo.first.boos).to eq(Boo.all) } - it { expect(Moo.first.boos.as_of(Time.new)).to eq(Boo.all.as_of(Time.new)) } + it { expect(Moo.first.boos.as_of(Time.now)).to eq(Boo.all.as_of(Time.now)) } end end diff --git a/spec/chrono_model/time_machine/manipulations_spec.rb b/spec/chrono_model/time_machine/manipulations_spec.rb index 470ec312..b5ba533f 100644 --- a/spec/chrono_model/time_machine/manipulations_spec.rb +++ b/spec/chrono_model/time_machine/manipulations_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -5,99 +7,108 @@ include ChronoTest::TimeMachine::Helpers describe '#save' do - subject { $t.bar.history.first } - + let(:historical_object) { $t.bar.history.first } let(:another_historical_object) { $t.bar.history.second } it do with_revert do - subject.name = 'modified bar history' - subject.save - subject.reload + historical_object.name = 'modified bar history' + historical_object.save + historical_object.reload - is_expected.to be_a(Bar::History) + expect(historical_object).to be_a(Bar::History) expect(another_historical_object.name).not_to eq 'modified bar history' - expect(subject.name).to eq 'modified bar history' + expect(historical_object.name).to eq 'modified bar history' end end end describe '#save!' do - subject { $t.bar.history.second } - let(:first_historical_object) { $t.bar.history.first } + let(:second_historical_object) { $t.bar.history.second } it do with_revert do - subject.name = 'another modified bar history' - subject.save! - subject.reload + second_historical_object.name = 'another modified bar history' + second_historical_object.save! + second_historical_object.reload - is_expected.to be_a(Bar::History) + expect(second_historical_object).to be_a(Bar::History) expect(first_historical_object.name).not_to eq 'another modified bar history' - expect(subject.name).to eq 'another modified bar history' + expect(second_historical_object.name).to eq 'another modified bar history' end end end describe '#update_columns' do - subject { $t.bar.history.first } - + let(:historical_object) { $t.bar.history.first } let(:another_historical_object) { $t.bar.history.second } it do with_revert do - subject.update_columns name: 'another modified bar history' - subject.reload + historical_object.update_columns name: 'another modified bar history' + historical_object.reload - is_expected.to be_a(Bar::History) + expect(historical_object).to be_a(Bar::History) expect(another_historical_object.name).not_to eq 'another modified bar history' - expect(subject.name).to eq 'another modified bar history' + expect(historical_object.name).to eq 'another modified bar history' end end end describe '#destroy' do describe 'on historical records' do - subject { $t.foo.history.first.destroy } - it { expect { subject }.to raise_error(ActiveRecord::ReadOnlyRecord) } + subject(:destroy) { $t.foo.history.first.destroy } + + it { expect { destroy }.to raise_error(ActiveRecord::ReadOnlyRecord) } end describe 'on current records' do rec = nil - before(:all) do + subject(:destroy) { rec.destroy } + + before do rec = ts_eval { Foo.create!(name: 'alive foo', fooity: 42) } ts_eval(rec) { update!(name: 'dying foo') } end - after(:all) do + + after do + rec.destroy rec.history.delete_all end - subject { rec.destroy } + it { expect { destroy }.not_to raise_error } - it { expect { subject }.to_not raise_error } - it { expect { rec.reload }.to raise_error(ActiveRecord::RecordNotFound) } + it 'raises an error when record is reloaded' do + destroy + + expect { rec.reload }.to raise_error(ActiveRecord::RecordNotFound) + end describe 'does not delete its history' do subject { record.name } - context do + context 'with first timestamp' do let(:record) { rec.as_of(rec.ts.first) } + it { is_expected.to eq 'alive foo' } end - context do + context 'with last timestamp' do let(:record) { rec.as_of(rec.ts.last) } + it { is_expected.to eq 'dying foo' } end - context do + context 'when searching with as_of' do let(:record) { Foo.as_of(rec.ts.first).where(fooity: 42).first } + it { is_expected.to eq 'alive foo' } end - context do + context 'when searching in history' do subject { Foo.history.where(fooity: 42).map(&:name) } + it { is_expected.to eq ['alive foo', 'dying foo'] } end end diff --git a/spec/chrono_model/time_machine/model_identification_spec.rb b/spec/chrono_model/time_machine/model_identification_spec.rb index 7168c027..3bc65529 100644 --- a/spec/chrono_model/time_machine/model_identification_spec.rb +++ b/spec/chrono_model/time_machine/model_identification_spec.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' +class Plain < ActiveRecord::Base; end + RSpec.describe ChronoModel::TimeMachine do include ChronoTest::TimeMachine::Helpers @@ -8,39 +12,41 @@ t.string :foo end - class ::Plain < ActiveRecord::Base - end - describe '.chrono?' do subject { model.chrono? } - context 'on a temporal model' do + context 'with a temporal model' do let(:model) { Foo } + it { is_expected.to be(true) } end - context 'on a plain model' do + context 'with a plain model' do let(:model) { Plain } + it { is_expected.to be(false) } end end describe '.history?' do - subject { model.history? } + subject(:history?) { model.history? } - context 'on a temporal parent model' do + context 'with a temporal parent model' do let(:model) { Foo } + it { is_expected.to be(false) } end - context 'on a temporal history model' do + context 'with a temporal history model' do let(:model) { Foo::History } + it { is_expected.to be(true) } end - context 'on a plain model' do + context 'with a plain model' do let(:model) { Plain } - it { expect { subject }.to raise_error(NoMethodError) } + + it { expect { history? }.to raise_error(NoMethodError) } end end end diff --git a/spec/chrono_model/time_machine/sequence_spec.rb b/spec/chrono_model/time_machine/sequence_spec.rb index b698e024..e76fd565 100644 --- a/spec/chrono_model/time_machine/sequence_spec.rb +++ b/spec/chrono_model/time_machine/sequence_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -5,74 +7,82 @@ include ChronoTest::TimeMachine::Helpers describe '#pred' do - context 'on the first history entry' do + context 'with the first history entry' do subject { $t.foo.history.first.pred } - it { is_expected.to be(nil) } + + it { is_expected.to be_nil } end - context 'on the second history entry' do + context 'with the second history entry' do subject { $t.foo.history.second.pred } + it { is_expected.to eq $t.foo.history.first } end - context 'on the last history entry' do + context 'with the last history entry' do subject { $t.foo.history.last.pred } + it { is_expected.to eq $t.foo.history[$t.foo.history.size - 2] } end context 'with current records without an associated timeline' do subject { $t.noo.pred } + it { is_expected.to eq $t.noo.history.first } end - context 'on records having history' do - subject { $t.bar.pred } - it { expect(subject.name).to eq 'bar bar' } + context 'with records having history' do + subject(:pred) { $t.bar.pred } + + it { expect(pred.name).to eq 'bar bar' } end context 'when there is enough history' do - subject { $t.bar.pred.pred.pred.pred } - it { expect(subject.name).to eq 'bar' } + subject(:pred) { $t.bar.pred.pred.pred.pred } + + it { expect(pred.name).to eq 'bar' } end context 'when no history is recorded' do - let(:record) { Bar.create!(name: 'quuuux') } - subject { record.pred } - it { is_expected.to be(nil) } + let(:record) { Bar.create!(name: 'quuuux') } after { record.destroy.history.delete_all } + + it { is_expected.to be_nil } end end describe '#succ' do - context 'on the first history entry' do + context 'with the first history entry' do subject { $t.foo.history.first.succ } it { is_expected.to eq $t.foo.history.second } end - context 'on the second history entry' do + context 'with the second history entry' do subject { $t.foo.history.second.succ } it { is_expected.to eq $t.foo.history.third } end - context 'on the last history entry' do + context 'with the last history entry' do subject { $t.foo.history.last.succ } - it { is_expected.to be(nil) } + it { is_expected.to be_nil } end end describe '#first' do subject { $t.foo.history.sample.first } + it { is_expected.to eq $t.foo.history.first } end describe '#last' do subject { $t.foo.history.sample.last } + it { is_expected.to eq $t.foo.history.last } end end diff --git a/spec/chrono_model/time_machine/sti_spec.rb b/spec/chrono_model/time_machine/sti_spec.rb index d86817e6..c79048ce 100644 --- a/spec/chrono_model/time_machine/sti_spec.rb +++ b/spec/chrono_model/time_machine/sti_spec.rb @@ -1,129 +1,131 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' +class Animal < ActiveRecord::Base + include ChronoModel::TimeMachine +end + +class Dog < Animal; end + +class Goat < Animal; end + +class Element < ActiveRecord::Base + include ChronoModel::TimeMachine +end + +class Publication < Element; end + +class Magazine < Publication; end + # STI cases # # - https://github.com/ifad/chronomodel/issues/5 # - https://github.com/ifad/chronomodel/issues/47 # -RSpec.describe 'models with STI' do +RSpec.describe ChronoModel::TimeMachine do include ChronoTest::TimeMachine::Helpers - adapter.create_table 'elements', temporal: true do |t| - t.string :title - t.string :type - end - - class ::Element < ActiveRecord::Base - include ChronoModel::TimeMachine - end - - class ::Publication < ::Element - end - - class ::Magazine < ::Publication - end - - describe '.descendants' do - subject { Element.descendants.map(&:name) } - - it { is_expected.to match_array %w[Publication Magazine] } - end + context 'with STI' do + adapter.create_table 'elements', temporal: true do |t| + t.string :title + t.string :type + end - describe '.descendants_with_history' do - subject { Element.descendants_with_history.map(&:name) } + describe '.descendants' do + subject { Element.descendants.map(&:name) } - it { is_expected.to match_array %w[Element::History Publication Publication::History Magazine Magazine::History] } - end + it { is_expected.to match_array %w[Publication Magazine] } + end - if Element.respond_to?(:direct_descendants) - describe '.direct_descendants' do - subject { Element.direct_descendants.map(&:name) } + describe '.descendants_with_history' do + subject { Element.descendants_with_history.map(&:name) } - it { is_expected.to match_array %w[Publication] } + it { is_expected.to match_array %w[Element::History Publication Publication::History Magazine Magazine::History] } end - describe '.direct_descendants_with_history' do - subject { Element.direct_descendants_with_history.map(&:name) } + if Element.respond_to?(:direct_descendants) + describe '.direct_descendants' do + subject { Element.direct_descendants.map(&:name) } - it { is_expected.to match_array %w[Element::History Publication] } - end - end + it { is_expected.to match_array %w[Publication] } + end - if Rails.version >= '7.0' - describe '.subclasses' do - subject { Element.subclasses.map(&:name) } + describe '.direct_descendants_with_history' do + subject { Element.direct_descendants_with_history.map(&:name) } - it { is_expected.to match_array %w[Publication] } + it { is_expected.to match_array %w[Element::History Publication] } + end end - describe '.subclasses_with_history' do - subject { Element.subclasses_with_history.map(&:name) } + if Rails.version >= '7.0' + describe '.subclasses' do + subject { Element.subclasses.map(&:name) } - it { is_expected.to match_array %w[Element::History Publication] } - end - end + it { is_expected.to match_array %w[Publication] } + end - describe 'timeline' do - let(:publication) do - pub = ts_eval { Publication.create! title: 'wrong title' } - ts_eval(pub) { update! title: 'correct title' } + describe '.subclasses_with_history' do + subject { Element.subclasses_with_history.map(&:name) } - pub + it { is_expected.to match_array %w[Element::History Publication] } + end end - it { expect(publication.history.map(&:title)).to eq ['wrong title', 'correct title'] } - end + describe 'timeline' do + let(:publication) do + pub = ts_eval { Publication.create! title: 'wrong title' } + ts_eval(pub) { update! title: 'correct title' } - describe 'identity' do - adapter.create_table 'animals', temporal: true do |t| - t.string :type - end + pub + end - class ::Animal < ActiveRecord::Base - include ChronoModel::TimeMachine + it { expect(publication.history.map(&:title)).to eq ['wrong title', 'correct title'] } end - class ::Dog < Animal - end + describe 'identity' do + let(:timestamp) { Time.now } - class ::Goat < Animal - end + adapter.create_table 'animals', temporal: true do |t| + t.string :type + end - before do - Dog.create! - @later = Time.new - Goat.create! - end + before do + Dog.create! + timestamp + Goat.create! + end - after do - tables = ['temporal.animals', 'history.animals'] - ActiveRecord::Base.connection.execute "truncate #{tables.join(', ')} cascade" - end + after do + tables = ['temporal.animals', 'history.animals'] + ActiveRecord::Base.connection.execute "truncate #{tables.join(', ')} cascade" + end - specify "select" do - expect(Animal.first).to be_a(Animal) - expect(Animal.as_of(@later).first).to be_a(Animal) + specify 'select' do + expect(Animal.first).to be_a(Animal) + expect(Animal.as_of(timestamp).first).to be_a(Animal) - expect(Animal.where(type: 'Dog').first).to be_a(Dog) - expect(Dog.first).to be_a(Dog) - expect(Dog.as_of(@later).first).to be_a(Dog) + expect(Animal.where(type: 'Dog').first).to be_a(Dog) + expect(Dog.first).to be_a(Dog) + expect(Dog.as_of(timestamp).first).to be_a(Dog) - expect(Animal.where(type: 'Goat').first).to be_a(Goat) - expect(Goat.first).to be_a(Goat) - expect(Goat.as_of(@later).first).to be(nil) - expect(Goat.as_of(Time.now).first).to be_a(Goat) - end + expect(Animal.where(type: 'Goat').first).to be_a(Goat) + expect(Goat.first).to be_a(Goat) + expect(Goat.as_of(timestamp).first).to be_nil + expect(Goat.as_of(Time.now).first).to be_a(Goat) + end - specify "count" do - expect(Animal.count).to eq(2) - expect(Animal.as_of(@later).count).to eq(1) + specify 'count' do + expect(Animal.count).to eq(2) + expect(Animal.as_of(timestamp).count).to eq(1) - expect(Dog.count).to eq(1) - expect(Dog.as_of(@later).count).to eq(1) + expect(Dog.count).to eq(1) + expect(Dog.as_of(timestamp).count).to eq(1) - expect(Goat.count).to eq(1) - expect(Goat.as_of(@later).count).to eq(0) + expect(Goat.count).to eq(1) + expect(Goat.as_of(timestamp).count).to eq(0) + end end end end diff --git a/spec/chrono_model/time_machine/time_query_spec.rb b/spec/chrono_model/time_machine/time_query_spec.rb index 6bd155bf..813278bd 100644 --- a/spec/chrono_model/time_machine/time_query_spec.rb +++ b/spec/chrono_model/time_machine/time_query_spec.rb @@ -1,6 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' +class Event < ActiveRecord::Base + extend ChronoModel::TimeMachine::TimeQuery +end + RSpec.describe ChronoModel::TimeMachine::TimeQuery do include ChronoTest::TimeMachine::Helpers @@ -9,10 +15,6 @@ t.daterange :interval end - class ::Event < ActiveRecord::Base - extend ChronoModel::TimeMachine::TimeQuery - end - # Main timeline quick test # it { expect(Foo.history.time_query(:after, :now, inclusive: true).count).to eq 3 } @@ -33,42 +35,49 @@ class ::Event < ActiveRecord::Base build = Event.create! name: 'build', interval: (7.days.ago.to_date...Date.yesterday) profit = Event.create! name: 'profit', interval: (Date.tomorrow...1.year.from_now.to_date) - describe :at do - describe 'with a single timestamp' do + describe 'at' do + context 'with a single timestamp' do subject { Event.time_query(:at, time.try(:to_date) || time, on: :interval).to_a } - context 'no records' do + context 'without records' do let(:time) { 16.days.ago } + it { is_expected.to be_empty } end - context 'single record' do + context 'with a single record' do let(:time) { 15.days.ago } + it { is_expected.to eq [think] } end - context 'multiple overlapping records' do + context 'with multiple overlapping records' do let(:time) { 14.days.ago } - it { is_expected.to match_array [think, plan] } + + it { is_expected.to contain_exactly(think, plan) } end - context 'on an edge of an open interval' do + context 'when on an edge of an open interval' do let(:time) { 10.days.ago } + it { is_expected.to be_empty } end - context 'in an hole' do + context 'with a hole' do let(:time) { 9.days.ago } + it { is_expected.to be_empty } end - context 'today' do + context 'when today' do let(:time) { Date.today } + it { is_expected.to be_empty } end - context 'server-side :today' do + context 'when server-side :today' do let(:time) { :today } + it { is_expected.to be_empty } end end @@ -76,183 +85,217 @@ class ::Event < ActiveRecord::Base describe 'with a range' do subject { Event.time_query(:at, times.map!(&:to_date), on: :interval, type: :daterange).to_a } - context 'that is empty' do + context 'with an empty range' do let(:times) { [14.days.ago, 14.days.ago] } - it { is_expected.to_not be_empty } + + it { is_expected.not_to be_empty } end - context 'overlapping no records' do + context 'when range has no records' do let(:times) { [20.days.ago, 16.days.ago] } + it { is_expected.to be_empty } end - context 'overlapping a single record' do + context 'when range has a single record' do let(:times) { [16.days.ago, 14.days.ago] } + it { is_expected.to eq [think] } end - context 'overlapping more records' do + context 'when range has multiple records' do let(:times) { [16.days.ago, 11.days.ago] } - it { is_expected.to match_array [think, plan, collect] } + + it { is_expected.to contain_exactly(think, plan, collect) } end - context 'on the edge of an open interval and an hole' do + context 'when on the edge of an open interval and a hole' do let(:times) { [10.days.ago, 9.days.ago] } + it { is_expected.to be_empty } end end end - describe :before do - let(:inclusive) { true } + describe 'before' do subject { Event.time_query(:before, time.try(:to_date) || time, on: :interval, type: :daterange, inclusive: inclusive).to_a } - context '16 days ago' do + let(:inclusive) { true } + + context 'when 16 days ago' do let(:time) { 16.days.ago } + it { is_expected.to be_empty } end - context '14 days ago' do + context 'when 14 days ago' do let(:time) { 14.days.ago } + it { is_expected.to eq [think] } - context 'not inclusive' do + context 'when not inclusive' do let(:inclusive) { false } + it { is_expected.to be_empty } end end - context '11 days ago' do + context 'when 11 days ago' do let(:time) { 11.days.ago } - it { is_expected.to match_array [think, plan, collect] } - context 'not inclusive' do + it { is_expected.to contain_exactly(think, plan, collect) } + + context 'when not inclusive' do let(:inclusive) { false } + it { is_expected.to eq [think, plan] } end end - context '10 days ago' do + context 'when 10 days ago' do let(:time) { 10.days.ago } - it { is_expected.to match_array [think, plan, collect] } + + it { is_expected.to contain_exactly(think, plan, collect) } end - context '8 days ago' do + context 'when 8 days ago' do let(:time) { 8.days.ago } - it { is_expected.to match_array [think, plan, collect] } + + it { is_expected.to contain_exactly(think, plan, collect) } end - context 'today' do + context 'when today' do let(:time) { Date.today } - it { is_expected.to match_array [think, plan, collect, start, build] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build) } end - context ':today' do + context 'when server-side :today' do let(:time) { :today } - it { is_expected.to match_array [think, plan, collect, start, build] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build) } end end - describe :after do - let(:inclusive) { true } + describe 'after' do subject { Event.time_query(:after, time.try(:to_date) || time, on: :interval, type: :daterange, inclusive: inclusive).to_a } - context 'one month ago' do + let(:inclusive) { true } + + context 'when one month ago' do let(:time) { 1.month.ago } - it { is_expected.to match_array [think, plan, collect, start, build, profit] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build, profit) } end - context '10 days ago' do + context 'when 10 days ago' do let(:time) { 10.days.ago } - it { is_expected.to match_array [start, build, profit] } + + it { is_expected.to contain_exactly(start, build, profit) } end - context 'yesterday' do + context 'when yesterday' do let(:time) { Date.yesterday } + it { is_expected.to eq [profit] } end - context 'today' do + context 'when today' do let(:time) { Date.today } + it { is_expected.to eq [profit] } end - context 'server-side :today' do + context 'when server-side :today' do let(:time) { :today } + it { is_expected.to eq [profit] } end - context 'tomorrow' do + context 'when tomorrow' do let(:time) { Date.tomorrow } + it { is_expected.to eq [profit] } end - context 'one month from now' do + context 'when one month from now' do let(:time) { 1.month.from_now } + it { is_expected.to eq [profit] } - context 'not inclusive' do + context 'when not inclusive' do let(:inclusive) { false } + it { is_expected.to be_empty } end end - context 'far future' do + context 'when far future' do let(:time) { 1.year.from_now } + it { is_expected.to be_empty } end end - describe :not do + describe 'not' do context 'with a single timestamp' do subject { Event.time_query(:not, time.try(:to_date) || time, on: :interval, type: :daterange).to_a } - context '14 days ago' do + context 'when 14 days ago' do let(:time) { 14.days.ago } - it { is_expected.to match_array [collect, start, build, profit] } + + it { is_expected.to contain_exactly(collect, start, build, profit) } end - context '9 days ago' do + context 'when 9 days ago' do let(:time) { 9.days.ago } - it { is_expected.to match_array [think, plan, collect, start, build, profit] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build, profit) } end - context '8 days ago' do + context 'when 8 days ago' do let(:time) { 8.days.ago } - it { is_expected.to match_array [think, plan, collect, build, profit] } + + it { is_expected.to contain_exactly(think, plan, collect, build, profit) } end - context 'today' do + context 'when today' do let(:time) { Date.today } - it { is_expected.to match_array [think, plan, collect, start, build, profit] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build, profit) } end - context ':today' do + context 'when server-side :today' do let(:time) { :today } - it { is_expected.to match_array [think, plan, collect, start, build, profit] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build, profit) } end - context '1 month from now' do + context 'when 1 month from now' do let(:time) { 1.month.from_now } - it { is_expected.to match_array [think, plan, collect, start, build] } + + it { is_expected.to contain_exactly(think, plan, collect, start, build) } end end context 'with a range' do subject { Event.time_query(:not, time.map(&:to_date), on: :interval, type: :daterange).to_a } - context 'eliminating a single record' do + context 'when eliminating a single record' do let(:time) { [1.month.ago, 14.days.ago] } - it { is_expected.to match_array [plan, collect, start, build, profit] } + + it { is_expected.to contain_exactly(plan, collect, start, build, profit) } end - context 'eliminating multiple records' do + context 'when eliminating multiple records' do let(:time) { [1.month.ago, Date.today] } + it { is_expected.to eq [profit] } end - context 'from an edge' do + context 'with an edge' do let(:time) { [14.days.ago, 10.days.ago] } + it { is_expected.to eq [start, build, profit] } end end diff --git a/spec/chrono_model/time_machine/timeline_spec.rb b/spec/chrono_model/time_machine/timeline_spec.rb index b71a7c63..02552228 100644 --- a/spec/chrono_model/time_machine/timeline_spec.rb +++ b/spec/chrono_model/time_machine/timeline_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -8,54 +10,50 @@ split = ->(ts) { ts.map! { |t| [t.to_i, t.usec] } } timestamps_from = lambda { |*records| - records.map(&:history).flatten!.inject([]) { |ret, rec| + records.map(&:history).flatten!.each_with_object([]) do |rec, ret| ret.push [rec.valid_from.to_i, rec.valid_from.usec] if rec.try(:valid_from) - ret.push [rec.valid_to .to_i, rec.valid_to .usec] if rec.try(:valid_to) - ret - }.sort.uniq + ret.push [rec.valid_to.to_i, rec.valid_to.usec] if rec.try(:valid_to) + end.sort.uniq } - describe 'on records having an :has_many relationship' do - describe 'by default returns timestamps of the record only' do - subject { split.call($t.foo.timeline) } + context 'with records having an :has_many relationship' do + context 'with default settings' do + subject(:timeline) { split.call($t.foo.timeline) } - it { expect(subject.size).to eq $t.foo.ts.size } + it { expect(timeline.size).to eq $t.foo.ts.size } it { is_expected.to eq timestamps_from.call($t.foo) } end - describe 'when asked, returns timestamps including the related objects' do - subject { split.call($t.foo.timeline(with: :bars)) } + context 'when association is requested in options' do + subject(:timeline) { split.call($t.foo.timeline(with: :bars)) } - it { expect(subject.size).to eq($t.foo.ts.size + $t.bar.ts.size) } + it { expect(timeline.size).to eq($t.foo.ts.size + $t.bar.ts.size) } it { is_expected.to eq(timestamps_from.call($t.foo, *$t.foo.bars)) } end end - describe 'on records using has_timeline :with' do - subject { split.call($t.bar.timeline) } + context 'with records using has_timeline :with' do + subject(:timeline) { split.call($t.bar.timeline) } - describe 'returns timestamps of the record and its associations' do - let!(:expected) do - creat = $t.bar.history.first.valid_from - c_sec, c_usec = creat.to_i, creat.usec + let!(:expected) do + creat = $t.bar.history.first.valid_from + c_sec = creat.to_i + c_usec = creat.usec - timestamps_from.call($t.foo, $t.bar).reject { |sec, usec| - sec < c_sec || (sec == c_sec && usec < c_usec) - } + timestamps_from.call($t.foo, $t.bar).reject do |sec, usec| + sec < c_sec || (sec == c_sec && usec < c_usec) end - - it { expect(subject.size).to eq expected.size } - it { is_expected.to eq expected } end + + it { expect(timeline.size).to eq expected.size } + it { is_expected.to eq expected } end - describe 'on non-temporal records using has_timeline :with' do - subject { split.call($t.baz.timeline) } + context 'with non-temporal records using has_timeline :with' do + subject(:timeline) { split.call($t.baz.timeline) } - describe 'returns timestamps of its temporal associations' do - it { expect(subject.size).to eq $t.bar.ts.size } - it { is_expected.to eq timestamps_from.call($t.bar) } - end + it { expect(timeline.size).to eq $t.bar.ts.size } + it { is_expected.to eq timestamps_from.call($t.bar) } end end end diff --git a/spec/chrono_model/time_machine/timestamps_spec.rb b/spec/chrono_model/time_machine/timestamps_spec.rb index 33d6ddbe..728c71ad 100644 --- a/spec/chrono_model/time_machine/timestamps_spec.rb +++ b/spec/chrono_model/time_machine/timestamps_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -7,7 +9,7 @@ history_methods = %w[valid_from valid_to recorded_at] current_methods = %w[as_of_time] - context 'on history records' do + context 'with historical records' do let(:record) { $t.foo.history.first } (history_methods + current_methods).each do |attr| @@ -21,14 +23,14 @@ end end - context 'on current records' do + context 'with current records' do let(:record) { $t.foo } history_methods.each do |attr| describe ['#', attr].join do - subject { record.public_send(attr) } + subject(:attribute) { record.public_send(attr) } - it { expect { subject }.to raise_error(NoMethodError) } + it { expect { attribute }.to raise_error(NoMethodError) } end end @@ -36,7 +38,7 @@ describe ['#', attr].join do subject { record.public_send(attr) } - it { is_expected.to be(nil) } + it { is_expected.to be_nil } end end end diff --git a/spec/chrono_model/time_machine/transactions_spec.rb b/spec/chrono_model/time_machine/transactions_spec.rb index 891a5d46..b6e74894 100644 --- a/spec/chrono_model/time_machine/transactions_spec.rb +++ b/spec/chrono_model/time_machine/transactions_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'support/time_machine/structure' @@ -5,64 +7,64 @@ include ChronoTest::TimeMachine::Helpers # Transactions - context 'multiple updates to an existing record' do + context 'with multiple updates to an existing record' do let!(:r1) do Foo.create!(name: 'xact test').tap do |record| Foo.transaction do - record.update_attribute 'name', 'lost into oblivion' - record.update_attribute 'name', 'does work' + record.update_column 'name', 'lost into oblivion' + record.update_column 'name', 'does work' end end end - it "generate only a single history record" do + after do + r1.destroy + r1.history.delete_all + end + + it 'generates a single history record' do expect(r1.history.size).to eq(2) expect(r1.history.first.name).to eq 'xact test' expect(r1.history.last.name).to eq 'does work' end - - after do - r1.destroy - r1.history.delete_all - end end - context 'insertion and subsequent update' do + context 'with an insertion and subsequent update' do let!(:r2) do Foo.transaction do Foo.create!(name: 'lost into oblivion').tap do |record| - record.update_attribute 'name', 'I am Bar' - record.update_attribute 'name', 'I am Foo' + record.update_column 'name', 'I am Bar' + record.update_column 'name', 'I am Foo' end end end - it 'generates a single history record' do - expect(r2.history.size).to eq(1) - expect(r2.history.first.name).to eq 'I am Foo' - end - after do r2.destroy r2.history.delete_all end + + it 'generates a single history record' do + expect(r2.history.size).to eq(1) + expect(r2.history.first.name).to eq 'I am Foo' + end end - context 'insertion and subsequent deletion' do + context 'with an insertion and subsequent deletion' do let!(:r3) do Foo.transaction do Foo.create!(name: 'it never happened').destroy end end - it 'does not generate any history' do - expect(Foo.history.where(id: r3.id)).to be_empty - end - after do r3.destroy r3.history.delete_all end + + it 'does not generate any history' do + expect(Foo.history.where(id: r3.id)).to be_empty + end end end diff --git a/spec/config.yml.example b/spec/config.yml.example index 4e12e7df..e72b0598 100644 --- a/spec/config.yml.example +++ b/spec/config.yml.example @@ -1,7 +1,7 @@ # CREATE ROLE chronomodel LOGIN ENCRYPTED PASSWORD 'chronomodel' CREATEDB; # adapter: chronomodel -hostname: localhost +host: localhost username: chronomodel password: chronomodel database: chronomodel diff --git a/spec/fixtures/migrations/56/20160812190335_create_impressions.rb b/spec/fixtures/migrations/56/20160812190335_create_impressions.rb index 7b630543..37117e62 100644 --- a/spec/fixtures/migrations/56/20160812190335_create_impressions.rb +++ b/spec/fixtures/migrations/56/20160812190335_create_impressions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateImpressions < ActiveRecord::Migration[5.0] def change create_table :impressions do |t| diff --git a/spec/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb b/spec/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb index 7b8308cd..b04b5614 100644 --- a/spec/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +++ b/spec/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddTemporalExtensionToImpressions < ActiveRecord::Migration[5.0] def self.up enable_extension 'btree_gist' unless extension_enabled?('btree_gist') diff --git a/spec/fixtures/railsapp/config/application.rb b/spec/fixtures/railsapp/config/application.rb index 6df38f78..84f73260 100644 --- a/spec/fixtures/railsapp/config/application.rb +++ b/spec/fixtures/railsapp/config/application.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + require_relative 'boot' -require "rails" +require 'rails' # Pick the frameworks you want: -require "active_model/railtie" -require "active_job/railtie" -require "active_record/railtie" +require 'active_model/railtie' +require 'active_job/railtie' +require 'active_record/railtie' # require "active_storage/engine" -require "action_controller/railtie" +require 'action_controller/railtie' # require "action_mailer/railtie" -require "action_view/railtie" +require 'action_view/railtie' # require "action_cable/engine" # require "sprockets/railtie" # require "rails/test_unit/railtie" diff --git a/spec/fixtures/railsapp/config/boot.rb b/spec/fixtures/railsapp/config/boot.rb index c9aef85d..7aa6c23d 100644 --- a/spec/fixtures/railsapp/config/boot.rb +++ b/spec/fixtures/railsapp/config/boot.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/spec/fixtures/railsapp/config/environments/development.rb b/spec/fixtures/railsapp/config/environments/development.rb index 33850035..594644cc 100644 --- a/spec/fixtures/railsapp/config/environments/development.rb +++ b/spec/fixtures/railsapp/config/environments/development.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 22b6a702..fab4aa01 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -121,8 +121,7 @@ # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 - # TODO: Refactor to allow random spec execution - # config.order = :random + config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce diff --git a/spec/support/adapter/helpers.rb b/spec/support/adapter/helpers.rb index 61d682e1..08b1881d 100644 --- a/spec/support/adapter/helpers.rb +++ b/spec/support/adapter/helpers.rb @@ -1,51 +1,55 @@ -module ChronoTest::Adapter - module Helpers - def self.included(base) - base.extend DSL - - base.instance_eval do - delegate :adapter, to: ChronoTest - delegate :columns, :table, :pk_type, to: DSL - end - end +# frozen_string_literal: true - module DSL - def with_temporal_table(&block) - context ':temporal => true' do - before(:all) { adapter.create_table(table, temporal: true, &DSL.columns) } - after(:all) { adapter.drop_table table } +module ChronoTest + module Adapter + module Helpers + def self.included(base) + base.extend DSL - instance_eval(&block) + base.instance_eval do + delegate :adapter, to: ChronoTest + delegate :columns, :table, :pk_type, to: DSL end end - def with_plain_table(&block) - context ':temporal => false' do - before(:all) { adapter.create_table(table, temporal: false, &DSL.columns) } - after(:all) { adapter.drop_table table } + module DSL + def with_temporal_table(&block) + context 'with temporal tables' do + before { adapter.create_table(table, temporal: true, &DSL.columns) } + after { adapter.drop_table table } - instance_eval(&block) + instance_eval(&block) + end end - end - def self.table(table = nil) - @table = table if table - @table - end + def with_plain_table(&block) + context 'with plain tables' do + before { adapter.create_table(table, temporal: false, &DSL.columns) } + after { adapter.drop_table table } - def self.columns(&block) - @columns = yield if block - @columns - end + instance_eval(&block) + end + end + + def self.table(table = nil) + @table = table if table + @table + end + + def self.columns(&block) + @columns = yield if block + @columns + end - def self.pk_type - @pk_type ||= if ActiveRecord::VERSION::STRING.to_f >= 5.1 - 'bigint' - else - 'integer' + def self.pk_type + @pk_type ||= if ActiveRecord::VERSION::STRING.to_f >= 5.1 + 'bigint' + else + 'integer' + end end + delegate :columns, :table, :pk_type, to: self end - delegate :columns, :table, :pk_type, to: self end end end diff --git a/spec/support/adapter/structure.rb b/spec/support/adapter/structure.rb index ada01c5d..2c99a3fe 100644 --- a/spec/support/adapter/structure.rb +++ b/spec/support/adapter/structure.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'support/adapter/helpers' # This module contains the definition of a test structure that is used by the @@ -8,34 +10,36 @@ # defined, ans as a reference of what it is expected to have been created by # the +ChronoModel::Adapter+ methods. # -module ChronoTest::Adapter - module Structure - extend ActiveSupport::Concern +module ChronoTest + module Adapter + module Structure + extend ActiveSupport::Concern - included do - table 'test_table' - subject { table } + included do + table 'test_table' + subject { table } - columns do - native = [ - ['test', 'character varying'], - %w[foo integer], - ['bar', 'double precision'], - %w[baz text] - ] + columns do + native = [ + ['test', 'character varying'], + %w[foo integer], + ['bar', 'double precision'], + %w[baz text] + ] - def native.to_proc - proc { |t| - t.string :test, null: false, default: 'default-value' - t.integer :foo - t.float :bar - t.text :baz - t.integer :ary, array: true, null: false, default: [] - t.boolean :bool, null: false, default: false - } - end + def native.to_proc + proc { |t| + t.string :test, null: false, default: 'default-value' + t.integer :foo + t.float :bar + t.text :baz + t.integer :ary, array: true, null: false, default: [] + t.boolean :bool, null: false, default: false + } + end - native + native + end end end end diff --git a/spec/support/aruba.rb b/spec/support/aruba.rb index 02efdd5a..09960c22 100644 --- a/spec/support/aruba.rb +++ b/spec/support/aruba.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'aruba/rspec' require 'arel' require 'rake' @@ -7,11 +9,11 @@ module ChronoTest module Aruba def load_schema_task(as_regexp: false) str = - if Rails.version < '7.0' - "db:structure:load" - else - "db:schema:load" - end + if Rails.version < '7.0' + 'db:structure:load' + else + 'db:schema:load' + end as_regexp ? Regexp.new(str) : str end @@ -21,11 +23,11 @@ def copy_db_config(file = 'database_without_username_and_password.yml') def dump_schema_task(as_regexp: false) str = - if Rails.version < '7.0' - "db:structure:dump" - else - "db:schema:dump" - end + if Rails.version < '7.0' + 'db:structure:dump' + else + 'db:schema:dump' + end as_regexp ? Regexp.new(str) : str end @@ -34,15 +36,15 @@ def aruba_working_directory end def dummy_app_directory - File.expand_path("../../tmp/railsapp/", __dir__) + File.expand_path('../../tmp/railsapp/', __dir__) end def copy_dummy_app_into_aruba_working_directory unless File.file?(Pathname.new(dummy_app_directory).join('Rakefile')) - raise %q( + raise ' The dummy application does not exist Run `bundle exec rake testapp:create` - ) + ' end FileUtils.rm_rf(Dir.glob("#{aruba_working_directory}/*")) FileUtils.cp_r(Dir.glob("#{dummy_app_directory}/*"), aruba_working_directory) @@ -55,7 +57,7 @@ def recreate_railsapp_database connection.create_database database end - def file_mangle!(file, &block) + def file_mangle!(file) # Read file_contents = read(file).join("\n") diff --git a/spec/support/connection.rb b/spec/support/connection.rb index b0e2f3c2..4cab6673 100644 --- a/spec/support/connection.rb +++ b/spec/support/connection.rb @@ -1,30 +1,33 @@ +# frozen_string_literal: true + require 'pathname' require 'active_record' module ChronoTest - extend self - AR = ActiveRecord::Base - log = ENV['VERBOSE'].present? ? $stderr : 'spec/debug.log'.tap { |f| File.open(f, "ab") { |ft| ft.truncate(0) } } + log = ENV['VERBOSE'].present? ? $stderr : 'spec/debug.log'.tap { |f| File.open(f, 'ab') { |ft| ft.truncate(0) } } AR.logger = ::Logger.new(log).tap do |l| l.level = 0 end + module_function + def connect!(spec = config) - unless ENV['VERBOSE'].present? - spec = spec.merge(min_messages: 'WARNING') - end + spec = spec.merge(min_messages: 'WARNING') if ENV['VERBOSE'].blank? AR.establish_connection spec end def logger - AR.logger + @logger ||= AR.logger end def connection AR.connection end - alias adapter connection + + def adapter + @adapter ||= connection + end def recreate_database! database = config.fetch(:database) @@ -39,30 +42,25 @@ def recreate_database! end def config - @config ||= YAML.load(config_file.read).tap do |conf| + @config ||= YAML.safe_load(config_file.read).tap do |conf| conf.symbolize_keys! conf.update(adapter: 'chronomodel') def conf.to_s - 'pgsql://%s:%s@%s/%s' % [ - self[:username], self[:password], self[:hostname], self[:database] - ] + format('pgsql://%s:%s@%s/%s', **slice(:username, :password, :host, :database)) end end rescue Errno::ENOENT - $stderr.puts < has_column?(name, type)) - end + @matches = @columns.inject({}) do |h, (name, type)| + h.update([name, type] => has_column?(name, type)) + end - @matches.values.all? - end + @matches.values.all? + end - def failure_message - message_matches("expected #{@schema}.#{table} to have") - end + def failure_message + message_matches("expected #{@schema}.#{table} to have") + end - def failure_message_when_negated - message_matches("expected #{@schema}.#{table} to not have") - end + def failure_message_when_negated + message_matches("expected #{@schema}.#{table} to not have") + end - protected + protected - def has_column?(name, type) - column_type(name) == [name, type] - end + def has_column?(name, type) + column_type(name) == [name, type] + end - def column_type(name) - table = "#{@schema}.#{self.table}" + def column_type(name) + table = "#{@schema}.#{self.table}" - select_rows(<<-SQL, [table, name], 'Check column').first + select_rows(<<-SQL.squish, [table, name], 'Check column').first SELECT attname, FORMAT_TYPE(atttypid, atttypmod) FROM pg_attribute WHERE attrelid = ?::regclass::oid AND attname = ? SQL - end + end - private + private - def message_matches(message) - (message << ' ').tap do |message| - message << @matches.map do |(name, type), match| - "a #{name}(#{type}) column" unless match - end.compact.to_sentence + def message_matches(message) + (message << ' ').tap do |m| + m << @matches.map do |(name, type), match| + "a #{name}(#{type}) column" unless match + end.compact.to_sentence + end end end - end - def have_columns(*args) - HaveColumns.new(*args) - end + def have_columns(*args) + HaveColumns.new(*args) + end - class HaveTemporalColumns < HaveColumns - def initialize(columns) - super(columns, temporal_schema) + class HaveTemporalColumns < HaveColumns + def initialize(columns) + super(columns, temporal_schema) + end end - end - def have_temporal_columns(*args) - HaveTemporalColumns.new(*args) - end + def have_temporal_columns(*args) + HaveTemporalColumns.new(*args) + end - class HaveHistoryColumns < HaveColumns - def initialize(columns) - super(columns, history_schema) + class HaveHistoryColumns < HaveColumns + def initialize(columns) + super(columns, history_schema) + end end - end - def have_history_columns(*args) - HaveHistoryColumns.new(*args) - end + def have_history_columns(*args) + HaveHistoryColumns.new(*args) + end - class HaveHistoryExtraColumns < HaveColumns - def initialize - super([ - %w[validity tsrange], - ['recorded_at', 'timestamp without time zone'], - %w[hid bigint] - ], history_schema) + class HaveHistoryExtraColumns < HaveColumns + def initialize + super([ + %w[validity tsrange], + ['recorded_at', 'timestamp without time zone'], + %w[hid bigint] + ], history_schema) + end end - end - def have_history_extra_columns - HaveHistoryExtraColumns.new + def have_history_extra_columns + HaveHistoryExtraColumns.new + end end end end diff --git a/spec/support/matchers/function.rb b/spec/support/matchers/function.rb index 062cdfcd..a8b35823 100644 --- a/spec/support/matchers/function.rb +++ b/spec/support/matchers/function.rb @@ -1,37 +1,40 @@ -module ChronoTest::Matchers - module Function - class HaveFunctions < ChronoTest::Matchers::Base - def initialize(functions, schema = 'public') - @functions = functions - @schema = schema - end - - def description - 'have functions' - end +# frozen_string_literal: true - def matches?(table) - super(table) +module ChronoTest + module Matchers + module Function + class HaveFunctions < ChronoTest::Matchers::Base + def initialize(functions, schema = 'public') + @functions = functions + @schema = schema + end - @matches = @functions.inject({}) do |h, name| - h.update(name => has_function?(name)) + def description + 'have functions' end - @matches.values.all? - end + def matches?(table) + super(table) - def failure_message - message_matches("expected #{@schema}.#{table} to have") - end + @matches = @functions.inject({}) do |h, name| + h.update(name => has_function?(name)) + end - def failure_message_when_negated - message_matches("expected #{@schema}.#{table} to not have") - end + @matches.values.all? + end + + def failure_message + message_matches("expected #{@schema}.#{table} to have") + end + + def failure_message_when_negated + message_matches("expected #{@schema}.#{table} to not have") + end - protected + protected - def has_function?(name) - select_value(<<-SQL, [@schema, name], 'Check function') == true + def has_function?(name) + select_value(<<-SQL.squish, [@schema, name], 'Check function') == true SELECT EXISTS( SELECT 1 FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n @@ -40,42 +43,43 @@ def has_function?(name) AND p.proname = ? ) SQL - end + end - private + private - def message_matches(message) - (message << ' ').tap do |m| - m << @matches.map do |name, match| - "a #{name} function" - end.compact.to_sentence + def message_matches(message) + (message << ' ').tap do |m| + m << @matches.map do |name, _match| + "a #{name} function" + end.compact.to_sentence + end end end - end - - def have_functions(*args) - HaveFunctions.new(*args) - end - class HaveHistoryFunctions < HaveFunctions - def initialize(schema = 'public') - @function_templates = [ - 'chronomodel_%s_insert', - 'chronomodel_%s_update', - 'chronomodel_%s_delete', - ] - @schema = schema + def have_functions(*args) + HaveFunctions.new(*args) end - def matches?(table) - @functions = @function_templates.map { |t| t % [table] } + class HaveHistoryFunctions < HaveFunctions + def initialize(schema = 'public') + @function_templates = [ + 'chronomodel_%s_insert', + 'chronomodel_%s_update', + 'chronomodel_%s_delete' + ] + @schema = schema + end + + def matches?(table) + @functions = @function_templates.map { |t| format(t, table) } - super(table) + super(table) + end end - end - def have_history_functions - HaveHistoryFunctions.new + def have_history_functions + HaveHistoryFunctions.new + end end end end diff --git a/spec/support/matchers/index.rb b/spec/support/matchers/index.rb index 8d80039e..0108d565 100644 --- a/spec/support/matchers/index.rb +++ b/spec/support/matchers/index.rb @@ -1,22 +1,25 @@ -module ChronoTest::Matchers - module Index - class HaveIndex < ChronoTest::Matchers::Base - attr_reader :name, :columns, :schema +# frozen_string_literal: true - def initialize(name, columns, schema = 'public') - @name = name - @columns = columns.sort - @schema = schema - end +module ChronoTest + module Matchers + module Index + class HaveIndex < ChronoTest::Matchers::Base + attr_reader :name, :columns, :schema - def description - 'have index' - end + def initialize(name, columns, schema = 'public') + @name = name + @columns = columns.sort + @schema = schema + end - def matches?(table) - super(table) + def description + 'have index' + end - select_values(<<-SQL, [table, name, schema], 'Check index') == columns + def matches?(table) + super(table) + + select_values(<<-SQL.squish, [table, name, schema], 'Check index') == columns SELECT a.attname FROM pg_class t JOIN pg_index d ON t.oid = d.indrelid @@ -29,40 +32,41 @@ def matches?(table) SELECT oid FROM pg_namespace WHERE nspname = ? ) ORDER BY a.attname - SQL - end + SQL + end - def failure_message - "expected #{schema}.#{table} to have a #{name} index on #{columns}" - end + def failure_message + "expected #{schema}.#{table} to have a #{name} index on #{columns}" + end - def failure_message_when_negated - "expected #{schema}.#{table} to not have a #{name} index on #{columns}" + def failure_message_when_negated + "expected #{schema}.#{table} to not have a #{name} index on #{columns}" + end end - end - def have_index(*args) - HaveIndex.new(*args) - end + def have_index(*args) + HaveIndex.new(*args) + end - class HaveTemporalIndex < HaveIndex - def initialize(name, columns) - super(name, columns, temporal_schema) + class HaveTemporalIndex < HaveIndex + def initialize(name, columns) + super(name, columns, temporal_schema) + end end - end - def have_temporal_index(*args) - HaveTemporalIndex.new(*args) - end + def have_temporal_index(*args) + HaveTemporalIndex.new(*args) + end - class HaveHistoryIndex < HaveIndex - def initialize(name, columns) - super(name, columns, history_schema) + class HaveHistoryIndex < HaveIndex + def initialize(name, columns) + super(name, columns, history_schema) + end end - end - def have_history_index(*args) - HaveHistoryIndex.new(*args) + def have_history_index(*args) + HaveHistoryIndex.new(*args) + end end end end diff --git a/spec/support/matchers/schema.rb b/spec/support/matchers/schema.rb index fd7e65f2..a3d5cae5 100644 --- a/spec/support/matchers/schema.rb +++ b/spec/support/matchers/schema.rb @@ -1,36 +1,40 @@ +# frozen_string_literal: true + require 'support/matchers/base' -module ChronoTest::Matchers - module Schema - class BeInSchema < ChronoTest::Matchers::Base - def initialize(expected) - @expected = expected - @expected = '"$user", public' if @expected == :default - end +module ChronoTest + module Matchers + module Schema + class BeInSchema < ChronoTest::Matchers::Base + def initialize(expected) + @expected = expected + @expected = '"$user", public' if @expected == :default + end - def description - 'be in schema' - end + def description + 'be in schema' + end - def failure_message - "expected to be in schema #@expected, but was in #@current" - end + def failure_message + "expected to be in schema #{@expected}, but was in #{@current}" + end - def failure_message_when_negated - "expected to be in schema #@current, but was in #@expected" - end + def failure_message_when_negated + "expected to be in schema #{@current}, but was in #{@expected}" + end - def matches?(*) - @current = select_value(<<-SQL, [], 'Current schema') + def matches?(*) + @current = select_value(<<-SQL.squish, [], 'Current schema') SHOW search_path - SQL + SQL - @current == @expected + @current == @expected + end end - end - def be_in_schema(schema) - BeInSchema.new(schema) + def be_in_schema(schema) + BeInSchema.new(schema) + end end end end diff --git a/spec/support/matchers/source.rb b/spec/support/matchers/source.rb index d145df50..c4bbd2a6 100644 --- a/spec/support/matchers/source.rb +++ b/spec/support/matchers/source.rb @@ -1,36 +1,40 @@ -module ChronoTest::Matchers - module Source - class HaveFunctionSource < ChronoTest::Matchers::Base - def initialize(function, source_regexp) - @function = function - @regexp = source_regexp - end +# frozen_string_literal: true - def description - "have function source matching #{@regexp}" - end +module ChronoTest + module Matchers + module Source + class HaveFunctionSource < ChronoTest::Matchers::Base + def initialize(function, source_regexp) + @function = function + @regexp = source_regexp + end - def matches?(table) - super(table) + def description + "have function source matching #{@regexp}" + end - source = select_value(<<-SQL, [@function], "Get #@function source") + def matches?(table) + super(table) + + source = select_value(<<-SQL.squish, [@function], "Get #{@function} source") SELECT prosrc FROM pg_catalog.pg_proc WHERE proname = ? - SQL + SQL - !(source =~ @regexp).nil? - end + !(source =~ @regexp).nil? + end - def failure_message - "expected #{table} to have a #{@function} matching #{@regexp}" - end + def failure_message + "expected #{table} to have a #{@function} matching #{@regexp}" + end - def failure_message_when_negated - "expected #{table} to not have a #{@function} matching #{@regexp}" + def failure_message_when_negated + "expected #{table} to not have a #{@function} matching #{@regexp}" + end end - end - def have_function_source(*args) - HaveFunctionSource.new(*args) + def have_function_source(*args) + HaveFunctionSource.new(*args) + end end end end diff --git a/spec/support/matchers/table.rb b/spec/support/matchers/table.rb index dd17154c..15d26b8a 100644 --- a/spec/support/matchers/table.rb +++ b/spec/support/matchers/table.rb @@ -1,17 +1,20 @@ +# frozen_string_literal: true + require 'support/matchers/base' -module ChronoTest::Matchers - module Table - class Base < ChronoTest::Matchers::Base - protected +module ChronoTest + module Matchers + module Table + class Base < ChronoTest::Matchers::Base + protected - # Database statements - # - def relation_exists?(options) - schema = options[:in] - kind = options[:kind] == :view ? 'v' : 'r' + # Database statements + # + def relation_exists?(options) + schema = options[:in] + kind = options[:kind] == :view ? 'v' : 'r' - select_value(<<-SQL, [table, schema], 'Check table exists') == true + select_value(<<-SQL.squish, [table, schema], 'Check table exists') == true SELECT EXISTS ( SELECT 1 FROM pg_class c @@ -21,156 +24,156 @@ def relation_exists?(options) AND n.nspname = ? ) SQL + end end - end - # ################################################################## - # Checks that a table exists in the Public schema - # - class HavePublicBacking < Base - def matches?(table) - super(table) + # ################################################################## + # Checks that a table exists in the Public schema + # + class HavePublicBacking < Base + def matches?(table) + super(table) - relation_exists? in: public_schema - end + relation_exists? in: public_schema + end - def description - 'be in the public schema' - end + def description + 'be in the public schema' + end + + def failure_message + "expected #{table} to exist in the #{public_schema} schema" + end - def failure_message - "expected #{table} to exist in the #{public_schema} schema" + def failure_message_when_negated + "expected #{table} to not exist in the #{public_schema} schema" + end end - def failure_message_when_negated - "expected #{table} to not exist in the #{public_schema} schema" + def have_public_backing + HavePublicBacking.new end - end - def have_public_backing - HavePublicBacking.new - end + # ################################################################## + # Checks that a table exists in the Temporal schema + # + class HaveTemporalBacking < Base + def matches?(table) + super(table) - # ################################################################## - # Checks that a table exists in the Temporal schema - # - class HaveTemporalBacking < Base - def matches?(table) - super(table) + relation_exists? in: temporal_schema + end - relation_exists? in: temporal_schema - end + def description + 'be in the temporal schema' + end - def description - 'be in the temporal schema' - end + def failure_message + "expected #{table} to exist in the #{temporal_schema} schema" + end - def failure_message - "expected #{table} to exist in the #{temporal_schema} schema" + def failure_message_when_negated + "expected #{table} to not exist in the #{temporal_schema} schema" + end end - def failure_message_when_negated - "expected #{table} to not exist in the #{temporal_schema} schema" + def have_temporal_backing + HaveTemporalBacking.new end - end - - def have_temporal_backing - HaveTemporalBacking.new - end - # ################################################################## - # Checks that a table exists in the History schema and inherits from - # the one in the Temporal schema - # - class HaveHistoryBacking < Base - def matches?(table) - super(table) - - table_exists? && - inherits_from_temporal? && - has_consistency_constraint? && - has_history_indexes? - end + # ################################################################## + # Checks that a table exists in the History schema and inherits from + # the one in the Temporal schema + # + class HaveHistoryBacking < Base + def matches?(table) + super(table) + + table_exists? && + inherits_from_temporal? && + has_consistency_constraint? && + has_history_indexes? + end - def description - 'be in history schema' - end + def description + 'be in history schema' + end - def failure_message - "expected #{table} ".tap do |message| - message << [ - ("to exist in the #{history_schema} schema" unless @existance), - ("to inherit from #{temporal_schema}.#{table}" unless @inheritance), - ("to have a timeline consistency constraint" unless @constraint), - ("to have history indexes" unless @indexes) - ].compact.to_sentence + def failure_message + "expected #{table} ".tap do |message| + message << [ + ("to exist in the #{history_schema} schema" unless @existance), + ("to inherit from #{temporal_schema}.#{table}" unless @inheritance), + ('to have a timeline consistency constraint' unless @constraint), + ('to have history indexes' unless @indexes) + ].compact.to_sentence + end end - end - def failure_message_when_negated - "expected #{table} ".tap do |message| - message << [ - ("to not exist in the #{history_schema} schema" if @existance), - ("to not inherit from #{temporal_schema}.#{table}" if @inheritance), - ("to not have a timeline consistency constraint" if @constraint), - ("to not have history indexes" if @indexes) - ].compact.to_sentence + def failure_message_when_negated + "expected #{table} ".tap do |message| + message << [ + ("to not exist in the #{history_schema} schema" if @existance), + ("to not inherit from #{temporal_schema}.#{table}" if @inheritance), + ('to not have a timeline consistency constraint' if @constraint), + ('to not have history indexes' if @indexes) + ].compact.to_sentence + end end - end - private + private - def table_exists? - @existance = relation_exists? in: history_schema - end + def table_exists? + @existance = relation_exists? in: history_schema + end - def inherits_from_temporal? - binds = ["#{history_schema}.#{table}", "#{temporal_schema}.#{table}"] + def inherits_from_temporal? + binds = ["#{history_schema}.#{table}", "#{temporal_schema}.#{table}"] - @inheritance = select_value(<<-SQL, binds, 'Check inheritance') == true + @inheritance = select_value(<<-SQL.squish, binds, 'Check inheritance') == true SELECT EXISTS ( SELECT 1 FROM pg_catalog.pg_inherits WHERE inhrelid = ?::regclass::oid AND inhparent = ?::regclass::oid ) SQL - end + end - def has_history_indexes? - binds = [history_schema, table] + def has_history_indexes? + binds = [history_schema, table] - indexes = select_values(<<-SQL, binds, 'Check history indexes') + indexes = select_values(<<-SQL.squish, binds, 'Check history indexes') SELECT indexdef FROM pg_indexes WHERE schemaname = ? AND tablename = ? SQL - fqtn = [history_schema, table].join('.') + fqtn = [history_schema, table].join('.') - expected = [ - "CREATE INDEX index_#{table}_temporal_on_lower_validity ON #{fqtn} USING btree (lower(validity))", - "CREATE INDEX index_#{table}_temporal_on_upper_validity ON #{fqtn} USING btree (upper(validity))", - "CREATE INDEX index_#{table}_temporal_on_validity ON #{fqtn} USING gist (validity)", + expected = [ + "CREATE INDEX index_#{table}_temporal_on_lower_validity ON #{fqtn} USING btree (lower(validity))", + "CREATE INDEX index_#{table}_temporal_on_upper_validity ON #{fqtn} USING btree (upper(validity))", + "CREATE INDEX index_#{table}_temporal_on_validity ON #{fqtn} USING gist (validity)", - "CREATE INDEX #{table}_inherit_pkey ON #{fqtn} USING btree (id)", - "CREATE INDEX #{table}_instance_history ON #{fqtn} USING btree (id, recorded_at)", - "CREATE UNIQUE INDEX #{table}_pkey ON #{fqtn} USING btree (hid)", - "CREATE INDEX #{table}_recorded_at ON #{fqtn} USING btree (recorded_at)", - "CREATE INDEX #{table}_timeline_consistency ON #{fqtn} USING gist (id, validity)" - ] + "CREATE INDEX #{table}_inherit_pkey ON #{fqtn} USING btree (id)", + "CREATE INDEX #{table}_instance_history ON #{fqtn} USING btree (id, recorded_at)", + "CREATE UNIQUE INDEX #{table}_pkey ON #{fqtn} USING btree (hid)", + "CREATE INDEX #{table}_recorded_at ON #{fqtn} USING btree (recorded_at)", + "CREATE INDEX #{table}_timeline_consistency ON #{fqtn} USING gist (id, validity)" + ] - @indexes = (expected - indexes).empty? - end + @indexes = (expected - indexes).empty? + end - def has_consistency_constraint? - binds = { - conname: connection.timeline_consistency_constraint_name(table), - connamespace: history_schema, - conrelid: [history_schema, table].join('.'), - attname: connection.primary_key(table) - } + def has_consistency_constraint? + binds = { + conname: connection.timeline_consistency_constraint_name(table), + connamespace: history_schema, + conrelid: [history_schema, table].join('.'), + attname: connection.primary_key(table) + } - @constraint = select_value(<<-SQL, binds, 'Check Consistency Constraint') == true + @constraint = select_value(<<-SQL.squish, binds, 'Check Consistency Constraint') == true SELECT EXISTS ( SELECT 1 FROM pg_catalog.pg_constraint WHERE conname = :conname @@ -186,69 +189,69 @@ def has_consistency_constraint? ) ) SQL + end end - end - def have_history_backing - HaveHistoryBacking.new - end + def have_history_backing + HaveHistoryBacking.new + end - # ################################################################## - # Checks that a table exists in the Public schema, is an updatable - # view and has an INSERT, UPDATE and DELETE triggers. - # - class HavePublicInterface < Base - def matches?(table) - super(table) + # ################################################################## + # Checks that a table exists in the Public schema, is an updatable + # view and has an INSERT, UPDATE and DELETE triggers. + # + class HavePublicInterface < Base + def matches?(table) + super(table) - view_exists? && [is_updatable?, has_triggers?].all? - end + view_exists? && [is_updatable?, has_triggers?].all? + end - def description - 'be an updatable view' - end + def description + 'be an updatable view' + end - def failure_message - "expected #{table} ".tap do |message| - message << [ - ("to exist in the #{public_schema} schema" unless @existance), - ('to be an updatable view' unless @updatable), - ('to have an INSERT trigger' unless @insert_trigger), - ('to have an UPDATE trigger' unless @update_trigger), - ('to have a DELETE trigger' unless @delete_trigger) - ].compact.to_sentence + def failure_message + "expected #{table} ".tap do |message| + message << [ + ("to exist in the #{public_schema} schema" unless @existance), + ('to be an updatable view' unless @updatable), + ('to have an INSERT trigger' unless @insert_trigger), + ('to have an UPDATE trigger' unless @update_trigger), + ('to have a DELETE trigger' unless @delete_trigger) + ].compact.to_sentence + end end - end - def failure_message_when_negated - "expected #{table} ".tap do |message| - message << [ - ("to not exist in the #{public_schema} schema" if @existance), - ('to not be an updatable view' if @updatable), - ('to not have an INSERT trigger' if @insert_trigger), - ('to not have an UPDATE trigger' if @update_trigger), - ('to not have a DELETE trigger' if @delete_trigger) - ].compact.to_sentence + def failure_message_when_negated + "expected #{table} ".tap do |message| + message << [ + ("to not exist in the #{public_schema} schema" if @existance), + ('to not be an updatable view' if @updatable), + ('to not have an INSERT trigger' if @insert_trigger), + ('to not have an UPDATE trigger' if @update_trigger), + ('to not have a DELETE trigger' if @delete_trigger) + ].compact.to_sentence + end end - end - private + private - def view_exists? - @existance = relation_exists? in: public_schema, kind: :view - end + def view_exists? + @existance = relation_exists? in: public_schema, kind: :view + end - def is_updatable? - binds = [public_schema, table] + def is_updatable? + binds = [public_schema, table] - @updatable = select_value(<<-SQL, binds, 'Check updatable') == 'YES' + @updatable = select_value(<<-SQL.squish, binds, 'Check updatable') == 'YES' SELECT is_updatable FROM information_schema.views WHERE table_schema = ? AND table_name = ? SQL - end + end - def has_triggers? - triggers = select_values(<<-SQL, [public_schema, table], 'Check triggers') + def has_triggers? + triggers = select_values(<<-SQL.squish, [public_schema, table], 'Check triggers') SELECT t.tgname FROM pg_catalog.pg_trigger t, pg_catalog.pg_class c, pg_catalog.pg_namespace n WHERE n.oid = c.relnamespace @@ -256,16 +259,17 @@ def has_triggers? AND c.relname = ?; SQL - @insert_trigger = triggers.include? 'chronomodel_insert' - @update_trigger = triggers.include? 'chronomodel_update' - @delete_trigger = triggers.include? 'chronomodel_delete' + @insert_trigger = triggers.include? 'chronomodel_insert' + @update_trigger = triggers.include? 'chronomodel_update' + @delete_trigger = triggers.include? 'chronomodel_delete' - @insert_trigger && @update_trigger && @delete_trigger + @insert_trigger && @update_trigger && @delete_trigger + end end - end - def have_public_interface - HavePublicInterface.new + def have_public_interface + HavePublicInterface.new + end end end end diff --git a/spec/support/time_machine/helpers.rb b/spec/support/time_machine/helpers.rb index 44c6b3e6..49ef554d 100644 --- a/spec/support/time_machine/helpers.rb +++ b/spec/support/time_machine/helpers.rb @@ -1,51 +1,57 @@ -module ChronoTest::TimeMachine - # This module contains helpers used throughout the - # +ChronoModel::TimeMachine+ specs. - # - module Helpers - def self.included(base) - base.extend(self) - end +# frozen_string_literal: true - def adapter - ChronoTest.connection - end +module ChronoTest + module TimeMachine + # This module contains helpers used throughout the + # +ChronoModel::TimeMachine+ specs. + # + module Helpers + def self.included(base) + base.extend(self) + end + + def adapter + ChronoTest.connection + end - def with_revert - adapter.transaction do - adapter.materialize_transactions if adapter.respond_to?(:materialize_transactions) - adapter.create_savepoint 'revert' + def with_revert + adapter.transaction do + adapter.materialize_transactions if adapter.respond_to?(:materialize_transactions) + adapter.create_savepoint 'revert' - yield + yield - adapter.exec_rollback_to_savepoint 'revert' + adapter.exec_rollback_to_savepoint 'revert' + end end - end - # If a context object is given, evaluates the given - # block in its instance context, then defines a `ts` - # on it, backed by an Array, and adds the current - # database timestamp to it. - # - # If a context object is not given, the block is - # evaluated in the current context and the above - # mangling is done on the blocks' return value. - # - def ts_eval(ctx = nil, &block) - ret = (ctx || self).instance_eval(&block) - (ctx || ret).tap do |obj| - obj.singleton_class.instance_eval do - define_method(:ts) { @_ts ||= [] } - end unless obj.methods.include?(:ts) + # If a context object is given, evaluates the given + # block in its instance context, then defines a `ts` + # on it, backed by an Array, and adds the current + # database timestamp to it. + # + # If a context object is not given, the block is + # evaluated in the current context and the above + # mangling is done on the blocks' return value. + # + def ts_eval(ctx = nil, &block) + ret = (ctx || self).instance_eval(&block) + (ctx || ret).tap do |obj| + unless obj.methods.include?(:ts) + obj.singleton_class.instance_eval do + define_method(:ts) { @ts ||= [] } + end + end - now = ChronoTest.connection.select_value('select now()::timestamp') - case now - when Time # Already parsed, thanks AR - obj.ts.push(now) - when String # ISO8601 Timestamp - obj.ts.push(Time.parse("#{now}Z")) - else - raise "Don't know how to deal with #{now.inspect}" + now = ChronoTest.connection.select_value('select now()::timestamp') + case now + when Time # Already parsed, thanks AR + obj.ts.push(now) + when String # ISO8601 Timestamp + obj.ts.push(Time.parse("#{now}Z")) + else + raise "Don't know how to deal with #{now.inspect}" + end end end end diff --git a/spec/support/time_machine/structure.rb b/spec/support/time_machine/structure.rb index ebe47710..cf536332 100644 --- a/spec/support/time_machine/structure.rb +++ b/spec/support/time_machine/structure.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'support/time_machine/helpers' # This module contains the test DDL and models used by most of the @@ -14,179 +16,181 @@ # the form of .create! and update!, aiming to mimic the most of AR with the # least of the effort. Full coverage exercises are most welcome. # -module ChronoTest::TimeMachine - include ChronoTest::TimeMachine::Helpers - - # Set up database structure - # - adapter.create_table 'bars', temporal: true do |t| - t.string :name - t.references :foo - end +module ChronoTest + module TimeMachine + include ChronoTest::TimeMachine::Helpers + + # Set up database structure + # + adapter.create_table 'bars', temporal: true do |t| + t.string :name + t.references :foo + end - adapter.create_table 'bazs' do |t| - t.string :name - t.references :bar - end + adapter.create_table 'bazs' do |t| + t.string :name + t.references :bar + end - adapter.create_table 'boos', temporal: true do |t| - t.string :name - end + adapter.create_table 'boos', temporal: true do |t| + t.string :name + end - adapter.create_table 'boos_moos', temporal: true do |t| - t.references :moo - t.references :boo - end + adapter.create_table 'boos_moos', temporal: true do |t| + t.references :moo + t.references :boo + end - adapter.create_table 'foo_goos' do |t| - t.string :name - end + adapter.create_table 'foo_goos' do |t| + t.string :name + end - adapter.create_table 'foos', temporal: true do |t| - t.string :name - t.integer :fooity - t.references :goo - t.string :refee_foo - end + adapter.create_table 'foos', temporal: true do |t| + t.string :name + t.integer :fooity + t.references :goo + t.string :refee_foo + end - adapter.create_table 'tars', temporal: true do |t| - t.string :name - t.string :foo_refering - end + adapter.create_table 'tars', temporal: true do |t| + t.string :name + t.string :foo_refering + end - adapter.change_table 'tars', temporal: true do - def up - add_reference :foos, column: :foo_refering, primary_key: :refee_foo + adapter.change_table 'tars', temporal: true do + def up + add_reference :foos, column: :foo_refering, primary_key: :refee_foo + end end - end - adapter.create_table 'moos', temporal: true do |t| - t.string :name - end + adapter.create_table 'moos', temporal: true do |t| + t.string :name + end - adapter.create_table 'noos', temporal: true do |t| - t.string :name - t.string :surname - end + adapter.create_table 'noos', temporal: true do |t| + t.string :name + t.string :surname + end - adapter.create_table 'sub_bars', temporal: true do |t| - t.string :name - t.references :bar - end + adapter.create_table 'sub_bars', temporal: true do |t| + t.string :name + t.references :bar + end - adapter.create_table 'sub_sub_bars', temporal: true do |t| - t.string :name - t.references :sub_bar - end + adapter.create_table 'sub_sub_bars', temporal: true do |t| + t.string :name + t.references :sub_bar + end - class ::Bar < ActiveRecord::Base - include ChronoModel::TimeMachine + class ::Bar < ActiveRecord::Base + include ChronoModel::TimeMachine - belongs_to :foo - has_many :sub_bars - has_one :baz + belongs_to :foo + has_many :sub_bars + has_one :baz - has_timeline with: :foo - end + has_timeline with: :foo + end - class ::Baz < ActiveRecord::Base - include ChronoModel::TimeGate + class ::Baz < ActiveRecord::Base + include ChronoModel::TimeGate - belongs_to :bar + belongs_to :bar - has_timeline with: :bar - end + has_timeline with: :bar + end - class ::Tar < ActiveRecord::Base - include ChronoModel::TimeGate + class ::Tar < ActiveRecord::Base + include ChronoModel::TimeGate - belongs_to :foo, foreign_key: :foo_refering, primary_key: :refee_foo, class_name: 'Foo' + belongs_to :foo, foreign_key: :foo_refering, primary_key: :refee_foo, class_name: 'Foo' - has_timeline with: :foo - end + has_timeline with: :foo + end - class ::Boo < ActiveRecord::Base - include ChronoModel::TimeMachine + class ::Boo < ActiveRecord::Base + include ChronoModel::TimeMachine - has_and_belongs_to_many :moos, join_table: 'boos_moos' - end + has_and_belongs_to_many :moos, join_table: 'boos_moos' + end - class ::Foo < ActiveRecord::Base - include ChronoModel::TimeMachine + class ::Foo < ActiveRecord::Base + include ChronoModel::TimeMachine - has_many :bars - has_many :tars, foreign_key: :foo_refering, primary_key: :refee_foo - has_many :sub_bars, through: :bars - has_many :sub_sub_bars, through: :sub_bars + has_many :bars + has_many :tars, foreign_key: :foo_refering, primary_key: :refee_foo + has_many :sub_bars, through: :bars + has_many :sub_sub_bars, through: :sub_bars - belongs_to :goo, class_name: 'FooGoo', optional: true - end + belongs_to :goo, class_name: 'FooGoo', optional: true + end - class ::FooGoo < ActiveRecord::Base - include ChronoModel::TimeGate + class ::FooGoo < ActiveRecord::Base + include ChronoModel::TimeGate - has_many :foos, inverse_of: :goo - end + has_many :foos, inverse_of: :goo + end - class ::Moo < ActiveRecord::Base - include ChronoModel::TimeMachine + class ::Moo < ActiveRecord::Base + include ChronoModel::TimeMachine - has_and_belongs_to_many :boos, join_table: 'boos_moos' - end + has_and_belongs_to_many :boos, join_table: 'boos_moos' + end - class ::Noo < ActiveRecord::Base - include ChronoModel::TimeMachine - end + class ::Noo < ActiveRecord::Base + include ChronoModel::TimeMachine + end - class ::SubBar < ActiveRecord::Base - include ChronoModel::TimeMachine + class ::SubBar < ActiveRecord::Base + include ChronoModel::TimeMachine - belongs_to :bar - has_many :sub_sub_bars + belongs_to :bar + has_many :sub_sub_bars - has_timeline with: :bar - end + has_timeline with: :bar + end - class ::SubSubBar < ActiveRecord::Base - include ChronoModel::TimeMachine + class ::SubSubBar < ActiveRecord::Base + include ChronoModel::TimeMachine - belongs_to :sub_bar - end + belongs_to :sub_bar + end - # Master timeline, used in multiple specs. It is defined here - # as a global variable to be able to be shared across specs. - # - $t = Struct.new(:foo, :bar, :baz, :subbar, :foos, :bars, :boos, :moos, :noos, :noo).new + # Master timeline, used in multiple specs. It is defined here + # as a global variable to be able to be shared across specs. + # + $t = Struct.new(:foo, :bar, :baz, :subbar, :foos, :bars, :boos, :moos, :noos, :noo).new - # Set up associated records, with intertwined updates - # - $t.foo = ts_eval { Foo.create! name: 'foo', fooity: 1 } - ts_eval($t.foo) { update! name: 'foo bar' } + # Set up associated records, with intertwined updates + # + $t.foo = ts_eval { Foo.create! name: 'foo', fooity: 1 } + ts_eval($t.foo) { update! name: 'foo bar' } - $t.bar = ts_eval { Bar.create! name: 'bar', foo: $t.foo } - ts_eval($t.bar) { update! name: 'foo bar' } + $t.bar = ts_eval { Bar.create! name: 'bar', foo: $t.foo } + ts_eval($t.bar) { update! name: 'foo bar' } - $t.subbar = ts_eval { SubBar.create! name: 'sub-bar', bar: $t.bar } - ts_eval($t.subbar) { update! name: 'bar sub-bar' } + $t.subbar = ts_eval { SubBar.create! name: 'sub-bar', bar: $t.bar } + ts_eval($t.subbar) { update! name: 'bar sub-bar' } - ts_eval { SubSubBar.create! name: 'sub-sub-bar', sub_bar: $t.subbar } + ts_eval { SubSubBar.create! name: 'sub-sub-bar', sub_bar: $t.subbar } - ts_eval($t.foo) { update! name: 'new foo' } + ts_eval($t.foo) { update! name: 'new foo' } - ts_eval($t.bar) { update! name: 'bar bar' } - ts_eval($t.bar) { update! name: 'new bar' } + ts_eval($t.bar) { update! name: 'bar bar' } + ts_eval($t.bar) { update! name: 'new bar' } - ts_eval($t.subbar) { update! name: 'sub-bar sub-bar' } - ts_eval($t.subbar) { update! name: 'new sub-bar' } + ts_eval($t.subbar) { update! name: 'sub-bar sub-bar' } + ts_eval($t.subbar) { update! name: 'new sub-bar' } - $t.foos = Array.new(2) { |i| ts_eval { Foo.create! name: "foo #{i}" } } - $t.bars = Array.new(2) { |i| ts_eval { Bar.create! name: "bar #{i}", foo: $t.foos[i] } } - $t.boos = Array.new(2) { |i| ts_eval { Boo.create! name: "boo #{i}" } } - $t.moos = Array.new(2) { |i| ts_eval { Moo.create! name: "moo #{i}", boos: $t.boos } } + $t.foos = Array.new(2) { |i| ts_eval { Foo.create! name: "foo #{i}" } } + $t.bars = Array.new(2) { |i| ts_eval { Bar.create! name: "bar #{i}", foo: $t.foos[i] } } + $t.boos = Array.new(2) { |i| ts_eval { Boo.create! name: "boo #{i}" } } + $t.moos = Array.new(2) { |i| ts_eval { Moo.create! name: "moo #{i}", boos: $t.boos } } - $t.baz = Baz.create! name: 'baz', bar: $t.bar + $t.baz = Baz.create! name: 'baz', bar: $t.bar - $t.noo = ts_eval { Noo.create! name: 'Historical Element 1' } - Noo.create! name: 'Historical Element 2' - ts_eval($t.noo) { update! name: 'Historical Element 3' } + $t.noo = ts_eval { Noo.create! name: 'Historical Element 1' } + Noo.create! name: 'Historical Element 2' + ts_eval($t.noo) { update! name: 'Historical Element 3' } + end end