diff options
Diffstat (limited to 'spec/models/internal_id_spec.rb')
-rw-r--r-- | spec/models/internal_id_spec.rb | 279 |
1 files changed, 140 insertions, 139 deletions
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 390d1552c16..696b5b48cbf 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -39,216 +39,217 @@ RSpec.describe InternalId do end end - describe '.generate_next' do - subject { described_class.generate_next(id_subject, scope, usage, init) } + shared_examples_for 'a monotonically increasing id generator' do + describe '.generate_next' do + subject { described_class.generate_next(id_subject, scope, usage, init) } - context 'in the absence of a record' do - it 'creates a record if not yet present' do - expect { subject }.to change { described_class.count }.from(0).to(1) - end + context 'in the absence of a record' do + it 'creates a record if not yet present' do + expect { subject }.to change { described_class.count }.from(0).to(1) + end - it 'stores record attributes' do - subject + it 'stores record attributes' do + subject - described_class.first.tap do |record| - expect(record.project).to eq(project) - expect(record.usage).to eq(usage.to_s) + described_class.first.tap do |record| + expect(record.project).to eq(project) + expect(record.usage).to eq(usage.to_s) + end end - end - context 'with existing issues' do - before do - create_list(:issue, 2, project: project) - described_class.delete_all - end + context 'with existing issues' do + before do + create_list(:issue, 2, project: project) + described_class.delete_all + end - it 'calculates last_value values automatically' do - expect(subject).to eq(project.issues.size + 1) + it 'calculates last_value values automatically' do + expect(subject).to eq(project.issues.size + 1) + end end end - context 'with concurrent inserts on table' do - it 'looks up the record if it was created concurrently' do - args = { **scope, usage: described_class.usages[usage.to_s] } - record = double - expect(described_class).to receive(:find_by).with(args).and_return(nil) # first call, record not present - expect(described_class).to receive(:find_by).with(args).and_return(record) # second call, record was created by another process - expect(described_class).to receive(:create!).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') - expect(record).to receive(:increment_and_save!) - - subject + it 'generates a strictly monotone, gapless sequence' do + seq = Array.new(10).map do + described_class.generate_next(issue, scope, usage, init) end - end - end + normalized = seq.map { |i| i - seq.min } - it 'generates a strictly monotone, gapless sequence' do - seq = Array.new(10).map do - described_class.generate_next(issue, scope, usage, init) + expect(normalized).to eq((0..seq.size - 1).to_a) end - normalized = seq.map { |i| i - seq.min } - - expect(normalized).to eq((0..seq.size - 1).to_a) - end - context 'there are no instances to pass in' do - let(:id_subject) { Issue } + context 'there are no instances to pass in' do + let(:id_subject) { Issue } - it 'accepts classes instead' do - expect(subject).to eq(1) + it 'accepts classes instead' do + expect(subject).to eq(1) + end end - end - context 'when executed outside of transaction' do - it 'increments counter with in_transaction: "false"' do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + context 'when executed outside of transaction' do + it 'increments counter with in_transaction: "false"' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } - expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) - .with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original + expect(InternalId.internal_id_transactions_total).to receive(:increment) + .with(operation: :generate, usage: 'issues', in_transaction: 'false').and_call_original - subject + subject + end end - end - context 'when executed within transaction' do - it 'increments counter with in_transaction: "true"' do - expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) - .with(operation: :generate, usage: 'issues', in_transaction: 'true').and_call_original + context 'when executed within transaction' do + it 'increments counter with in_transaction: "true"' do + expect(InternalId.internal_id_transactions_total).to receive(:increment) + .with(operation: :generate, usage: 'issues', in_transaction: 'true').and_call_original - InternalId.transaction { subject } + InternalId.transaction { subject } + end end end - end - describe '.reset' do - subject { described_class.reset(issue, scope, usage, value) } + describe '.reset' do + subject { described_class.reset(issue, scope, usage, value) } - context 'in the absence of a record' do - let(:value) { 2 } + context 'in the absence of a record' do + let(:value) { 2 } - it 'does not revert back the value' do - expect { subject }.not_to change { described_class.count } - expect(subject).to be_falsey + it 'does not revert back the value' do + expect { subject }.not_to change { described_class.count } + expect(subject).to be_falsey + end end - end - context 'when valid iid is used to reset' do - let!(:value) { generate_next } + context 'when valid iid is used to reset' do + let!(:value) { generate_next } - context 'and iid is a latest one' do - it 'does rewind and next generated value is the same' do - expect(subject).to be_truthy - expect(generate_next).to eq(value) + context 'and iid is a latest one' do + it 'does rewind and next generated value is the same' do + expect(subject).to be_truthy + expect(generate_next).to eq(value) + end end - end - context 'and iid is not a latest one' do - it 'does not rewind' do - generate_next + context 'and iid is not a latest one' do + it 'does not rewind' do + generate_next - expect(subject).to be_falsey - expect(generate_next).to be > value + expect(subject).to be_falsey + expect(generate_next).to be > value + end end - end - def generate_next - described_class.generate_next(issue, scope, usage, init) + def generate_next + described_class.generate_next(issue, scope, usage, init) + end end - end - context 'when executed outside of transaction' do - let(:value) { 2 } + context 'when executed outside of transaction' do + let(:value) { 2 } - it 'increments counter with in_transaction: "false"' do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + it 'increments counter with in_transaction: "false"' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } - expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) - .with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original + expect(InternalId.internal_id_transactions_total).to receive(:increment) + .with(operation: :reset, usage: 'issues', in_transaction: 'false').and_call_original - subject + subject + end end - end - context 'when executed within transaction' do - let(:value) { 2 } + context 'when executed within transaction' do + let(:value) { 2 } - it 'increments counter with in_transaction: "true"' do - expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) - .with(operation: :reset, usage: 'issues', in_transaction: 'true').and_call_original + it 'increments counter with in_transaction: "true"' do + expect(InternalId.internal_id_transactions_total).to receive(:increment) + .with(operation: :reset, usage: 'issues', in_transaction: 'true').and_call_original - InternalId.transaction { subject } + InternalId.transaction { subject } + end end end - end - describe '.track_greatest' do - let(:value) { 9001 } + describe '.track_greatest' do + let(:value) { 9001 } - subject { described_class.track_greatest(id_subject, scope, usage, value, init) } + subject { described_class.track_greatest(id_subject, scope, usage, value, init) } - context 'in the absence of a record' do - it 'creates a record if not yet present' do - expect { subject }.to change { described_class.count }.from(0).to(1) + context 'in the absence of a record' do + it 'creates a record if not yet present' do + expect { subject }.to change { described_class.count }.from(0).to(1) + end end - end - it 'stores record attributes' do - subject + it 'stores record attributes' do + subject - described_class.first.tap do |record| - expect(record.project).to eq(project) - expect(record.usage).to eq(usage.to_s) - expect(record.last_value).to eq(value) + described_class.first.tap do |record| + expect(record.project).to eq(project) + expect(record.usage).to eq(usage.to_s) + expect(record.last_value).to eq(value) + end end - end - context 'with existing issues' do - before do - create(:issue, project: project) - described_class.delete_all - end + context 'with existing issues' do + before do + create(:issue, project: project) + described_class.delete_all + end - it 'still returns the last value to that of the given value' do - expect(subject).to eq(value) + it 'still returns the last value to that of the given value' do + expect(subject).to eq(value) + end end - end - context 'when value is less than the current last_value' do - it 'returns the current last_value' do - described_class.create!(**scope, usage: usage, last_value: 10_001) + context 'when value is less than the current last_value' do + it 'returns the current last_value' do + described_class.create!(**scope, usage: usage, last_value: 10_001) - expect(subject).to eq 10_001 + expect(subject).to eq 10_001 + end end - end - context 'there are no instances to pass in' do - let(:id_subject) { Issue } + context 'there are no instances to pass in' do + let(:id_subject) { Issue } - it 'accepts classes instead' do - expect(subject).to eq(value) + it 'accepts classes instead' do + expect(subject).to eq(value) + end end - end - context 'when executed outside of transaction' do - it 'increments counter with in_transaction: "false"' do - allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } + context 'when executed outside of transaction' do + it 'increments counter with in_transaction: "false"' do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?) { false } - expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) - .with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original + expect(InternalId.internal_id_transactions_total).to receive(:increment) + .with(operation: :track_greatest, usage: 'issues', in_transaction: 'false').and_call_original - subject + subject + end end - end - context 'when executed within transaction' do - it 'increments counter with in_transaction: "true"' do - expect(InternalId::InternalIdGenerator.internal_id_transactions_total).to receive(:increment) - .with(operation: :track_greatest, usage: 'issues', in_transaction: 'true').and_call_original + context 'when executed within transaction' do + it 'increments counter with in_transaction: "true"' do + expect(InternalId.internal_id_transactions_total).to receive(:increment) + .with(operation: :track_greatest, usage: 'issues', in_transaction: 'true').and_call_original - InternalId.transaction { subject } + InternalId.transaction { subject } + end end end end + context 'when the feature flag is disabled' do + stub_feature_flags(generate_iids_without_explicit_locking: false) + + it_behaves_like 'a monotonically increasing id generator' + end + + context 'when the feature flag is enabled' do + stub_feature_flags(generate_iids_without_explicit_locking: true) + + it_behaves_like 'a monotonically increasing id generator' + end + describe '#increment_and_save!' do let(:id) { create(:internal_id) } |