internal_id_spec.rb 7.3 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5 6 7
require 'spec_helper'

describe InternalId do
  let(:project) { create(:project) }
  let(:usage) { :issues }
8
  let(:issue) { build(:issue, project: project) }
9
  let(:scope) { { project: project } }
10
  let(:init) { ->(s) { s.project.issues.size } }
11

S
Shinya Maeda 已提交
12 13
  it_behaves_like 'having unique enum values'

14 15 16 17
  context 'validations' do
    it { is_expected.to validate_presence_of(:usage) }
  end

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
  describe '.flush_records!' do
    subject { described_class.flush_records!(project: project) }

    let(:another_project) { create(:project) }

    before do
      create_list(:issue, 2, project: project)
      create_list(:issue, 2, project: another_project)
    end

    it 'deletes all records for the given project' do
      expect { subject }.to change { described_class.where(project: project).count }.from(1).to(0)
    end

    it 'retains records for other projects' do
      expect { subject }.not_to change { described_class.where(project: another_project).count }
    end

    it 'does not allow an empty filter' do
      expect { described_class.flush_records!({}) }.to raise_error(/filter cannot be empty/)
    end
  end

41
  describe '.generate_next' do
42
    subject { described_class.generate_next(issue, scope, usage, init) }
43

44
    context 'in the absence of a record' do
45 46 47 48 49 50 51 52 53
      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

        described_class.first.tap do |record|
          expect(record.project).to eq(project)
54
          expect(record.usage).to eq(usage.to_s)
55 56 57 58 59
        end
      end

      context 'with existing issues' do
        before do
60
          create_list(:issue, 2, project: project)
61
          described_class.delete_all
62 63 64 65 66 67
        end

        it 'calculates last_value values automatically' do
          expect(subject).to eq(project.issues.size + 1)
        end
      end
68 69 70 71 72 73 74 75 76 77 78 79 80

      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
        end
      end
81 82 83
    end

    it 'generates a strictly monotone, gapless sequence' do
84
      seq = Array.new(10).map do
85
        described_class.generate_next(issue, scope, usage, init)
86 87
      end
      normalized = seq.map { |i| i - seq.min }
A
Andreas Brandl 已提交
88

89 90
      expect(normalized).to eq((0..seq.size - 1).to_a)
    end
91 92 93 94

    context 'with an insufficient schema version' do
      before do
        described_class.reset_column_information
95
        # Project factory will also call the current_version
96
        expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
97 98 99 100 101 102
      end

      let(:init) { double('block') }

      it 'calculates next internal ids on the fly' do
        val = rand(1..100)
A
Andreas Brandl 已提交
103

104 105 106
        expect(init).to receive(:call).with(issue).and_return(val)
        expect(subject).to eq(val + 1)
      end
107 108 109 110 111 112 113 114 115

      it 'always attempts to generate internal IDs in production mode' do
        allow(Rails.env).to receive(:test?).and_return(false)
        val = rand(1..100)
        generator = double(generate: val)
        expect(InternalId::InternalIdGenerator).to receive(:new).and_return(generator)

        expect(subject).to eq(val)
      end
116
    end
117 118
  end

K
Kamil Trzciński 已提交
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
  describe '.reset' do
    subject { described_class.reset(issue, scope, usage, value) }

    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
      end
    end

    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)
        end
      end

      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
        end
      end

      def generate_next
        described_class.generate_next(issue, scope, usage, init)
      end
    end

    context 'with an insufficient schema version' do
      let(:value) { 2 }

      before do
        described_class.reset_column_information
        # Project factory will also call the current_version
        expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
      end

      it 'does not reset any of the iids' do
        expect(subject).to be_falsey
      end
    end
  end

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
  describe '.track_greatest' do
    let(:value) { 9001 }
    subject { described_class.track_greatest(issue, 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)
      end
    end

    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)
      end
    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)
      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)

        expect(subject).to eq 10_001
      end
    end
  end

210 211
  describe '#increment_and_save!' do
    let(:id) { create(:internal_id) }
212
    subject { id.increment_and_save! }
213 214 215

    it 'returns incremented iid' do
      value = id.last_value
A
Andreas Brandl 已提交
216

217 218 219 220 221
      expect(subject).to eq(value + 1)
    end

    it 'saves the record' do
      subject
A
Andreas Brandl 已提交
222

223 224 225 226 227 228 229 230 231 232 233
      expect(id.changed?).to be_falsey
    end

    context 'with last_value=nil' do
      let(:id) { build(:internal_id, last_value: nil) }

      it 'returns 1' do
        expect(subject).to eq(1)
      end
    end
  end
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259

  describe '#track_greatest_and_save!' do
    let(:id) { create(:internal_id) }
    let(:new_last_value) { 9001 }
    subject { id.track_greatest_and_save!(new_last_value) }

    it 'returns new last value' do
      expect(subject).to eq new_last_value
    end

    it 'saves the record' do
      subject

      expect(id.changed?).to be_falsey
    end

    context 'when new last value is lower than the max' do
      it 'does not update the last value' do
        id.update!(last_value: 10_001)

        subject

        expect(id.reload.last_value).to eq 10_001
      end
    end
  end
260
end