flipper 0.16.0 → 1.4.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 (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
@@ -1,4 +1,3 @@
1
- require 'helper'
2
1
  require 'flipper/gate_values'
3
2
 
4
3
  RSpec.describe Flipper::GateValues do
@@ -81,13 +80,13 @@ RSpec.describe Flipper::GateValues do
81
80
  it 'raises argument error for percentage of time value that cannot be converted to an integer' do
82
81
  expect do
83
82
  described_class.new(percentage_of_time: ['asdf'])
84
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
83
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
85
84
  end
86
85
 
87
86
  it 'raises argument error for percentage of actors value that cannot be converted to an int' do
88
87
  expect do
89
88
  described_class.new(percentage_of_actors: ['asdf'])
90
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
89
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
91
90
  end
92
91
 
93
92
  it 'raises argument error for actors value that cannot be converted to a set' do
@@ -101,35 +100,4 @@ RSpec.describe Flipper::GateValues do
101
100
  described_class.new(groups: 'asdf')
102
101
  end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
103
102
  end
104
-
105
- describe '#[]' do
106
- it 'can read the boolean value' do
107
- expect(described_class.new(boolean: true)[:boolean]).to be(true)
108
- expect(described_class.new(boolean: true)['boolean']).to be(true)
109
- end
110
-
111
- it 'can read the actors value' do
112
- expect(described_class.new(actors: Set[1, 2])[:actors]).to eq(Set[1, 2])
113
- expect(described_class.new(actors: Set[1, 2])['actors']).to eq(Set[1, 2])
114
- end
115
-
116
- it 'can read the groups value' do
117
- expect(described_class.new(groups: Set[:admins])[:groups]).to eq(Set[:admins])
118
- expect(described_class.new(groups: Set[:admins])['groups']).to eq(Set[:admins])
119
- end
120
-
121
- it 'can read the percentage of time value' do
122
- expect(described_class.new(percentage_of_time: 15)[:percentage_of_time]).to eq(15)
123
- expect(described_class.new(percentage_of_time: 15)['percentage_of_time']).to eq(15)
124
- end
125
-
126
- it 'can read the percentage of actors value' do
127
- expect(described_class.new(percentage_of_actors: 15)[:percentage_of_actors]).to eq(15)
128
- expect(described_class.new(percentage_of_actors: 15)['percentage_of_actors']).to eq(15)
129
- end
130
-
131
- it 'returns nil for value that is not present' do
132
- expect(described_class.new({})['not legit']).to be(nil)
133
- end
134
- end
135
103
  end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Gates::Actor do
4
2
  let(:feature_name) { :search }
5
3
 
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Gates::Boolean do
4
2
  let(:feature_name) { :search }
5
3
 
@@ -11,7 +9,7 @@ RSpec.describe Flipper::Gates::Boolean do
11
9
  Flipper::FeatureCheckContext.new(
12
10
  feature_name: feature_name,
13
11
  values: Flipper::GateValues.new(boolean: bool),
14
- thing: Flipper::Types::Actor.new(Flipper::Actor.new(1))
12
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
15
13
  )
16
14
  end
17
15
 
@@ -0,0 +1,190 @@
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 actor in context' do
66
+ it 'passes actor to expression context' do
67
+ actor = Flipper::Actor.new("User;1", {type: "User"})
68
+ wrapped_actor = Flipper::Types::Actor.new(actor)
69
+ expression = Flipper.property(:flipper_id).eq("User;1")
70
+ ctx = Flipper::FeatureCheckContext.new(
71
+ feature_name: feature_name,
72
+ values: Flipper::GateValues.new(expression: expression.value),
73
+ actors: [wrapped_actor]
74
+ )
75
+ expect(subject.open?(ctx)).to be(true)
76
+ end
77
+
78
+ it 'passes nil actor when no actors provided' do
79
+ expression = Flipper.boolean(true).eq(true)
80
+ ctx = Flipper::FeatureCheckContext.new(
81
+ feature_name: feature_name,
82
+ values: Flipper::GateValues.new(expression: expression.value),
83
+ actors: nil
84
+ )
85
+ expect(subject.open?(ctx)).to be(true)
86
+ end
87
+ end
88
+
89
+ context 'for properties that have symbol keys' do
90
+ it 'returns true when expression evalutes to true' do
91
+ expression = Flipper.property(:type).eq("User")
92
+ context = context(expression.value, properties: {type: "User"})
93
+ expect(subject.open?(context)).to be(true)
94
+ end
95
+
96
+ it 'returns false when expression evaluates to false' do
97
+ expression = Flipper.property(:type).eq("User")
98
+ context = context(expression.value, properties: {type: "Org"})
99
+ expect(subject.open?(context)).to be(false)
100
+ end
101
+ end
102
+
103
+ context 'for time-based expressions' do
104
+ it 'enables when now is past a scheduled epoch' do
105
+ past_epoch = Time.now.to_i - 86_400
106
+ expression = Flipper.now.gte(Flipper.time(past_epoch))
107
+ expect(subject.open?(context(expression.value))).to be(true)
108
+ end
109
+
110
+ it 'does not enable when now is before a future epoch' do
111
+ future_epoch = Time.now.to_i + 86_400
112
+ expression = Flipper.now.gte(Flipper.time(future_epoch))
113
+ expect(subject.open?(context(expression.value))).to be(false)
114
+ end
115
+
116
+ it 'enables when now is past a scheduled datetime' do
117
+ past_time = (Time.now.utc - 86_400).iso8601
118
+ expression = Flipper.now.gte(Flipper.time(past_time))
119
+ expect(subject.open?(context(expression.value))).to be(true)
120
+ end
121
+
122
+ it 'does not enable when now is before a future datetime' do
123
+ future_time = (Time.now.utc + 86_400).iso8601
124
+ expression = Flipper.now.gte(Flipper.time(future_time))
125
+ expect(subject.open?(context(expression.value))).to be(false)
126
+ end
127
+
128
+ it 'enables expiring features with lt' do
129
+ future_time = (Time.now.utc + 86_400).iso8601
130
+ expression = Flipper.now.lt(Flipper.time(future_time))
131
+ expect(subject.open?(context(expression.value))).to be(true)
132
+ end
133
+
134
+ it 'disables expired features with lt' do
135
+ past_time = (Time.now.utc - 86_400).iso8601
136
+ expression = Flipper.now.lt(Flipper.time(past_time))
137
+ expect(subject.open?(context(expression.value))).to be(false)
138
+ end
139
+
140
+ it 'enables within a time window using all' do
141
+ start_time = (Time.now.utc - 86_400).iso8601
142
+ end_time = (Time.now.utc + 86_400).iso8601
143
+ expression = Flipper.all(
144
+ Flipper.now.gte(Flipper.time(start_time)),
145
+ Flipper.now.lt(Flipper.time(end_time))
146
+ )
147
+ expect(subject.open?(context(expression.value))).to be(true)
148
+ end
149
+
150
+ it 'does not enable outside a time window' do
151
+ start_time = (Time.now.utc + 86_400).iso8601
152
+ end_time = (Time.now.utc + 172_800).iso8601
153
+ expression = Flipper.all(
154
+ Flipper.now.gte(Flipper.time(start_time)),
155
+ Flipper.now.lt(Flipper.time(end_time))
156
+ )
157
+ expect(subject.open?(context(expression.value))).to be(false)
158
+ end
159
+ end
160
+ end
161
+
162
+ describe '#protects?' do
163
+ it 'returns true for Flipper::Expression' do
164
+ expression = Flipper.number(20).eq(20)
165
+ expect(subject.protects?(expression)).to be(true)
166
+ end
167
+
168
+ it 'returns true for Hash' do
169
+ expression = Flipper.number(20).eq(20)
170
+ expect(subject.protects?(expression.value)).to be(true)
171
+ end
172
+
173
+ it 'returns false for other things' do
174
+ expect(subject.protects?(false)).to be(false)
175
+ end
176
+ end
177
+
178
+ describe '#wrap' do
179
+ it 'returns self for Flipper::Expression' do
180
+ expression = Flipper.number(20).eq(20)
181
+ expect(subject.wrap(expression)).to be(expression)
182
+ end
183
+
184
+ it 'returns Flipper::Expression for Hash' do
185
+ expression = Flipper.number(20).eq(20)
186
+ expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expression)
187
+ expect(subject.wrap(expression.value)).to eq(expression)
188
+ end
189
+ end
190
+ end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Gates::Group do
4
2
  let(:feature_name) { :search }
