sdk-reforge 1.9.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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/pull_request_template.md +8 -0
  5. data/.github/workflows/ruby.yml +48 -0
  6. data/.gitmodules +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.tool-versions +1 -0
  9. data/CHANGELOG.md +257 -0
  10. data/CODEOWNERS +1 -0
  11. data/Gemfile +29 -0
  12. data/Gemfile.lock +182 -0
  13. data/LICENSE.txt +20 -0
  14. data/README.md +105 -0
  15. data/Rakefile +63 -0
  16. data/VERSION +1 -0
  17. data/compile_protos.sh +20 -0
  18. data/dev/allocation_stats +60 -0
  19. data/dev/benchmark +40 -0
  20. data/dev/console +12 -0
  21. data/dev/script_setup.rb +18 -0
  22. data/lib/prefab_pb.rb +77 -0
  23. data/lib/reforge/caching_http_connection.rb +95 -0
  24. data/lib/reforge/client.rb +133 -0
  25. data/lib/reforge/config_client.rb +275 -0
  26. data/lib/reforge/config_client_presenter.rb +18 -0
  27. data/lib/reforge/config_loader.rb +67 -0
  28. data/lib/reforge/config_resolver.rb +84 -0
  29. data/lib/reforge/config_value_unwrapper.rb +123 -0
  30. data/lib/reforge/config_value_wrapper.rb +18 -0
  31. data/lib/reforge/context.rb +241 -0
  32. data/lib/reforge/context_shape.rb +20 -0
  33. data/lib/reforge/context_shape_aggregator.rb +70 -0
  34. data/lib/reforge/criteria_evaluator.rb +345 -0
  35. data/lib/reforge/duration.rb +58 -0
  36. data/lib/reforge/encryption.rb +65 -0
  37. data/lib/reforge/error.rb +6 -0
  38. data/lib/reforge/errors/env_var_parse_error.rb +11 -0
  39. data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/reforge/errors/missing_default_error.rb +13 -0
  42. data/lib/reforge/errors/missing_env_var_error.rb +11 -0
  43. data/lib/reforge/errors/uninitialized_error.rb +13 -0
  44. data/lib/reforge/evaluation.rb +53 -0
  45. data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
  46. data/lib/reforge/example_contexts_aggregator.rb +77 -0
  47. data/lib/reforge/exponential_backoff.rb +21 -0
  48. data/lib/reforge/feature_flag_client.rb +43 -0
  49. data/lib/reforge/fixed_size_hash.rb +14 -0
  50. data/lib/reforge/http_connection.rb +45 -0
  51. data/lib/reforge/internal_logger.rb +43 -0
  52. data/lib/reforge/javascript_stub.rb +99 -0
  53. data/lib/reforge/local_config_parser.rb +151 -0
  54. data/lib/reforge/murmer3.rb +50 -0
  55. data/lib/reforge/options.rb +191 -0
  56. data/lib/reforge/periodic_sync.rb +74 -0
  57. data/lib/reforge/prefab.rb +120 -0
  58. data/lib/reforge/rate_limit_cache.rb +41 -0
  59. data/lib/reforge/resolved_config_presenter.rb +86 -0
  60. data/lib/reforge/semver.rb +132 -0
  61. data/lib/reforge/sse_config_client.rb +112 -0
  62. data/lib/reforge/time_helpers.rb +7 -0
  63. data/lib/reforge/weighted_value_resolver.rb +42 -0
  64. data/lib/reforge/yaml_config_parser.rb +34 -0
  65. data/lib/reforge-sdk.rb +57 -0
  66. data/test/fixtures/datafile.json +87 -0
  67. data/test/integration_test.rb +171 -0
  68. data/test/integration_test_helpers.rb +114 -0
  69. data/test/support/common_helpers.rb +201 -0
  70. data/test/support/mock_base_client.rb +41 -0
  71. data/test/support/mock_config_client.rb +19 -0
  72. data/test/support/mock_config_loader.rb +1 -0
  73. data/test/test_caching_http_connection.rb +218 -0
  74. data/test/test_client.rb +351 -0
  75. data/test/test_config_client.rb +84 -0
  76. data/test/test_config_loader.rb +82 -0
  77. data/test/test_config_resolver.rb +502 -0
  78. data/test/test_config_value_unwrapper.rb +270 -0
  79. data/test/test_config_value_wrapper.rb +42 -0
  80. data/test/test_context.rb +271 -0
  81. data/test/test_context_shape.rb +50 -0
  82. data/test/test_context_shape_aggregator.rb +150 -0
  83. data/test/test_criteria_evaluator.rb +1180 -0
  84. data/test/test_duration.rb +37 -0
  85. data/test/test_encryption.rb +16 -0
  86. data/test/test_evaluation_summary_aggregator.rb +162 -0
  87. data/test/test_example_contexts_aggregator.rb +233 -0
  88. data/test/test_exponential_backoff.rb +18 -0
  89. data/test/test_feature_flag_client.rb +16 -0
  90. data/test/test_fixed_size_hash.rb +119 -0
  91. data/test/test_helper.rb +17 -0
  92. data/test/test_integration.rb +75 -0
  93. data/test/test_internal_logger.rb +25 -0
  94. data/test/test_javascript_stub.rb +176 -0
  95. data/test/test_local_config_parser.rb +147 -0
  96. data/test/test_logger_initialization.rb +12 -0
  97. data/test/test_options.rb +93 -0
  98. data/test/test_prefab.rb +16 -0
  99. data/test/test_rate_limit_cache.rb +44 -0
  100. data/test/test_semver.rb +108 -0
  101. data/test/test_sse_config_client.rb +211 -0
  102. data/test/test_weighted_value_resolver.rb +71 -0
  103. metadata +345 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class DurationTest < Minitest::Test
