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.
Files changed (199) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +19 -13
  4. data/.github/workflows/examples.yml +32 -15
  5. data/Changelog.md +294 -154
  6. data/Gemfile +15 -10
  7. data/README.md +13 -11
  8. data/benchmark/enabled_ips.rb +10 -0
  9. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  10. data/benchmark/enabled_profile.rb +20 -0
  11. data/benchmark/instrumentation_ips.rb +21 -0
  12. data/benchmark/typecast_ips.rb +27 -0
  13. data/docs/images/flipper_cloud.png +0 -0
  14. data/examples/api/basic.ru +3 -4
  15. data/examples/api/custom_memoized.ru +3 -4
  16. data/examples/api/memoized.ru +3 -4
  17. data/examples/cloud/app.ru +12 -0
  18. data/examples/cloud/backoff_policy.rb +13 -0
  19. data/examples/cloud/basic.rb +22 -0
  20. data/examples/cloud/cloud_setup.rb +20 -0
  21. data/examples/cloud/forked.rb +36 -0
  22. data/examples/cloud/import.rb +17 -0
  23. data/examples/cloud/threaded.rb +33 -0
  24. data/examples/dsl.rb +1 -15
  25. data/examples/enabled_for_actor.rb +4 -2
  26. data/examples/expressions.rb +213 -0
  27. data/examples/mirroring.rb +59 -0
  28. data/examples/strict.rb +18 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +3 -5
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +33 -7
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/dual_write.rb +1 -3
  35. data/lib/flipper/adapters/failover.rb +0 -4
  36. data/lib/flipper/adapters/failsafe.rb +0 -4
  37. data/lib/flipper/adapters/http/client.rb +26 -7
  38. data/lib/flipper/adapters/http/error.rb +1 -1
  39. data/lib/flipper/adapters/http.rb +29 -16
  40. data/lib/flipper/adapters/instrumented.rb +25 -6
  41. data/lib/flipper/adapters/memoizable.rb +33 -21
  42. data/lib/flipper/adapters/memory.rb +81 -46
  43. data/lib/flipper/adapters/operation_logger.rb +16 -7
  44. data/lib/flipper/adapters/poll/poller.rb +2 -125
  45. data/lib/flipper/adapters/poll.rb +5 -3
  46. data/lib/flipper/adapters/pstore.rb +17 -11
  47. data/lib/flipper/adapters/read_only.rb +4 -4
  48. data/lib/flipper/adapters/strict.rb +47 -0
  49. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  50. data/lib/flipper/adapters/sync.rb +0 -4
  51. data/lib/flipper/cloud/configuration.rb +258 -0
  52. data/lib/flipper/cloud/dsl.rb +27 -0
  53. data/lib/flipper/cloud/message_verifier.rb +95 -0
  54. data/lib/flipper/cloud/middleware.rb +63 -0
  55. data/lib/flipper/cloud/routes.rb +14 -0
  56. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  57. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  58. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  59. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  60. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  61. data/lib/flipper/cloud/telemetry.rb +183 -0
  62. data/lib/flipper/cloud.rb +53 -0
  63. data/lib/flipper/configuration.rb +25 -4
  64. data/lib/flipper/dsl.rb +46 -45
  65. data/lib/flipper/engine.rb +88 -0
  66. data/lib/flipper/errors.rb +3 -3
  67. data/lib/flipper/export.rb +26 -0
  68. data/lib/flipper/exporter.rb +17 -0
  69. data/lib/flipper/exporters/json/export.rb +32 -0
  70. data/lib/flipper/exporters/json/v1.rb +33 -0
  71. data/lib/flipper/expression/builder.rb +73 -0
  72. data/lib/flipper/expression/constant.rb +25 -0
  73. data/lib/flipper/expression.rb +71 -0
  74. data/lib/flipper/expressions/all.rb +11 -0
  75. data/lib/flipper/expressions/any.rb +9 -0
  76. data/lib/flipper/expressions/boolean.rb +9 -0
  77. data/lib/flipper/expressions/comparable.rb +13 -0
  78. data/lib/flipper/expressions/duration.rb +28 -0
  79. data/lib/flipper/expressions/equal.rb +9 -0
  80. data/lib/flipper/expressions/greater_than.rb +9 -0
  81. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  82. data/lib/flipper/expressions/less_than.rb +9 -0
  83. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  84. data/lib/flipper/expressions/not_equal.rb +9 -0
  85. data/lib/flipper/expressions/now.rb +9 -0
  86. data/lib/flipper/expressions/number.rb +9 -0
  87. data/lib/flipper/expressions/percentage.rb +9 -0
  88. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  89. data/lib/flipper/expressions/property.rb +9 -0
  90. data/lib/flipper/expressions/random.rb +9 -0
  91. data/lib/flipper/expressions/string.rb +9 -0
  92. data/lib/flipper/expressions/time.rb +9 -0
  93. data/lib/flipper/feature.rb +87 -26
  94. data/lib/flipper/feature_check_context.rb +10 -6
  95. data/lib/flipper/gate.rb +13 -11
  96. data/lib/flipper/gate_values.rb +5 -18
  97. data/lib/flipper/gates/actor.rb +10 -17
  98. data/lib/flipper/gates/boolean.rb +1 -1
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/gates/group.rb +5 -7
  101. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  102. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  103. data/lib/flipper/identifier.rb +2 -2
  104. data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
  105. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  106. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  107. data/lib/flipper/metadata.rb +5 -1
  108. data/lib/flipper/middleware/memoizer.rb +30 -14
  109. data/lib/flipper/poller.rb +117 -0
  110. data/lib/flipper/serializers/gzip.rb +24 -0
  111. data/lib/flipper/serializers/json.rb +19 -0
  112. data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
  113. data/lib/flipper/test/shared_adapter_test.rb +91 -48
  114. data/lib/flipper/typecast.rb +56 -15
  115. data/lib/flipper/types/actor.rb +13 -13
  116. data/lib/flipper/types/group.rb +4 -4
  117. data/lib/flipper/types/percentage.rb +1 -1
  118. data/lib/flipper/version.rb +1 -1
  119. data/lib/flipper.rb +47 -10
  120. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  121. data/spec/flipper/adapter_builder_spec.rb +73 -0
  122. data/spec/flipper/adapter_spec.rb +30 -2
  123. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  124. data/spec/flipper/adapters/http_spec.rb +64 -8
  125. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  126. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  127. data/spec/flipper/adapters/memory_spec.rb +14 -3
  128. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  129. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  130. data/spec/flipper/adapters/strict_spec.rb +62 -0
  131. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  132. data/spec/flipper/cloud/configuration_spec.rb +252 -0
  133. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  134. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  135. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  136. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  137. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  138. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  139. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  140. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  141. data/spec/flipper/cloud_spec.rb +180 -0
  142. data/spec/flipper/configuration_spec.rb +17 -0
  143. data/spec/flipper/dsl_spec.rb +54 -73
  144. data/spec/flipper/engine_spec.rb +291 -0
  145. data/spec/flipper/export_spec.rb +13 -0
  146. data/spec/flipper/exporter_spec.rb +16 -0
  147. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  148. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  149. data/spec/flipper/expression/builder_spec.rb +248 -0
  150. data/spec/flipper/expression_spec.rb +188 -0
  151. data/spec/flipper/expressions/all_spec.rb +15 -0
  152. data/spec/flipper/expressions/any_spec.rb +15 -0
  153. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  154. data/spec/flipper/expressions/duration_spec.rb +43 -0
  155. data/spec/flipper/expressions/equal_spec.rb +24 -0
  156. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  157. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  159. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  160. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  161. data/spec/flipper/expressions/now_spec.rb +11 -0
  162. data/spec/flipper/expressions/number_spec.rb +21 -0
  163. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  164. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  165. data/spec/flipper/expressions/property_spec.rb +13 -0
  166. data/spec/flipper/expressions/random_spec.rb +9 -0
  167. data/spec/flipper/expressions/string_spec.rb +11 -0
  168. data/spec/flipper/expressions/time_spec.rb +13 -0
  169. data/spec/flipper/feature_check_context_spec.rb +17 -17
  170. data/spec/flipper/feature_spec.rb +436 -33
  171. data/spec/flipper/gate_values_spec.rb +2 -33
  172. data/spec/flipper/gates/boolean_spec.rb +1 -1
  173. data/spec/flipper/gates/expression_spec.rb +108 -0
  174. data/spec/flipper/gates/group_spec.rb +2 -3
  175. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  176. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  177. data/spec/flipper/identifier_spec.rb +4 -5
  178. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
  179. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  180. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  181. data/spec/flipper/poller_spec.rb +47 -0
  182. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  183. data/spec/flipper/serializers/json_spec.rb +13 -0
  184. data/spec/flipper/typecast_spec.rb +121 -6
  185. data/spec/flipper/types/actor_spec.rb +63 -46
  186. data/spec/flipper/types/group_spec.rb +2 -2
  187. data/spec/flipper_integration_spec.rb +168 -58
  188. data/spec/flipper_spec.rb +92 -28
  189. data/spec/spec_helper.rb +6 -13
  190. data/spec/support/actor_names.yml +1 -0
  191. data/spec/support/climate_control.rb +7 -0
  192. data/spec/support/fake_backoff_policy.rb +15 -0
  193. data/spec/support/skippable.rb +18 -0
  194. data/spec/support/spec_helpers.rb +11 -3
  195. metadata +166 -13
  196. data/.github/workflows/release.yml +0 -44
  197. data/.tool-versions +0 -1
  198. data/lib/flipper/railtie.rb +0 -47
  199. 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
- thing: Flipper::Types::Actor.new(Flipper::Actor.new('5'))
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) { |_thing| true }
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, thing = nil)
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
- thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
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, thing = nil)
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
- thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
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
- expect(User.new(5).flipper_id).to eq('User;5')
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 |thing|
16
- thing.respond_to?(:admin?) && thing.admin?
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('[ thing=nil ]')
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 a thing' do
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 thing for feature' do
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
- require 'statsd'
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 "#to_percentage for #{value.inspect}" do
68
+ context "#to_number for #{value.inspect}" do
69
69
  it "returns #{expected}" do
70
- expect(described_class.to_percentage(value)).to be(expected)
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.to_percentage(['asdf'])
103
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
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.to_percentage(['asdf.0'])
109
- end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a float))
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