5
3
 
@@ -11,18 +9,17 @@ RSpec.describe Flipper::Gates::Group do
11
9
  Flipper::FeatureCheckContext.new(
12
10
  feature_name: feature_name,
13
11
  values: Flipper::GateValues.new(groups: set),
14
- thing: Flipper::Types::Actor.new(Flipper::Actor.new('5'))
12
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new('5'))]
15
13
  )
16
14
  end
17
15
 
18
16
  describe '#open?' do
19
17
  context 'with a group in adapter, but not registered' do
20
18
  before do
21
- Flipper.register(:staff) { |_thing| true }
19
+ Flipper.register(:staff) { |actor| true }
22
20
  end
23
21
 
24
22
  it 'ignores group' do
25
- thing = Flipper::Actor.new('5')
26
23
  expect(subject.open?(context(Set[:newbs, :staff]))).to be(true)
27
24
  end
28
25
  end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Gates::PercentageOfActors do
4
2
  let(:feature_name) { :search }
5
3
 
@@ -7,11 +5,11 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
7
5
  described_class.new
8
6
  end
9
7
 
10
- def context(percentage_of_actors_value, feature = feature_name, thing = nil)
8
+ def context(percentage_of_actors_value, feature = feature_name, actors = nil)
11
9
  Flipper::FeatureCheckContext.new(
12
10
  feature_name: feature,
13
11
  values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
14
- thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
12
+ actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
15
13
  )
