flipper 0.26.0 → 1.3.6
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 +61 -16
- data/.github/workflows/examples.yml +55 -18
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -486
- data/Gemfile +23 -11
- data/README.md +31 -27
- data/Rakefile +2 -2
- 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/banner.jpg +0 -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/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +8 -6
- 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/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -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 +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +30 -17
- 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 +17 -78
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +20 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +266 -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 +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 +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +102 -0
- data/lib/flipper/errors.rb +3 -3
- data/lib/flipper/export.rb +24 -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 +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/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 +94 -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 +35 -8
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -5
- 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 +118 -0
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
- data/lib/flipper/test/shared_adapter_test.rb +101 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +59 -18
- 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 +11 -1
- data/lib/flipper.rb +50 -11
- 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/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +138 -55
- 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/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +166 -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/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 +54 -76
- data/spec/flipper/engine_spec.rb +374 -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 +453 -39
- 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 +24 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -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 +94 -30
- data/spec/spec_helper.rb +18 -18
- 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/skippable.rb +18 -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 +203 -20
- 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
|
@@ -1,6 +1,16 @@
|
|
1
1
|
require 'logger'
|
2
|
-
require '
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
3
|
require 'flipper/instrumentation/log_subscriber'
|
4
|
+
require 'flipper/adapters/instrumented'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'active_support/isolated_execution_state'
|
8
|
+
rescue LoadError
|
9
|
+
# ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
|
10
|
+
end
|
11
|
+
|
12
|
+
# Don't log in other tests, we'll manually re-attach when this one starts
|
13
|
+
Flipper::Instrumentation::LogSubscriber.detach
|
4
14
|
|
5
15
|
RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
6
16
|
let(:adapter) do
|
@@ -12,8 +22,8 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
12
22
|
end
|
13
23
|
|
14
24
|
before do
|
15
|
-
Flipper.register(:admins) do |
|
16
|
-
|
25
|
+
Flipper.register(:admins) do |actor|
|
26
|
+
actor.respond_to?(:admin?) && actor.admin?
|
17
27
|
end
|
18
28
|
|
19
29
|
@io = StringIO.new
|
@@ -26,6 +36,14 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
26
36
|
described_class.logger = nil
|
27
37
|
end
|
28
38
|
|
39
|
+
before(:all) do
|
40
|
+
described_class.attach
|
41
|
+
end
|
42
|
+
|
43
|
+
after(:all) do
|
44
|
+
described_class.detach
|
45
|
+
end
|
46
|
+
|
29
47
|
let(:log) { @io.string }
|
30
48
|
|
31
49
|
context 'feature enabled checks' do
|
@@ -36,7 +54,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
36
54
|
|
37
55
|
it 'logs feature calls with result after operation' do
|
38
56
|
feature_line = find_line('Flipper feature(search) enabled? false')
|
39
|
-
expect(feature_line).to include('[
|
57
|
+
expect(feature_line).to include('[ actors=nil ]')
|
40
58
|
end
|
41
59
|
|
42
60
|
it 'logs adapter calls' do
|
@@ -46,7 +64,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
46
64
|
end
|
47
65
|
end
|
48
66
|
|
49
|
-
context 'feature enabled checks with
|
67
|
+
context 'feature enabled checks with an actor' do
|
50
68
|
let(:user) { Flipper::Types::Actor.new(Flipper::Actor.new('1')) }
|
51
69
|
|
52
70
|
before do
|
@@ -54,7 +72,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
|
|
54
72
|
flipper[:search].enabled?(user)
|
55
73
|
end
|
56
74
|
|
57
|
-
it 'logs
|
75
|
+
it 'logs actors for feature' do
|
58
76
|
feature_line = find_line('Flipper feature(search) enabled?')
|
59
77
|
expect(feature_line).to include(user.inspect)
|
60
78
|
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 }
|
@@ -13,7 +18,7 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
|
|
13
18
|
Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
|
14
19
|
end
|
15
20
|
|
16
|
-
let(:user) {
|
21
|
+
let(:user) { Flipper::Actor.new('1') }
|
17
22
|
|
18
23
|
before do
|
19
24
|
described_class.client = statsd_client
|
@@ -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
|
@@ -80,7 +80,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
80
80
|
context 'with preload: true' do
|
81
81
|
let(:app) do
|
82
82
|
# ensure scoped for builder block, annoying...
|
83
|
-
|
83
|
+
flipper
|
84
84
|
middleware = described_class
|
85
85
|
|
86
86
|
Rack::Builder.new do
|
@@ -141,7 +141,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
141
141
|
context 'with preload specific' do
|
142
142
|
let(:app) do
|
143
143
|
# ensure scoped for builder block, annoying...
|
144
|
-
|
144
|
+
flipper
|
145
145
|
middleware = described_class
|
146
146
|
|
147
147
|
Rack::Builder.new do
|
@@ -196,10 +196,77 @@ 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...
|
202
|
-
|
269
|
+
flipper
|
203
270
|
middleware = described_class
|
204
271
|
|
205
272
|
Rack::Builder.new do
|
@@ -218,7 +285,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
218
285
|
end
|
219
286
|
|
220
287
|
def get(uri, params = {}, env = {}, &block)
|
221
|
-
|
288
|
+
capture_output { super(uri, params, env, &block) }
|
222
289
|
end
|
223
290
|
|
224
291
|
include_examples 'flipper middleware'
|
@@ -249,7 +316,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
249
316
|
context 'with flipper setup in env' do
|
250
317
|
let(:app) do
|
251
318
|
# ensure scoped for builder block, annoying...
|
252
|
-
|
319
|
+
flipper
|
253
320
|
middleware = described_class
|
254
321
|
|
255
322
|
Rack::Builder.new do
|
@@ -391,9 +458,8 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
391
458
|
logged_memory = Flipper::Adapters::OperationLogger.new(memory)
|
392
459
|
cache = ActiveSupport::Cache::MemoryStore.new
|
393
460
|
cache.clear
|
394
|
-
cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache
|
461
|
+
cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache)
|
395
462
|
logged_cached = Flipper::Adapters::OperationLogger.new(cached)
|
396
|
-
memo = {}
|
397
463
|
flipper = Flipper.new(logged_cached)
|
398
464
|
flipper[:stats].enable
|
399
465
|
flipper[:shiny].enable
|
@@ -404,15 +470,18 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
404
470
|
|
405
471
|
get '/', {}, 'flipper' => flipper
|
406
472
|
expect(logged_cached.count(:get_all)).to be(1)
|
407
|
-
expect(logged_memory.count(:
|
473
|
+
expect(logged_memory.count(:features)).to be(1)
|
474
|
+
expect(logged_memory.count(:get_multi)).to be(1)
|
408
475
|
|
409
476
|
get '/', {}, 'flipper' => flipper
|
410
477
|
expect(logged_cached.count(:get_all)).to be(2)
|
411
|
-
expect(logged_memory.count(:
|
478
|
+
expect(logged_memory.count(:features)).to be(1)
|
479
|
+
expect(logged_memory.count(:get_multi)).to be(1)
|
412
480
|
|
413
481
|
get '/', {}, 'flipper' => flipper
|
414
482
|
expect(logged_cached.count(:get_all)).to be(3)
|
415
|
-
expect(logged_memory.count(:
|
483
|
+
expect(logged_memory.count(:features)).to be(1)
|
484
|
+
expect(logged_memory.count(:get_multi)).to be(1)
|
416
485
|
end
|
417
486
|
end
|
418
487
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'flipper/model/active_record'
|
3
|
+
|
4
|
+
# Turn off migration logging for specs
|
5
|
+
ActiveRecord::Migration.verbose = false
|
6
|
+
|
7
|
+
RSpec.describe Flipper::Model::ActiveRecord do
|
8
|
+
before(:all) do
|
9
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
10
|
+
end
|
11
|
+
|
12
|
+
before(:each) do
|
13
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
14
|
+
CREATE TABLE users (
|
15
|
+
id integer PRIMARY KEY,
|
16
|
+
name string NOT NULL,
|
17
|
+
age integer,
|
18
|
+
is_confirmed boolean,
|
19
|
+
created_at datetime NOT NULL,
|
20
|
+
updated_at datetime NOT NULL
|
21
|
+
)
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:each) do
|
26
|
+
ActiveRecord::Base.connection.execute("DROP table IF EXISTS `users`")
|
27
|
+
end
|
28
|
+
|
29
|
+
class User < ActiveRecord::Base
|
30
|
+
include Flipper::Model::ActiveRecord
|
31
|
+
end
|
32
|
+
|
33
|
+
class DelegatedUser < DelegateClass(User)
|
34
|
+
end
|
35
|
+
|
36
|
+
class Admin < User
|
37
|
+
end
|
38
|
+
|
39
|
+
it "doesn't warn for to_ary" do
|
40
|
+
# looks like we should remove this but you are wrong, we have specs that
|
41
|
+
# fail if there are warnings and if this regresses it will print a warning
|
42
|
+
# so it is in fact testing something
|
43
|
+
user = User.create!(name: "Test")
|
44
|
+
Flipper.enabled?(:something, DelegatedUser.new(user))
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "flipper_id" do
|
48
|
+
it "returns class name and id" do
|
49
|
+
expect(User.new(id: 1).flipper_id).to eq("User;1")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "uses base class name" do
|
53
|
+
expect(Admin.new(id: 2).flipper_id).to eq("User;2")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "flipper_properties" do
|
58
|
+
subject { User.create!(name: "Test", age: 22, is_confirmed: true) }
|
59
|
+
|
60
|
+
it "includes all attributes" do
|
61
|
+
expect(subject.flipper_properties).to eq({
|
62
|
+
"type" => "User",
|
63
|
+
"id" => subject.id,
|
64
|
+
"name" => "Test",
|
65
|
+
"age" => 22,
|
66
|
+
"is_confirmed" => true,
|
67
|
+
"created_at" => subject.created_at,
|
68
|
+
"updated_at" => subject.updated_at
|
69
|
+
})
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -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
|