flipper 1.3.4 → 1.4.2
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 +4 -4
- data/.github/workflows/ci.yml +9 -6
- data/.github/workflows/examples.yml +5 -4
- data/.github/workflows/release.yml +54 -0
- data/.superset/config.json +4 -0
- data/CLAUDE.md +93 -0
- data/README.md +4 -3
- data/examples/cloud/poll_interval/README.md +111 -0
- data/examples/cloud/poll_interval/client.rb +108 -0
- data/examples/cloud/poll_interval/server.rb +98 -0
- data/examples/expressions.rb +35 -11
- data/lib/flipper/adapter.rb +17 -1
- data/lib/flipper/adapters/actor_limit.rb +27 -1
- data/lib/flipper/adapters/cache_base.rb +21 -3
- data/lib/flipper/adapters/dual_write.rb +6 -2
- data/lib/flipper/adapters/failover.rb +9 -3
- data/lib/flipper/adapters/failsafe.rb +2 -2
- data/lib/flipper/adapters/http/client.rb +15 -4
- data/lib/flipper/adapters/http.rb +37 -2
- data/lib/flipper/adapters/instrumented.rb +2 -2
- data/lib/flipper/adapters/memoizable.rb +3 -3
- data/lib/flipper/adapters/memory.rb +1 -1
- data/lib/flipper/adapters/pstore.rb +1 -1
- data/lib/flipper/adapters/strict.rb +30 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +5 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
- data/lib/flipper/adapters/sync.rb +7 -3
- data/lib/flipper/cli.rb +51 -0
- data/lib/flipper/cloud/configuration.rb +3 -2
- data/lib/flipper/cloud/dsl.rb +2 -2
- data/lib/flipper/cloud/middleware.rb +1 -1
- data/lib/flipper/cloud/migrate.rb +71 -0
- data/lib/flipper/cloud/telemetry.rb +1 -1
- data/lib/flipper/cloud.rb +1 -0
- data/lib/flipper/dsl.rb +1 -1
- data/lib/flipper/expressions/feature_enabled.rb +34 -0
- data/lib/flipper/expressions/time.rb +8 -1
- data/lib/flipper/gates/expression.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +1 -1
- data/lib/flipper/poller.rb +49 -8
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +17 -1
- data/spec/flipper/adapter_spec.rb +20 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +55 -0
- data/spec/flipper/adapters/dual_write_spec.rb +13 -0
- data/spec/flipper/adapters/failover_spec.rb +12 -0
- data/spec/flipper/adapters/http_spec.rb +240 -0
- data/spec/flipper/adapters/strict_spec.rb +62 -4
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +12 -0
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
- data/spec/flipper/adapters/sync_spec.rb +13 -0
- data/spec/flipper/cli_spec.rb +51 -0
- data/spec/flipper/cloud/middleware_spec.rb +1 -1
- data/spec/flipper/cloud/migrate_spec.rb +160 -0
- data/spec/flipper/cloud/telemetry_spec.rb +1 -1
- data/spec/flipper/engine_spec.rb +2 -2
- data/spec/flipper/expressions/time_spec.rb +16 -0
- data/spec/flipper/gates/expression_spec.rb +82 -0
- data/spec/flipper/middleware/memoizer_spec.rb +37 -6
- data/spec/flipper/poller_spec.rb +347 -4
- data/spec/flipper_integration_spec.rb +133 -0
- data/spec/flipper_spec.rb +5 -0
- data/spec/spec_helper.rb +7 -0
- metadata +18 -112
- data/lib/flipper/expressions/duration.rb +0 -28
- data/spec/flipper/expressions/duration_spec.rb +0 -43
data/spec/flipper/poller_spec.rb
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
require "flipper/poller"
|
|
2
|
+
require "flipper/adapters/http"
|
|
2
3
|
|
|
3
4
|
RSpec.describe Flipper::Poller do
|
|
4
|
-
let(:
|
|
5
|
-
let(:
|
|
5
|
+
let(:url) { "http://app.com/flipper" }
|
|
6
|
+
let(:remote_adapter) { Flipper::Adapters::Http.new(url: url) }
|
|
6
7
|
let(:local) { Flipper.new(subject.adapter) }
|
|
7
8
|
|
|
8
9
|
subject do
|
|
9
10
|
described_class.new(
|
|
10
11
|
remote_adapter: remote_adapter,
|
|
11
12
|
start_automatically: false,
|
|
12
|
-
interval:
|
|
13
|
+
interval: 3600 # 1 hour
|
|
13
14
|
)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
before do
|
|
18
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
19
|
+
.to_return(status: 200, body: JSON.generate(features: []))
|
|
20
|
+
|
|
17
21
|
allow(subject).to receive(:loop).and_yield # Make loop just call once
|
|
18
22
|
allow(subject).to receive(:sleep) # Disable sleep
|
|
19
23
|
allow(Thread).to receive(:new).and_yield # Disable separate thread
|
|
@@ -28,12 +32,320 @@ RSpec.describe Flipper::Poller do
|
|
|
28
32
|
|
|
29
33
|
describe "#sync" do
|
|
30
34
|
it "syncs remote adapter to local adapter" do
|
|
31
|
-
|
|
35
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
36
|
+
.to_return(status: 200, body: JSON.generate(
|
|
37
|
+
features: [
|
|
38
|
+
{
|
|
39
|
+
key: "polling",
|
|
40
|
+
gates: [
|
|
41
|
+
{ key: "boolean", value: true }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
))
|
|
32
46
|
|
|
33
47
|
expect(local.enabled?(:polling)).to be(false)
|
|
34
48
|
subject.sync
|
|
35
49
|
expect(local.enabled?(:polling)).to be(true)
|
|
36
50
|
end
|
|
51
|
+
|
|
52
|
+
context "when poll-shutdown header is present" do
|
|
53
|
+
before do
|
|
54
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
55
|
+
.to_return(
|
|
56
|
+
status: 200,
|
|
57
|
+
body: JSON.generate(
|
|
58
|
+
features: [
|
|
59
|
+
{
|
|
60
|
+
key: "polling",
|
|
61
|
+
gates: [
|
|
62
|
+
{ key: "boolean", value: true }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
),
|
|
67
|
+
headers: { "poll-shutdown" => "true" }
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "stops the poller when poll-shutdown header is true" do
|
|
72
|
+
expect(subject).to receive(:stop).and_call_original
|
|
73
|
+
subject.sync
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "prevents poller from restarting after shutdown" do
|
|
77
|
+
subject.sync # This should trigger shutdown
|
|
78
|
+
|
|
79
|
+
# Try to start again - should be a no-op
|
|
80
|
+
expect(Thread).not_to receive(:new)
|
|
81
|
+
subject.start
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "instruments the shutdown_requested event" do
|
|
85
|
+
instrumenter = subject.instance_variable_get(:@instrumenter)
|
|
86
|
+
|
|
87
|
+
expect(instrumenter).to receive(:instrument).with(
|
|
88
|
+
"poller.#{Flipper::InstrumentationNamespace}",
|
|
89
|
+
{ operation: :poll }
|
|
90
|
+
).and_call_original
|
|
91
|
+
|
|
92
|
+
expect(instrumenter).to receive(:instrument).with(
|
|
93
|
+
"poller.#{Flipper::InstrumentationNamespace}",
|
|
94
|
+
{ operation: :shutdown_requested }
|
|
95
|
+
).and_call_original
|
|
96
|
+
|
|
97
|
+
expect(instrumenter).to receive(:instrument).with(
|
|
98
|
+
"poller.#{Flipper::InstrumentationNamespace}",
|
|
99
|
+
{ operation: :stop }
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
subject.sync
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
context "when poll-shutdown header is present on error response" do
|
|
107
|
+
before do
|
|
108
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
109
|
+
.to_return(
|
|
110
|
+
status: 404,
|
|
111
|
+
body: JSON.generate({ error: "Not found" }),
|
|
112
|
+
headers: { "poll-shutdown" => "true" }
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "stops polling even when sync fails with error response" do
|
|
117
|
+
# sync will raise an error, but should still check shutdown header
|
|
118
|
+
expect { subject.sync }.to raise_error(Flipper::Adapters::Http::Error)
|
|
119
|
+
|
|
120
|
+
# Verify shutdown was triggered
|
|
121
|
+
expect(Thread).not_to receive(:new)
|
|
122
|
+
subject.start
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
context "when poll-shutdown header is false" do
|
|
127
|
+
before do
|
|
128
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
129
|
+
.to_return(
|
|
130
|
+
status: 200,
|
|
131
|
+
body: JSON.generate(
|
|
132
|
+
features: [
|
|
133
|
+
{
|
|
134
|
+
key: "polling",
|
|
135
|
+
gates: [
|
|
136
|
+
{ key: "boolean", value: true }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
),
|
|
141
|
+
headers: { "poll-shutdown" => "false" }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "does not stop the poller" do
|
|
146
|
+
expect(subject).not_to receive(:stop)
|
|
147
|
+
subject.sync
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
context "when poll-shutdown header is missing" do
|
|
152
|
+
before do
|
|
153
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
154
|
+
.to_return(
|
|
155
|
+
status: 200,
|
|
156
|
+
body: JSON.generate(
|
|
157
|
+
features: [
|
|
158
|
+
{
|
|
159
|
+
key: "polling",
|
|
160
|
+
gates: [
|
|
161
|
+
{ key: "boolean", value: true }
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "does not stop the poller" do
|
|
170
|
+
expect(subject).not_to receive(:stop)
|
|
171
|
+
subject.sync
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
context "when poll-interval header is lower than initial interval" do
|
|
176
|
+
before do
|
|
177
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
178
|
+
.to_return(
|
|
179
|
+
status: 200,
|
|
180
|
+
body: JSON.generate(
|
|
181
|
+
features: [
|
|
182
|
+
{
|
|
183
|
+
key: "polling",
|
|
184
|
+
gates: [
|
|
185
|
+
{ key: "boolean", value: true }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
),
|
|
190
|
+
headers: { "poll-interval" => "30" }
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "uses the initial interval as minimum" do
|
|
195
|
+
expect(subject.interval).to eq(3600.0)
|
|
196
|
+
subject.sync
|
|
197
|
+
expect(subject.interval).to eq(3600.0) # Keeps 3600 because it's the initial interval
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
context "when poll-interval header is below minimum" do
|
|
202
|
+
subject do
|
|
203
|
+
described_class.new(
|
|
204
|
+
remote_adapter: remote_adapter,
|
|
205
|
+
start_automatically: false,
|
|
206
|
+
interval: 10 # Set initial to minimum
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
before do
|
|
211
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
212
|
+
.to_return(
|
|
213
|
+
status: 200,
|
|
214
|
+
body: JSON.generate(
|
|
215
|
+
features: [
|
|
216
|
+
{
|
|
217
|
+
key: "polling",
|
|
218
|
+
gates: [
|
|
219
|
+
{ key: "boolean", value: true }
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
),
|
|
224
|
+
headers: { "poll-interval" => "5" }
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "enforces minimum poll interval" do
|
|
229
|
+
expect(subject.interval).to eq(10.0)
|
|
230
|
+
subject.sync
|
|
231
|
+
# Header says 5, minimum is 10, initial is 10, so max(5->10, 10) = 10
|
|
232
|
+
expect(subject.interval).to eq(Flipper::Poller::MINIMUM_POLL_INTERVAL)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
context "when poll-interval header is higher than initial interval" do
|
|
237
|
+
subject do
|
|
238
|
+
described_class.new(
|
|
239
|
+
remote_adapter: remote_adapter,
|
|
240
|
+
start_automatically: false,
|
|
241
|
+
interval: 20
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
before do
|
|
246
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
247
|
+
.to_return(
|
|
248
|
+
status: 200,
|
|
249
|
+
body: JSON.generate(
|
|
250
|
+
features: [
|
|
251
|
+
{
|
|
252
|
+
key: "polling",
|
|
253
|
+
gates: [
|
|
254
|
+
{ key: "boolean", value: true }
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
),
|
|
259
|
+
headers: { "poll-interval" => "60" }
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it "updates to the higher interval from header" do
|
|
264
|
+
expect(subject.interval).to eq(20.0)
|
|
265
|
+
subject.sync
|
|
266
|
+
expect(subject.interval).to eq(60.0) # Uses 60 because it's higher than initial 20
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context "when poll-interval header can decrease back to initial interval" do
|
|
271
|
+
subject do
|
|
272
|
+
described_class.new(
|
|
273
|
+
remote_adapter: remote_adapter,
|
|
274
|
+
start_automatically: false,
|
|
275
|
+
interval: 10
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
before do
|
|
280
|
+
# First sync increases interval to 60
|
|
281
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
282
|
+
.to_return(
|
|
283
|
+
status: 200,
|
|
284
|
+
body: JSON.generate(
|
|
285
|
+
features: [
|
|
286
|
+
{
|
|
287
|
+
key: "polling",
|
|
288
|
+
gates: [
|
|
289
|
+
{ key: "boolean", value: true }
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
),
|
|
294
|
+
headers: { "poll-interval" => "60" }
|
|
295
|
+
).times(1).then
|
|
296
|
+
.to_return(
|
|
297
|
+
status: 200,
|
|
298
|
+
body: JSON.generate(
|
|
299
|
+
features: [
|
|
300
|
+
{
|
|
301
|
+
key: "polling",
|
|
302
|
+
gates: [
|
|
303
|
+
{ key: "boolean", value: true }
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
),
|
|
308
|
+
headers: { "poll-interval" => "10" }
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it "allows interval to go back down to initial after being increased" do
|
|
313
|
+
expect(subject.interval).to eq(10.0)
|
|
314
|
+
|
|
315
|
+
# First sync: header says 60, initial is 10, so use 60
|
|
316
|
+
subject.sync
|
|
317
|
+
expect(subject.interval).to eq(60.0)
|
|
318
|
+
|
|
319
|
+
# Second sync: header says 10, initial is 10, so use 10
|
|
320
|
+
subject.sync
|
|
321
|
+
expect(subject.interval).to eq(10.0)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
context "when poll-interval header is missing" do
|
|
326
|
+
before do
|
|
327
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
328
|
+
.to_return(
|
|
329
|
+
status: 200,
|
|
330
|
+
body: JSON.generate(
|
|
331
|
+
features: [
|
|
332
|
+
{
|
|
333
|
+
key: "polling",
|
|
334
|
+
gates: [
|
|
335
|
+
{ key: "boolean", value: true }
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "does not change the interval" do
|
|
344
|
+
original_interval = subject.interval
|
|
345
|
+
subject.sync
|
|
346
|
+
expect(subject.interval).to eq(original_interval)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
37
349
|
end
|
|
38
350
|
|
|
39
351
|
describe "#start" do
|
|
@@ -43,5 +355,36 @@ RSpec.describe Flipper::Poller do
|
|
|
43
355
|
expect(subject).to receive(:sync)
|
|
44
356
|
subject.start
|
|
45
357
|
end
|
|
358
|
+
|
|
359
|
+
context "after shutdown_requested" do
|
|
360
|
+
before do
|
|
361
|
+
stub_request(:get, "#{url}/features?exclude_gate_names=true")
|
|
362
|
+
.to_return(
|
|
363
|
+
status: 200,
|
|
364
|
+
body: JSON.generate(features: []),
|
|
365
|
+
headers: { "poll-shutdown" => "true" }
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it "does not start when shutdown was requested" do
|
|
370
|
+
subject.sync # This triggers shutdown
|
|
371
|
+
|
|
372
|
+
expect(Thread).not_to receive(:new)
|
|
373
|
+
subject.start
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
it "allows starting after a fork" do
|
|
377
|
+
subject.sync # This triggers shutdown
|
|
378
|
+
|
|
379
|
+
# Simulate fork by changing PID
|
|
380
|
+
allow(Process).to receive(:pid).and_return(subject.instance_variable_get(:@pid) + 1)
|
|
381
|
+
|
|
382
|
+
# After fork, start should work again
|
|
383
|
+
expect(Thread).to receive(:new).and_yield
|
|
384
|
+
expect(subject).to receive(:loop).and_yield
|
|
385
|
+
expect(subject).to receive(:sync)
|
|
386
|
+
subject.start
|
|
387
|
+
end
|
|
388
|
+
end
|
|
46
389
|
end
|
|
47
390
|
end
|
|
@@ -600,6 +600,139 @@ RSpec.describe Flipper do
|
|
|
600
600
|
end
|
|
601
601
|
end
|
|
602
602
|
|
|
603
|
+
context "for FeatureEnabled" do
|
|
604
|
+
before do
|
|
605
|
+
@original_instance = Flipper.instance
|
|
606
|
+
Flipper.instance = flipper
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
after do
|
|
610
|
+
Flipper.instance = @original_instance
|
|
611
|
+
Thread.current[:flipper_evaluating_features] = nil
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
it "returns true when referenced feature is enabled" do
|
|
615
|
+
flipper.enable(:search_beta)
|
|
616
|
+
feature.enable Flipper.feature_enabled(:search_beta)
|
|
617
|
+
|
|
618
|
+
expect(feature.enabled?).to be(true)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
it "returns false when referenced feature is disabled" do
|
|
622
|
+
feature.enable Flipper.feature_enabled(:search_beta)
|
|
623
|
+
|
|
624
|
+
expect(feature.enabled?).to be(false)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
it "passes actor through to referenced feature" do
|
|
628
|
+
flipper.enable_actor(:search_beta, pitt)
|
|
629
|
+
feature.enable Flipper.feature_enabled(:search_beta)
|
|
630
|
+
|
|
631
|
+
expect(feature.enabled?(pitt)).to be(true)
|
|
632
|
+
expect(feature.enabled?(clooney)).to be(false)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
it "returns false on circular dependency" do
|
|
636
|
+
feature.enable Flipper.feature_enabled(:other)
|
|
637
|
+
flipper[:other].enable Flipper.feature_enabled(:search)
|
|
638
|
+
|
|
639
|
+
expect(feature.enabled?).to be(false)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
it "returns false on self-reference" do
|
|
643
|
+
feature.enable Flipper.feature_enabled(:search)
|
|
644
|
+
|
|
645
|
+
expect(feature.enabled?).to be(false)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
it "cleans up thread-local state after evaluation" do
|
|
649
|
+
flipper.enable(:search_beta)
|
|
650
|
+
feature.enable Flipper.feature_enabled(:search_beta)
|
|
651
|
+
|
|
652
|
+
feature.enabled?
|
|
653
|
+
evaluating = Thread.current[:flipper_evaluating_features]
|
|
654
|
+
expect(evaluating.nil? || evaluating.empty?).to be(true)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
it "cleans up thread-local state after self-reference" do
|
|
658
|
+
feature.enable Flipper.feature_enabled(:search)
|
|
659
|
+
|
|
660
|
+
feature.enabled?
|
|
661
|
+
evaluating = Thread.current[:flipper_evaluating_features]
|
|
662
|
+
expect(evaluating.nil? || evaluating.empty?).to be(true)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
it "cleans up thread-local state after circular dependency" do
|
|
666
|
+
feature.enable Flipper.feature_enabled(:other)
|
|
667
|
+
flipper[:other].enable Flipper.feature_enabled(:search)
|
|
668
|
+
|
|
669
|
+
feature.enabled?
|
|
670
|
+
evaluating = Thread.current[:flipper_evaluating_features]
|
|
671
|
+
expect(evaluating.nil? || evaluating.empty?).to be(true)
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
context "for FeatureDisabled" do
|
|
676
|
+
before do
|
|
677
|
+
@original_instance = Flipper.instance
|
|
678
|
+
Flipper.instance = flipper
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
after do
|
|
682
|
+
Flipper.instance = @original_instance
|
|
683
|
+
Thread.current[:flipper_evaluating_features] = nil
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
it "returns true when referenced feature is disabled" do
|
|
687
|
+
feature.enable Flipper.feature_disabled(:old_checkout)
|
|
688
|
+
|
|
689
|
+
expect(feature.enabled?).to be(true)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
it "returns false when referenced feature is enabled" do
|
|
693
|
+
flipper.enable(:old_checkout)
|
|
694
|
+
feature.enable Flipper.feature_disabled(:old_checkout)
|
|
695
|
+
|
|
696
|
+
expect(feature.enabled?).to be(false)
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
context "for FeatureEnabled composed with other expressions" do
|
|
701
|
+
before do
|
|
702
|
+
@original_instance = Flipper.instance
|
|
703
|
+
Flipper.instance = flipper
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
after do
|
|
707
|
+
Flipper.instance = @original_instance
|
|
708
|
+
Thread.current[:flipper_evaluating_features] = nil
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
it "works with Any" do
|
|
712
|
+
feature.enable Flipper.any(
|
|
713
|
+
Flipper.feature_enabled(:beta_program),
|
|
714
|
+
Flipper.property(:plan).eq("basic")
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
expect(feature.enabled?(basic_plan_actor)).to be(true)
|
|
718
|
+
expect(feature.enabled?(premium_plan_actor)).to be(false)
|
|
719
|
+
|
|
720
|
+
flipper.enable(:beta_program)
|
|
721
|
+
expect(feature.enabled?(premium_plan_actor)).to be(true)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
it "works with All" do
|
|
725
|
+
flipper.enable(:basic_search)
|
|
726
|
+
feature.enable Flipper.all(
|
|
727
|
+
Flipper.feature_enabled(:basic_search),
|
|
728
|
+
Flipper.property(:plan).eq("basic")
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
expect(feature.enabled?(basic_plan_actor)).to be(true)
|
|
732
|
+
expect(feature.enabled?(premium_plan_actor)).to be(false)
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
603
736
|
context "for All" do
|
|
604
737
|
it "works" do
|
|
605
738
|
true_actor = Flipper::Actor.new("User;1", {
|
data/spec/flipper_spec.rb
CHANGED
|
@@ -218,6 +218,11 @@ RSpec.describe Flipper do
|
|
|
218
218
|
expect(described_class.adapter).to eq(described_class.instance.adapter)
|
|
219
219
|
end
|
|
220
220
|
|
|
221
|
+
it 'delegates adapter_stack to instance' do
|
|
222
|
+
expect(described_class.adapter_stack).to eq(described_class.instance.adapter_stack)
|
|
223
|
+
expect(described_class.adapter_stack).to eq("memoizable -> memory")
|
|
224
|
+
end
|
|
225
|
+
|
|
221
226
|
it 'delegates memoize= to instance' do
|
|
222
227
|
expect(described_class.adapter.memoizing?).to be(false)
|
|
223
228
|
described_class.memoize = true
|
data/spec/spec_helper.rb
CHANGED
|
@@ -39,6 +39,13 @@ RSpec.configure do |config|
|
|
|
39
39
|
Flipper.configuration = nil
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
config.after(:example) do
|
|
43
|
+
# Stop any pollers started during the test BEFORE WebMock clears stubs
|
|
44
|
+
# in its own after hook (RSpec runs after hooks in reverse registration
|
|
45
|
+
# order, so ours runs first since webmock/rspec was required before this).
|
|
46
|
+
Flipper::Poller.reset if defined?(Flipper::Poller)
|
|
47
|
+
end
|
|
48
|
+
|
|
42
49
|
config.disable_monkey_patching!
|
|
43
50
|
|
|
44
51
|
config.filter_run focus: true
|