flipper 1.0.0 → 1.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 +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +7 -3
- data/.github/workflows/examples.yml +27 -5
- data/Changelog.md +42 -0
- data/Gemfile +4 -4
- data/README.md +13 -11
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/flipper.gemspec +1 -2
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +26 -7
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +18 -13
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +0 -4
- data/lib/flipper/adapters/poll.rb +1 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +4 -4
- data/lib/flipper/adapters/strict.rb +47 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/cloud/configuration.rb +121 -52
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -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 +98 -0
- data/lib/flipper/cloud/telemetry.rb +183 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +28 -3
- 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 +11 -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/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -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 +9 -0
- data/lib/flipper/feature.rb +55 -0
- data/lib/flipper/gate.rb +1 -0
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/poller.rb +1 -1
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
- data/lib/flipper/test/shared_adapter_test.rb +24 -5
- data/lib/flipper/typecast.rb +34 -6
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +38 -1
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/http_spec.rb +39 -5
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +62 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -23
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -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 +156 -0
- data/spec/flipper/cloud_spec.rb +12 -12
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -0
- data/spec/flipper/engine_spec.rb +108 -7
- 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/duration_spec.rb +43 -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 +13 -0
- data/spec/flipper/feature_spec.rb +360 -1
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- 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 +102 -4
- data/spec/flipper_spec.rb +89 -1
- data/spec/spec_helper.rb +5 -0
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +104 -18
- data/lib/flipper/cloud/instrumenter.rb +0 -48
data/lib/flipper.rb
CHANGED
@@ -57,15 +57,49 @@ module Flipper
|
|
57
57
|
# interface of Flipper::DSL.
|
58
58
|
def_delegators :instance,
|
59
59
|
:enabled?, :enable, :disable,
|
60
|
+
:enable_expression, :disable_expression,
|
61
|
+
:expression, :add_expression, :remove_expression,
|
60
62
|
:enable_actor, :disable_actor,
|
61
63
|
:enable_group, :disable_group,
|
62
64
|
:enable_percentage_of_actors, :disable_percentage_of_actors,
|
63
65
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
64
66
|
:features, :feature, :[], :preload, :preload_all,
|
65
67
|
:adapter, :add, :exist?, :remove, :import, :export,
|
66
|
-
:memoize=, :memoizing?,
|
68
|
+
:memoize=, :memoizing?, :read_only?,
|
67
69
|
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
68
70
|
|
71
|
+
def any(*args)
|
72
|
+
Expression.build({ Any: args.flatten })
|
73
|
+
end
|
74
|
+
|
75
|
+
def all(*args)
|
76
|
+
Expression.build({ All: args.flatten })
|
77
|
+
end
|
78
|
+
|
79
|
+
def constant(value)
|
80
|
+
Expression.build(value)
|
81
|
+
end
|
82
|
+
|
83
|
+
def property(name)
|
84
|
+
Expression.build({ Property: name })
|
85
|
+
end
|
86
|
+
|
87
|
+
def string(value)
|
88
|
+
Expression.build({ String: value })
|
89
|
+
end
|
90
|
+
|
91
|
+
def number(value)
|
92
|
+
Expression.build({ Number: value })
|
93
|
+
end
|
94
|
+
|
95
|
+
def boolean(value)
|
96
|
+
Expression.build({ Boolean: value })
|
97
|
+
end
|
98
|
+
|
99
|
+
def random(max)
|
100
|
+
Expression.build({ Random: max })
|
101
|
+
end
|
102
|
+
|
69
103
|
# Public: Use this to register a group by name.
|
70
104
|
#
|
71
105
|
# name - The Symbol name of the group.
|
@@ -142,7 +176,9 @@ require 'flipper/actor'
|
|
142
176
|
require 'flipper/adapter'
|
143
177
|
require 'flipper/adapters/memoizable'
|
144
178
|
require 'flipper/adapters/memory'
|
179
|
+
require 'flipper/adapters/strict'
|
145
180
|
require 'flipper/adapters/instrumented'
|
181
|
+
require 'flipper/adapter_builder'
|
146
182
|
require 'flipper/configuration'
|
147
183
|
require 'flipper/dsl'
|
148
184
|
require 'flipper/errors'
|
@@ -155,6 +191,7 @@ require 'flipper/middleware/memoizer'
|
|
155
191
|
require 'flipper/middleware/setup_env'
|
156
192
|
require 'flipper/poller'
|
157
193
|
require 'flipper/registry'
|
194
|
+
require 'flipper/expression'
|
158
195
|
require 'flipper/type'
|
159
196
|
require 'flipper/types/actor'
|
160
197
|
require 'flipper/types/boolean'
|
@@ -0,0 +1,73 @@
|
|
1
|
+
RSpec.describe Flipper::AdapterBuilder do
|
2
|
+
describe "#initialize" do
|
3
|
+
it "instance_eval's block with no arg" do
|
4
|
+
called = false
|
5
|
+
self_in_block = nil
|
6
|
+
|
7
|
+
described_class.new do
|
8
|
+
called = true
|
9
|
+
self_in_block = self
|
10
|
+
end
|
11
|
+
|
12
|
+
expect(self_in_block).to be_instance_of(described_class)
|
13
|
+
expect(called).to be(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "evals block with arg" do
|
17
|
+
called = false
|
18
|
+
self_outside_block = self
|
19
|
+
self_in_block = nil
|
20
|
+
|
21
|
+
described_class.new do |arg|
|
22
|
+
called = true
|
23
|
+
self_in_block = self
|
24
|
+
expect(arg).to be_instance_of(described_class)
|
25
|
+
end
|
26
|
+
|
27
|
+
expect(self_in_block).to be(self_outside_block)
|
28
|
+
expect(called).to be(true)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#use" do
|
33
|
+
it "wraps the store adapter with the given adapter" do
|
34
|
+
subject.use(Flipper::Adapters::Memoizable)
|
35
|
+
subject.use(Flipper::Adapters::Strict, :warn)
|
36
|
+
|
37
|
+
memoizable_adapter = subject.to_adapter
|
38
|
+
strict_adapter = memoizable_adapter.adapter
|
39
|
+
memory_adapter = strict_adapter.adapter
|
40
|
+
|
41
|
+
|
42
|
+
expect(memoizable_adapter).to be_instance_of(Flipper::Adapters::Memoizable)
|
43
|
+
expect(strict_adapter).to be_instance_of(Flipper::Adapters::Strict)
|
44
|
+
expect(strict_adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
|
45
|
+
expect(memory_adapter).to be_instance_of(Flipper::Adapters::Memory)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "passes block to adapter initializer" do
|
49
|
+
expected_block = lambda {}
|
50
|
+
adapter_class = double('adapter class')
|
51
|
+
|
52
|
+
subject.use(adapter_class, &expected_block)
|
53
|
+
|
54
|
+
expect(adapter_class).to receive(:new) { |&block| expect(block).to be(expected_block) }.and_return(:adapter)
|
55
|
+
expect(subject.to_adapter).to be(:adapter)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#store" do
|
60
|
+
it "defaults to memory adapter" do
|
61
|
+
expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "only saves one store" do
|
65
|
+
require "flipper/adapters/pstore"
|
66
|
+
subject.store(Flipper::Adapters::PStore)
|
67
|
+
expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::PStore)
|
68
|
+
|
69
|
+
subject.store(Flipper::Adapters::Memory)
|
70
|
+
expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -84,16 +84,50 @@ RSpec.describe Flipper::Adapters::Http do
|
|
84
84
|
}
|
85
85
|
stub_request(:get, "http://app.com/flipper/features/feature_panel")
|
86
86
|
.with(headers: headers)
|
87
|
-
.to_return(status: 404
|
87
|
+
.to_return(status: 404)
|
88
88
|
|
89
89
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
90
90
|
adapter.get(flipper[:feature_panel])
|
91
91
|
end
|
92
92
|
|
93
|
+
it "sends framework versions" do
|
94
|
+
stub_const("Rails", double(version: "7.1.0"))
|
95
|
+
stub_const("Sinatra::VERSION", "3.1.0")
|
96
|
+
stub_const("Hanami::VERSION", "0.7.2")
|
97
|
+
|
98
|
+
headers = {
|
99
|
+
"Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0", "hanami=0.7.2"]
|
100
|
+
}
|
101
|
+
|
102
|
+
stub_request(:get, "http://app.com/flipper/features/feature_panel")
|
103
|
+
.with(headers: headers)
|
104
|
+
.to_return(status: 404)
|
105
|
+
|
106
|
+
adapter = described_class.new(url: 'http://app.com/flipper')
|
107
|
+
adapter.get(flipper[:feature_panel])
|
108
|
+
end
|
109
|
+
|
110
|
+
it "does not send undefined framework versions" do
|
111
|
+
stub_const("Rails", double(version: "7.1.0"))
|
112
|
+
stub_const("Sinatra::VERSION", "3.1.0")
|
113
|
+
|
114
|
+
headers = {
|
115
|
+
"Client-Framework" => ["rails=7.1.0", "sinatra=3.1.0"]
|
116
|
+
}
|
117
|
+
|
118
|
+
stub_request(:get, "http://app.com/flipper/features/feature_panel")
|
119
|
+
.with(headers: headers)
|
120
|
+
.to_return(status: 404)
|
121
|
+
|
122
|
+
adapter = described_class.new(url: 'http://app.com/flipper')
|
123
|
+
adapter.get(flipper[:feature_panel])
|
124
|
+
end
|
125
|
+
|
126
|
+
|
93
127
|
describe "#get" do
|
94
128
|
it "raises error when not successful response" do
|
95
129
|
stub_request(:get, "http://app.com/flipper/features/feature_panel")
|
96
|
-
.to_return(status: 503
|
130
|
+
.to_return(status: 503)
|
97
131
|
|
98
132
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
99
133
|
expect {
|
@@ -105,7 +139,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
105
139
|
describe "#get_multi" do
|
106
140
|
it "raises error when not successful response" do
|
107
141
|
stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
|
108
|
-
.to_return(status: 503
|
142
|
+
.to_return(status: 503)
|
109
143
|
|
110
144
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
111
145
|
expect {
|
@@ -117,7 +151,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
117
151
|
describe "#get_all" do
|
118
152
|
it "raises error when not successful response" do
|
119
153
|
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
120
|
-
.to_return(status: 503
|
154
|
+
.to_return(status: 503)
|
121
155
|
|
122
156
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
123
157
|
expect {
|
@@ -129,7 +163,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
129
163
|
describe "#features" do
|
130
164
|
it "raises error when not successful response" do
|
131
165
|
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
132
|
-
.to_return(status: 503
|
166
|
+
.to_return(status: 503)
|
133
167
|
|
134
168
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
135
169
|
expect {
|
@@ -2,7 +2,7 @@ require 'flipper/adapters/memoizable'
|
|
2
2
|
require 'flipper/adapters/operation_logger'
|
3
3
|
|
4
4
|
RSpec.describe Flipper::Adapters::Memoizable do
|
5
|
-
let(:features_key) {
|
5
|
+
let(:features_key) { :flipper_features }
|
6
6
|
let(:adapter) { Flipper::Adapters::Memory.new }
|
7
7
|
let(:flipper) { Flipper.new(adapter) }
|
8
8
|
let(:cache) { {} }
|
@@ -54,7 +54,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
54
54
|
it 'memoizes feature' do
|
55
55
|
feature = flipper[:stats]
|
56
56
|
result = subject.get(feature)
|
57
|
-
expect(cache[
|
57
|
+
expect(cache["feature/#{feature.key}"]).to be(result)
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
@@ -83,8 +83,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
83
83
|
features = names.map { |name| flipper[name] }
|
84
84
|
results = subject.get_multi(features)
|
85
85
|
features.each do |feature|
|
86
|
-
expect(cache[
|
87
|
-
expect(cache[
|
86
|
+
expect(cache["feature/#{feature.key}"]).not_to be(nil)
|
87
|
+
expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -115,10 +115,10 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
115
115
|
features = names.map { |name| flipper[name].tap(&:enable) }
|
116
116
|
results = subject.get_all
|
117
117
|
features.each do |feature|
|
118
|
-
expect(cache[
|
119
|
-
expect(cache[
|
118
|
+
expect(cache["feature/#{feature.key}"]).not_to be(nil)
|
119
|
+
expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
|
120
120
|
end
|
121
|
-
expect(cache[
|
121
|
+
expect(cache[:flipper_features]).to eq(names.map(&:to_s).to_set)
|
122
122
|
end
|
123
123
|
|
124
124
|
it 'only calls get_all once for memoized adapter' do
|
@@ -188,9 +188,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
188
188
|
it 'unmemoizes feature' do
|
189
189
|
feature = flipper[:stats]
|
190
190
|
gate = feature.gate(:boolean)
|
191
|
-
cache[
|
191
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
192
192
|
subject.enable(feature, gate, Flipper::Types::Boolean.new)
|
193
|
-
expect(cache[
|
193
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
194
194
|
end
|
195
195
|
end
|
196
196
|
|
@@ -218,9 +218,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
218
218
|
it 'unmemoizes feature' do
|
219
219
|
feature = flipper[:stats]
|
220
220
|
gate = feature.gate(:boolean)
|
221
|
-
cache[
|
221
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
222
222
|
subject.disable(feature, gate, Flipper::Types::Boolean.new)
|
223
|
-
expect(cache[
|
223
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
224
224
|
end
|
225
225
|
end
|
226
226
|
|
@@ -332,9 +332,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
332
332
|
|
333
333
|
it 'unmemoizes the feature' do
|
334
334
|
feature = flipper[:stats]
|
335
|
-
cache[
|
335
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
336
336
|
subject.remove(feature)
|
337
|
-
expect(cache[
|
337
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
338
338
|
end
|
339
339
|
end
|
340
340
|
|
@@ -357,9 +357,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
357
357
|
|
358
358
|
it 'unmemoizes feature' do
|
359
359
|
feature = flipper[:stats]
|
360
|
-
cache[
|
360
|
+
cache["feature/#{feature.key}"] = { some: 'thing' }
|
361
361
|
subject.clear(feature)
|
362
|
-
expect(cache[
|
362
|
+
expect(cache["feature/#{feature.key}"]).to be_nil
|
363
363
|
end
|
364
364
|
end
|
365
365
|
|
@@ -5,11 +5,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
5
5
|
let(:flipper) { Flipper.new(subject) }
|
6
6
|
let(:feature) { flipper[:stats] }
|
7
7
|
|
8
|
-
let(:boolean_gate)
|
9
|
-
let(:group_gate)
|
10
|
-
let(:actor_gate)
|
11
|
-
let(:
|
12
|
-
let(:
|
8
|
+
let(:boolean_gate) { feature.gate(:boolean) }
|
9
|
+
let(:group_gate) { feature.gate(:group) }
|
10
|
+
let(:actor_gate) { feature.gate(:actor) }
|
11
|
+
let(:expression_gate) { feature.gate(:expression) }
|
12
|
+
let(:actors_gate) { feature.gate(:percentage_of_actors) }
|
13
|
+
let(:time_gate) { feature.gate(:percentage_of_time) }
|
13
14
|
|
14
15
|
subject { described_class.new(adapter) }
|
15
16
|
|
@@ -41,18 +42,28 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
41
42
|
end
|
42
43
|
|
43
44
|
it 'can get feature' do
|
45
|
+
expression = Flipper.property(:plan).eq("basic")
|
44
46
|
actor22 = Flipper::Actor.new('22')
|
45
47
|
adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
|
46
48
|
adapter.enable(feature, group_gate, flipper.group(:admins))
|
47
49
|
adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
|
48
50
|
adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
|
49
51
|
adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
adapter.enable(feature, expression_gate, expression)
|
53
|
+
|
54
|
+
expect(subject.get(feature)).to eq({
|
55
|
+
boolean: 'true',
|
56
|
+
groups: Set['admins'],
|
57
|
+
actors: Set['22'],
|
58
|
+
expression: {
|
59
|
+
"Equal" => [
|
60
|
+
{"Property" => ["plan"]},
|
61
|
+
"basic",
|
62
|
+
]
|
63
|
+
},
|
64
|
+
percentage_of_actors: '25',
|
65
|
+
percentage_of_time: '45',
|
66
|
+
})
|
56
67
|
end
|
57
68
|
|
58
69
|
it 'can get features' do
|
@@ -61,6 +72,10 @@ RSpec.describe Flipper::Adapters::ReadOnly do
|
|
61
72
|
expect(subject.features).to eq(Set['stats'])
|
62
73
|
end
|
63
74
|
|
75
|
+
it 'is configured as read only' do
|
76
|
+
expect(subject.read_only?).to eq(true)
|
77
|
+
end
|
78
|
+
|
64
79
|
it 'raises error on add' do
|
65
80
|
expect { subject.add(feature) }.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
|
66
81
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
RSpec.describe Flipper::Adapters::Strict do
|
2
|
+
let(:flipper) { Flipper.new(subject) }
|
3
|
+
let(:feature) { flipper[:unknown] }
|
4
|
+
|
5
|
+
it_should_behave_like 'a flipper adapter' do
|
6
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :noop) }
|
7
|
+
end
|
8
|
+
|
9
|
+
context "handler = :raise" do
|
10
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :raise) }
|
11
|
+
|
12
|
+
context "#get" do
|
13
|
+
it "raises an error for unknown feature" do
|
14
|
+
expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "#get_multi" do
|
19
|
+
it "raises an error for unknown feature" do
|
20
|
+
expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "handler = :warn" do
|
26
|
+
subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
|
27
|
+
|
28
|
+
context "#get" do
|
29
|
+
it "raises an error for unknown feature" do
|
30
|
+
expect(silence { subject.get(feature) }).to match(/Could not find feature "unknown"/)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "#get_multi" do
|
35
|
+
it "raises an error for unknown feature" do
|
36
|
+
expect(silence { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "handler = Block" do
|
42
|
+
let(:unknown_features) { [] }
|
43
|
+
subject do
|
44
|
+
described_class.new(Flipper::Adapters::Memory.new) { |feature| unknown_features << feature.key}
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
context "#get" do
|
49
|
+
it "raises an error for unknown feature" do
|
50
|
+
subject.get(feature)
|
51
|
+
expect(unknown_features).to eq(["unknown"])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "#get_multi" do
|
56
|
+
it "raises an error for unknown feature" do
|
57
|
+
subject.get_multi([flipper[:foo], flipper[:bar]])
|
58
|
+
expect(unknown_features).to eq(["foo", "bar"])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -8,6 +8,8 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
|
8
8
|
end
|
9
9
|
let(:flipper) { Flipper.new(adapter) }
|
10
10
|
let(:feature) { flipper[:search] }
|
11
|
+
let(:plan_expression) { Flipper.property(:plan).eq("basic") }
|
12
|
+
let(:age_expression) { Flipper.property(:age).gte(21) }
|
11
13
|
|
12
14
|
context "when remote disabled" do
|
13
15
|
let(:remote) { Flipper::GateValues.new({}) }
|
@@ -63,6 +65,7 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
|
63
65
|
boolean: nil,
|
64
66
|
actors: Set["1"],
|
65
67
|
groups: Set["staff"],
|
68
|
+
expression: plan_expression.value,
|
66
69
|
percentage_of_time: 10,
|
67
70
|
percentage_of_actors: 15,
|
68
71
|
}
|
@@ -74,10 +77,34 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
|
|
74
77
|
expect(local_gate_values_hash.fetch(:boolean)).to be(nil)
|
75
78
|
expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"])
|
76
79
|
expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"])
|
80
|
+
expect(local_gate_values_hash.fetch(:expression)).to eq(plan_expression.value)
|
77
81
|
expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10")
|
78
82
|
expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15")
|
79
83
|
end
|
80
84
|
|
85
|
+
it "updates expression when remote is updated" do
|
86
|
+
any_expression = Flipper.any(plan_expression, age_expression)
|
87
|
+
remote = Flipper::GateValues.new(expression: any_expression.value)
|
88
|
+
feature.enable_expression(age_expression)
|
89
|
+
adapter.reset
|
90
|
+
|
91
|
+
described_class.new(feature, feature.gate_values, remote).call
|
92
|
+
|
93
|
+
expect(feature.expression_value).to eq(any_expression.value)
|
94
|
+
expect_only_enable
|
95
|
+
end
|
96
|
+
|
97
|
+
it "does nothing to expression if in sync" do
|
98
|
+
remote = Flipper::GateValues.new(expression: plan_expression.value)
|
99
|
+
feature.enable_expression(plan_expression)
|
100
|
+
adapter.reset
|
101
|
+
|
102
|
+
described_class.new(feature, feature.gate_values, remote).call
|
103
|
+
|
104
|
+
expect(feature.expression_value).to eq(plan_expression.value)
|
105
|
+
expect_no_enable_or_disable
|
106
|
+
end
|
107
|
+
|
81
108
|
it "adds remotely added actors" do
|
82
109
|
remote = Flipper::GateValues.new(actors: Set["1", "2"])
|
83
110
|
feature.enable_actor(Flipper::Actor.new("1"))
|
@@ -61,14 +61,14 @@ RSpec.describe Flipper::Cloud::Configuration do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
it "can set sync_interval" do
|
64
|
-
instance = described_class.new(required_options.merge(sync_interval:
|
65
|
-
expect(instance.sync_interval).to eq(
|
64
|
+
instance = described_class.new(required_options.merge(sync_interval: 15))
|
65
|
+
expect(instance.sync_interval).to eq(15)
|
66
66
|
end
|
67
67
|
|
68
68
|
it "can set sync_interval from ENV var" do
|
69
|
-
with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "
|
69
|
+
with_env "FLIPPER_CLOUD_SYNC_INTERVAL" => "15" do
|
70
70
|
instance = described_class.new(required_options.reject { |k, v| k == :sync_interval })
|
71
|
-
expect(instance.sync_interval).to eq(
|
71
|
+
expect(instance.sync_interval).to eq(15)
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
@@ -76,9 +76,9 @@ RSpec.describe Flipper::Cloud::Configuration do
|
|
76
76
|
# The initial sync of http to local invokes this web request.
|
77
77
|
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
78
78
|
|
79
|
-
instance = described_class.new(required_options.merge(sync_interval:
|
79
|
+
instance = described_class.new(required_options.merge(sync_interval: 20))
|
80
80
|
poller = instance.send(:poller)
|
81
|
-
expect(poller.interval).to eq(
|
81
|
+
expect(poller.interval).to eq(20)
|
82
82
|
end
|
83
83
|
|
84
84
|
it "can set debug_output" do
|
@@ -249,21 +249,4 @@ RSpec.describe Flipper::Cloud::Configuration do
|
|
249
249
|
expect(all["search"][:boolean]).to eq("true")
|
250
250
|
expect(all["history"][:boolean]).to eq(nil)
|
251
251
|
end
|
252
|
-
|
253
|
-
it "can setup brow to report events to cloud" do
|
254
|
-
# skip logging brow
|
255
|
-
Brow.logger = Logger.new(File::NULL)
|
256
|
-
brow = described_class.new(required_options).brow
|
257
|
-
|
258
|
-
stub = stub_request(:post, "https://www.flippercloud.io/adapter/events")
|
259
|
-
.with { |request|
|
260
|
-
data = JSON.parse(request.body)
|
261
|
-
data.keys == ["uuid", "messages"] && data["messages"] == [{"n" => 1}]
|
262
|
-
}
|
263
|
-
.to_return(status: 201, body: "{}", headers: {})
|
264
|
-
|
265
|
-
brow.push({"n" => 1})
|
266
|
-
brow.worker.stop
|
267
|
-
expect(stub).to have_been_requested.times(1)
|
268
|
-
end
|
269
252
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'flipper/cloud/telemetry/backoff_policy'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Cloud::Telemetry::BackoffPolicy do
|
4
|
+
context "#initialize" do
|
5
|
+
it "with no options" do
|
6
|
+
policy = described_class.new
|
7
|
+
expect(policy.min_timeout_ms).to eq(1_000)
|
8
|
+
expect(policy.max_timeout_ms).to eq(30_000)
|
9
|
+
expect(policy.multiplier).to eq(1.5)
|
10
|
+
expect(policy.randomization_factor).to eq(0.5)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "with options" do
|
14
|
+
policy = described_class.new({
|
15
|
+
min_timeout_ms: 1234,
|
16
|
+
max_timeout_ms: 5678,
|
17
|
+
multiplier: 24,
|
18
|
+
randomization_factor: 0.4,
|
19
|
+
})
|
20
|
+
expect(policy.min_timeout_ms).to eq(1234)
|
21
|
+
expect(policy.max_timeout_ms).to eq(5678)
|
22
|
+
expect(policy.multiplier).to eq(24)
|
23
|
+
expect(policy.randomization_factor).to eq(0.4)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "with min higher than max" do
|
27
|
+
expect {
|
28
|
+
described_class.new({
|
29
|
+
min_timeout_ms: 2,
|
30
|
+
max_timeout_ms: 1,
|
31
|
+
})
|
32
|
+
}.to raise_error(ArgumentError, ":min_timeout_ms (2) must be <= :max_timeout_ms (1)")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "with invalid min_timeout_ms" do
|
36
|
+
expect {
|
37
|
+
described_class.new({
|
38
|
+
min_timeout_ms: -1,
|
39
|
+
})
|
40
|
+
}.to raise_error(ArgumentError, ":min_timeout_ms must be >= 0 but was -1")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "with invalid max_timeout_ms" do
|
44
|
+
expect {
|
45
|
+
described_class.new({
|
46
|
+
max_timeout_ms: -1,
|
47
|
+
})
|
48
|
+
}.to raise_error(ArgumentError, ":max_timeout_ms must be >= 0 but was -1")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "from env" do
|
52
|
+
env = {
|
53
|
+
"FLIPPER_BACKOFF_MIN_TIMEOUT_MS" => "1000",
|
54
|
+
"FLIPPER_BACKOFF_MAX_TIMEOUT_MS" => "2000",
|
55
|
+
"FLIPPER_BACKOFF_MULTIPLIER" => "1.9",
|
56
|
+
"FLIPPER_BACKOFF_RANDOMIZATION_FACTOR" => "0.1",
|
57
|
+
}
|
58
|
+
with_env env do
|
59
|
+
policy = described_class.new
|
60
|
+
expect(policy.min_timeout_ms).to eq(1000)
|
61
|
+
expect(policy.max_timeout_ms).to eq(2000)
|
62
|
+
expect(policy.multiplier).to eq(1.9)
|
63
|
+
expect(policy.randomization_factor).to eq(0.1)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "#next_interval" do
|
69
|
+
it "works" do
|
70
|
+
policy = described_class.new({
|
71
|
+
min_timeout_ms: 1_000,
|
72
|
+
max_timeout_ms: 10_000,
|
73
|
+
multiplier: 2,
|
74
|
+
randomization_factor: 0.5,
|
75
|
+
})
|
76
|
+
|
77
|
+
expect(policy.next_interval).to be_within(500).of(1000)
|
78
|
+
expect(policy.next_interval).to be_within(1000).of(2000)
|
79
|
+
expect(policy.next_interval).to be_within(2000).of(4000)
|
80
|
+
expect(policy.next_interval).to be_within(4000).of(8000)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "caps maximum duration at max_timeout_secs" do
|
84
|
+
policy = described_class.new({
|
85
|
+
min_timeout_ms: 1_000,
|
86
|
+
max_timeout_ms: 10_000,
|
87
|
+
multiplier: 2,
|
88
|
+
randomization_factor: 0.5,
|
89
|
+
})
|
90
|
+
10.times { policy.next_interval }
|
91
|
+
expect(policy.next_interval).to eq(10_000)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it "can reset" do
|
96
|
+
policy = described_class.new({
|
97
|
+
min_timeout_ms: 1_000,
|
98
|
+
max_timeout_ms: 10_000,
|
99
|
+
multiplier: 2,
|
100
|
+
randomization_factor: 0.5,
|
101
|
+
})
|
102
|
+
10.times { policy.next_interval }
|
103
|
+
|
104
|
+
expect(policy.attempts).to eq(10)
|
105
|
+
policy.reset
|
106
|
+
expect(policy.attempts).to eq(0)
|
107
|
+
end
|
108
|
+
end
|