rollout 2.6.2 → 3.2.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 +54 -28
- data/.github/workflows/test.yml +148 -6
- data/.gitignore +2 -0
- data/README.md +105 -25
- data/Rakefile +25 -2
- data/docs/upgrading-to-v3.md +110 -0
- data/lib/rollout/feature.rb +28 -29
- data/lib/rollout/feature_state.rb +44 -0
- data/lib/rollout/logging.rb +32 -76
- data/lib/rollout/version.rb +1 -1
- data/lib/rollout.rb +65 -52
- 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 +155 -586
- data/spec/spec_helper.rb +83 -10
- data/spec/support/backend_contract.rb +95 -0
- data/spec/support/history_contract.rb +174 -0
- data/spec/support/rollout_backend_integration.rb +77 -0
- metadata +14 -29
- data/.circleci/config.yml +0 -119
data/spec/rollout_spec.rb
CHANGED
|
@@ -1,677 +1,246 @@
|
|
|
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
|
|
6
|
+
describe "#initialize" do
|
|
7
|
+
it "exposes the adapter" do
|
|
8
|
+
adapter = RolloutMemoryBackend.new
|
|
9
|
+
rollout = described_class.new(adapter: adapter)
|
|
15
10
|
|
|
16
|
-
|
|
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))
|
|
11
|
+
expect(rollout.adapter).to eq adapter
|
|
23
12
|
end
|
|
24
13
|
end
|
|
25
14
|
|
|
26
|
-
describe "
|
|
27
|
-
|
|
28
|
-
rollout.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
it "evaluates to true no matter what" do
|
|
32
|
-
expect(rollout).to be_active(:chat, double(id: 0))
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
describe "deactivating a group" do
|
|
37
|
-
before do
|
|
38
|
-
rollout.define_group(:fivesonly) { |user| user.id == 5 }
|
|
39
|
-
rollout.activate_group(:chat, :all)
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
before { rollout.activate_users(:chat, users) }
|
|
38
|
+
it "persists a mutation when nested without resets the thread flag" do
|
|
39
|
+
rollout = described_class.new(adapter: RolloutMemoryBackend.new, logging: true)
|
|
150
40
|
|
|
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
|
|
51
|
+
it "does not notify an observer registered during a mutation" do
|
|
52
|
+
observer = double("observer")
|
|
53
|
+
expect(observer).not_to receive(:update)
|
|
187
54
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
it "is not active for inactive users" do
|
|
203
|
-
inactive_users.each { |user| expect(rollout).not_to be_active(:chat, user) }
|
|
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)
|
|
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
|
|
214
67
|
end
|
|
215
68
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
it "is not active for inactive users" do
|
|
221
|
-
inactive_users.each { |user| expect(rollout).not_to be_active(:chat, user) }
|
|
222
|
-
end
|
|
69
|
+
rollout.add_observer(observer)
|
|
70
|
+
rollout.activate_percentage(:chat, 25)
|
|
223
71
|
end
|
|
224
|
-
end
|
|
225
72
|
|
|
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")
|
|
226
77
|
|
|
227
|
-
|
|
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)
|
|
78
|
+
expect(rollout.exists?(:chat)).to eq false
|
|
233
79
|
end
|
|
234
|
-
end
|
|
235
80
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
rollout.
|
|
239
|
-
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)
|
|
240
84
|
|
|
241
|
-
|
|
242
|
-
expect(
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
it "sets @data to empty hash" do
|
|
246
|
-
expect(rollout.get(:chat).data).to eq({})
|
|
247
|
-
end
|
|
248
|
-
end
|
|
85
|
+
rollout.activate_percentage(:chat, 25)
|
|
86
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
249
87
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
rollout.activate_percentage(:chat, 20)
|
|
88
|
+
rollout.activate_percentage(:chat, 25)
|
|
89
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
253
90
|
end
|
|
254
91
|
|
|
255
|
-
it "
|
|
256
|
-
|
|
257
|
-
end
|
|
258
|
-
end
|
|
92
|
+
it "canonicalizes metadata assigned during a mutation" do
|
|
93
|
+
released_at = Time.utc(2026, 1, 1)
|
|
259
94
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
95
|
+
rollout.with_feature(:chat) do |feature|
|
|
96
|
+
feature.data["released_at"] = released_at
|
|
97
|
+
feature.data["label"] = :beta
|
|
98
|
+
end
|
|
264
99
|
|
|
265
|
-
|
|
266
|
-
|
|
100
|
+
expect(rollout.get(:chat).data).to eq(
|
|
101
|
+
"released_at" => JSON.parse({ "value" => released_at }.to_json)["value"],
|
|
102
|
+
"label" => "beta",
|
|
103
|
+
)
|
|
267
104
|
end
|
|
268
|
-
end
|
|
269
105
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
rollout.
|
|
273
|
-
|
|
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)
|
|
274
110
|
|
|
275
|
-
|
|
276
|
-
expect(
|
|
277
|
-
end
|
|
278
|
-
end
|
|
111
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: "beta")
|
|
112
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
279
113
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
rollout.activate_percentage(:chat, 0.1)
|
|
114
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: :beta)
|
|
115
|
+
expect(backend.feature_events(:chat).count).to eq 1
|
|
283
116
|
end
|
|
284
117
|
|
|
285
|
-
it "
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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)
|
|
289
122
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
rollout.activate_percentage(:chat, 20)
|
|
293
|
-
rollout.activate_percentage(:beta, 20)
|
|
294
|
-
@options = rollout.instance_variable_get("@options")
|
|
295
|
-
end
|
|
123
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: :beta)
|
|
124
|
+
event = backend.feature_events(:chat).last
|
|
296
125
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
+
)
|
|
308
133
|
end
|
|
309
134
|
end
|
|
310
135
|
|
|
311
|
-
describe "
|
|
312
|
-
|
|
313
|
-
|
|
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))
|
|
136
|
+
describe "#set_feature_data" do
|
|
137
|
+
def json_value(value)
|
|
138
|
+
JSON.parse({ "value" => value }.to_json)["value"]
|
|
319
139
|
end
|
|
320
140
|
|
|
321
|
-
it "
|
|
322
|
-
|
|
323
|
-
end
|
|
324
|
-
end
|
|
141
|
+
it "canonicalizes JSON-serializable metadata" do
|
|
142
|
+
released_at = Time.utc(2026, 1, 1)
|
|
325
143
|
|
|
326
|
-
|
|
327
|
-
before do
|
|
328
|
-
rollout.activate_percentage(:chat, 100)
|
|
329
|
-
rollout.deactivate_percentage(:chat)
|
|
330
|
-
end
|
|
144
|
+
rollout.set_feature_data(:chat, released_at: released_at, label: :beta)
|
|
331
145
|
|
|
332
|
-
|
|
333
|
-
|
|
146
|
+
expect(rollout.get(:chat).data).to eq(
|
|
147
|
+
"released_at" => json_value(released_at),
|
|
148
|
+
"label" => "beta",
|
|
149
|
+
)
|
|
334
150
|
end
|
|
335
|
-
end
|
|
336
151
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
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
|
|
342
156
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
describe "setting a feature on" do
|
|
349
|
-
before do
|
|
350
|
-
rollout.set(:chat, true)
|
|
351
|
-
end
|
|
157
|
+
expect {
|
|
158
|
+
rollout.set_feature_data(:chat, cyclic)
|
|
159
|
+
}.to raise_error(JSON::JSONError)
|
|
352
160
|
|
|
353
|
-
|
|
354
|
-
expect(rollout).to be_active(:chat)
|
|
161
|
+
expect(rollout.get(:chat).data).to eq("description" => "foo")
|
|
355
162
|
end
|
|
356
163
|
end
|
|
357
164
|
|
|
358
|
-
describe "
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
362
169
|
|
|
363
|
-
|
|
364
|
-
expect(rollout).not_to be_active(:chat)
|
|
170
|
+
described_class.new(adapter: backend).clear!
|
|
365
171
|
end
|
|
366
172
|
end
|
|
367
173
|
|
|
368
|
-
describe "
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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)
|
|
372
179
|
|
|
373
|
-
|
|
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
|
|
378
|
-
end
|
|
379
|
-
end
|
|
180
|
+
described_class.new(adapter: backend).delete(:chat)
|
|
380
181
|
|
|
381
|
-
|
|
382
|
-
expect(
|
|
383
|
-
rollout.delete(:chat)
|
|
384
|
-
expect(rollout.features.size).to eq 0
|
|
182
|
+
expect(logged.exists?(:chat)).to eq false
|
|
183
|
+
expect(logged.logging.events(:chat)).not_to eq []
|
|
385
184
|
end
|
|
386
185
|
|
|
387
|
-
it "
|
|
388
|
-
|
|
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)
|
|
389
190
|
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
191
|
|
|
400
|
-
|
|
401
|
-
rollout.activate(:chat)
|
|
402
|
-
rollout.activate(:chat)
|
|
403
|
-
expect(rollout.features.size).to eq(1)
|
|
192
|
+
expect(rollout.logging.events(:chat)).to eq []
|
|
404
193
|
end
|
|
405
194
|
|
|
406
|
-
it "
|
|
407
|
-
|
|
408
|
-
rollout.
|
|
409
|
-
|
|
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
|
|
195
|
+
it "records a global deletion event with the current context" do
|
|
196
|
+
backend = RolloutMemoryBackend.new
|
|
197
|
+
rollout = described_class.new(adapter: backend, logging: { global: true })
|
|
198
|
+
rollout.activate_percentage(:chat, 25)
|
|
421
199
|
|
|
422
|
-
|
|
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: {},
|
|
432
|
-
)
|
|
200
|
+
rollout.logging.with_context(actor: "alice") { rollout.delete(:chat) }
|
|
433
201
|
|
|
434
|
-
|
|
435
|
-
expect(
|
|
436
|
-
expect(feature
|
|
437
|
-
expect(
|
|
202
|
+
event = rollout.logging.global_events.last
|
|
203
|
+
expect(event.name).to eq :delete
|
|
204
|
+
expect(event.feature).to eq "chat"
|
|
205
|
+
expect(event.data).to eq({})
|
|
206
|
+
expect(event.context).to eq(actor: "alice")
|
|
207
|
+
expect(event.created_at).to be_a(Time)
|
|
438
208
|
end
|
|
439
209
|
|
|
440
|
-
it "
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
end
|
|
470
|
-
|
|
471
|
-
it "each feature is cleared" do
|
|
472
|
-
features.each do |feature|
|
|
473
|
-
expect(rollout.get(feature).to_hash).to eq(
|
|
474
|
-
percentage: 0,
|
|
475
|
-
users: [],
|
|
476
|
-
groups: [],
|
|
477
|
-
data: {},
|
|
478
|
-
)
|
|
479
|
-
end
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
it "each feature is cleared with sets" do
|
|
483
|
-
@options = rollout.instance_variable_get("@options")
|
|
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
|
|
494
|
-
|
|
495
|
-
it "removes all features" do
|
|
496
|
-
expect(rollout.features).to be_empty
|
|
497
|
-
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
|
-
|
|
519
|
-
it "maps inactive feature as false" do
|
|
520
|
-
state = rollout.feature_states[:vr]
|
|
521
|
-
expect(state).to eq(false)
|
|
522
|
-
end
|
|
523
|
-
end
|
|
210
|
+
it "does not record a deletion event for a missing feature" do
|
|
211
|
+
backend = RolloutMemoryBackend.new
|
|
212
|
+
rollout = described_class.new(adapter: backend, logging: { global: true })
|
|
524
213
|
|
|
525
|
-
|
|
526
|
-
it "maps active feature as true" do
|
|
527
|
-
state = rollout.feature_states[:chat]
|
|
528
|
-
expect(state).to eq(true)
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
it "maps inactive feature as false" do
|
|
532
|
-
state = rollout.feature_states[:video]
|
|
533
|
-
expect(state).to eq(false)
|
|
534
|
-
end
|
|
535
|
-
end
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
describe "#active_features" do
|
|
539
|
-
let(:user_double) { double(id: 19) }
|
|
540
|
-
|
|
541
|
-
before do
|
|
542
|
-
rollout.activate(:chat)
|
|
543
|
-
rollout.activate_user(:video, user_double)
|
|
544
|
-
rollout.deactivate(:vr)
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
it "returns an array" do
|
|
548
|
-
expect(rollout.active_features).to be_a(Array)
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
context "with user argument" do
|
|
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
|
|
557
|
-
|
|
558
|
-
it "excludes inactive feature" do
|
|
559
|
-
features = rollout.active_features(user_double)
|
|
560
|
-
expect(features).to_not include(:vr)
|
|
561
|
-
end
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
context "with no argument" do
|
|
565
|
-
it "includes active feature" do
|
|
566
|
-
features = rollout.active_features
|
|
567
|
-
expect(features).to include(:chat)
|
|
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
|
|
214
|
+
rollout.delete(:chat)
|
|
576
215
|
|
|
577
|
-
|
|
578
|
-
it "returns true if activated for user" do
|
|
579
|
-
rollout.activate_user(:chat, double(id: 5))
|
|
580
|
-
expect(rollout.user_in_active_users?(:chat, "5")).to eq(true)
|
|
216
|
+
expect(rollout.logging.global_events).to eq []
|
|
581
217
|
end
|
|
582
218
|
|
|
583
|
-
it "
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
end
|
|
219
|
+
it "does not record a deletion event when logging is suppressed" do
|
|
220
|
+
backend = RolloutMemoryBackend.new
|
|
221
|
+
rollout = described_class.new(adapter: backend, logging: { global: true })
|
|
222
|
+
rollout.activate_percentage(:chat, 25)
|
|
588
223
|
|
|
589
|
-
|
|
590
|
-
before do
|
|
591
|
-
rollout.activate_percentage(:chat, 10)
|
|
592
|
-
rollout.activate_group(:chat, :caretakers)
|
|
593
|
-
rollout.activate_group(:videos, :greeters)
|
|
594
|
-
rollout.activate(:signup)
|
|
595
|
-
rollout.activate_user(:photos, double(id: 42))
|
|
596
|
-
end
|
|
224
|
+
rollout.logging.without { rollout.delete(:chat) }
|
|
597
225
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
expect(
|
|
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
|
|
226
|
+
expect(rollout.exists?(:chat)).to eq false
|
|
227
|
+
expect(rollout.logging.events(:chat)).to eq []
|
|
228
|
+
expect(rollout.logging.global_events.map(&:name)).to eq [:update]
|
|
608
229
|
end
|
|
609
230
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
231
|
+
it "preserves deletion behavior for adapters without event-aware deletion" do
|
|
232
|
+
backend_class = Class.new(RolloutMemoryBackend) do
|
|
233
|
+
undef_method :delete_feature_with_history
|
|
613
234
|
end
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
describe "#set_feature_data" do
|
|
618
|
-
before do
|
|
619
|
-
rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar')
|
|
620
|
-
end
|
|
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
|
|
630
|
-
|
|
631
|
-
it 'only sets data on specified feature' do
|
|
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
|
|
636
|
-
|
|
637
|
-
it 'does not modify @data if param is nil' do
|
|
638
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
639
|
-
rollout.set_feature_data(:chat, nil)
|
|
640
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
it 'does not modify @data if param is empty string' do
|
|
644
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
645
|
-
rollout.set_feature_data(:chat, " ")
|
|
646
|
-
expect(rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar')
|
|
647
|
-
end
|
|
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
|
|
235
|
+
backend = backend_class.new
|
|
236
|
+
rollout = described_class.new(adapter: backend, logging: { global: true })
|
|
237
|
+
rollout.activate_percentage(:chat, 25)
|
|
666
238
|
|
|
667
|
-
|
|
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
|
|
239
|
+
expect { rollout.delete(:chat) }.not_to raise_error
|
|
672
240
|
|
|
673
|
-
|
|
674
|
-
expect(rollout.
|
|
241
|
+
expect(rollout.exists?(:chat)).to eq false
|
|
242
|
+
expect(rollout.logging.events(:chat)).to eq []
|
|
243
|
+
expect(rollout.logging.global_events.map(&:name)).to eq [:update]
|
|
675
244
|
end
|
|
676
245
|
end
|
|
677
246
|
end
|