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.
Files changed (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. 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
@@ -1,6 +1,16 @@
1
1
  require 'logger'
2
- require 'flipper/adapters/instrumented'
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 |thing|
16
- thing.respond_to?(:admin?) && thing.admin?
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('[ thing=nil ]')
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 a thing' do
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 thing for feature' do
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
- 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 }
@@ -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) { user = Flipper::Actor.new('1') }
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
- instance = flipper
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
- instance = flipper
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
- instance = flipper
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
- silence { super(uri, params, env, &block) }
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
- instance = flipper
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, expires_in: 10)
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(:get_all)).to be(1)
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(:get_all)).to be(1)
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(:get_all)).to be(1)
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