6
+ MINUTES_IN_SECONDS = 60
7
+ HOURS_IN_SECONDS = 60 * MINUTES_IN_SECONDS
8
+ DAYS_IN_SECONDS = 24 * HOURS_IN_SECONDS
9
+
10
+ TESTS = [
11
+ ['PT0M0S', 0],
12
+ ['PT6M', 6 * MINUTES_IN_SECONDS],
13
+ ['PT90S', 90],
14
+ ['P1D', DAYS_IN_SECONDS],
15
+ ['PT1.5M', 1.5 * MINUTES_IN_SECONDS],
16
+ ['P0.75D', 0.75 * DAYS_IN_SECONDS],
17
+ ['PT1M90.3S', MINUTES_IN_SECONDS + 90.3],
18
+ ['PT1H', HOURS_IN_SECONDS],
19
+ ['PT1.3H', 1.3 * HOURS_IN_SECONDS],
20
+ ['P1.5DT1.5M', 1.5 * DAYS_IN_SECONDS + MINUTES_IN_SECONDS * 1.5],
21
+ ['P1.5DT1.5H1.5M', 1.5 * DAYS_IN_SECONDS + 1.5 * HOURS_IN_SECONDS + MINUTES_IN_SECONDS * 1.5],
22
+ ['P1.5DT1.5H1.5M3.5S', 1.5 * DAYS_IN_SECONDS + 1.5 * HOURS_IN_SECONDS + MINUTES_IN_SECONDS * 1.5 + 3.5],
23
+ ['P1DT2H3M4S', DAYS_IN_SECONDS + 2 * HOURS_IN_SECONDS + 3 * MINUTES_IN_SECONDS + 4],
24
+ ['PT1H30M', HOURS_IN_SECONDS + 30 * MINUTES_IN_SECONDS],
25
+ ['P0.5DT0.25H', 0.5 * DAYS_IN_SECONDS + 0.25 * HOURS_IN_SECONDS],
26
+ ['PT15M30S', 15 * MINUTES_IN_SECONDS + 30],
27
+ ['PT0.000347222H', 0.000347222 * HOURS_IN_SECONDS],
28
+ ['PT23H59M59S', 23 * HOURS_IN_SECONDS + 59 * MINUTES_IN_SECONDS + 59],
29
+ ['P0.25DT3.75H', 0.25 * DAYS_IN_SECONDS + 3.75 * HOURS_IN_SECONDS],
30
+ ]
31
+
32
+ def test_parsing
33
+ TESTS.each do |test|
34
+ assert_equal test[1], Reforge::Duration.parse(test[0]), "Failed parsing #{test[0]}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestEncryption < Minitest::Test
6
+ def test_encryption
7
+ secret = Reforge::Encryption.generate_new_hex_key
8
+
9
+ enc = Reforge::Encryption.new(secret)
10
+
11
+ clear_text = "hello world"
12
+ encrypted = enc.encrypt(clear_text)
13
+ decrypted = enc.decrypt(encrypted)
14
+ assert_equal clear_text, decrypted
15
+ end
16
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'timecop'
5
+
6
+ class TestEvaluationSummaryAggregator < Minitest::Test
7
+ EFFECTIVELY_NEVER = 99_999 # we sync manually
8
+
9
+ EXAMPLE_VALUE_1 = PrefabProto::ConfigValue.new(bool: true)
10
+ EXAMPLE_VALUE_2 = PrefabProto::ConfigValue.new(bool: false)
11
+
12
+ EXAMPLE_COUNTER = {
13
+ config_id: 1,
14
+ selected_index: 2,
15
+ config_row_index: 3,
16
+ conditional_value_index: 4,
17
+ weighted_value_index: 5,
18
+ seleced_value: EXAMPLE_VALUE_1
19
+ }.freeze
20
+
21
+ def test_increments_counts
22
+ aggregator = Reforge::EvaluationSummaryAggregator.new(client: MockBaseClient.new, max_keys: 10,
23
+ sync_interval: EFFECTIVELY_NEVER)
24
+
25
+ aggregator.record(config_key: 'foo', config_type: 'bar', counter: EXAMPLE_COUNTER)
26
+
27
+ assert_equal 1, aggregator.data[%w[foo bar]][EXAMPLE_COUNTER]
28
+
29
+ 2.times { aggregator.record(config_key: 'foo', config_type: 'bar', counter: EXAMPLE_COUNTER) }
30
+ assert_equal 3, aggregator.data[%w[foo bar]][EXAMPLE_COUNTER]
31
+
32
+ another_counter = EXAMPLE_COUNTER.merge(selected_index: EXAMPLE_COUNTER[:selected_index] + 1)
33
+
34
+ aggregator.record(config_key: 'foo', config_type: 'bar', counter: another_counter)
35
+ assert_equal 3, aggregator.data[%w[foo bar]][EXAMPLE_COUNTER]
36
+ assert_equal 1, aggregator.data[%w[foo bar]][another_counter]
37
+ end
38
+
39
+ def test_prepare_data
40
+ aggregator = Reforge::EvaluationSummaryAggregator.new(client: MockBaseClient.new, max_keys: 10,
41
+ sync_interval: EFFECTIVELY_NEVER)
42
+
43
+ expected = {
44
+ ['config-1', :CONFIG] => {
45
+ { config_id: 1, selected_index: 2, config_row_index: 3, conditional_value_index: 4,
46
+ weighted_value_index: 5, selected_value: EXAMPLE_VALUE_1 } => 3,
47
+ { config_id: 1, selected_index: 3, config_row_index: 7, conditional_value_index: 8,
48
+ weighted_value_index: 10, selected_value: EXAMPLE_VALUE_2 } => 1
49
+ },
50
+ ['config-2', :FEATURE_FLAG] => {
51
+ { config_id: 2, selected_index: 3, config_row_index: 5, conditional_value_index: 7,
52
+ weighted_value_index: 6, selected_value: EXAMPLE_VALUE_1 } => 9
53
+ }
54
+ }
55
+
56
+ add_example_data(aggregator)
57
+ assert_equal expected, aggregator.prepare_data
58
+ assert aggregator.data.empty?
59
+ end
60
+
61
+ def test_sync
62
+ Timecop.freeze('2023-08-09 15:18:12 -0400') do
63
+ awhile_ago = Time.now - 60
64
+ now = Time.now
65
+
66
+ client = MockBaseClient.new
67
+
68
+ aggregator = nil
69
+
70
+ Timecop.freeze(awhile_ago) do
71
+ # start the aggregator in the past
72
+ aggregator = Reforge::EvaluationSummaryAggregator.new(client: client, max_keys: 10,
73
+ sync_interval: EFFECTIVELY_NEVER)
74
+ end
75
+
76
+ add_example_data(aggregator)
77
+
78
+ expected_post = PrefabProto::TelemetryEvents.new(
79
+ instance_hash: client.instance_hash,
80
+ events: [
81
+ PrefabProto::TelemetryEvent.new(
82
+ summaries: PrefabProto::ConfigEvaluationSummaries.new(
83
+ start: awhile_ago.to_i * 1000,
84
+ end: now.to_i * 1000,
85
+ summaries: [
86
+ PrefabProto::ConfigEvaluationSummary.new(
87
+ key: 'config-1',
88
+ type: :CONFIG,
89
+ counters: [
90
+ PrefabProto::ConfigEvaluationCounter.new(
91
+ config_id: 1,
92
+ selected_index: 2,
93
+ config_row_index: 3,
94
+ conditional_value_index: 4,
95
+ weighted_value_index: 5,
96
+ selected_value: EXAMPLE_VALUE_1,
97
+ count: 3
98
+ ),
99
+ PrefabProto::ConfigEvaluationCounter.new(
100
+ config_id: 1,
101
+ selected_index: 3,
102
+ config_row_index: 7,
103
+ conditional_value_index: 8,
104
+ weighted_value_index: 10,
105
+ selected_value: EXAMPLE_VALUE_2,
106
+ count: 1
107
+ )
108
+ ]
109
+ ),
110
+ PrefabProto::ConfigEvaluationSummary.new(
111
+ key: 'config-2',
112
+ type: :FEATURE_FLAG,
113
+ counters: [
114
+ PrefabProto::ConfigEvaluationCounter.new(
115
+ config_id: 2,
116
+ selected_index: 3,
117
+ config_row_index: 5,
118
+ conditional_value_index: 7,
119
+ weighted_value_index: 6,
120
+ selected_value: EXAMPLE_VALUE_1,
121
+ count: 9
122
+ )
123
+ ]
124
+ )
125
+ ]
126
+ )
127
+ )
128
+ ]
129
+ )
130
+
131
+ requests = wait_for_post_requests(client) do
132
+ Timecop.freeze(now) do
133
+ aggregator.sync
134
+ end
135
+ end
136
+
137
+ assert_equal [[
138
+ '/api/v1/telemetry',
139
+ expected_post
140
+ ]], requests
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def add_example_data(aggregator)
147
+ data = {
148
+ ['config-1', :CONFIG] => {
149
+ { config_id: 1, selected_index: 2, config_row_index: 3, conditional_value_index: 4,
150
+ weighted_value_index: 5, selected_value: EXAMPLE_VALUE_1 } => 3,
151
+ { config_id: 1, selected_index: 3, config_row_index: 7, conditional_value_index: 8,
152
+ weighted_value_index: 10, selected_value: EXAMPLE_VALUE_2 } => 1
153
+ },
154
+ ['config-2', :FEATURE_FLAG] => {
155
+ { config_id: 2, selected_index: 3, config_row_index: 5, conditional_value_index: 7,
156
+ weighted_value_index: 6, selected_value: EXAMPLE_VALUE_1 } => 9
157
+ }
158
+ }
159
+
160
+ aggregator.instance_variable_set('@data', data)
161
+ end
162
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'timecop'
5
+
6
+ class TestExampleContextsAggregator < Minitest::Test
7
+ def test_record
8
+ aggregator = Reforge::ExampleContextsAggregator.new(client: MockBaseClient.new, max_contexts: 2,
9
+ sync_interval: EFFECTIVELY_NEVER)
10
+
11
+ context = Reforge::Context.new(user: { key: 'abc' }, device: { key: 'def', mobile: true })
12
+
13
+ aggregator.record(context)
14
+ assert_equal [context], aggregator.data
15
+
16
+ # This doesn't get updated because we already have a context for this user/device
17
+ aggregator.record(context)
18
+ assert_equal [context], aggregator.data
19
+
20
+ new_context = Reforge::Context.new(
21
+ user: { key: 'ghi', admin: true },
22
+ team: { key: '999' }
23
+ )
24
+
25
+ aggregator.record(new_context)
26
+ assert_equal [context, new_context], aggregator.data
27
+
28
+ # this doesn't get recorded because we're at max_contexts
29
+ aggregator.record(Reforge::Context.new(user: { key: 'new' }))
30
+ assert_equal [context, new_context], aggregator.data
31
+ end
32
+
33
+ def test_prepare_data
34
+ aggregator = Reforge::ExampleContextsAggregator.new(client: MockBaseClient.new, max_contexts: 10,
35
+ sync_interval: EFFECTIVELY_NEVER)
36
+
37
+ context = Reforge::Context.new(
38
+ user: { key: 'abc' },
39
+ device: { key: 'def', mobile: true }
40
+ )
41
+
42
+ aggregator.record(context)
43
+
44
+ assert_equal [context], aggregator.prepare_data
45
+ assert aggregator.data.empty?
46
+ end
47
+
48
+ def test_record_with_expiry
49
+ aggregator = Reforge::ExampleContextsAggregator.new(client: MockBaseClient.new, max_contexts: 10,
50
+ sync_interval: EFFECTIVELY_NEVER)
51
+
52
+ context = Reforge::Context.new(
53
+ user: { key: 'abc' },
54
+ device: { key: 'def', mobile: true }
55
+ )
56
+
57
+ aggregator.record(context)
58
+
59
+ assert_equal [context], aggregator.data
60
+
61
+ Timecop.travel(Time.now + (60 * 60) - 1) do
62
+ aggregator.record(context)
63
+
64
+ # This doesn't get updated because we already have a context for this user/device in the timeframe
65
+ assert_equal [context], aggregator.data
66
+ end
67
+
68
+ Timecop.travel(Time.now + ((60 * 60) + 1)) do
69
+ # this is new because we've passed the expiry
70
+ aggregator.record(context)
71
+
72
+ assert_equal [context, context], aggregator.data
73
+ end
74
+ end
75
+
76
+ def test_sync
77
+ now = Time.now
78
+
79
+ client = MockBaseClient.new
80
+
81
+ aggregator = Reforge::ExampleContextsAggregator.new(client: client, max_contexts: 10,
82
+ sync_interval: EFFECTIVELY_NEVER)
83
+
84
+ context = Reforge::Context.new(
85
+ user: { key: 'abc' },
86
+ device: { key: 'def', mobile: true }
87
+ )
88
+ aggregator.record(context)
89
+
90
+ # This is the same as above so we shouldn't get anything new
91
+ aggregator.record(context)
92
+
93
+ aggregator.record(
94
+ Reforge::Context.new(
95
+ user: { key: 'ghi' },
96
+ device: { key: 'jkl', mobile: false }
97
+ )
98
+ )
99
+
100
+ aggregator.record(Reforge::Context.new(user: { key: 'kev', name: 'kevin', age: 48.5 }))
101
+
102
+ assert_equal 3, aggregator.cache.data.size
103
+
104
+ expected_post = PrefabProto::TelemetryEvents.new(
105
+ instance_hash: client.instance_hash,
106
+ events: [
107
+ PrefabProto::TelemetryEvent.new(
108
+ example_contexts: PrefabProto::ExampleContexts.new(
109
+ examples: [
110
+ PrefabProto::ExampleContext.new(
111
+ timestamp: now.utc.to_i * 1000,
112
+ contextSet: PrefabProto::ContextSet.new(
113
+ contexts: [
114
+ PrefabProto::Context.new(
115
+ type: 'user',
116
+ values: {
117
+ 'key' => PrefabProto::ConfigValue.new(string: 'abc')
118
+ }
119
+ ),
120
+ PrefabProto::Context.new(
121
+ type: 'device',
122
+ values: {
123
+ 'key' => PrefabProto::ConfigValue.new(string: 'def'),
124
+ 'mobile' => PrefabProto::ConfigValue.new(bool: true)
125
+ }
126
+ )
127
+ ]
128
+ )
129
+ ),
130
+
131
+ PrefabProto::ExampleContext.new(
132
+ timestamp: now.utc.to_i * 1000,
133
+ contextSet: PrefabProto::ContextSet.new(
134
+ contexts: [
135
+ PrefabProto::Context.new(
136
+ type: 'user',
137
+ values: {
138
+ 'key' => PrefabProto::ConfigValue.new(string: 'ghi')
139
+ }
140
+ ),
141
+ PrefabProto::Context.new(
142
+ type: 'device',
143
+ values: {
144
+ 'key' => PrefabProto::ConfigValue.new(string: 'jkl'),
145
+ 'mobile' => PrefabProto::ConfigValue.new(bool: false)
146
+ }
147
+ )
148
+ ]
149
+ )
150
+ ),
151
+
152
+ PrefabProto::ExampleContext.new(
153
+ timestamp: now.utc.to_i * 1000,
154
+ contextSet: PrefabProto::ContextSet.new(
155
+ contexts: [
156
+ PrefabProto::Context.new(
157
+ type: 'user',
158
+ values: {
159
+ 'key' => PrefabProto::ConfigValue.new(string: 'kev'),
160
+ 'name' => PrefabProto::ConfigValue.new(string: 'kevin'),
161
+ 'age' => PrefabProto::ConfigValue.new(double: 48.5)
162
+ }
163
+ )
164
+ ]
165
+ )
166
+ )
167
+ ]
168
+ )
169
+ )
170
+ ]
171
+ )
172
+
173
+ requests = wait_for_post_requests(client) do
174
+ Timecop.freeze(now + (60 * 60) - 1) do
175
+ aggregator.sync
176
+ end
177
+ end
178
+
179
+ assert_equal [[
180
+ '/api/v1/telemetry',
181
+ expected_post
182
+ ]], requests
183
+
184
+ # this hasn't changed because not enough time has passed
185
+ assert_equal 3, aggregator.cache.data.size
186
+
187
+ # a sync past the expiry should clear the cache
188
+ Timecop.freeze(now + (60 * 60) + 1) do
189
+ # we need a new piece of data for the sync to happen
190
+ aggregator.record(Reforge::Context.new(user: { key: 'bozo', name: 'Bozo', age: 99 }))
191
+
192
+ requests = wait_for_post_requests(client) do
193
+ aggregator.sync
194
+ end
195
+ end
196
+
197
+ expected_post = PrefabProto::TelemetryEvents.new(
198
+ instance_hash: client.instance_hash,
199
+ events: [
200
+ PrefabProto::TelemetryEvent.new(
201
+ example_contexts: PrefabProto::ExampleContexts.new(
202
+ examples: [
203
+ PrefabProto::ExampleContext.new(
204
+ timestamp: (now.utc.to_i + (60 * 60) + 1) * 1000,
205
+ contextSet: PrefabProto::ContextSet.new(
206
+ contexts: [
207
+ PrefabProto::Context.new(
208
+ type: 'user',
209
+ values: {
210
+ 'key' => PrefabProto::ConfigValue.new(string: 'bozo'),
211
+ 'name' => PrefabProto::ConfigValue.new(string: 'Bozo'),
212
+ 'age' => PrefabProto::ConfigValue.new(int: 99)
213
+ }
214
+ )
215
+ ]
216
+ )
217
+ )
218
+ ]
219
+ )
220
+ )
221
+ ]
222
+ )
223
+
224
+ assert_equal [[
225
+ '/api/v1/telemetry',
226
+ expected_post
227
+ ]], requests
228
+
229
+ # The last sync should have cleared the cache of everything except the latest context
230
+ assert_equal 1, aggregator.cache.data.size
231
+ assert_equal ['user:bozo'], aggregator.cache.data.keys
232
+ end
233
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestExponentialBackoff < Minitest::Test
6
+ def test_backoff
7
+ backoff = Reforge::ExponentialBackoff.new(max_delay: 120)
8
+
9
+ assert_equal 2, backoff.call
10
+ assert_equal 4, backoff.call
11
+ assert_equal 8, backoff.call
12
+ assert_equal 16, backoff.call
13
+ assert_equal 32, backoff.call
14
+ assert_equal 64, backoff.call
15
+ assert_equal 120, backoff.call
16
+ assert_equal 120, backoff.call
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestFeatureFlagClient < Minitest::Test
6
+ DEFAULT = 'default'
7
+
8
+
9
+
10
+
11
+ private
12
+
13
+ def new_client(overrides = {})
14
+ super(overrides).feature_flag_client
15
+ end
16
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'test_helper'
5
+
6
+ module Reforge
7
+ class FixedSizeHashTest < Minitest::Test
8
+ def setup
9
+ @max_size = 3
10
+ @hash = FixedSizeHash.new(@max_size)
11
+ end
12
+
13
+ def test_acts_like_a_regular_hash_when_under_max_size
14
+ @hash[:a] = 1
15
+ @hash[:b] = 2
16
+
17
+ assert_equal 1, @hash[:a]
18
+ assert_equal 2, @hash[:b]
19
+ assert_equal 2, @hash.size
20
+ end
21
+
22
+ def test_enforces_max_size_by_evicting_first_added_item
23
+ @hash[:a] = 1
24
+ @hash[:b] = 2
25
+ @hash[:c] = 3
26
+ assert_equal @max_size, @hash.size
27
+
28
+ @hash[:d] = 4
29
+ assert_equal @max_size, @hash.size
30
+ assert_nil @hash[:a] # First item should be evicted
31
+ assert_equal 4, @hash[:d]
32
+ end
33
+
34
+ def test_updating_existing_key_does_not_trigger_eviction
35
+ @hash[:a] = 1
36
+ @hash[:b] = 2
37
+ @hash[:c] = 3
38
+
39
+ @hash[:b] = 'new value' # Update existing key
40
+
41
+ assert_equal @max_size, @hash.size
42
+ assert_equal 1, @hash[:a] # First item should still be present
43
+ assert_equal 'new value', @hash[:b]
44
+ assert_equal 3, @hash[:c]
45
+ end
46
+
47
+ def test_handles_nil_values
48
+ @hash[:a] = nil
49
+ @hash[:b] = 2
50
+ @hash[:c] = 3
51
+ @hash[:d] = 4
52
+
53
+ assert_nil @hash[:a] # First item should be evicted
54
+ assert_equal 4, @hash[:d]
55
+ end
56
+
57
+ def test_preserves_hash_methods
58
+ @hash[:a] = 1
59
+ @hash[:b] = 2
60
+
61
+ assert_equal [:a, :b], @hash.keys
62
+ assert_equal [1, 2], @hash.values
63
+ assert @hash.key?(:a)
64
+ refute @hash.key?(:z)
65
+ end
66
+
67
+ def test_handles_string_keys
68
+ @hash['a'] = 1
69
+ @hash['b'] = 2
70
+ @hash['c'] = 3
71
+ @hash['d'] = 4
72
+
73
+ assert_nil @hash['a'] # First item should be evicted
74
+ assert_equal 4, @hash['d']
75
+ end
76
+
77
+ def test_handles_object_keys
78
+ key1 = Object.new
79
+ key2 = Object.new
80
+ key3 = Object.new
81
+ key4 = Object.new
82
+
83
+ @hash[key1] = 1
84
+ @hash[key2] = 2
85
+ @hash[key3] = 3
86
+ @hash[key4] = 4
87
+
88
+ assert_nil @hash[key1] # First item should be evicted
89
+ assert_equal 4, @hash[key4]
90
+ end
91
+
92
+ def test_can_be_initialized_empty
93
+ assert_equal 0, @hash.size
94
+ end
95
+
96
+ def test_enumerable_methods
97
+ @hash[:a] = 1
98
+ @hash[:b] = 2
99
+
100
+ mapped = @hash.map { |k, v| [k, v * 2] }.to_h
101
+ assert_equal({ a: 2, b: 4 }, mapped)
102
+
103
+ filtered = @hash.select { |_, v| v > 1 }
104
+ assert_equal({ b: 2 }, filtered.to_h)
105
+ end
106
+
107
+ def test_clear_maintains_max_size_constraint
108
+ @hash[:a] = 1
109
+ @hash[:b] = 2
110
+ @hash.clear
111
+
112
+ assert_equal 0, @hash.size
113
+
114
+ # Should still enforce max size after clear
115
+ (@max_size + 1).times { |i| @hash[i] = i }
116
+ assert_equal @max_size, @hash.size
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/focus'
5
+ require 'minitest/reporters'
6
+ Minitest::Reporters.use! unless ENV['RM_INFO']
7
+
8
+ require 'reforge-sdk'
9
+
10
+ Dir.glob(File.join(File.dirname(__FILE__), 'support', '**', '*.rb')).each do |file|
11
+ require file
12
+ end
13
+
14
+ Minitest::Test.class_eval do
15
+ include CommonHelpers
16
+ extend CommonHelpers
17
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'integration_test_helpers'
5
+ require 'integration_test'
6
+ require 'yaml'
7
+
8
+ class TestIntegration < Minitest::Test
9
+ IntegrationTestHelpers.find_integration_tests.map do |test_file|
10
+ tests = YAML.load(File.read(test_file))['tests']
11
+ test_names = []
12
+
13
+ tests.each do |test|
14
+ test['cases'].each do |test_case|
15
+ new_name = "test_#{test['name']}_#{test_case['name']}"
16
+
17
+ if test_names.include?(new_name)
18
+ raise "Duplicate test name: #{new_name}"
19
+ end
20
+
21
+ test_names << new_name
22
+
23
+ define_method(:"#{new_name}") do
24
+ it = IntegrationTest.new(test_case)
25
+
26
+ IntegrationTestHelpers.with_block_context_maybe(it.block_context) do
27
+ case it.test_type
28
+ when :raise
29
+ err = assert_raises(it.expected[:error]) do
30
+ it.test_client.send(it.func, *it.input)
31
+ end
32
+ assert_match(/#{it.expected[:message]}/, err.message)
33
+ when :nil
34
+ assert_nil it.test_client.send(it.func, *it.input)
35
+ when :simple_equality
36
+ if it.func == :enabled?
37
+ flag, _default, context = *it.input
38
+ assert_equal it.expected[:value], it.test_client.send(it.func, flag, context)
39
+ else
40
+ assert_equal it.expected[:value], it.test_client.send(it.func, *it.input)
41
+ end
42
+ when :log_level
43
+ assert_equal it.expected[:value].to_sym, it.test_client.send(it.func, *it.input)
44
+ when :telemetry
45
+ aggregator, get_actual_data, expected = IntegrationTestHelpers.prepare_post_data(it)
46
+ aggregator.sync
47
+
48
+ wait_for -> { it.last_post_result&.status == 200 }
49
+
50
+ assert_equal "/api/v1/telemetry", it.last_post_endpoint
51
+
52
+ actual = get_actual_data[it.last_data_sent]
53
+
54
+ expected.all? do |expected|
55
+ assert actual.include?(expected), "#{actual} should include #{expected}"
56
+ end
57
+ when :duration
58
+ assert_equal it.expected[:millis], it.test_client.send(it.func, *it.input).in_seconds * 1000
59
+ else
60
+ raise "Unknown test type: #{it.test_type}"
61
+ end
62
+
63
+ if test_case["name"].match(/doesn't raise on init timeout/)
64
+ assert_logged [
65
+ "Reforge::ConfigClient -- Couldn't Initialize In 0.01. Key any-key. Returning what we have"
66
+ ]
67
+ end
68
+ ensure
69
+ it.teardown
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end