rollout 2.6.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/release.yml +51 -27
- data/.github/workflows/test.yml +46 -6
- data/.gitignore +2 -0
- data/README.md +87 -24
- data/Rakefile +15 -2
- data/docs/upgrading-to-v3.md +101 -0
- data/lib/rollout/feature.rb +28 -29
- data/lib/rollout/feature_state.rb +44 -0
- data/lib/rollout/logging.rb +20 -76
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +56 -50
- data/rollout.gemspec +6 -5
- data/spec/rollout/feature_spec.rb +70 -39
- data/spec/rollout/feature_state_spec.rb +219 -0
- data/spec/rollout/logging_spec.rb +81 -110
- data/spec/rollout/memory_backend_contract_spec.rb +8 -0
- data/spec/rollout_spec.rb +120 -602
- data/spec/spec_helper.rb +69 -10
- data/spec/support/backend_contract.rb +88 -0
- metadata +12 -29
- data/.circleci/config.yml +0 -119
data/spec/rollout_spec.rb
CHANGED
|
@@ -1,677 +1,195 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
|
|
3
|
-
RSpec.describe
|
|
4
|
-
let(:rollout) {
|
|
3
|
+
RSpec.describe Rollout do
|
|
4
|
+
let(:rollout) { described_class.new(adapter: RolloutMemoryBackend.new) }
|
|
5
5
|
|
|
6
|
-
describe "
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
rollout.
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
it "the feature is active for users for which the block evaluates to true" do
|
|
13
|
-
expect(rollout).to be_active(:chat, double(id: 5))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
it "is not active for users for which the block evaluates to false" do
|
|
17
|
-
expect(rollout).not_to be_active(:chat, double(id: 1))
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "is not active if a group is found in Redis but not defined in Rollout" do
|
|
21
|
-
rollout.activate_group(:chat, :fake)
|
|
22
|
-
expect(rollout).not_to be_active(:chat, double(id: 1))
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
describe "the default all group" do
|
|
27
|
-
before do
|
|
28
|
-
rollout.activate_group(:chat, :all)
|
|
29
|
-
end
|
|
6
|
+
describe "#initialize" do
|
|
7
|
+
it "exposes the adapter" do
|
|
8
|
+
adapter = RolloutMemoryBackend.new
|
|
9
|
+
rollout = described_class.new(adapter: adapter)
|
|
30
10
|
|
|
31
|
-
|
|
32
|
-
expect(rollout).to be_active(:chat, double(id: 0))
|
|
11
|
+
expect(rollout.adapter).to eq adapter
|
|
33
12
|
end
|
|
34
13
|
end
|
|
35
14
|
|
|
36
|
-
describe "
|
|
37
|
-
|
|
38
|
-
rollout.
|
|
39
|
-
rollout.
|
|
40
|
-
rollout.activate_group(:chat, :some)
|
|
41
|
-
rollout.activate_group(:chat, :fivesonly)
|
|
42
|
-
rollout.deactivate_group(:chat, :all)
|
|
43
|
-
rollout.deactivate_group(:chat, "some")
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
it "deactivates the rules for that group" do
|
|
47
|
-
expect(rollout).not_to be_active(:chat, double(id: 10))
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it "leaves the other groups active" do
|
|
51
|
-
expect(rollout.get(:chat).groups).to eq [:fivesonly]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "leaves the other groups active using sets" do
|
|
55
|
-
@options = rollout.instance_variable_get("@options")
|
|
56
|
-
@options[:use_sets] = true
|
|
57
|
-
expect(rollout.get(:chat).groups).to eq [:fivesonly].to_set
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
describe "deactivating a feature completely" do
|
|
62
|
-
before do
|
|
63
|
-
rollout.define_group(:fivesonly) { |user| user.id == 5 }
|
|
64
|
-
rollout.activate_group(:chat, :all)
|
|
65
|
-
rollout.activate_group(:chat, :fivesonly)
|
|
66
|
-
rollout.activate_user(:chat, double(id: 51))
|
|
67
|
-
rollout.activate_percentage(:chat, 100)
|
|
68
|
-
rollout.activate(:chat)
|
|
69
|
-
rollout.deactivate(:chat)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
it "removes all of the groups" do
|
|
73
|
-
expect(rollout).not_to be_active(:chat, double(id: 0))
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
it "removes all of the users" do
|
|
77
|
-
expect(rollout).not_to be_active(:chat, double(id: 51))
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
it "removes the percentage" do
|
|
81
|
-
expect(rollout).not_to be_active(:chat, double(id: 24))
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it "removes globally" do
|
|
85
|
-
expect(rollout).not_to be_active(:chat)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
describe "activating a specific user" do
|
|
90
|
-
before do
|
|
91
|
-
rollout.activate_user(:chat, double(id: 42))
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it "is active for that user" do
|
|
95
|
-
expect(rollout).to be_active(:chat, double(id: 42))
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
it "remains inactive for other users" do
|
|
99
|
-
expect(rollout).not_to be_active(:chat, double(id: 24))
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
describe "activating a specific user by ID" do
|
|
104
|
-
before do
|
|
105
|
-
rollout.activate_user(:chat, 42)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
it "is active for that user" do
|
|
109
|
-
expect(rollout).to be_active(:chat, double(id: 42))
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
it "remains inactive for other users" do
|
|
113
|
-
expect(rollout).not_to be_active(:chat, double(id: 24))
|
|
15
|
+
describe "#get" do
|
|
16
|
+
it "preserves the requested name type" do
|
|
17
|
+
expect(rollout.get("chat").name).to eq "chat"
|
|
18
|
+
expect(rollout.get(:chat).name).to eq :chat
|
|
114
19
|
end
|
|
115
20
|
end
|
|
116
21
|
|
|
117
|
-
describe "
|
|
118
|
-
|
|
119
|
-
rollout.
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it "is active for that user" do
|
|
123
|
-
expect(rollout).to be_active(:chat, double(id: "user-72"))
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
it "remains inactive for other users" do
|
|
127
|
-
expect(rollout).not_to be_active(:chat, double(id: "user-12"))
|
|
22
|
+
describe "#multi_get" do
|
|
23
|
+
it "preserves the requested name types" do
|
|
24
|
+
expect(rollout.multi_get("chat", :beta).map(&:name)).to eq ["chat", :beta]
|
|
128
25
|
end
|
|
129
26
|
end
|
|
130
27
|
|
|
131
|
-
describe "
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
before { rollout.activate_users(:chat, users) }
|
|
136
|
-
|
|
137
|
-
it "is active for the given users" do
|
|
138
|
-
users.each { |user| expect(rollout).to be_active(:chat, user) }
|
|
28
|
+
describe "#with_feature" do
|
|
29
|
+
it "preserves the requested name type" do
|
|
30
|
+
feature = rollout.with_feature("chat") do |current|
|
|
31
|
+
current.percentage = 25
|
|
139
32
|
end
|
|
140
33
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
end
|
|
34
|
+
expect(feature.name).to eq "chat"
|
|
35
|
+
expect(rollout.get("chat").name).to eq "chat"
|
|
144
36
|
end
|
|
145
37
|
|
|
146
|
-
|
|
147
|
-
|
|
38
|
+
it "persists a mutation when nested without resets the thread flag" do
|
|
39
|
+
rollout = described_class.new(adapter: RolloutMemoryBackend.new, logging: true)
|
|
148
40
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
41
|
+
rollout.logging.without do
|
|
42
|
+
rollout.with_feature(:chat) do |feature|
|
|
43
|
+
feature.percentage = 25
|
|
44
|
+
rollout.logging.without {}
|
|
45
|
+
end
|
|
153
46
|
end
|
|
154
47
|
|
|
155
|
-
|
|
156
|
-
expect(rollout).not_to be_active(:chat, 4)
|
|
157
|
-
end
|
|
48
|
+
expect(rollout.get(:chat).percentage).to eq 25.0
|
|
158
49
|
end
|
|
159
|
-
end
|
|
160
50
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
rollout.activate_user(:chat, double(id: 4242))
|
|
165
|
-
rollout.activate_user(:chat, double(id: 24))
|
|
166
|
-
rollout.deactivate_user(:chat, double(id: 42))
|
|
167
|
-
rollout.deactivate_user(:chat, double(id: "4242"))
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
it "that user should no longer be active" do
|
|
171
|
-
expect(rollout).not_to be_active(:chat, double(id: 42))
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
it "remains active for other active users" do
|
|
175
|
-
@options = rollout.instance_variable_get("@options")
|
|
176
|
-
@options[:use_sets] = false
|
|
177
|
-
expect(rollout.get(:chat).users).to eq %w(24)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
it "remains active for other active users using sets" do
|
|
181
|
-
@options = rollout.instance_variable_get("@options")
|
|
182
|
-
@options[:use_sets] = true
|
|
183
|
-
|
|
184
|
-
expect(rollout.get(:chat).users).to eq %w(24).to_set
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
describe "deactivating a group of users" do
|
|
189
|
-
context "specified by user objects" do
|
|
190
|
-
let(:active_users) { [double(id: 1), double(id: 2)] }
|
|
191
|
-
let(:inactive_users) { [double(id: 3), double(id: 4)] }
|
|
192
|
-
|
|
193
|
-
before do
|
|
194
|
-
rollout.activate_users(:chat, active_users + inactive_users)
|
|
195
|
-
rollout.deactivate_users(:chat, inactive_users)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
it "is active for the active users" do
|
|
199
|
-
active_users.each { |user| expect(rollout).to be_active(:chat, user) }
|
|
200
|
-
end
|
|
51
|
+
it "does not notify an observer registered during a mutation" do
|
|
52
|
+
observer = double("observer")
|
|
53
|
+
expect(observer).not_to receive(:update)
|
|
201
54
|
|
|
202
|
-
|
|
203
|
-
|
|
55
|
+
rollout.with_feature(:chat) do |feature|
|
|
56
|
+
feature.percentage = 25
|
|
57
|
+
rollout.add_observer(observer)
|
|
204
58
|
end
|
|
205
59
|
end
|
|
206
60
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
rollout.deactivate_users(:chat, inactive_users)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
it "is active for the active users" do
|
|
217
|
-
active_users.each { |user| expect(rollout).to be_active(:chat, user) }
|
|
61
|
+
it "notifies observers registered before a mutation" do
|
|
62
|
+
observer = double("observer")
|
|
63
|
+
expect(observer).to receive(:update) do |_event, before, after|
|
|
64
|
+
expect(before.name).to eq :chat
|
|
65
|
+
expect(before.percentage).to eq 0
|
|
66
|
+
expect(after.percentage).to eq 25
|
|
218
67
|
end
|
|
219
68
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
describe 'set a group of users' do
|
|
228
|
-
it 'should replace the users with the given array' do
|
|
229
|
-
users = %w(1 2 3 4)
|
|
230
|
-
rollout.activate_users(:chat, %w(10 20 30))
|
|
231
|
-
rollout.set_users(:chat, users)
|
|
232
|
-
expect(rollout.get(:chat).users).to eq(users)
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
describe "activating a feature globally" do
|
|
237
|
-
before do
|
|
238
|
-
rollout.activate(:chat)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
it "activates the feature" do
|
|
242
|
-
expect(rollout).to be_active(:chat)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
it "sets @data to empty hash" do
|
|
246
|
-
expect(rollout.get(:chat).data).to eq({})
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
describe "activating a feature for a percentage of users" do
|
|
251
|
-
before do
|
|
252
|
-
rollout.activate_percentage(:chat, 20)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
it "activates the feature for that percentage of the users" do
|
|
256
|
-
expect((1..100).select { |id| rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(20)
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
describe "activating a feature for a percentage of users" do
|
|
261
|
-
before do
|
|
262
|
-
rollout.activate_percentage(:chat, 20)
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
it "activates the feature for that percentage of the users" do
|
|
266
|
-
expect((1..200).select { |id| rollout.active?(:chat, double(id: id)) }.length).to be_within(4).of(40)
|
|
69
|
+
rollout.add_observer(observer)
|
|
70
|
+
rollout.activate_percentage(:chat, 25)
|
|
267
71
|
end
|
|
268
|
-
end
|
|
269
72
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
73
|
+
it "does not persist when the mutation block raises" do
|
|
74
|
+
expect {
|
|
75
|
+
rollout.with_feature(:chat) { raise "nope" }
|
|
76
|
+
}.to raise_error("nope")
|
|
274
77
|
|
|
275
|
-
|
|
276
|
-
expect((1..100).select { |id| rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(5)
|
|
78
|
+
expect(rollout.exists?(:chat)).to eq false
|
|
277
79
|
end
|
|
278
|
-
end
|
|
279
80
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
rollout.
|
|
283
|
-
end
|
|
81
|
+
it "records a history event only when logging is enabled and state changes" do
|
|
82
|
+
backend = RolloutMemoryBackend.new
|
|
83
|
+
rollout = described_class.new(adapter: backend, logging: true)
|
|
284
84
|
|
|
285
|
-
|
|
286
|
-
expect(
|
|
287
|
-
end
|
|
288
|
-
end
|
|
85
|
+
rollout.activate_percentage(:chat, 25)
|
|
86
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
289
87
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
rollout.activate_percentage(:chat, 20)
|
|
293
|
-
rollout.activate_percentage(:beta, 20)
|
|
294
|
-
@options = rollout.instance_variable_get("@options")
|
|
88
|
+
rollout.activate_percentage(:chat, 25)
|
|
89
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
295
90
|
end
|
|
296
91
|
|
|
297
|
-
it "
|
|
298
|
-
|
|
299
|
-
chat_users = (1..100).select { |id| rollout.active?(:chat, double(id: id)) }
|
|
300
|
-
beta_users = (1..100).select { |id| rollout.active?(:beta, double(id: id)) }
|
|
301
|
-
expect(chat_users).not_to eq beta_users
|
|
302
|
-
end
|
|
303
|
-
it "activates the feature for the same set of users when opt is not set" do
|
|
304
|
-
@options[:randomize_percentage] = false
|
|
305
|
-
chat_users = (1..100).select { |id| rollout.active?(:chat, double(id: id)) }
|
|
306
|
-
beta_users = (1..100).select { |id| rollout.active?(:beta, double(id: id)) }
|
|
307
|
-
expect(chat_users).to eq beta_users
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
describe "activating a feature for a group as a string" do
|
|
312
|
-
before do
|
|
313
|
-
rollout.define_group(:admins) { |user| user.id == 5 }
|
|
314
|
-
rollout.activate_group(:chat, "admins")
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
it "the feature is active for users for which the block evaluates to true" do
|
|
318
|
-
expect(rollout).to be_active(:chat, double(id: 5))
|
|
319
|
-
end
|
|
92
|
+
it "canonicalizes metadata assigned during a mutation" do
|
|
93
|
+
released_at = Time.utc(2026, 1, 1)
|
|
320
94
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
describe "deactivating the percentage of users" do
|
|
327
|
-
before do
|
|
328
|
-
rollout.activate_percentage(:chat, 100)
|
|
329
|
-
rollout.deactivate_percentage(:chat)
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
it "becomes inactivate for all users" do
|
|
333
|
-
expect(rollout).not_to be_active(:chat, double(id: 24))
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
describe "deactivating the feature globally" do
|
|
338
|
-
before do
|
|
339
|
-
rollout.activate(:chat)
|
|
340
|
-
rollout.deactivate(:chat)
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
it "becomes inactivate" do
|
|
344
|
-
expect(rollout).not_to be_active(:chat)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
describe "setting a feature on" do
|
|
349
|
-
before do
|
|
350
|
-
rollout.set(:chat, true)
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
it "becomes activated" do
|
|
354
|
-
expect(rollout).to be_active(:chat)
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
describe "setting a feature off" do
|
|
359
|
-
before do
|
|
360
|
-
rollout.set(:chat, false)
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
it "becomes inactivated" do
|
|
364
|
-
expect(rollout).not_to be_active(:chat)
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
describe "deleting a feature" do
|
|
369
|
-
before do
|
|
370
|
-
rollout.set(:chat, true)
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
context "when feature was passed as string" do
|
|
374
|
-
it "should be removed from features list" do
|
|
375
|
-
expect(rollout.features.size).to eq 1
|
|
376
|
-
rollout.delete('chat')
|
|
377
|
-
expect(rollout.features.size).to eq 0
|
|
95
|
+
rollout.with_feature(:chat) do |feature|
|
|
96
|
+
feature.data["released_at"] = released_at
|
|
97
|
+
feature.data["label"] = :beta
|
|
378
98
|
end
|
|
379
|
-
end
|
|
380
99
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
expect(rollout.features.size).to eq 0
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
it "should have metadata cleared" do
|
|
388
|
-
expect(rollout.get(:chat).percentage).to eq 100
|
|
389
|
-
rollout.delete(:chat)
|
|
390
|
-
expect(rollout.get(:chat).percentage).to eq 0
|
|
391
|
-
end
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
describe "keeps a list of features" do
|
|
395
|
-
it "saves the feature" do
|
|
396
|
-
rollout.activate(:chat)
|
|
397
|
-
expect(rollout.features).to be_include(:chat)
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
it "does not contain doubles" do
|
|
401
|
-
rollout.activate(:chat)
|
|
402
|
-
rollout.activate(:chat)
|
|
403
|
-
expect(rollout.features.size).to eq(1)
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
it "does not contain doubles when using string" do
|
|
407
|
-
rollout.activate(:chat)
|
|
408
|
-
rollout.activate("chat")
|
|
409
|
-
expect(rollout.features.size).to eq(1)
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
describe "#get" do
|
|
414
|
-
before do
|
|
415
|
-
rollout.activate_percentage(:chat, 10)
|
|
416
|
-
rollout.activate_group(:chat, :caretakers)
|
|
417
|
-
rollout.activate_group(:chat, :greeters)
|
|
418
|
-
rollout.activate(:signup)
|
|
419
|
-
rollout.activate_user(:chat, double(id: 42))
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
it "returns the feature object" do
|
|
423
|
-
feature = rollout.get(:chat)
|
|
424
|
-
expect(feature.groups).to eq [:caretakers, :greeters]
|
|
425
|
-
expect(feature.percentage).to eq 10
|
|
426
|
-
expect(feature.users).to eq %w(42)
|
|
427
|
-
expect(feature.to_hash).to eq(
|
|
428
|
-
groups: [:caretakers, :greeters],
|
|
429
|
-
percentage: 10,
|
|
430
|
-
users: %w(42),
|
|
431
|
-
data: {},
|
|
100
|
+
expect(rollout.get(:chat).data).to eq(
|
|
101
|
+
"released_at" => JSON.parse({ "value" => released_at }.to_json)["value"],
|
|
102
|
+
"label" => "beta",
|
|
432
103
|
)
|
|
433
|
-
|
|
434
|
-
feature = rollout.get(:signup)
|
|
435
|
-
expect(feature.groups).to be_empty
|
|
436
|
-
expect(feature.users).to be_empty
|
|
437
|
-
expect(feature.percentage).to eq(100)
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
it "returns the feature objects using sets" do
|
|
441
|
-
@options = rollout.instance_variable_get("@options")
|
|
442
|
-
@options[:use_sets] = true
|
|
443
|
-
|
|
444
|
-
feature = rollout.get(:chat)
|
|
445
|
-
expect(feature.groups).to eq [:caretakers, :greeters].to_set
|
|
446
|
-
expect(feature.percentage).to eq 10
|
|
447
|
-
expect(feature.users).to eq %w(42).to_set
|
|
448
|
-
expect(feature.to_hash).to eq(
|
|
449
|
-
groups: [:caretakers, :greeters].to_set,
|
|
450
|
-
percentage: 10,
|
|
451
|
-
users: %w(42).to_set,
|
|
452
|
-
data: {},
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
feature = rollout.get(:signup)
|
|
456
|
-
expect(feature.groups).to be_empty
|
|
457
|
-
expect(feature.users).to be_empty
|
|
458
|
-
expect(feature.percentage).to eq(100)
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
describe "#clear" do
|
|
463
|
-
let(:features) { %w(signup beta alpha gm) }
|
|
464
|
-
|
|
465
|
-
before do
|
|
466
|
-
features.each { |f| rollout.activate(f) }
|
|
467
|
-
|
|
468
|
-
rollout.clear!
|
|
469
104
|
end
|
|
470
105
|
|
|
471
|
-
it "
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
users: [],
|
|
476
|
-
groups: [],
|
|
477
|
-
data: {},
|
|
478
|
-
)
|
|
479
|
-
end
|
|
480
|
-
end
|
|
106
|
+
it "does not record a history event when metadata JSON is unchanged" do
|
|
107
|
+
backend = RolloutMemoryBackend.new
|
|
108
|
+
rollout = described_class.new(adapter: backend, logging: true)
|
|
109
|
+
released_at = Time.utc(2026, 1, 1)
|
|
481
110
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
@options[:use_sets] = true
|
|
485
|
-
features.each do |feature|
|
|
486
|
-
expect(rollout.get(feature).to_hash).to eq(
|
|
487
|
-
percentage: 0,
|
|
488
|
-
users: Set.new,
|
|
489
|
-
groups: Set.new,
|
|
490
|
-
data: {},
|
|
491
|
-
)
|
|
492
|
-
end
|
|
493
|
-
end
|
|
111
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: "beta")
|
|
112
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
494
113
|
|
|
495
|
-
|
|
496
|
-
expect(
|
|
114
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: :beta)
|
|
115
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
497
116
|
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
describe "#feature_states" do
|
|
501
|
-
let(:user_double) { double(id: 7) }
|
|
502
|
-
|
|
503
|
-
before do
|
|
504
|
-
rollout.activate(:chat)
|
|
505
|
-
rollout.activate_user(:video, user_double)
|
|
506
|
-
rollout.deactivate(:vr)
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
it "returns a hash" do
|
|
510
|
-
expect(rollout.feature_states).to be_a(Hash)
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
context "with user argument" do
|
|
514
|
-
it "maps active feature as true" do
|
|
515
|
-
state = rollout.feature_states(user_double)[:video]
|
|
516
|
-
expect(state).to eq(true)
|
|
517
|
-
end
|
|
518
117
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
end
|
|
118
|
+
it "records canonical metadata in history events" do
|
|
119
|
+
backend = RolloutMemoryBackend.new
|
|
120
|
+
rollout = described_class.new(adapter: backend, logging: true)
|
|
121
|
+
released_at = Time.utc(2026, 1, 1)
|
|
524
122
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
state = rollout.feature_states[:chat]
|
|
528
|
-
expect(state).to eq(true)
|
|
529
|
-
end
|
|
123
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: :beta)
|
|
124
|
+
event = backend.feature_events(:chat).last
|
|
530
125
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
126
|
+
expect(event.data).to eq(
|
|
127
|
+
before: { "data.released_at" => nil, "data.label" => nil },
|
|
128
|
+
after: {
|
|
129
|
+
"data.released_at" => JSON.parse({ "value" => released_at }.to_json)["value"],
|
|
130
|
+
"data.label" => "beta",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
535
133
|
end
|
|
536
134
|
end
|
|
537
135
|
|
|
538
|
-
describe "#
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
before do
|
|
542
|
-
rollout.activate(:chat)
|
|
543
|
-
rollout.activate_user(:video, user_double)
|
|
544
|
-
rollout.deactivate(:vr)
|
|
136
|
+
describe "#set_feature_data" do
|
|
137
|
+
def json_value(value)
|
|
138
|
+
JSON.parse({ "value" => value }.to_json)["value"]
|
|
545
139
|
end
|
|
546
140
|
|
|
547
|
-
it "
|
|
548
|
-
|
|
549
|
-
end
|
|
141
|
+
it "canonicalizes JSON-serializable metadata" do
|
|
142
|
+
released_at = Time.utc(2026, 1, 1)
|
|
550
143
|
|
|
551
|
-
|
|
552
|
-
it "includes active feature" do
|
|
553
|
-
features = rollout.active_features(user_double)
|
|
554
|
-
expect(features).to include(:video)
|
|
555
|
-
expect(features).to include(:chat)
|
|
556
|
-
end
|
|
144
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: :beta)
|
|
557
145
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
146
|
+
expect(rollout.get(:chat).data).to eq(
|
|
147
|
+
"released_at" => json_value(released_at),
|
|
148
|
+
"label" => "beta",
|
|
149
|
+
)
|
|
562
150
|
end
|
|
563
151
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
it "excludes inactive feature" do
|
|
571
|
-
features = rollout.active_features
|
|
572
|
-
expect(features).to_not include(:video)
|
|
573
|
-
end
|
|
574
|
-
end
|
|
575
|
-
end
|
|
152
|
+
it "does not persist metadata that cannot be serialized as JSON" do
|
|
153
|
+
rollout.set_feature_data(:chat, description: "foo")
|
|
154
|
+
cyclic = {}
|
|
155
|
+
cyclic["self"] = cyclic
|
|
576
156
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
expect(rollout.user_in_active_users?(:chat, "5")).to eq(true)
|
|
581
|
-
end
|
|
157
|
+
expect {
|
|
158
|
+
rollout.set_feature_data(:chat, cyclic)
|
|
159
|
+
}.to raise_error(JSON::JSONError)
|
|
582
160
|
|
|
583
|
-
|
|
584
|
-
rollout.activate_group(:chat, :all)
|
|
585
|
-
expect(rollout.user_in_active_users?(:chat, "5")).to eq(false)
|
|
161
|
+
expect(rollout.get(:chat).data).to eq("description" => "foo")
|
|
586
162
|
end
|
|
587
163
|
end
|
|
588
164
|
|
|
589
|
-
describe "#
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
rollout.activate_group(:videos, :greeters)
|
|
594
|
-
rollout.activate(:signup)
|
|
595
|
-
rollout.activate_user(:photos, double(id: 42))
|
|
596
|
-
end
|
|
165
|
+
describe "#clear!" do
|
|
166
|
+
it "asks the backend to clear remaining registry state" do
|
|
167
|
+
backend = RolloutMemoryBackend.new
|
|
168
|
+
expect(backend).to receive(:clear_features).and_call_original
|
|
597
169
|
|
|
598
|
-
|
|
599
|
-
features = rollout.multi_get(:chat, :videos, :signup)
|
|
600
|
-
expect(features[0].name).to eq :chat
|
|
601
|
-
expect(features[0].groups).to eq [:caretakers]
|
|
602
|
-
expect(features[0].percentage).to eq 10
|
|
603
|
-
expect(features[1].name).to eq :videos
|
|
604
|
-
expect(features[1].groups).to eq [:greeters]
|
|
605
|
-
expect(features[2].name).to eq :signup
|
|
606
|
-
expect(features[2].percentage).to eq 100
|
|
607
|
-
expect(features.size).to eq 3
|
|
608
|
-
end
|
|
609
|
-
|
|
610
|
-
describe 'when given feature keys is empty' do
|
|
611
|
-
it 'returns empty array' do
|
|
612
|
-
expect(rollout.multi_get(*[])).to match_array([])
|
|
613
|
-
end
|
|
170
|
+
described_class.new(adapter: backend).clear!
|
|
614
171
|
end
|
|
615
172
|
end
|
|
616
173
|
|
|
617
|
-
describe "#
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
it 'sets the data attribute on feature' do
|
|
623
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
it 'updates a data attribute' do
|
|
627
|
-
rollout.set_feature_data(:chat, description: 'baz')
|
|
628
|
-
expect(rollout.get(:chat).data).to include('description' => 'baz', 'release_date' => 'bar')
|
|
629
|
-
end
|
|
174
|
+
describe "#delete" do
|
|
175
|
+
it "does not delete feature history when logging is disabled" do
|
|
176
|
+
backend = RolloutMemoryBackend.new
|
|
177
|
+
logged = described_class.new(adapter: backend, logging: true)
|
|
178
|
+
logged.activate_percentage(:chat, 25)
|
|
630
179
|
|
|
631
|
-
|
|
632
|
-
rollout.set_feature_data(:talk, image_url: 'kittens.png')
|
|
633
|
-
expect(rollout.get(:chat).data).not_to include('image_url' => 'kittens.png')
|
|
634
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
635
|
-
end
|
|
180
|
+
described_class.new(adapter: backend).delete(:chat)
|
|
636
181
|
|
|
637
|
-
|
|
638
|
-
expect(
|
|
639
|
-
rollout.set_feature_data(:chat, nil)
|
|
640
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
182
|
+
expect(logged.exists?(:chat)).to eq false
|
|
183
|
+
expect(logged.logging.events(:chat)).not_to eq []
|
|
641
184
|
end
|
|
642
185
|
|
|
643
|
-
it
|
|
644
|
-
|
|
645
|
-
rollout.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
it 'properly parses data when it contains a |' do
|
|
650
|
-
user = double("User", id: 8)
|
|
651
|
-
rollout.activate_user(:chat, user)
|
|
652
|
-
rollout.set_feature_data(:chat, "|call||text|" => "a|bunch|of|stuff")
|
|
653
|
-
expect(rollout.get(:chat).data).to include("|call||text|" => "a|bunch|of|stuff")
|
|
654
|
-
expect(rollout.active?(:chat, user)).to be true
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
describe "#clear_feature_data" do
|
|
659
|
-
it 'resets data to empty string' do
|
|
660
|
-
rollout.set_feature_data(:chat, description: 'foo')
|
|
661
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo')
|
|
662
|
-
rollout.clear_feature_data(:chat)
|
|
663
|
-
expect(rollout.get(:chat).data).to eq({})
|
|
664
|
-
end
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
describe 'Check if feature exists' do
|
|
668
|
-
it 'it should return true if the feature is exist' do
|
|
669
|
-
rollout.activate_percentage(:chat, 1)
|
|
670
|
-
expect(rollout.exists?(:chat)).to be true
|
|
671
|
-
end
|
|
186
|
+
it "deletes feature history when logging is enabled" do
|
|
187
|
+
backend = RolloutMemoryBackend.new
|
|
188
|
+
rollout = described_class.new(adapter: backend, logging: true)
|
|
189
|
+
rollout.activate_percentage(:chat, 25)
|
|
190
|
+
rollout.delete(:chat)
|
|
672
191
|
|
|
673
|
-
|
|
674
|
-
expect(rollout.exists?(:chat)).to be false
|
|
192
|
+
expect(rollout.logging.events(:chat)).to eq []
|
|
675
193
|
end
|
|
676
194
|
end
|
|
677
195
|
end
|