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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestInternalLogger < Minitest::Test
6
+
7
+ def test_levels
8
+ logger_a = Reforge::InternalLogger.new(A)
9
+ logger_b = Reforge::InternalLogger.new(B)
10
+
11
+ assert_equal :warn, logger_a.level
12
+ assert_equal :warn, logger_b.level
13
+
14
+ Reforge::InternalLogger.using_reforge_log_filter!
15
+ assert_equal :trace, logger_a.level
16
+ assert_equal :trace, logger_b.level
17
+ end
18
+
19
+ end
20
+
21
+ class A
22
+ end
23
+
24
+ class B
25
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class JavascriptStubTest < Minitest::Test
6
+ PROJECT_ENV_ID = 1
7
+ DEFAULT_VALUE = 'default_value'
8
+ DEFAULT_VALUE_CONFIG = PrefabProto::ConfigValue.new(string: DEFAULT_VALUE)
9
+ TRUE_CONFIG = PrefabProto::ConfigValue.new(bool: true)
10
+ FALSE_CONFIG = PrefabProto::ConfigValue.new(bool: false)
11
+ DEFAULT_ROW = PrefabProto::ConfigRow.new(
12
+ values: [
13
+ PrefabProto::ConditionalValue.new(value: DEFAULT_VALUE_CONFIG)
14
+ ]
15
+ )
16
+
17
+ def setup
18
+ super
19
+
20
+ log_level = PrefabProto::Config.new(
21
+ id: 999,
22
+ key: 'log-level',
23
+ config_type: PrefabProto::ConfigType::LOG_LEVEL,
24
+ rows: [
25
+ PrefabProto::ConfigRow.new(
26
+ values: [
27
+ PrefabProto::ConditionalValue.new(
28
+ criteria: [],
29
+ value: PrefabProto::ConfigValue.new(log_level: PrefabProto::LogLevel::INFO)
30
+ )
31
+ ]
32
+ )
33
+ ]
34
+ )
35
+
36
+ config_for_sdk = PrefabProto::Config.new(
37
+ id: 123,
38
+ key: 'basic-config',
39
+ config_type: PrefabProto::ConfigType::CONFIG,
40
+ rows: [DEFAULT_ROW],
41
+ send_to_client_sdk: true
42
+ )
43
+
44
+ config_not_for_sdk = PrefabProto::Config.new(
45
+ id: 787,
46
+ key: 'non-sdk-basic-config',
47
+ config_type: PrefabProto::ConfigType::CONFIG,
48
+ rows: [DEFAULT_ROW]
49
+ )
50
+
51
+ json_config = PrefabProto::Config.new(
52
+ id: 234,
53
+ key: 'json-config',
54
+ config_type: PrefabProto::ConfigType::CONFIG,
55
+ send_to_client_sdk: true,
56
+ rows: [
57
+ PrefabProto::ConfigRow.new(
58
+ values: [
59
+ PrefabProto::ConditionalValue.new(value: PrefabProto::ConfigValue.new(json: PrefabProto::Json.new(json: '{"key":"value"}')))
60
+ ]
61
+ )
62
+ ]
63
+ )
64
+
65
+ duration_config = PrefabProto::Config.new(
66
+ id: 236,
67
+ key: 'duration-config',
68
+ config_type: PrefabProto::ConfigType::CONFIG,
69
+ send_to_client_sdk: true,
70
+ rows: [
71
+ PrefabProto::ConfigRow.new(
72
+ values: [
73
+ PrefabProto::ConditionalValue.new(value: PrefabProto::ConfigValue.new(duration: PrefabProto::IsoDuration.new(definition: "P4DT12H30M5S")))
74
+ ]
75
+ )
76
+ ]
77
+ )
78
+
79
+ ff = PrefabProto::Config.new(
80
+ id: 456,
81
+ key: 'feature-flag',
82
+ config_type: PrefabProto::ConfigType::FEATURE_FLAG,
83
+ rows: [
84
+ PrefabProto::ConfigRow.new(
85
+ values: [
86
+ PrefabProto::ConditionalValue.new(
87
+ value: TRUE_CONFIG,
88
+ criteria: [
89
+ PrefabProto::Criterion.new(
90
+ operator: PrefabProto::Criterion::CriterionOperator::PROP_ENDS_WITH_ONE_OF,
91
+ value_to_match: string_list(['hotmail.com', 'gmail.com']),
92
+ property_name: 'user.email'
93
+ )
94
+ ]
95
+ ),
96
+ PrefabProto::ConditionalValue.new(value: FALSE_CONFIG)
97
+ ]
98
+ )
99
+ ]
100
+ )
101
+
102
+ @client = new_client(
103
+ config: [log_level, config_for_sdk, config_not_for_sdk, ff, json_config, duration_config],
104
+ project_env_id: PROJECT_ENV_ID,
105
+ collect_evaluation_summaries: true,
106
+ context_upload_mode: :periodic_example,
107
+ allow_telemetry_in_local_mode: true
108
+ )
109
+ end
110
+
111
+ def test_bootstrap
112
+ result = Reforge::JavaScriptStub.new(@client).bootstrap({})
113
+
114
+
115
+ File.open('/tmp/prefab_config.json', 'w') do |f|
116
+ f.write(result)
117
+ end
118
+ assert_equal %(
119
+ window._prefabBootstrap = {
120
+ evaluations: {"log-level":{"value":{"logLevel":"INFO"}},"basic-config":{"value":{"string":"default_value"}},"feature-flag":{"value":{"bool":false}},"json-config":{"value":{"json":"{\\"key\\":\\"value\\"}"}},"duration-config":{"value":{"duration":{"ms":390605000.0,"seconds":390605.0}}}},
121
+ context: {}
122
+ }
123
+ ).strip, result.strip
124
+
125
+ result = Reforge::JavaScriptStub.new(@client).bootstrap({ user: { email: 'gmail.com' } })
126
+
127
+ File.open('/tmp/prefab_config.json', 'w') do |f|
128
+ f.write(result)
129
+ end
130
+
131
+ assert_equal %(
132
+ window._prefabBootstrap = {
133
+ evaluations: {"log-level":{"value":{"logLevel":"INFO"}},"basic-config":{"value":{"string":"default_value"}},"feature-flag":{"value":{"bool":true}},"json-config":{"value":{"json":"{\\"key\\":\\"value\\"}"}},"duration-config":{"value":{"duration":{"ms":390605000.0,"seconds":390605.0}}}},
134
+ context: {"user":{"email":"gmail.com"}}
135
+ }
136
+
137
+ ).strip, result.strip
138
+ end
139
+
140
+ def test_generate_stub
141
+ result = Reforge::JavaScriptStub.new(@client).generate_stub({})
142
+
143
+ assert_equal %(
144
+ window.prefab = window.prefab || {};
145
+ window.prefab.config = {"log-level":"INFO","basic-config":"default_value","feature-flag":false,"json-config":"{\\"key\\":\\"value\\"}","duration-config":{"ms":390605000.0,"seconds":390605.0}};
146
+ window.prefab.get = function(key) {
147
+ var value = window.prefab.config[key];
148
+
149
+ return value;
150
+ };
151
+ window.prefab.isEnabled = function(key) {
152
+ var value = window.prefab.config[key] === true;
153
+
154
+ return value;
155
+ };
156
+ ).strip, result.strip
157
+
158
+ result = Reforge::JavaScriptStub.new(@client).generate_stub({ user: { email: 'gmail.com' } }, 'myEvalCallback')
159
+
160
+ assert_equal %(
161
+ window.prefab = window.prefab || {};
162
+ window.prefab.config = {"log-level":"INFO","basic-config":"default_value","feature-flag":true,"json-config":"{\\"key\\":\\"value\\"}","duration-config":{"ms":390605000.0,"seconds":390605.0}};
163
+ window.prefab.get = function(key) {
164
+ var value = window.prefab.config[key];
165
+ myEvalCallback(key, value);
166
+ return value;
167
+ };
168
+ window.prefab.isEnabled = function(key) {
169
+ var value = window.prefab.config[key] === true;
170
+ myEvalCallback(key, value);
171
+ return value;
172
+ };
173
+
174
+ ).strip, result.strip
175
+ end
176
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestLocalConfigParser < Minitest::Test
6
+ FILE_NAME = 'example-config.yaml'
7
+ DEFAULT_MATCH = 'default'
8
+
9
+ def setup
10
+ super
11
+ @mock_resolver = MockResolver.new
12
+ end
13
+
14
+ def test_parse_int_config
15
+ key = :sample_int
16
+ parsed = Reforge::LocalConfigParser.parse(key, 123, {}, FILE_NAME)[key]
17
+ config = parsed[:config]
18
+
19
+ assert_equal FILE_NAME, parsed[:source]
20
+ assert_equal DEFAULT_MATCH, parsed[:match]
21
+ assert_equal :CONFIG, config.config_type
22
+ assert_equal key.to_s, config.key
23
+ assert_equal 1, config.rows.size
24
+ assert_equal 1, config.rows[0].values.size
25
+ assert_equal 123, config.rows[0].values[0].value.int
26
+ end
27
+
28
+ def test_flag_with_a_value
29
+ key = :flag_with_a_value
30
+ value = stringify_keys({ feature_flag: true, value: 'all-features' })
31
+ parsed = Reforge::LocalConfigParser.parse(key, value, {}, FILE_NAME)[key]
32
+ config = parsed[:config]
33
+
34
+ assert_equal FILE_NAME, parsed[:source]
35
+ assert_equal key, parsed[:match]
36
+ assert_equal :FEATURE_FLAG, config.config_type
37
+ assert_equal key.to_s, config.key
38
+ assert_equal 1, config.rows.size
39
+ assert_equal 1, config.rows[0].values.size
40
+
41
+ value_row = config.rows[0].values[0]
42
+ assert_equal 'all-features', Reforge::ConfigValueUnwrapper.deepest_value(value_row.value, key, {}, @mock_resolver).unwrap
43
+ end
44
+
45
+ def test_flag_in_user_key
46
+ key = :flag_in_user_key
47
+ value = stringify_keys({ 'feature_flag': 'true', value: true,
48
+ criterion: { operator: 'PROP_IS_ONE_OF', property: 'user.key', values: %w[abc123 xyz987] } })
49
+ parsed = Reforge::LocalConfigParser.parse(key, value, {}, FILE_NAME)[key]
50
+ config = parsed[:config]
51
+
52
+ assert_equal FILE_NAME, parsed[:source]
53
+ assert_equal key, parsed[:match]
54
+ assert_equal :FEATURE_FLAG, config.config_type
55
+ assert_equal key.to_s, config.key
56
+ assert_equal 1, config.rows.size
57
+ assert_equal 1, config.rows[0].values.size
58
+ assert_equal 1, config.rows[0].values[0].criteria.size
59
+
60
+ value_row = config.rows[0].values[0]
61
+ assert_equal true, Reforge::ConfigValueUnwrapper.deepest_value(value_row.value, key, {}, @mock_resolver).unwrap
62
+
63
+ assert_equal 'user.key', value_row.criteria[0].property_name
64
+ assert_equal :PROP_IS_ONE_OF, value_row.criteria[0].operator
65
+ assert_equal %w[abc123 xyz987], value_row.criteria[0].value_to_match.string_list.values
66
+ end
67
+
68
+ def test_provided_values
69
+ with_env('LOOKUP_ENV', 'from env') do
70
+ key = :test_provided
71
+ value = stringify_keys({type: 'provided', source: 'ENV_VAR', lookup: 'LOOKUP_ENV'})
72
+ parsed = Reforge::LocalConfigParser.parse(key, value, {}, FILE_NAME)[key]
73
+ config = parsed[:config]
74
+
75
+ assert_equal FILE_NAME, parsed[:source]
76
+ assert_equal 'LOOKUP_ENV', parsed[:match]
77
+ assert_equal :CONFIG, config.config_type
78
+ assert_equal key.to_s, config.key
79
+ assert_equal 1, config.rows.size
80
+ assert_equal 1, config.rows[0].values.size
81
+
82
+ value_row = config.rows[0].values[0]
83
+ provided = value_row.value.provided
84
+ assert_equal :ENV_VAR, provided.source
85
+ assert_equal 'LOOKUP_ENV', provided.lookup
86
+ assert_equal 'from env', Reforge::ConfigValueUnwrapper.deepest_value(value_row.value, config, {}, @mock_resolver).unwrap
87
+ reportable_value = Reforge::ConfigValueUnwrapper.deepest_value(value_row.value, config, {}, @mock_resolver).reportable_value
88
+ assert_equal 'from env', reportable_value
89
+ end
90
+ end
91
+
92
+ def test_confidential_provided_values
93
+ with_env('LOOKUP_ENV', 'from env') do
94
+ key = :test_provided
95
+ value = stringify_keys({type: 'provided', source: 'ENV_VAR', lookup: 'LOOKUP_ENV', confidential: true})
96
+ parsed = Reforge::LocalConfigParser.parse(key, value, {}, FILE_NAME)[key]
97
+ config = parsed[:config]
98
+
99
+ value_row = config.rows[0].values[0]
100
+ provided = value_row.value.provided
101
+ assert_equal :ENV_VAR, provided.source
102
+ assert_equal 'LOOKUP_ENV', provided.lookup
103
+ assert_equal 'from env', Reforge::ConfigValueUnwrapper.deepest_value(value_row.value, config, {}, @mock_resolver).unwrap
104
+ reportable_value = Reforge::ConfigValueUnwrapper.deepest_value(value_row.value, config, {}, @mock_resolver).reportable_value
105
+ assert reportable_value.start_with? Reforge::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
106
+ end
107
+ end
108
+
109
+ def test_confidential_values
110
+ key = :test_confidential
111
+ value = stringify_keys({value: 'a confidential string', confidential: true})
112
+ parsed = Reforge::LocalConfigParser.parse(key, value, {}, FILE_NAME)[key]
113
+ config = parsed[:config]
114
+
115
+ assert_equal FILE_NAME, parsed[:source]
116
+ assert_equal :CONFIG, config.config_type
117
+ assert_equal key.to_s, config.key
118
+ assert_equal 1, config.rows.size
119
+ assert_equal 1, config.rows[0].values.size
120
+
121
+ value_row = config.rows[0].values[0]
122
+ config_value = value_row.value
123
+ assert_equal 'a confidential string', Reforge::ConfigValueUnwrapper.deepest_value(config_value, key, {}, @mock_resolver).unwrap
124
+ reportable_value = Reforge::ConfigValueUnwrapper.deepest_value(config_value, key, {}, @mock_resolver).reportable_value
125
+ assert reportable_value.start_with? Reforge::ConfigValueUnwrapper::CONFIDENTIAL_PREFIX
126
+ end
127
+
128
+ private
129
+
130
+ def stringify_keys(hash)
131
+ deep_transform_keys(hash, &:to_s)
132
+ end
133
+
134
+ def deep_transform_keys(hash, &block)
135
+ result = {}
136
+ hash.each do |key, value|
137
+ result[yield(key)] = value.is_a?(Hash) ? deep_transform_keys(value, &block) : value
138
+ end
139
+ result
140
+ end
141
+
142
+ class MockResolver
143
+ def get(key)
144
+ raise "unexpected key"
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestLoggerInitialization < Minitest::Test
6
+
7
+ def test_init_out_of_order
8
+ # assert nothing blows up
9
+ Reforge.log_filter
10
+ end
11
+
12
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestOptions < Minitest::Test
6
+ API_KEY = 'abcdefg'
7
+
8
+ def test_api_override_env_var
9
+ assert_equal Reforge::Options::DEFAULT_SOURCES, Reforge::Options.new.sources
10
+
11
+ # blank doesn't take effect
12
+ with_env('PREFAB_API_URL_OVERRIDE', '') do
13
+ assert_equal Reforge::Options::DEFAULT_SOURCES, Reforge::Options.new.sources
14
+ end
15
+
16
+ # non-blank does take effect
17
+ with_env('PREFAB_API_URL_OVERRIDE', 'https://override.example.com') do
18
+ assert_equal ["https://override.example.com"], Reforge::Options.new.sources
19
+ end
20
+ end
21
+
22
+ def test_overriding_sources
23
+ assert_equal Reforge::Options::DEFAULT_SOURCES, Reforge::Options.new.sources
24
+
25
+ # a plain string ends up wrapped in an array
26
+ source = 'https://example.com'
27
+ assert_equal [source], Reforge::Options.new(sources: source).sources
28
+
29
+ sources = ['https://example.com', 'https://example2.com']
30
+ assert_equal sources, Reforge::Options.new(sources: sources).sources
31
+ end
32
+
33
+ def test_works_with_named_arguments
34
+ assert_equal API_KEY, Reforge::Options.new(sdk_key: API_KEY).sdk_key
35
+ end
36
+
37
+ def test_works_with_hash
38
+ assert_equal API_KEY, Reforge::Options.new({ sdk_key: API_KEY }).sdk_key
39
+ end
40
+
41
+ def test_collect_max_paths
42
+ assert_equal 1000, Reforge::Options.new.collect_max_paths
43
+ assert_equal 100, Reforge::Options.new(collect_max_paths: 100).collect_max_paths
44
+ end
45
+
46
+ def test_collect_max_paths_with_local_only
47
+ options = Reforge::Options.new(collect_max_paths: 100,
48
+ prefab_datasources: Reforge::Options::DATASOURCES::LOCAL_ONLY)
49
+ assert_equal 0, options.collect_max_paths
50
+ end
51
+
52
+ def test_collect_max_paths_with_collect_logger_counts_false
53
+ options = Reforge::Options.new(collect_max_paths: 100,
54
+ collect_logger_counts: false)
55
+ assert_equal 0, options.collect_max_paths
56
+ end
57
+
58
+ def test_collect_max_evaluation_summaries
59
+ assert_equal 100_000, Reforge::Options.new.collect_max_evaluation_summaries
60
+ assert_equal 0, Reforge::Options.new(collect_evaluation_summaries: false).collect_max_evaluation_summaries
61
+ assert_equal 3,
62
+ Reforge::Options.new(collect_max_evaluation_summaries: 3).collect_max_evaluation_summaries
63
+ end
64
+
65
+ def test_context_upload_mode_periodic
66
+ options = Reforge::Options.new(context_upload_mode: :periodic_example, context_max_size: 100)
67
+ assert_equal 100, options.collect_max_example_contexts
68
+
69
+ options = Reforge::Options.new(context_upload_mode: :none)
70
+ assert_equal 0, options.collect_max_example_contexts
71
+ end
72
+
73
+ def test_context_upload_mode_shape_only
74
+ options = Reforge::Options.new(context_upload_mode: :shape_only, context_max_size: 100)
75
+ assert_equal 100, options.collect_max_shapes
76
+
77
+ options = Reforge::Options.new(context_upload_mode: :none)
78
+ assert_equal 0, options.collect_max_shapes
79
+ end
80
+
81
+ def test_context_upload_mode_none
82
+ options = Reforge::Options.new(context_upload_mode: :none)
83
+ assert_equal 0, options.collect_max_example_contexts
84
+
85
+ options = Reforge::Options.new(context_upload_mode: :none)
86
+ assert_equal 0, options.collect_max_shapes
87
+ end
88
+
89
+ def test_loading_a_datafile
90
+ options = Reforge::Options.new(datafile: "#{Dir.pwd}/test/fixtures/datafile.json")
91
+ assert_equal "#{Dir.pwd}/test/fixtures/datafile.json", options.datafile
92
+ end
93
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class TestPrefab < Minitest::Test
6
+
7
+
8
+
9
+ private
10
+
11
+ def init_once
12
+ unless Reforge.instance_variable_get("@singleton")
13
+ Reforge.init(prefab_options)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'timecop'
5
+
6
+ class RateLimitCacheTest < Minitest::Test
7
+ def test_set_and_fresh
8
+ cache = Reforge::RateLimitCache.new(5)
9
+ cache.set('key')
10
+ assert cache.fresh?('key')
11
+ end
12
+
13
+ def test_fresh_with_no_set
14
+ cache = Reforge::RateLimitCache.new(5)
15
+ refute cache.fresh?('key')
16
+ end
17
+
18
+ def test_get_after_expiration
19
+ cache = Reforge::RateLimitCache.new(5)
20
+
21
+ Timecop.freeze(Time.now - 6) do
22
+ cache.set('key')
23
+ assert cache.fresh?('key')
24
+ end
25
+
26
+ refute cache.fresh?('key')
27
+
28
+ # but the data is still there
29
+ assert cache.data.get('key')
30
+ end
31
+
32
+ def test_prune
33
+ cache = Reforge::RateLimitCache.new(5)
34
+
35
+ Timecop.freeze(Time.now - 6) do
36
+ cache.set('key')
37
+ assert cache.fresh?('key')
38
+ end
39
+
40
+ cache.prune
41
+
42
+ refute cache.fresh?('key')
43
+ end
44
+ end
@@ -0,0 +1,108 @@
1
+ require 'test_helper'
2
+ class TestSemanticVersion < Minitest::Test
3
+ def test_parse_valid_version
4
+ version = SemanticVersion.parse('1.2.3')
5
+ assert_equal 1, version.major
6
+ assert_equal 2, version.minor
7
+ assert_equal 3, version.patch
8
+ assert_nil version.prerelease
9
+ assert_nil version.build_metadata
10
+ end
11
+
12
+ def test_parse_version_with_prerelease
13
+ version = SemanticVersion.parse('1.2.3-alpha.1')
14
+ assert_equal 1, version.major
15
+ assert_equal 2, version.minor
16
+ assert_equal 3, version.patch
17
+ assert_equal 'alpha.1', version.prerelease
18
+ assert_nil version.build_metadata
19
+ end
20
+
21
+ def test_parse_version_with_build_metadata
22
+ version = SemanticVersion.parse('1.2.3+build.123')
23
+ assert_equal 1, version.major
24
+ assert_equal 2, version.minor
25
+ assert_equal 3, version.patch
26
+ assert_nil version.prerelease
27
+ assert_equal 'build.123', version.build_metadata
28
+ end
29
+
30
+ def test_parse_full_version
31
+ version = SemanticVersion.parse('1.2.3-alpha.1+build.123')
32
+ assert_equal 1, version.major
33
+ assert_equal 2, version.minor
34
+ assert_equal 3, version.patch
35
+ assert_equal 'alpha.1', version.prerelease
36
+ assert_equal 'build.123', version.build_metadata
37
+ end
38
+
39
+ def test_parse_invalid_version
40
+ assert_raises(ArgumentError) { SemanticVersion.parse('invalid') }
41
+ assert_raises(ArgumentError) { SemanticVersion.parse('1.2') }
42
+ assert_raises(ArgumentError) { SemanticVersion.parse('1.2.3.4') }
43
+ assert_raises(ArgumentError) { SemanticVersion.parse('') }
44
+ end
45
+
46
+ def test_parse_quietly
47
+ assert_nil SemanticVersion.parse_quietly('invalid')
48
+ refute_nil SemanticVersion.parse_quietly('1.2.3')
49
+ end
50
+
51
+ def test_to_string
52
+ assert_equal '1.2.3', SemanticVersion.parse('1.2.3').to_s
53
+ assert_equal '1.2.3-alpha.1', SemanticVersion.parse('1.2.3-alpha.1').to_s
54
+ assert_equal '1.2.3+build.123', SemanticVersion.parse('1.2.3+build.123').to_s
55
+ assert_equal '1.2.3-alpha.1+build.123', SemanticVersion.parse('1.2.3-alpha.1+build.123').to_s
56
+ end
57
+
58
+ def test_equality
59
+ v1 = SemanticVersion.parse('1.2.3')
60
+ v2 = SemanticVersion.parse('1.2.3')
61
+ v3 = SemanticVersion.parse('1.2.4')
62
+ v4 = SemanticVersion.parse('1.2.3-alpha')
63
+ v5 = SemanticVersion.parse('1.2.3+build.123')
64
+
65
+ assert_equal v1, v2
66
+ refute_equal v1, v3
67
+ refute_equal v1, v4
68
+ assert_equal v1, v5 # build metadata is ignored in equality
69
+ end
70
+
71
+ def test_comparison
72
+ versions = [
73
+ '1.0.0-alpha',
74
+ '1.0.0-alpha.1',
75
+ '1.0.0-beta.2',
76
+ '1.0.0-beta.11',
77
+ '1.0.0-rc.1',
78
+ '1.0.0',
79
+ '2.0.0',
80
+ '2.1.0',
81
+ '2.1.1'
82
+ ].map { |v| SemanticVersion.parse(v) }
83
+
84
+ # Test that each version is less than the next version
85
+ (versions.length - 1).times do |i|
86
+ assert versions[i] < versions[i + 1], "Expected #{versions[i]} < #{versions[i + 1]}"
87
+ end
88
+ end
89
+
90
+ def test_prerelease_comparison
91
+ # Test specific prerelease comparison cases
92
+ cases = [
93
+ ['1.0.0-alpha', '1.0.0-alpha.1', -1],
94
+ ['1.0.0-alpha.1', '1.0.0-alpha.beta', -1],
95
+ ['1.0.0-alpha.beta', '1.0.0-beta', -1],
96
+ ['1.0.0-beta', '1.0.0-beta.2', -1],
97
+ ['1.0.0-beta.2', '1.0.0-beta.11', -1],
98
+ ['1.0.0-beta.11', '1.0.0-rc.1', -1],
99
+ ['1.0.0-rc.1', '1.0.0', -1]
100
+ ]
101
+
102
+ cases.each do |v1_str, v2_str, expected|
103
+ v1 = SemanticVersion.parse(v1_str)
104
+ v2 = SemanticVersion.parse(v2_str)
105
+ assert_equal expected, (v1 <=> v2), "Expected #{v1} <=> #{v2} to be #{expected}"
106
+ end
107
+ end
108
+ end