16
14
  end
17
15
 
@@ -22,7 +20,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
22
20
  let(:number_of_actors) { 10_000 }
23
21
 
24
22
  let(:actors) do
25
- (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)) }
26
24
  end
27
25
 
28
26
  let(:feature_one_enabled_actors) do
@@ -50,13 +48,69 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
50
48
  end
51
49
  end
52
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
+
53
107
  context 'for fractional percentage' do
54
108
  let(:decimal) { 0.001 }
55
109
  let(:percentage) { decimal * 100 }
56
110
  let(:number_of_actors) { 10_000 }
57
111
 
58
112
  let(:actors) do
59
- (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)) }
60
114
  end
61
115
 
62
116
  subject { described_class.new }
@@ -66,7 +120,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
66
120
  expected_open_count = number_of_actors * decimal
67
121
 
68
122
  open_count = actors.select do |actor|
69
- context = context(percentage, :feature, actor)
123
+ context = context(percentage, :feature, [actor])
70
124
  subject.open?(context)
71
125
  end.size
72
126
 
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Gates::PercentageOfTime do
4
2
  let(:feature_name) { :search }
5
3
 
@@ -7,11 +5,11 @@ RSpec.describe Flipper::Gates::PercentageOfTime do
7
5
  described_class.new
8
6
  end
9
7
 
10
- def context(percentage_of_time_value, feature = feature_name, thing = nil)
8
+ def context(percentage_of_time_value, feature = feature_name, actors = nil)
11
9
  Flipper::FeatureCheckContext.new(
12
10
  feature_name: feature,
13
11
  values: Flipper::GateValues.new(percentage_of_time: percentage_of_time_value),
14
- thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
12
+ actors: Array(actors) || [Flipper::Types::Actor.new(Flipper::Actor.new('1'))]
15
13
  )
16
14
  end
17
15
 
@@ -0,0 +1,12 @@
1
+ require 'flipper/identifier'
2
+
3
+ RSpec.describe Flipper::Identifier do
4
+ describe '#flipper_id' do
5
+ it 'uses class name and id' do
6
+ class BlahBlah < Struct.new(:id)
7
+ include Flipper::Identifier
8
+ end
9
+ expect(BlahBlah.new(5).flipper_id).to eq('BlahBlah;5')
10
+ end
11
+ end
12
+ end
@@ -1,7 +1,16 @@
1
1
  require 'logger'
2
- require 'helper'
3
- require 'flipper/adapters/instrumented'
2
+ require 'active_support/core_ext/object/blank'
4
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
5
14
 
6
15
  RSpec.describe Flipper::Instrumentation::LogSubscriber do
7
16
  let(:adapter) do
@@ -13,8 +22,8 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
13
22
  end
14
23
 
15
24
  before do
16
- Flipper.register(:admins) do |thing|
17
- thing.respond_to?(:admin?) && thing.admin?
25
+ Flipper.register(:admins) do |actor|
26
+ actor.respond_to?(:admin?) && actor.admin?
18
27
  end
19
28
 
20
29
  @io = StringIO.new
@@ -27,6 +36,14 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
27
36
  described_class.logger = nil
28
37
  end
29
38
 
39
+ before(:all) do
40
+ described_class.attach
41
+ end
42
+
43
+ after(:all) do
44
+ described_class.detach
45
+ end
46
+
30
47
  let(:log) { @io.string }
31
48
 
