flipper 0.26.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 +19 -13
- data/.github/workflows/examples.yml +32 -15
- data/Changelog.md +294 -154
- data/Gemfile +15 -10
- data/README.md +13 -11
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +27 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- 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/threaded.rb +33 -0
- data/examples/dsl.rb +1 -15
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/expressions.rb +213 -0
- data/examples/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +3 -5
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +33 -7
- 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 +29 -16
- data/lib/flipper/adapters/instrumented.rb +25 -6
- data/lib/flipper/adapters/memoizable.rb +33 -21
- data/lib/flipper/adapters/memory.rb +81 -46
- data/lib/flipper/adapters/operation_logger.rb +16 -7
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +5 -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 +258 -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/routes.rb +14 -0
- 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/cloud.rb +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +88 -0
- data/lib/flipper/errors.rb +3 -3
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- 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 +87 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +13 -11
- data/lib/flipper/gate_values.rb +5 -18
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/metadata.rb +5 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
- data/lib/flipper/test/shared_adapter_test.rb +91 -48
- data/lib/flipper/typecast.rb +56 -15
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +47 -10
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/http_spec.rb +64 -8
- data/spec/flipper/adapters/instrumented_spec.rb +29 -11
- data/spec/flipper/adapters/memoizable_spec.rb +51 -31
- data/spec/flipper/adapters/memory_spec.rb +14 -3
- data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- 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 +252 -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/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 +180 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -73
- data/spec/flipper/engine_spec.rb +291 -0
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- 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_check_context_spec.rb +17 -17
- data/spec/flipper/feature_spec.rb +436 -33
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- data/spec/flipper/poller_spec.rb +47 -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 +121 -6
- data/spec/flipper/types/actor_spec.rb +63 -46
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +168 -58
- data/spec/flipper_spec.rb +92 -28
- data/spec/spec_helper.rb +6 -13
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/climate_control.rb +7 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +166 -13
- data/.github/workflows/release.yml +0 -44
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,108 @@
|
|
1
|
+
RSpec.describe Flipper::Gates::Expression do
|
2
|
+
let(:feature_name) { :search }
|
3
|
+
|
4
|
+
subject do
|
5
|
+
described_class.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def context(expression, properties: {})
|
9
|
+
Flipper::FeatureCheckContext.new(
|
10
|
+
feature_name: feature_name,
|
11
|
+
values: Flipper::GateValues.new(expression: expression),
|
12
|
+
actors: [Flipper::Types::Actor.new(Flipper::Actor.new(1, properties))]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#enabled?' do
|
17
|
+
context 'for nil value' do
|
18
|
+
it 'returns false' do
|
19
|
+
expect(subject.enabled?(nil)).to eq(false)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'for empty value' do
|
24
|
+
it 'returns false' do
|
25
|
+
expect(subject.enabled?({})).to eq(false)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "for not empty value" do
|
30
|
+
it 'returns true' do
|
31
|
+
expect(subject.enabled?({"Boolean" => [true]})).to eq(true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#open?' do
|
37
|
+
context 'for expression that evaluates to true' do
|
38
|
+
it 'returns true' do
|
39
|
+
expression = Flipper.boolean(true).eq(true)
|
40
|
+
expect(subject.open?(context(expression.value))).to be(true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'for expression that evaluates to false' do
|
45
|
+
it 'returns false' do
|
46
|
+
expression = Flipper.boolean(true).eq(false)
|
47
|
+
expect(subject.open?(context(expression.value))).to be(false)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'for properties that have string keys' do
|
52
|
+
it 'returns true when expression evalutes to true' do
|
53
|
+
expression = Flipper.property(:type).eq("User")
|
54
|
+
context = context(expression.value, properties: {"type" => "User"})
|
55
|
+
expect(subject.open?(context)).to be(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns false when expression evaluates to false' do
|
59
|
+
expression = Flipper.property(:type).eq("User")
|
60
|
+
context = context(expression.value, properties: {"type" => "Org"})
|
61
|
+
expect(subject.open?(context)).to be(false)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'for properties that have symbol keys' do
|
66
|
+
it 'returns true when expression evalutes to true' do
|
67
|
+
expression = Flipper.property(:type).eq("User")
|
68
|
+
context = context(expression.value, properties: {type: "User"})
|
69
|
+
expect(subject.open?(context)).to be(true)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns false when expression evaluates to false' do
|
73
|
+
expression = Flipper.property(:type).eq("User")
|
74
|
+
context = context(expression.value, properties: {type: "Org"})
|
75
|
+
expect(subject.open?(context)).to be(false)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#protects?' do
|
81
|
+
it 'returns true for Flipper::Expression' do
|
82
|
+
expression = Flipper.number(20).eq(20)
|
83
|
+
expect(subject.protects?(expression)).to be(true)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'returns true for Hash' do
|
87
|
+
expression = Flipper.number(20).eq(20)
|
88
|
+
expect(subject.protects?(expression.value)).to be(true)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns false for other things' do
|
92
|
+
expect(subject.protects?(false)).to be(false)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#wrap' do
|
97
|
+
it 'returns self for Flipper::Expression' do
|
98
|
+
expression = Flipper.number(20).eq(20)
|
99
|
+
expect(subject.wrap(expression)).to be(expression)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns Flipper::Expression for Hash' do
|
103
|
+
expression = Flipper.number(20).eq(20)
|
104
|
+
expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expression)
|
105
|
+
expect(subject.wrap(expression.value)).to eq(expression)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -9,18 +9,17 @@ RSpec.describe Flipper::Gates::Group do
|
|
9
9
|
Flipper::FeatureCheckContext.new(
|
10
10
|
feature_name: feature_name,
|
11
11
|
values: Flipper::GateValues.new(groups: set),
|
12
|
-
|
12
|
+
actors: [Flipper::Types::Actor.new(Flipper::Actor.new('5'))]
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
16
16
|
describe '#open?' do
|
17
17
|
context 'with a group in adapter, but not registered' do
|
18
18
|
before do
|
19
|
-
Flipper.register(:staff) { |
|
19
|
+
Flipper.register(:staff) { |actor| true }
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'ignores group' do
|
23
|
-
thing = Flipper::Actor.new('5')
|
24
23
|
expect(subject.open?(context(Set[:newbs, :staff]))).to be(true)
|
25
24
|
end
|
26
25
|
end
|
@@ -5,11 +5,11 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
5
5
|
described_class.new
|
6
6
|
end
|
7
7
|
|
8
|
-
def context(percentage_of_actors_value, feature = feature_name,
|
8
|
+
def context(percentage_of_actors_value, feature = feature_name, actors = nil)
|
9
9
|
Flipper::FeatureCheckContext.new(
|
10
10
|
feature_name: feature,
|
11
11
|
values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
|
12
|
-
|
12
|
+
actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
@@ -20,7 +20,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
20
20
|
let(:number_of_actors) { 10_000 }
|
21
21
|
|
22
22
|
let(:actors) do
|
23
|
-
(1..number_of_actors).map { |n| Flipper::Actor.new(n) }
|
23
|
+
(1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new(n.to_s)) }
|
24
24
|
end
|
25
25
|
|
26
26
|
let(:feature_one_enabled_actors) do
|
@@ -48,13 +48,69 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
context "with an array of actors" do
|
52
|
+
let(:percentage) { 0.05 }
|
53
|
+
let(:percentage_as_integer) { percentage * 100 }
|
54
|
+
let(:number_of_actors) { 3_000 }
|
55
|
+
|
56
|
+
let(:user_actors) do
|
57
|
+
(1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("User;#{n}")) }
|
58
|
+
end
|
59
|
+
|
60
|
+
let(:team_actors) do
|
61
|
+
(1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("Team;#{n}")) }
|
62
|
+
end
|
63
|
+
|
64
|
+
let(:org_actors) do
|
65
|
+
(1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new("Org;#{n}")) }
|
66
|
+
end
|
67
|
+
|
68
|
+
let(:actors) { user_actors + team_actors + org_actors }
|
69
|
+
|
70
|
+
let(:feature_one_enabled_actors) do
|
71
|
+
actors.each_slice(3).select do |group|
|
72
|
+
context = context(percentage_as_integer, :name_one, group)
|
73
|
+
subject.open?(context)
|
74
|
+
end.flatten
|
75
|
+
end
|
76
|
+
|
77
|
+
let(:feature_two_enabled_actors) do
|
78
|
+
actors.each_slice(3).select do |group|
|
79
|
+
context = context(percentage_as_integer, :name_two, group)
|
80
|
+
subject.open?(context)
|
81
|
+
end.flatten
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not enable both features for same set of actors' do
|
85
|
+
expect(feature_one_enabled_actors).not_to eq(feature_two_enabled_actors)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'enables feature for accurate number of actors for each feature' do
|
89
|
+
margin_of_error = 0.02 * actors.size # 2 percent margin of error
|
90
|
+
expected_enabled_size = actors.size * percentage
|
91
|
+
|
92
|
+
[
|
93
|
+
feature_one_enabled_actors.size,
|
94
|
+
feature_two_enabled_actors.size,
|
95
|
+
].each do |size|
|
96
|
+
expect(size).to be_within(margin_of_error).of(expected_enabled_size)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it "is consistent regardless of order of actors" do
|
101
|
+
actors = user_actors.first(10)
|
102
|
+
results = 100.times.map { |n| subject.open?(context(75, :some_feature, actors.shuffle)) }
|
103
|
+
expect(results.uniq).to eq([true])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
51
107
|
context 'for fractional percentage' do
|
52
108
|
let(:decimal) { 0.001 }
|
53
109
|
let(:percentage) { decimal * 100 }
|
54
110
|
let(:number_of_actors) { 10_000 }
|
55
111
|
|
56
112
|
let(:actors) do
|
57
|
-
(1..number_of_actors).map { |n| Flipper::Actor.new(n) }
|
113
|
+
(1..number_of_actors).map { |n| Flipper::Types::Actor.new(Flipper::Actor.new(n.to_s)) }
|
58
114
|
end
|
59
115
|
|
60
116
|
subject { described_class.new }
|
@@ -64,7 +120,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
64
120
|
expected_open_count = number_of_actors * decimal
|
65
121
|
|
66
122
|
open_count = actors.select do |actor|
|
67
|
-
context = context(percentage, :feature, actor)
|
123
|
+
context = context(percentage, :feature, [actor])
|
68
124
|
subject.open?(context)
|
69
125
|
end.size
|
70
126
|
|
@@ -5,11 +5,11 @@ RSpec.describe Flipper::Gates::PercentageOfTime do
|
|
5
5
|
described_class.new
|
6
6
|
end
|
7
7
|
|
8
|
-
def context(percentage_of_time_value, feature = feature_name,
|
8
|
+
def context(percentage_of_time_value, feature = feature_name, actors = nil)
|
9
9
|
Flipper::FeatureCheckContext.new(
|
10
10
|
feature_name: feature,
|
11
11
|
values: Flipper::GateValues.new(percentage_of_time: percentage_of_time_value),
|
12
|
-
|
12
|
+
actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
@@ -2,12 +2,11 @@ require 'flipper/identifier'
|
|
2
2
|
|
3
3
|
RSpec.describe Flipper::Identifier do
|
4
4
|
describe '#flipper_id' do
|
5
|
-
class User < Struct.new(:id)
|
6
|
-
include Flipper::Identifier
|
7
|
-
end
|
8
|
-
|
9
5
|
it 'uses class name and id' do
|
10
|
-
|
6
|
+
class BlahBlah < Struct.new(:id)
|
7
|
+
include Flipper::Identifier
|
8
|
+
end
|
9
|
+
expect(BlahBlah.new(5).flipper_id).to eq('BlahBlah;5')
|
11
10
|
end
|
12
11
|
end
|
13
12
|
end
|
@@ -2,6 +2,12 @@ require 'logger'
|
|
2
2
|
require 'flipper/adapters/instrumented'
|
3
3
|
require 'flipper/instrumentation/log_subscriber'
|
4
4
|
|
5
|
+
begin
|
6
|
+
require 'active_support/isolated_execution_state'
|
7
|
+
rescue LoadError
|
8
|
+
# ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
|
9
|
+
end
|
10
|
+
|
5
11
|
RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
6
12
|
let(:adapter) do
|
7
13
|
memory = Flipper::Adapters::Memory.new
|
@@ -12,8 +18,8 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
12
18
|
end
|
13
19
|
|
14
20
|
before do
|
15
|
-
Flipper.register(:admins) do |
|
16
|
-
|
21
|
+
Flipper.register(:admins) do |actor|
|
22
|
+
actor.respond_to?(:admin?) && actor.admin?
|
17
23
|
end
|
18
24
|
|
19
25
|
@io = StringIO.new
|
@@ -26,6 +32,10 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
26
32
|
described_class.logger = nil
|
27
33
|
end
|
28
34
|
|
35
|
+
after(:all) do
|
36
|
+
ActiveSupport::Notifications.unsubscribe("flipper")
|
37
|
+
end
|
38
|
+
|
29
39
|
let(:log) { @io.string }
|
30
40
|
|
31
41
|
context 'feature enabled checks' do
|
@@ -36,7 +46,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
36
46
|
|
37
47
|
it 'logs feature calls with result after operation' do
|
38
48
|
feature_line = find_line('Flipper feature(search) enabled? false')
|
39
|
-
expect(feature_line).to include('[
|
49
|
+
expect(feature_line).to include('[ actors=nil ]')
|
40
50
|
end
|
41
51
|
|
42
52
|
it 'logs adapter calls' do
|
@@ -46,7 +56,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
46
56
|
end
|
47
57
|
end
|
48
58
|
|
49
|
-
context 'feature enabled checks with
|
59
|
+
context 'feature enabled checks with an actor' do
|
50
60
|
let(:user) { Flipper::Types::Actor.new(Flipper::Actor.new('1')) }
|
51
61
|
|
52
62
|
before do
|
@@ -54,7 +64,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
54
64
|
flipper[:search].enabled?(user)
|
55
65
|
end
|
56
66
|
|
57
|
-
it 'logs
|
67
|
+
it 'logs actors for feature' do
|
58
68
|
feature_line = find_line('Flipper feature(search) enabled?')
|
59
69
|
expect(feature_line).to include(user.inspect)
|
60
70
|
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'flipper/adapters/instrumented'
|
2
2
|
require 'flipper/instrumentation/statsd'
|
3
|
-
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'active_support/isolated_execution_state'
|
6
|
+
rescue LoadError
|
7
|
+
# ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
|
8
|
+
end
|
4
9
|
|
5
10
|
RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
|
6
11
|
let(:statsd_client) { Statsd.new }
|
@@ -25,6 +30,10 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
|
|
25
30
|
Thread.current[:statsd_socket] = nil
|
26
31
|
end
|
27
32
|
|
33
|
+
after(:all) do
|
34
|
+
ActiveSupport::Notifications.unsubscribe("flipper")
|
35
|
+
end
|
36
|
+
|
28
37
|
def assert_timer(metric)
|
29
38
|
regex = /#{Regexp.escape metric}\:\d+\|ms/
|
30
39
|
result = socket.buffer.detect { |op| op.first =~ regex }
|
@@ -68,4 +77,19 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
|
|
68
77
|
flipper[:stats].disable(user)
|
69
78
|
assert_timer 'flipper.adapter.memory.disable'
|
70
79
|
end
|
80
|
+
|
81
|
+
context 'when client is nil' do
|
82
|
+
before do
|
83
|
+
described_class.client = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'does not raise error' do
|
87
|
+
expect { flipper[:stats].enable(user) }.not_to raise_error
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'does not update metrics' do
|
91
|
+
flipper[:stats].enable(user)
|
92
|
+
expect(socket.buffer).to be_empty
|
93
|
+
end
|
94
|
+
end
|
71
95
|
end
|
@@ -196,6 +196,73 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
196
196
|
end
|
197
197
|
end
|
198
198
|
|
199
|
+
context 'with preload block' do
|
200
|
+
let(:app) do
|
201
|
+
app = lambda do |_env|
|
202
|
+
flipper[:stats].enabled?
|
203
|
+
flipper[:stats].enabled?
|
204
|
+
flipper[:shiny].enabled?
|
205
|
+
flipper[:shiny].enabled?
|
206
|
+
[200, {}, []]
|
207
|
+
end
|
208
|
+
|
209
|
+
described_class.new(app, preload: ->(request) {
|
210
|
+
case request.path
|
211
|
+
when "/true"
|
212
|
+
true
|
213
|
+
when "/specific"
|
214
|
+
[:stats]
|
215
|
+
else
|
216
|
+
false
|
217
|
+
end
|
218
|
+
})
|
219
|
+
end
|
220
|
+
|
221
|
+
include_examples 'flipper middleware'
|
222
|
+
|
223
|
+
it 'eagerly caches known features for duration of request if block returns true' do
|
224
|
+
flipper[:stats].enable
|
225
|
+
flipper[:shiny].enable
|
226
|
+
|
227
|
+
# clear the log of operations
|
228
|
+
adapter.reset
|
229
|
+
|
230
|
+
get '/true', {}, 'flipper' => flipper
|
231
|
+
|
232
|
+
expect(adapter.operations.size).to be(1)
|
233
|
+
expect(adapter.count(:get_all)).to be(1)
|
234
|
+
expect(adapter.count(:get)).to be(0)
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'does not eagerly cache known features if block returns false' do
|
238
|
+
flipper[:stats].enable
|
239
|
+
flipper[:shiny].enable
|
240
|
+
|
241
|
+
# clear the log of operations
|
242
|
+
adapter.reset
|
243
|
+
|
244
|
+
get '/false', {}, 'flipper' => flipper
|
245
|
+
|
246
|
+
expect(adapter.operations.size).to be(2)
|
247
|
+
expect(adapter.count(:get_all)).to be(0)
|
248
|
+
expect(adapter.count(:get)).to be(2)
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'eagerly caches specified features for duration of request if block returns array of specified features' do
|
252
|
+
flipper[:stats].enable
|
253
|
+
flipper[:shiny].enable
|
254
|
+
|
255
|
+
# clear the log of operations
|
256
|
+
adapter.reset
|
257
|
+
|
258
|
+
get '/specific', {}, 'flipper' => flipper
|
259
|
+
|
260
|
+
expect(adapter.operations.size).to be(2)
|
261
|
+
expect(adapter.count(:get_multi)).to be(1)
|
262
|
+
expect(adapter.count(:get)).to be(1)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
199
266
|
context 'with multiple instances' do
|
200
267
|
let(:app) do
|
201
268
|
# ensure scoped for builder block, annoying...
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "flipper/poller"
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Poller do
|
4
|
+
let(:remote_adapter) { Flipper::Adapters::Memory.new }
|
5
|
+
let(:remote) { Flipper.new(remote_adapter) }
|
6
|
+
let(:local) { Flipper.new(subject.adapter) }
|
7
|
+
|
8
|
+
subject do
|
9
|
+
described_class.new(
|
10
|
+
remote_adapter: remote_adapter,
|
11
|
+
start_automatically: false,
|
12
|
+
interval: Float::INFINITY
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
allow(subject).to receive(:loop).and_yield # Make loop just call once
|
18
|
+
allow(subject).to receive(:sleep) # Disable sleep
|
19
|
+
allow(Thread).to receive(:new).and_yield # Disable separate thread
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#adapter" do
|
23
|
+
it "always returns same memory adapter instance" do
|
24
|
+
expect(subject.adapter).to be_a(Flipper::Adapters::Memory)
|
25
|
+
expect(subject.adapter.object_id).to eq(subject.adapter.object_id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#sync" do
|
30
|
+
it "syncs remote adapter to local adapter" do
|
31
|
+
remote.enable :polling
|
32
|
+
|
33
|
+
expect(local.enabled?(:polling)).to be(false)
|
34
|
+
subject.sync
|
35
|
+
expect(local.enabled?(:polling)).to be(true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#start" do
|
40
|
+
it "starts the poller thread" do
|
41
|
+
expect(Thread).to receive(:new).and_yield
|
42
|
+
expect(subject).to receive(:loop).and_yield
|
43
|
+
expect(subject).to receive(:sync)
|
44
|
+
subject.start
|
45
|
+
end
|
46
|
+
end
|
47
|
+
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
|
@@ -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
|
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
|
@@ -114,4 +114,119 @@ RSpec.describe Flipper::Typecast do
|
|
114
114
|
described_class.to_set('asdf')
|
115
115
|
end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
|
116
116
|
end
|
117
|
+
|
118
|
+
describe "#features_hash" do
|
119
|
+
it "returns new hash" do
|
120
|
+
hash = {
|
121
|
+
"search" => {
|
122
|
+
boolean: nil,
|
123
|
+
}
|
124
|
+
}
|
125
|
+
result = described_class.features_hash(hash)
|
126
|
+
expect(result).not_to be(hash)
|
127
|
+
expect(result["search"]).not_to be(hash["search"])
|
128
|
+
end
|
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
|
+
|
154
|
+
it "converts gate value arrays to sets" do
|
155
|
+
hash = {
|
156
|
+
"search" => {
|
157
|
+
boolean: nil,
|
158
|
+
groups: ['a', 'b'],
|
159
|
+
actors: ['User;1'],
|
160
|
+
percentage_of_actors: nil,
|
161
|
+
percentage_of_time: nil,
|
162
|
+
},
|
163
|
+
}
|
164
|
+
result = described_class.features_hash(hash)
|
165
|
+
expect(result).to eq({
|
166
|
+
"search" => {
|
167
|
+
boolean: nil,
|
168
|
+
groups: Set['a', 'b'],
|
169
|
+
actors: Set['User;1'],
|
170
|
+
percentage_of_actors: nil,
|
171
|
+
percentage_of_time: nil,
|
172
|
+
},
|
173
|
+
})
|
174
|
+
end
|
175
|
+
|
176
|
+
it "converts gate value boolean and integers to strings" do
|
177
|
+
hash = {
|
178
|
+
"search" => {
|
179
|
+
boolean: true,
|
180
|
+
groups: Set.new,
|
181
|
+
actors: Set.new,
|
182
|
+
percentage_of_actors: 10,
|
183
|
+
percentage_of_time: 15,
|
184
|
+
},
|
185
|
+
}
|
186
|
+
result = described_class.features_hash(hash)
|
187
|
+
expect(result).to eq({
|
188
|
+
"search" => {
|
189
|
+
boolean: "true",
|
190
|
+
groups: Set.new,
|
191
|
+
actors: Set.new,
|
192
|
+
percentage_of_actors: "10",
|
193
|
+
percentage_of_time: "15",
|
194
|
+
},
|
195
|
+
})
|
196
|
+
end
|
197
|
+
|
198
|
+
it "converts string gate keys to symbols" do
|
199
|
+
hash = {
|
200
|
+
"search" => {
|
201
|
+
"boolean" => nil,
|
202
|
+
"groups" => Set.new,
|
203
|
+
"actors" => Set.new,
|
204
|
+
"percentage_of_actors" => nil,
|
205
|
+
"percentage_of_time" => nil,
|
206
|
+
},
|
207
|
+
}
|
208
|
+
result = described_class.features_hash(hash)
|
209
|
+
expect(result).to eq({
|
210
|
+
"search" => {
|
211
|
+
boolean: nil,
|
212
|
+
groups: Set.new,
|
213
|
+
actors: Set.new,
|
214
|
+
percentage_of_actors: nil,
|
215
|
+
percentage_of_time: nil,
|
216
|
+
},
|
217
|
+
})
|
218
|
+
end
|
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
|
117
232
|
end
|