flipper 0.28.3 → 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/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +56 -10
- data/.github/workflows/examples.yml +54 -11
- data/.github/workflows/release.yml +54 -0
- data/.superset/config.json +4 -0
- data/CLAUDE.md +93 -0
- data/Changelog.md +1 -559
- data/Gemfile +17 -8
- data/README.md +34 -29
- data/Rakefile +2 -2
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/app.ru +12 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +20 -0
- data/examples/cloud/forked.rb +36 -0
- data/examples/cloud/import.rb +17 -0
- 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/cloud/threaded.rb +33 -0
- data/examples/dsl.rb +0 -14
- data/examples/expressions.rb +237 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +8 -4
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +27 -1
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +54 -0
- data/lib/flipper/adapters/cache_base.rb +161 -0
- data/lib/flipper/adapters/dual_write.rb +7 -5
- data/lib/flipper/adapters/failover.rb +9 -7
- data/lib/flipper/adapters/failsafe.rb +2 -6
- data/lib/flipper/adapters/http/client.rb +53 -14
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +56 -16
- data/lib/flipper/adapters/instrumented.rb +2 -6
- data/lib/flipper/adapters/memoizable.rb +17 -22
- data/lib/flipper/adapters/memory.rb +5 -7
- data/lib/flipper/adapters/operation_logger.rb +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- data/lib/flipper/adapters/pstore.rb +18 -12
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +75 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
- data/lib/flipper/adapters/sync.rb +7 -7
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +314 -0
- data/lib/flipper/cloud/configuration.rb +267 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/migrate.rb +71 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +54 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +48 -43
- data/lib/flipper/engine.rb +102 -0
- data/lib/flipper/export.rb +0 -2
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +9 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/feature_enabled.rb +34 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +16 -0
- data/lib/flipper/feature.rb +63 -1
- data/lib/flipper/gate.rb +2 -1
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +8 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +54 -12
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
- data/lib/flipper/test/shared_adapter_test.rb +77 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +37 -9
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +61 -8
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +21 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
- data/spec/flipper/adapters/dual_write_spec.rb +15 -2
- data/spec/flipper/adapters/failover_spec.rb +12 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +375 -74
- data/spec/flipper/adapters/instrumented_spec.rb +1 -1
- data/spec/flipper/adapters/memoizable_spec.rb +21 -21
- data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +122 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -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 +217 -0
- data/spec/flipper/cloud/configuration_spec.rb +251 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud/migrate_spec.rb +160 -0
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +186 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +34 -73
- data/spec/flipper/engine_spec.rb +374 -0
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +29 -0
- data/spec/flipper/feature_spec.rb +380 -10
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +190 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
- data/spec/flipper/middleware/memoizer_spec.rb +107 -7
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/poller_spec.rb +347 -4
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +247 -16
- data/spec/flipper_spec.rb +92 -29
- data/spec/spec_helper.rb +24 -17
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +139 -75
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -109
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
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'flipper/serializers/gzip'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Flipper::Serializers::Gzip do
|
|
4
|
+
it "serializes and deserializes" do
|
|
5
|
+
serialized = described_class.serialize("my data")
|
|
6
|
+
expect(described_class.deserialize(serialized)).to eq("my data")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "doesn't fail with nil" do
|
|
10
|
+
expect(described_class.serialize(nil)).to be(nil)
|
|
11
|
+
expect(described_class.deserialize(nil)).to be(nil)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'flipper/serializers/json'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Flipper::Serializers::Json do
|
|
4
|
+
it "serializes and deserializes" do
|
|
5
|
+
serialized = described_class.serialize("my data")
|
|
6
|
+
expect(described_class.deserialize(serialized)).to eq("my data")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "doesn't fail with nil" do
|
|
10
|
+
expect(described_class.serialize(nil)).to be(nil)
|
|
11
|
+
expect(described_class.deserialize(nil)).to be(nil)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -56,7 +56,7 @@ RSpec.describe Flipper::Typecast do
|
|
|
56
56
|
nil => 0,
|
|
57
57
|
'' => 0,
|
|
58
58
|
0 => 0,
|
|
59
|
-
0.0 => 0,
|
|
59
|
+
0.0 => 0.0,
|
|
60
60
|
1 => 1,
|
|
61
61
|
1.1 => 1.1,
|
|
62
62
|
'0.01' => 0.01,
|
|
@@ -65,9 +65,9 @@ RSpec.describe Flipper::Typecast do
|
|
|
65
65
|
'99' => 99,
|
|
66
66
|
'99.9' => 99.9,
|
|
67
67
|
}.each do |value, expected|
|
|
68
|
-
context "#
|
|
68
|
+
context "#to_number for #{value.inspect}" do
|
|
69
69
|
it "returns #{expected}" do
|
|
70
|
-
expect(described_class.
|
|
70
|
+
expect(described_class.to_number(value)).to be(expected)
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
end
|
|
@@ -99,14 +99,14 @@ RSpec.describe Flipper::Typecast do
|
|
|
99
99
|
|
|
100
100
|
it 'raises argument error for bad integer percentage' do
|
|
101
101
|
expect do
|
|
102
|
-
described_class.
|
|
103
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a
|
|
102
|
+
described_class.to_number(['asdf'])
|
|
103
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
it 'raises argument error for bad float percentage' do
|
|
107
107
|
expect do
|
|
108
|
-
described_class.
|
|
109
|
-
end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a
|
|
108
|
+
described_class.to_number(['asdf.0'])
|
|
109
|
+
end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a number))
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
it 'raises argument error for set value that cannot be converted to a set' do
|
|
@@ -127,6 +127,30 @@ RSpec.describe Flipper::Typecast do
|
|
|
127
127
|
expect(result["search"]).not_to be(hash["search"])
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
it "converts does not convert expressions" do
|
|
131
|
+
hash = {
|
|
132
|
+
"search" => {
|
|
133
|
+
boolean: nil,
|
|
134
|
+
expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
|
|
135
|
+
groups: ['a', 'b'],
|
|
136
|
+
actors: ['User;1'],
|
|
137
|
+
percentage_of_actors: nil,
|
|
138
|
+
percentage_of_time: nil,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
result = described_class.features_hash(hash)
|
|
142
|
+
expect(result).to eq({
|
|
143
|
+
"search" => {
|
|
144
|
+
boolean: nil,
|
|
145
|
+
expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
|
|
146
|
+
groups: Set['a', 'b'],
|
|
147
|
+
actors: Set['User;1'],
|
|
148
|
+
percentage_of_actors: nil,
|
|
149
|
+
percentage_of_time: nil,
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
end
|
|
153
|
+
|
|
130
154
|
it "converts gate value arrays to sets" do
|
|
131
155
|
hash = {
|
|
132
156
|
"search" => {
|
|
@@ -193,4 +217,16 @@ RSpec.describe Flipper::Typecast do
|
|
|
193
217
|
})
|
|
194
218
|
end
|
|
195
219
|
end
|
|
220
|
+
|
|
221
|
+
it "converts to and from json" do
|
|
222
|
+
source = {"foo" => "bar"}
|
|
223
|
+
output = described_class.to_json(source)
|
|
224
|
+
expect(described_class.from_json(output)).to eq(source)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "converts to and from gzip" do
|
|
228
|
+
source = "foo bar"
|
|
229
|
+
output = described_class.to_gzip(source)
|
|
230
|
+
expect(described_class.from_gzip(output)).to eq(source)
|
|
231
|
+
end
|
|
196
232
|
end
|
|
@@ -11,12 +11,19 @@ RSpec.describe Flipper::Types::Actor do
|
|
|
11
11
|
attr_reader :flipper_id
|
|
12
12
|
|
|
13
13
|
def initialize(flipper_id)
|
|
14
|
-
@flipper_id = flipper_id
|
|
14
|
+
@flipper_id = flipper_id.to_s
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def admin?
|
|
18
18
|
true
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
def flipper_properties
|
|
22
|
+
{
|
|
23
|
+
"flipper_id" => flipper_id,
|
|
24
|
+
"admin" => admin?,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
|
|
@@ -87,6 +94,15 @@ RSpec.describe Flipper::Types::Actor do
|
|
|
87
94
|
expect(actor.admin?).to eq(true)
|
|
88
95
|
end
|
|
89
96
|
|
|
97
|
+
it 'proxies flipper_properties to actor' do
|
|
98
|
+
actor = actor_class.new(10)
|
|
99
|
+
actor = described_class.new(actor)
|
|
100
|
+
expect(actor.flipper_properties).to eq({
|
|
101
|
+
"flipper_id" => "10",
|
|
102
|
+
"admin" => true,
|
|
103
|
+
})
|
|
104
|
+
end
|
|
105
|
+
|
|
90
106
|
it 'exposes actor' do
|
|
91
107
|
actor = actor_class.new(10)
|
|
92
108
|
actor_type_instance = described_class.new(actor)
|
|
@@ -104,6 +120,7 @@ RSpec.describe Flipper::Types::Actor do
|
|
|
104
120
|
actor = actor_class.new(10)
|
|
105
121
|
actor_type_instance = described_class.new(actor)
|
|
106
122
|
expect(actor_type_instance.respond_to?(:admin?)).to eq(true)
|
|
123
|
+
expect(actor_type_instance.respond_to?(:flipper_properties)).to eq(true)
|
|
107
124
|
end
|
|
108
125
|
|
|
109
126
|
it 'returns false if does not respond to method and actor does not respond to method' do
|