32
49
  context 'feature enabled checks' do
@@ -37,7 +54,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
37
54
 
38
55
  it 'logs feature calls with result after operation' do
39
56
  feature_line = find_line('Flipper feature(search) enabled? false')
40
- expect(feature_line).to include('[ thing=nil ]')
57
+ expect(feature_line).to include('[ actors=nil ]')
41
58
  end
42
59
 
43
60
  it 'logs adapter calls' do
@@ -47,7 +64,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
47
64
  end
48
65
  end
49
66
 
50
- context 'feature enabled checks with a thing' do
67
+ context 'feature enabled checks with an actor' do
51
68
  let(:user) { Flipper::Types::Actor.new(Flipper::Actor.new('1')) }
52
69
 
53
70
  before do
@@ -55,7 +72,7 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
55
72
  flipper[:search].enabled?(user)
56
73
  end
57
74
 
58
- it 'logs thing for feature' do
75
+ it 'logs actors for feature' do
59
76
  feature_line = find_line('Flipper feature(search) enabled?')
60
77
  expect(feature_line).to include(user.inspect)
61
78
  end
@@ -1,7 +1,11 @@
1
- require 'helper'
2
1
  require 'flipper/adapters/instrumented'
3
2
  require 'flipper/instrumentation/statsd'
4
- 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
5
9
 
6
10
  RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
7
11
  let(:statsd_client) { Statsd.new }
@@ -14,7 +18,7 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
14
18
  Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
15
19
  end
16
20
 
17
- let(:user) { user = Flipper::Actor.new('1') }
21
+ let(:user) { Flipper::Actor.new('1') }
18
22
 
19
23
  before do
20
24
  described_class.client = statsd_client
@@ -26,6 +30,10 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
26
30
  Thread.current[:statsd_socket] = nil
27
31
  end
28
32
 
33
+ after(:all) do
34
+ ActiveSupport::Notifications.unsubscribe("flipper")
35
+ end
36
+
29
37
  def assert_timer(metric)
30
38
  regex = /#{Regexp.escape metric}\:\d+\|ms/
31
39
  result = socket.buffer.detect { |op| op.first =~ regex }
@@ -69,4 +77,19 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
69
77
  flipper[:stats].disable(user)
70
78
  assert_timer 'flipper.adapter.memory.disable'
71
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
72
95
  end
@@ -1,4 +1,3 @@
1
- require 'helper'
2
1
  require 'flipper/instrumenters/memory'
3
2
 
4
3
  RSpec.describe Flipper::Instrumenters::Memory do
@@ -22,5 +21,23 @@ RSpec.describe Flipper::Instrumenters::Memory do
22
21
  event = described_class::Event.new(name, payload, block_result)
23
22
  expect(instrumenter.events).to eq([event])
24
23
  end
24
+
25
+ context 'when an error is raised' do
26
+ subject do
27
+ instrumenter.instrument(:name) { raise IOError }
28
+ end
29
+
30
+ let(:instrumenter) { described_class.new }
31
+
32
+ it 'captures and propagates the error' do
33
+ expect { subject }.to raise_error(IOError)
34
+
35
+ expect(instrumenter.events.count).to be 1
36
+
37
+ payload = instrumenter.events[0].payload
38
+ expect(payload.keys).to include(:exception, :exception_object)
39
+ expect(payload[:exception_object]).to be_a IOError
40
+ end
41
+ end
25
42
  end
26
43
  end
@@ -1,20 +1,26 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Instrumenters::Noop do
4
2
  describe '.instrument' do
5
3
  context 'with name' do
6
4
  it 'yields block' do
7
- yielded = false
8
- described_class.instrument(:foo) { yielded = true }
9
- expect(yielded).to eq(true)
5
+ expect { |block|
6
+ described_class.instrument(:foo, &block)
7
+ }.to yield_control
10
8
  end
11
9
  end
12
10
 
13
11
  context 'with name and payload' do
12
+ let(:payload) { { pay: :load } }
13
+
14
14
  it 'yields block' do
15
- yielded = false
16
- described_class.instrument(:foo, pay: :load) { yielded = true }
17
- expect(yielded).to eq(true)
15
+ expect { |block|
16
+ described_class.instrument(:foo, payload, &block)
17
+ }.to yield_control
18
+ end
19
+
20
+ it 'yields the payload' do
21
+ described_class.instrument(:foo, payload) do |block_payload|
22
+ expect(block_payload).to eq payload
23
+ end
18
24
  end
19
25
  end
20
26
  end