diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-28 21:10:48 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-28 21:10:48 +0300 |
commit | 0076bbc67375ff1507e42ce479406daf92c0a6a2 (patch) | |
tree | aa0a1c6f575ac050504c397c7edf8f9789d46046 /spec/models | |
parent | 8966e39395e22465ac3ff58407868b872a3ecffe (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/models')
-rw-r--r-- | spec/models/ci/build_metadata_spec.rb | 22 | ||||
-rw-r--r-- | spec/models/concerns/ci/partitionable/switch_spec.rb | 294 | ||||
-rw-r--r-- | spec/models/concerns/ci/partitionable_spec.rb | 21 | ||||
-rw-r--r-- | spec/models/concerns/issuable_spec.rb | 16 | ||||
-rw-r--r-- | spec/models/concerns/pg_full_text_searchable_spec.rb | 25 |
5 files changed, 374 insertions, 4 deletions
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index 16cff72db64..b4c20637ce2 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -182,4 +182,26 @@ RSpec.describe Ci::BuildMetadata do end end end + + describe 'routing table switch' do + context 'with ff disabled' do + before do + stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: false) + end + + it 'uses the legacy table' do + expect(described_class.table_name).to eq('ci_builds_metadata') + end + end + + context 'with ff enabled' do + before do + stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: true) + end + + it 'uses the routing table' do + expect(described_class.table_name).to eq('p_ci_builds_metadata') + end + end + end end diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb new file mode 100644 index 00000000000..09005489268 --- /dev/null +++ b/spec/models/concerns/ci/partitionable/switch_spec.rb @@ -0,0 +1,294 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do + let(:model) do + Class.new(Ci::ApplicationRecord) do + self.primary_key = :id + self.table_name = :_test_ci_jobs_metadata + self.sequence_name = :_test_ci_jobs_metadata_id_seq + + def self.name + 'TestSwitchJobMetadata' + end + end + end + + let(:table_rollout_flag) { :ci_partitioning_use_test_routing_table } + + let(:partitioned_model) { model::Partitioned } + + let(:jobs_model) do + Class.new(Ci::ApplicationRecord) do + self.primary_key = :id + self.table_name = :_test_ci_jobs + + def self.name + 'TestSwitchJob' + end + end + end + + before do + allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited) + + create_tables(<<~SQL) + CREATE TABLE _test_ci_jobs_metadata( + id serial NOT NULL PRIMARY KEY, + job_id int, + partition_id int NOT NULL DEFAULT 1, + expanded_environment_name text); + + CREATE TABLE _test_p_ci_jobs_metadata ( + LIKE _test_ci_jobs_metadata INCLUDING DEFAULTS + ) PARTITION BY LIST(partition_id); + + ALTER TABLE _test_p_ci_jobs_metadata + ADD CONSTRAINT _test_p_ci_jobs_metadata_id_partition_id + UNIQUE (id, partition_id); + + ALTER TABLE _test_p_ci_jobs_metadata + ATTACH PARTITION _test_ci_jobs_metadata FOR VALUES IN (1); + + CREATE TABLE _test_ci_jobs(id serial NOT NULL PRIMARY KEY); + SQL + + stub_const('Ci::Partitionable::Testing::PARTITIONABLE_MODELS', [model.name]) + + model.include(Ci::Partitionable) + + model.partitionable scope: ->(r) { 1 }, + through: { table: :_test_p_ci_jobs_metadata, flag: table_rollout_flag } + + model.belongs_to :job, anonymous_class: jobs_model + + jobs_model.has_one :metadata, anonymous_class: model, + foreign_key: :job_id, inverse_of: :job, + dependent: :destroy + + allow(Feature::Definition).to receive(:get).and_call_original + allow(Feature::Definition).to receive(:get).with(table_rollout_flag) + .and_return( + Feature::Definition.new("development/#{table_rollout_flag}.yml", + { type: 'development', name: table_rollout_flag } + ) + ) + end + + it { expect(model).not_to be_routing_class } + + it { expect(partitioned_model).to be_routing_class } + + it { expect(partitioned_model.table_name).to eq('_test_p_ci_jobs_metadata') } + + it { expect(partitioned_model.quoted_table_name).to eq('"_test_p_ci_jobs_metadata"') } + + it { expect(partitioned_model.arel_table.name).to eq('_test_p_ci_jobs_metadata') } + + it { expect(partitioned_model.sequence_name).to eq('_test_ci_jobs_metadata_id_seq') } + + context 'when switching the tables' do + before do + stub_feature_flags(table_rollout_flag => false) + end + + %i[table_name quoted_table_name arel_table predicate_builder].each do |name| + it "switches #{name} to routing table and rollbacks" do + old_value = model.public_send(name) + routing_value = partitioned_model.public_send(name) + + expect(old_value).not_to eq(routing_value) + + expect { stub_feature_flags(table_rollout_flag => true) } + .to change(model, name).from(old_value).to(routing_value) + + expect { stub_feature_flags(table_rollout_flag => false) } + .to change(model, name).from(routing_value).to(old_value) + end + end + + it 'can switch aggregate methods' do + rollout_and_rollback_flag( + -> { expect(sql { model.count }).to all match(/FROM "_test_ci_jobs_metadata"/) }, + -> { expect(sql { model.count }).to all match(/FROM "_test_p_ci_jobs_metadata"/) } + ) + end + + it 'can switch reads' do + rollout_and_rollback_flag( + -> { expect(sql { model.last }).to all match(/FROM "_test_ci_jobs_metadata"/) }, + -> { expect(sql { model.last }).to all match(/FROM "_test_p_ci_jobs_metadata"/) } + ) + end + + it 'can switch inserts' do + rollout_and_rollback_flag( + -> { + expect(sql(filter: /INSERT/) { model.create! }) + .to all match(/INSERT INTO "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /INSERT/) { model.create! }) + .to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'can switch deletes' do + 3.times { model.create! } + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /DELETE/) { model.last.destroy! }) + .to all match(/DELETE FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /DELETE/) { model.last.destroy! }) + .to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + context 'with associations' do + let(:job) { jobs_model.create! } + + it 'reads' do + model.create!(job_id: job.id) + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.find(job.id).metadata }) + .to all match(/FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.find(job.id).metadata }) + .to all match(/FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'writes' do + rollout_and_rollback_flag( + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! }) + .to all match(/INSERT INTO "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! }) + .to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'deletes' do + 3.times do + job = jobs_model.create! + job.create_metadata! + end + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! }) + .to all match(/DELETE FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! }) + .to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'can switch joins from jobs' do + rollout_and_rollback_flag( + -> { + expect(sql { jobs_model.joins(:metadata).last }) + .to all match(/INNER JOIN "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql { jobs_model.joins(:metadata).last }) + .to all match(/INNER JOIN "_test_p_ci_jobs_metadata"/) + } + ) + end + + it 'can switch joins from metadata' do + rollout_and_rollback_flag( + -> { + expect(sql { model.joins(:job).last }) + .to all match(/FROM "_test_ci_jobs_metadata" INNER JOIN "_test_ci_jobs"/) + }, + -> { + expect(sql { model.joins(:job).last }) + .to all match(/FROM "_test_p_ci_jobs_metadata" INNER JOIN "_test_ci_jobs"/) + } + ) + end + + it 'preloads' do + job = jobs_model.create! + job.create_metadata! + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.preload(:metadata).last }) + .to all match(/FROM "_test_ci_jobs_metadata"/) + }, + -> { + expect(sql(filter: /jobs_metadata/) { jobs_model.preload(:metadata).last }) + .to all match(/FROM "_test_p_ci_jobs_metadata"/) + } + ) + end + + context 'with nested attributes' do + before do + jobs_model.accepts_nested_attributes_for :metadata + end + + it 'writes' do + attrs = { metadata_attributes: { expanded_environment_name: 'test_env_name' } } + + rollout_and_rollback_flag( + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) }) + .to all match(/INSERT INTO "_test_ci_jobs_metadata" .* 'test_env_name'/) + }, + -> { + expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) }) + .to all match(/INSERT INTO "_test_p_ci_jobs_metadata" .* 'test_env_name'/) + } + ) + end + end + end + end + + def rollout_and_rollback_flag(old, new) + # Load class and SQL statements cache + old.call + + stub_feature_flags(table_rollout_flag => true) + + # Test switch + new.call + + stub_feature_flags(table_rollout_flag => false) + + # Test that it can switch back in the same process + old.call + end + + def create_tables(table_sql) + Ci::ApplicationRecord.connection.execute(table_sql) + end + + def sql(filter: nil, &block) + result = ActiveRecord::QueryRecorder.new(&block) + result = result.log + + return result unless filter + + result.select { |statement| statement.match?(filter) } + end +end diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb index d53501ccc3d..f3d33c971c7 100644 --- a/spec/models/concerns/ci/partitionable_spec.rb +++ b/spec/models/concerns/ci/partitionable_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Ci::Partitionable do - describe 'partitionable models inclusion' do - let(:ci_model) { Class.new(Ci::ApplicationRecord) } + let(:ci_model) { Class.new(Ci::ApplicationRecord) } + describe 'partitionable models inclusion' do subject { ci_model.include(described_class) } it 'raises an exception' do @@ -23,4 +23,21 @@ RSpec.describe Ci::Partitionable do end end end + + context 'with through options' do + before do + allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited) + stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name]) + + ci_model.include(described_class) + ci_model.partitionable scope: ->(r) { 1 }, + through: { table: :_test_table_name, flag: :some_flag } + end + + it { expect(ci_model.routing_table_name).to eq(:_test_table_name) } + + it { expect(ci_model.routing_table_name_flag).to eq(:some_flag) } + + it { expect(ci_model.ancestors).to include(described_class::Switch) } + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 8842a36f40a..43ec0559eb3 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -1055,6 +1055,22 @@ RSpec.describe Issuable do end end + describe '#supports_confidentiality?' do + where(:issuable_type, :supports_confidentiality) do + :issue | true + :incident | true + :merge_request | false + end + + with_them do + let(:issuable) { build_stubbed(issuable_type) } + + subject { issuable.supports_confidentiality? } + + it { is_expected.to eq(supports_confidentiality) } + end + end + describe '#severity' do subject { issuable.severity } diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb index 3e42a3504ac..5a693f084e6 100644 --- a/spec/models/concerns/pg_full_text_searchable_spec.rb +++ b/spec/models/concerns/pg_full_text_searchable_spec.rb @@ -76,7 +76,7 @@ RSpec.describe PgFullTextSearchable do end describe '.pg_full_text_search' do - let(:english) { model_class.create!(project: project, title: 'title', description: 'something english') } + let(:english) { model_class.create!(project: project, title: 'title', description: 'something description english') } let(:with_accent) { model_class.create!(project: project, title: 'Jürgen', description: 'Ærøskøbing') } let(:japanese) { model_class.create!(project: project, title: '日本語 title', description: 'another english description') } @@ -90,8 +90,19 @@ RSpec.describe PgFullTextSearchable do expect(model_class.pg_full_text_search('title english')).to contain_exactly(english, japanese) end + it 'searches specified columns only' do + matching_object = model_class.create!(project: project, title: 'english', description: 'some description') + matching_object.update_search_data! + + expect(model_class.pg_full_text_search('english', matched_columns: %w(title))).to contain_exactly(matching_object) + end + + it 'uses prefix matching' do + expect(model_class.pg_full_text_search('tit eng')).to contain_exactly(english, japanese) + end + it 'searches for exact term with quotes' do - expect(model_class.pg_full_text_search('"something english"')).to contain_exactly(english) + expect(model_class.pg_full_text_search('"description english"')).to contain_exactly(english) end it 'ignores accents' do @@ -113,6 +124,16 @@ RSpec.describe PgFullTextSearchable do expect(model_class.pg_full_text_search('gopher://gitlab.com/gitlab-org/gitlab')).to contain_exactly(with_url) end end + + context 'when text has numbers preceded by a dash' do + let(:with_dash) { model_class.create!(project: project, title: 'issue with dash', description: 'ABC-123') } + + it 'allows searching by numbers only' do + with_dash.update_search_data! + + expect(model_class.pg_full_text_search('123')).to contain_exactly(with_dash) + end + end end describe '#update_search_data!' do |