1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
|
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe InternalId do
let(:project) { create(:project) }
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:id_subject) { issue }
let(:scope) { { project: project } }
let(:init) { ->(issue, scope) { issue&.project&.issues&.size || Issue.where(**scope).count } }
it_behaves_like 'having unique enum values'
context 'validations' do
it { is_expected.to validate_presence_of(:usage) }
end
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
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
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)
end
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)
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
end
end
end
it 'generates a strictly monotone, gapless sequence' do
seq = Array.new(10).map do
described_class.generate_next(issue, scope, usage, init)
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 }
it 'accepts classes instead' do
expect(subject).to eq(1)
end
end
end
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
end
describe '.track_greatest' do
let(:value) { 9001 }
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)
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
context 'there are no instances to pass in' do
let(:id_subject) { Issue }
it 'accepts classes instead' do
expect(subject).to eq(value)
end
end
end
describe '#increment_and_save!' do
let(:id) { create(:internal_id) }
subject { id.increment_and_save! }
it 'returns incremented iid' do
value = id.last_value
expect(subject).to eq(value + 1)
end
it 'saves the record' do
subject
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
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
end
|