featurevisor 0.1.1 → 0.3.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.
data/bin/commands/test.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "json"
2
2
  require "find"
3
3
  require "open3"
4
+ require "shellwords"
4
5
 
5
6
  module FeaturevisorCLI
6
7
  module Commands
@@ -19,17 +20,17 @@ module FeaturevisorCLI
19
20
 
20
21
  # Get project configuration
21
22
  config = get_config
22
- environments = config["environments"] || []
23
+ environments = get_environments(config)
23
24
  segments_by_key = get_segments
24
25
 
25
26
  # Use CLI schemaVersion option or fallback to config
26
27
  schema_version = @options.schema_version
27
28
  if schema_version.nil? || schema_version.empty?
28
- schema_version = config["schemaVersion"]
29
+ schema_version = config[:schemaVersion]
29
30
  end
30
31
 
31
- # Build datafiles for all environments
32
- datafiles_by_environment = build_datafiles(environments, schema_version, @options.inflate)
32
+ # Build datafiles for all environments (+ scoped/tagged variants)
33
+ datafiles_by_key = build_datafiles(config, environments, schema_version, @options.inflate)
33
34
 
34
35
  puts ""
35
36
 
@@ -42,22 +43,19 @@ module FeaturevisorCLI
42
43
  return
43
44
  end
44
45
 
45
- # Create SDK instances for each environment
46
- sdk_instances_by_environment = create_sdk_instances(environments, datafiles_by_environment, level)
47
-
48
46
  # Run tests
49
- run_tests(tests, sdk_instances_by_environment, datafiles_by_environment, segments_by_key, level)
47
+ run_tests(tests, datafiles_by_key, segments_by_key, level, config)
50
48
  end
51
49
 
52
50
  private
53
51
 
54
52
  def get_config
55
53
  puts "Getting config..."
56
- command = "(cd #{@project_path} && npx featurevisor config --json)"
54
+ command = "(cd #{Shellwords.escape(@project_path)} && npx featurevisor config --json)"
57
55
  config_output = execute_command(command)
58
56
 
59
57
  begin
60
- JSON.parse(config_output)
58
+ JSON.parse(config_output, symbolize_names: true)
61
59
  rescue JSON::ParserError => e
62
60
  puts "Error: Failed to parse config JSON: #{e.message}"
63
61
  puts "Command output: #{config_output}"
@@ -67,15 +65,15 @@ module FeaturevisorCLI
67
65
 
68
66
  def get_segments
69
67
  puts "Getting segments..."
70
- command = "(cd #{@project_path} && npx featurevisor list --segments --json)"
68
+ command = "(cd #{Shellwords.escape(@project_path)} && npx featurevisor list --segments --json)"
71
69
  segments_output = execute_command(command)
72
70
 
73
71
  begin
74
- segments = JSON.parse(segments_output)
72
+ segments = JSON.parse(segments_output, symbolize_names: true)
75
73
  segments_by_key = {}
76
74
  segments.each do |segment|
77
- if segment["key"]
78
- segments_by_key[segment["key"]] = segment
75
+ if segment[:key]
76
+ segments_by_key[segment[:key]] = segment
79
77
  end
80
78
  end
81
79
  segments_by_key
@@ -86,40 +84,111 @@ module FeaturevisorCLI
86
84
  end
87
85
  end
88
86
 
89
- def build_datafiles(environments, schema_version, inflate)
90
- datafiles_by_environment = {}
87
+ def get_environments(config)
88
+ environments = config[:environments]
89
+
90
+ return [false] if environments == false
91
+ return environments if environments.is_a?(Array) && !environments.empty?
92
+
93
+ [false]
94
+ end
95
+
96
+ def base_datafile_key(environment)
97
+ environment == false ? false : environment
98
+ end
99
+
100
+ def scoped_datafile_key(environment, scope_name)
101
+ if environment == false
102
+ "scope-#{scope_name}"
103
+ else
104
+ "#{environment}-scope-#{scope_name}"
105
+ end
106
+ end
107
+
108
+ def tagged_datafile_key(environment, tag)
109
+ if environment == false
110
+ "tag-#{tag}"
111
+ else
112
+ "#{environment}-tag-#{tag}"
113
+ end
114
+ end
115
+
116
+ def build_datafiles(config, environments, schema_version, inflate)
117
+ datafiles_by_key = {}
91
118
 
92
119
  environments.each do |environment|
93
- puts "Building datafile for environment: #{environment}..."
120
+ environment_label = environment == false ? "default (no environment)" : environment
121
+ puts "Building datafile for environment: #{environment_label}..."
94
122
 
95
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
123
+ datafiles_by_key[base_datafile_key(environment)] = build_single_datafile(
124
+ environment: environment,
125
+ schema_version: schema_version,
126
+ inflate: inflate
127
+ )
96
128
 
97
- if schema_version && !schema_version.empty?
98
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{schema_version}", "--json"]
129
+ if @options.with_scopes && config[:scopes].is_a?(Array)
130
+ config[:scopes].each do |scope|
131
+ next unless scope[:name]
132
+
133
+ puts "Building scoped datafile for scope: #{scope[:name]}..."
134
+ datafiles_by_key[scoped_datafile_key(environment, scope[:name])] = build_single_datafile(
135
+ environment: environment,
136
+ schema_version: schema_version,
137
+ inflate: inflate,
138
+ scope: scope[:name]
139
+ )
140
+ end
99
141
  end
100
142
 
101
- if inflate && inflate > 0
102
- if schema_version && !schema_version.empty?
103
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{schema_version}", "--inflate=#{inflate}", "--json"]
104
- else
105
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--inflate=#{inflate}", "--json"]
143
+ if @options.with_tags && config[:tags].is_a?(Array)
144
+ config[:tags].each do |tag|
145
+ puts "Building tagged datafile for tag: #{tag}..."
146
+ datafiles_by_key[tagged_datafile_key(environment, tag)] = build_single_datafile(
147
+ environment: environment,
148
+ schema_version: schema_version,
149
+ inflate: inflate,
150
+ tag: tag
151
+ )
106
152
  end
107
153
  end
154
+ end
108
155
 
109
- command = command_parts.join(" ")
110
- datafile_output = execute_command(command)
156
+ datafiles_by_key
157
+ end
111
158
 
112
- begin
113
- datafile = JSON.parse(datafile_output)
114
- datafiles_by_environment[environment] = datafile
115
- rescue JSON::ParserError => e
116
- puts "Error: Failed to parse datafile JSON for #{environment}: #{e.message}"
117
- puts "Command output: #{datafile_output}"
118
- exit 1
119
- end
159
+ def build_single_datafile(environment:, schema_version:, inflate:, scope: nil, tag: nil)
160
+ command_parts = ["npx", "featurevisor", "build", "--json"]
161
+
162
+ if environment != false && !environment.nil?
163
+ command_parts << "--environment=#{Shellwords.escape(environment.to_s)}"
120
164
  end
121
165
 
122
- datafiles_by_environment
166
+ if schema_version && !schema_version.empty?
167
+ command_parts << "--schemaVersion=#{Shellwords.escape(schema_version.to_s)}"
168
+ end
169
+
170
+ if inflate && inflate > 0
171
+ command_parts << "--inflate=#{inflate}"
172
+ end
173
+
174
+ if scope
175
+ command_parts << "--scope=#{Shellwords.escape(scope.to_s)}"
176
+ end
177
+
178
+ if tag
179
+ command_parts << "--tag=#{Shellwords.escape(tag.to_s)}"
180
+ end
181
+
182
+ command = "(cd #{Shellwords.escape(@project_path)} && #{command_parts.join(' ')})"
183
+ datafile_output = execute_command(command)
184
+
185
+ begin
186
+ JSON.parse(datafile_output, symbolize_names: true)
187
+ rescue JSON::ParserError => e
188
+ puts "Error: Failed to parse datafile JSON: #{e.message}"
189
+ puts "Command output: #{datafile_output}"
190
+ exit 1
191
+ end
123
192
  end
124
193
 
125
194
  def get_logger_level
@@ -133,21 +202,21 @@ module FeaturevisorCLI
133
202
  end
134
203
 
135
204
  def get_tests
136
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "list", "--tests", "--applyMatrix", "--json"]
205
+ command_parts = ["npx", "featurevisor", "list", "--tests", "--applyMatrix", "--json"]
137
206
 
138
207
  if @options.key_pattern && !@options.key_pattern.empty?
139
- command_parts << "--keyPattern=#{@options.key_pattern}"
208
+ command_parts << "--keyPattern=#{Shellwords.escape(@options.key_pattern)}"
140
209
  end
141
210
 
142
211
  if @options.assertion_pattern && !@options.assertion_pattern.empty?
143
- command_parts << "--assertionPattern=#{@options.assertion_pattern}"
212
+ command_parts << "--assertionPattern=#{Shellwords.escape(@options.assertion_pattern)}"
144
213
  end
145
214
 
146
- command = command_parts.join(" ")
215
+ command = "(cd #{Shellwords.escape(@project_path)} && #{command_parts.join(' ')})"
147
216
  tests_output = execute_command(command)
148
217
 
149
218
  begin
150
- JSON.parse(tests_output)
219
+ JSON.parse(tests_output, symbolize_names: true)
151
220
  rescue JSON::ParserError => e
152
221
  puts "Error: Failed to parse tests JSON: #{e.message}"
153
222
  puts "Command output: #{tests_output}"
@@ -155,42 +224,66 @@ module FeaturevisorCLI
155
224
  end
156
225
  end
157
226
 
158
- def create_sdk_instances(environments, datafiles_by_environment, level)
159
- sdk_instances_by_environment = {}
227
+ def get_scope_context(config, scope_name)
228
+ return {} unless scope_name && config[:scopes].is_a?(Array)
160
229
 
161
- environments.each do |environment|
162
- datafile = datafiles_by_environment[environment]
230
+ scope = config[:scopes].find { |s| s[:name] == scope_name }
231
+ return {} unless scope && scope[:context].is_a?(Hash)
163
232
 
164
- # Convert string keys to symbols for the SDK
165
- symbolized_datafile = symbolize_keys(datafile)
166
-
167
- # Create SDK instance
168
- instance = Featurevisor.create_instance(
169
- datafile: symbolized_datafile,
170
- log_level: level,
171
- hooks: [
172
- {
173
- name: "tester-hook",
174
- bucket_value: ->(options) { options.bucket_value }
175
- }
176
- ]
177
- )
233
+ parse_context(scope[:context])
234
+ end
235
+
236
+ def resolve_datafile_for_assertion(assertion, datafiles_by_key)
237
+ environment = assertion.key?(:environment) ? assertion[:environment] : false
238
+ environment = false if environment.nil?
239
+
240
+ scoped_key = assertion[:scope] ? scoped_datafile_key(environment, assertion[:scope]) : nil
241
+ tagged_key = assertion[:tag] ? tagged_datafile_key(environment, assertion[:tag]) : nil
242
+ base_key = base_datafile_key(environment)
243
+
244
+ if scoped_key && datafiles_by_key.key?(scoped_key)
245
+ return datafiles_by_key[scoped_key]
246
+ end
178
247
 
179
- sdk_instances_by_environment[environment] = instance
248
+ if tagged_key && datafiles_by_key.key?(tagged_key)
249
+ return datafiles_by_key[tagged_key]
180
250
  end
181
251
 
182
- sdk_instances_by_environment
252
+ datafiles_by_key[base_key]
183
253
  end
184
254
 
185
- def run_tests(tests, sdk_instances_by_environment, datafiles_by_environment, segments_by_key, level)
255
+ def create_tester_instance(datafile, level, assertion)
256
+ sticky = parse_sticky(assertion[:sticky])
257
+
258
+ Featurevisor.create_instance(
259
+ datafile: datafile,
260
+ sticky: sticky,
261
+ log_level: level,
262
+ hooks: [
263
+ {
264
+ name: "tester-hook",
265
+ bucket_value: ->(options) do
266
+ at = assertion[:at]
267
+ if at.is_a?(Numeric)
268
+ (at * 1000).to_i
269
+ else
270
+ options.bucket_value
271
+ end
272
+ end
273
+ }
274
+ ]
275
+ )
276
+ end
277
+
278
+ def run_tests(tests, datafiles_by_key, segments_by_key, level, config)
186
279
  passed_tests_count = 0
187
280
  failed_tests_count = 0
188
281
  passed_assertions_count = 0
189
282
  failed_assertions_count = 0
190
283
 
191
284
  tests.each do |test|
192
- test_key = test["key"]
193
- assertions = test["assertions"] || []
285
+ test_key = test[:key]
286
+ assertions = test[:assertions] || []
194
287
  results = ""
195
288
  test_has_error = false
196
289
  test_duration = 0.0
@@ -199,47 +292,36 @@ module FeaturevisorCLI
199
292
  if assertion.is_a?(Hash)
200
293
  test_result = nil
201
294
 
202
- if test["feature"]
203
- environment = assertion["environment"]
204
- instance = sdk_instances_by_environment[environment]
205
-
206
- # Show datafile if requested
207
- if @options.show_datafile
208
- datafile = datafiles_by_environment[environment]
209
- puts ""
210
- puts JSON.pretty_generate(datafile)
211
- puts ""
295
+ if test[:feature]
296
+ datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
297
+ if datafile.nil?
298
+ test_result = {
299
+ has_error: true,
300
+ errors: " ✘ no datafile found for assertion scope/tag/environment combination\n",
301
+ duration: 0
302
+ }
212
303
  end
213
304
 
214
- # If "at" parameter is provided, create a new instance with the specific hook
215
- if assertion["at"]
216
- datafile = datafiles_by_environment[environment]
217
- symbolized_datafile = symbolize_keys(datafile)
218
-
219
- instance = Featurevisor.create_instance(
220
- datafile: symbolized_datafile,
221
- log_level: level,
222
- hooks: [
223
- {
224
- name: "tester-hook",
225
- bucket_value: ->(options) do
226
- # Match JavaScript implementation: assertion.at * (MAX_BUCKETED_NUMBER / 100)
227
- # MAX_BUCKETED_NUMBER is 100000, so this becomes assertion.at * 1000
228
- at = assertion["at"]
229
- if at.is_a?(Numeric)
230
- (at * 1000).to_i
231
- else
232
- options.bucket_value
233
- end
234
- end
235
- }
236
- ]
237
- )
238
- end
305
+ if datafile
306
+ instance = create_tester_instance(datafile, level, assertion)
307
+ scope_context = {}
308
+
309
+ if assertion[:scope] && !@options.with_scopes
310
+ # If not using scoped datafiles, mimic JS behavior by merging scope context.
311
+ scope_context = get_scope_context(config, assertion[:scope])
312
+ end
239
313
 
240
- test_result = run_test_feature(assertion, test["feature"], instance, level)
241
- elsif test["segment"]
242
- segment_key = test["segment"]
314
+ # Show datafile if requested
315
+ if @options.show_datafile
316
+ puts ""
317
+ puts JSON.pretty_generate(datafile)
318
+ puts ""
319
+ end
320
+
321
+ test_result = run_test_feature(assertion, test[:feature], instance, level, scope_context)
322
+ end
323
+ elsif test[:segment]
324
+ segment_key = test[:segment]
243
325
  segment = segments_by_key[segment_key]
244
326
  if segment.is_a?(Hash)
245
327
  test_result = run_test_segment(assertion, segment, level)
@@ -250,12 +332,12 @@ module FeaturevisorCLI
250
332
  test_duration += test_result[:duration]
251
333
 
252
334
  if test_result[:has_error]
253
- results += " ✘ #{assertion['description']} (#{(test_result[:duration] * 1000).round(2)}ms)\n"
335
+ results += " ✘ #{assertion[:description]} (#{(test_result[:duration] * 1000).round(2)}ms)\n"
254
336
  results += test_result[:errors]
255
337
  test_has_error = true
256
338
  failed_assertions_count += 1
257
339
  else
258
- results += " ✔ #{assertion['description']} (#{(test_result[:duration] * 1000).round(2)}ms)\n"
340
+ results += " ✔ #{assertion[:description]} (#{(test_result[:duration] * 1000).round(2)}ms)\n"
259
341
  passed_assertions_count += 1
260
342
  end
261
343
  end
@@ -284,9 +366,10 @@ module FeaturevisorCLI
284
366
  end
285
367
  end
286
368
 
287
- def run_test_feature(assertion, feature_key, instance, level)
288
- context = parse_context(assertion["context"])
289
- sticky = parse_sticky(assertion["sticky"])
369
+ def run_test_feature(assertion, feature_key, instance, level, scope_context = {})
370
+ context = parse_context(assertion[:context])
371
+ context = { **scope_context, **context } if scope_context && !scope_context.empty?
372
+ sticky = parse_sticky(assertion[:sticky])
290
373
 
291
374
  # Set context and sticky for this assertion
292
375
  instance.set_context(context, false)
@@ -302,8 +385,8 @@ module FeaturevisorCLI
302
385
  start_time = Time.now
303
386
 
304
387
  # Test expectedToBeEnabled
305
- if assertion.key?("expectedToBeEnabled")
306
- expected_to_be_enabled = assertion["expectedToBeEnabled"]
388
+ if assertion.key?(:expectedToBeEnabled)
389
+ expected_to_be_enabled = assertion[:expectedToBeEnabled]
307
390
  is_enabled = instance.is_enabled(feature_key, context, override_options)
308
391
 
309
392
  if is_enabled != expected_to_be_enabled
@@ -313,8 +396,8 @@ module FeaturevisorCLI
313
396
  end
314
397
 
315
398
  # Test expectedVariation
316
- if assertion.key?("expectedVariation")
317
- expected_variation = assertion["expectedVariation"]
399
+ if assertion.key?(:expectedVariation)
400
+ expected_variation = assertion[:expectedVariation]
318
401
  variation = instance.get_variation(feature_key, context, override_options)
319
402
 
320
403
  variation_value = variation.nil? ? nil : variation
@@ -325,12 +408,12 @@ module FeaturevisorCLI
325
408
  end
326
409
 
327
410
  # Test expectedVariables
328
- if assertion["expectedVariables"]
329
- expected_variables = assertion["expectedVariables"]
411
+ if assertion[:expectedVariables]
412
+ expected_variables = assertion[:expectedVariables]
330
413
  expected_variables.each do |variable_key, expected_value|
331
414
  # Set default variable value for this specific variable
332
- if assertion["defaultVariableValues"] && assertion["defaultVariableValues"][variable_key]
333
- override_options[:default_variable_value] = assertion["defaultVariableValues"][variable_key]
415
+ if assertion[:defaultVariableValues].is_a?(Hash) && assertion[:defaultVariableValues].key?(variable_key)
416
+ override_options[:default_variable_value] = assertion[:defaultVariableValues][variable_key]
334
417
  end
335
418
 
336
419
  actual_value = instance.get_variable(feature_key, variable_key, context, override_options)
@@ -369,13 +452,13 @@ module FeaturevisorCLI
369
452
  end
370
453
 
371
454
  # Test expectedEvaluations
372
- if assertion["expectedEvaluations"]
373
- expected_evaluations = assertion["expectedEvaluations"]
455
+ if assertion[:expectedEvaluations]
456
+ expected_evaluations = assertion[:expectedEvaluations]
374
457
 
375
458
  # Test flag evaluations
376
- if expected_evaluations["flag"]
459
+ if expected_evaluations[:flag]
377
460
  evaluation = instance.evaluate_flag(feature_key, context, override_options)
378
- expected_evaluations["flag"].each do |key, expected_value|
461
+ expected_evaluations[:flag].each do |key, expected_value|
379
462
  actual_value = get_evaluation_value(evaluation, key)
380
463
  if !compare_values(actual_value, expected_value)
381
464
  has_error = true
@@ -385,9 +468,9 @@ module FeaturevisorCLI
385
468
  end
386
469
 
387
470
  # Test variation evaluations
388
- if expected_evaluations["variation"]
471
+ if expected_evaluations[:variation]
389
472
  evaluation = instance.evaluate_variation(feature_key, context, override_options)
390
- expected_evaluations["variation"].each do |key, expected_value|
473
+ expected_evaluations[:variation].each do |key, expected_value|
391
474
  actual_value = get_evaluation_value(evaluation, key)
392
475
  if !compare_values(actual_value, expected_value)
393
476
  has_error = true
@@ -397,8 +480,8 @@ module FeaturevisorCLI
397
480
  end
398
481
 
399
482
  # Test variable evaluations
400
- if expected_evaluations["variables"]
401
- expected_evaluations["variables"].each do |variable_key, expected_eval|
483
+ if expected_evaluations[:variables]
484
+ expected_evaluations[:variables].each do |variable_key, expected_eval|
402
485
  if expected_eval.is_a?(Hash)
403
486
  evaluation = instance.evaluate_variable(feature_key, variable_key, context, override_options)
404
487
  expected_eval.each do |key, expected_value|
@@ -414,10 +497,10 @@ module FeaturevisorCLI
414
497
  end
415
498
 
416
499
  # Test children
417
- if assertion["children"]
418
- assertion["children"].each do |child|
500
+ if assertion[:children]
501
+ assertion[:children].each do |child|
419
502
  if child.is_a?(Hash)
420
- child_context = parse_context(child["context"])
503
+ child_context = parse_context(child[:context])
421
504
 
422
505
  # Create override options for child with sticky values
423
506
  child_override_options = create_override_options(child)
@@ -452,7 +535,7 @@ module FeaturevisorCLI
452
535
  end
453
536
 
454
537
  def run_test_feature_child(assertion, feature_key, instance, level)
455
- context = parse_context(assertion["context"])
538
+ context = parse_context(assertion[:context])
456
539
  override_options = create_override_options(assertion)
457
540
 
458
541
  has_error = false
@@ -460,8 +543,8 @@ module FeaturevisorCLI
460
543
  start_time = Time.now
461
544
 
462
545
  # Test expectedToBeEnabled
463
- if assertion.key?("expectedToBeEnabled")
464
- expected_to_be_enabled = assertion["expectedToBeEnabled"]
546
+ if assertion.key?(:expectedToBeEnabled)
547
+ expected_to_be_enabled = assertion[:expectedToBeEnabled]
465
548
  is_enabled = instance.is_enabled(feature_key, context, override_options)
466
549
 
467
550
  if is_enabled != expected_to_be_enabled
@@ -471,8 +554,8 @@ module FeaturevisorCLI
471
554
  end
472
555
 
473
556
  # Test expectedVariation
474
- if assertion.key?("expectedVariation")
475
- expected_variation = assertion["expectedVariation"]
557
+ if assertion.key?(:expectedVariation)
558
+ expected_variation = assertion[:expectedVariation]
476
559
  variation = instance.get_variation(feature_key, context, override_options)
477
560
 
478
561
  variation_value = variation.nil? ? nil : variation
@@ -483,12 +566,12 @@ module FeaturevisorCLI
483
566
  end
484
567
 
485
568
  # Test expectedVariables
486
- if assertion["expectedVariables"]
487
- expected_variables = assertion["expectedVariables"]
569
+ if assertion[:expectedVariables]
570
+ expected_variables = assertion[:expectedVariables]
488
571
  expected_variables.each do |variable_key, expected_value|
489
572
  # Set default variable value for this specific variable
490
- if assertion["defaultVariableValues"] && assertion["defaultVariableValues"][variable_key]
491
- override_options[:default_variable_value] = assertion["defaultVariableValues"][variable_key]
573
+ if assertion[:defaultVariableValues].is_a?(Hash) && assertion[:defaultVariableValues].key?(variable_key)
574
+ override_options[:default_variable_value] = assertion[:defaultVariableValues][variable_key]
492
575
  end
493
576
 
494
577
  actual_value = instance.get_variable(feature_key, variable_key, context, override_options)
@@ -526,6 +609,55 @@ module FeaturevisorCLI
526
609
  end
527
610
  end
528
611
 
612
+ # Test expectedEvaluations
613
+ if assertion[:expectedEvaluations]
614
+ expected_evaluations = assertion[:expectedEvaluations]
615
+
616
+ if expected_evaluations[:flag]
617
+ evaluation = evaluate_from_instance(instance, :flag, feature_key, context, override_options)
618
+ expected_evaluations[:flag].each do |key, expected_value|
619
+ actual_value = get_evaluation_value(evaluation, key)
620
+ if !compare_values(actual_value, expected_value)
621
+ has_error = true
622
+ errors += " ✘ expectedEvaluations.flag.#{key}: expected #{expected_value} but received #{actual_value}\n"
623
+ end
624
+ end
625
+ end
626
+
627
+ if expected_evaluations[:variation]
628
+ evaluation = evaluate_from_instance(instance, :variation, feature_key, context, override_options)
629
+ expected_evaluations[:variation].each do |key, expected_value|
630
+ actual_value = get_evaluation_value(evaluation, key)
631
+ if !compare_values(actual_value, expected_value)
632
+ has_error = true
633
+ errors += " ✘ expectedEvaluations.variation.#{key}: expected #{expected_value} but received #{actual_value}\n"
634
+ end
635
+ end
636
+ end
637
+
638
+ if expected_evaluations[:variables]
639
+ expected_evaluations[:variables].each do |variable_key, expected_eval|
640
+ if expected_eval.is_a?(Hash)
641
+ evaluation = evaluate_from_instance(
642
+ instance,
643
+ :variable,
644
+ feature_key,
645
+ context,
646
+ override_options,
647
+ variable_key
648
+ )
649
+ expected_eval.each do |key, expected_value|
650
+ actual_value = get_evaluation_value(evaluation, key)
651
+ if !compare_values(actual_value, expected_value)
652
+ has_error = true
653
+ errors += " ✘ expectedEvaluations.variables.#{variable_key}.#{key}: expected #{expected_value} but received #{actual_value}\n"
654
+ end
655
+ end
656
+ end
657
+ end
658
+ end
659
+ end
660
+
529
661
  duration = Time.now - start_time
530
662
 
531
663
  {
@@ -536,8 +668,8 @@ module FeaturevisorCLI
536
668
  end
537
669
 
538
670
  def run_test_segment(assertion, segment, level)
539
- context = parse_context(assertion["context"])
540
- conditions = segment["conditions"]
671
+ context = parse_context(assertion[:context])
672
+ conditions = segment[:conditions]
541
673
 
542
674
  # Create a minimal datafile for segment testing
543
675
  datafile = {
@@ -549,7 +681,7 @@ module FeaturevisorCLI
549
681
 
550
682
  # Create SDK instance for segment testing
551
683
  instance = Featurevisor.create_instance(
552
- datafile: symbolize_keys(datafile),
684
+ datafile: datafile,
553
685
  log_level: level
554
686
  )
555
687
 
@@ -557,8 +689,8 @@ module FeaturevisorCLI
557
689
  errors = ""
558
690
  start_time = Time.now
559
691
 
560
- if assertion.key?("expectedToMatch")
561
- expected_to_match = assertion["expectedToMatch"]
692
+ if assertion.key?(:expectedToMatch)
693
+ expected_to_match = assertion[:expectedToMatch]
562
694
  actual = instance.instance_variable_get(:@datafile_reader).all_conditions_are_matched(conditions, context)
563
695
 
564
696
  if actual != expected_to_match
@@ -593,16 +725,16 @@ module FeaturevisorCLI
593
725
  if value.is_a?(Hash)
594
726
  evaluated_feature = {}
595
727
 
596
- if value.key?("enabled")
597
- evaluated_feature[:enabled] = value["enabled"]
728
+ if value.key?(:enabled)
729
+ evaluated_feature[:enabled] = value[:enabled]
598
730
  end
599
731
 
600
- if value.key?("variation")
601
- evaluated_feature[:variation] = value["variation"]
732
+ if value.key?(:variation)
733
+ evaluated_feature[:variation] = value[:variation]
602
734
  end
603
735
 
604
- if value["variables"] && value["variables"].is_a?(Hash)
605
- evaluated_feature[:variables] = value["variables"].transform_keys(&:to_sym)
736
+ if value[:variables] && value[:variables].is_a?(Hash)
737
+ evaluated_feature[:variables] = value[:variables].transform_keys(&:to_sym)
606
738
  end
607
739
 
608
740
  sticky_features[key.to_sym] = evaluated_feature
@@ -618,51 +750,87 @@ module FeaturevisorCLI
618
750
  def create_override_options(assertion)
619
751
  options = {}
620
752
 
621
- if assertion["defaultVariationValue"]
622
- options[:default_variation_value] = assertion["defaultVariationValue"]
753
+ if assertion.key?(:defaultVariationValue)
754
+ options[:default_variation_value] = assertion[:defaultVariationValue]
623
755
  end
624
756
 
625
757
  options
626
758
  end
627
759
 
760
+ def evaluate_from_instance(instance, type, feature_key, context, override_options, variable_key = nil)
761
+ method_name = :"evaluate_#{type}"
762
+
763
+ if instance.respond_to?(method_name)
764
+ if variable_key.nil?
765
+ return instance.send(method_name, feature_key, context, override_options)
766
+ end
767
+
768
+ return instance.send(method_name, feature_key, variable_key, context, override_options)
769
+ end
770
+
771
+ if instance.respond_to?(:parent) && instance.respond_to?(:sticky)
772
+ parent = instance.parent
773
+ combined_context = if instance.respond_to?(:context)
774
+ { **(instance.context || {}), **context }
775
+ else
776
+ context
777
+ end
778
+
779
+ combined_options = {
780
+ sticky: instance.sticky,
781
+ **override_options
782
+ }
783
+
784
+ if variable_key.nil?
785
+ return parent.send(method_name, feature_key, combined_context, combined_options)
786
+ end
787
+
788
+ return parent.send(method_name, feature_key, variable_key, combined_context, combined_options)
789
+ end
790
+
791
+ {}
792
+ end
793
+
628
794
  def get_evaluation_value(evaluation, key)
629
795
  case key
630
- when "type"
796
+ when :type
631
797
  evaluation[:type]
632
- when "featureKey"
798
+ when :featureKey
633
799
  evaluation[:feature_key]
634
- when "reason"
800
+ when :reason
635
801
  evaluation[:reason]
636
- when "bucketKey"
802
+ when :bucketKey
637
803
  evaluation[:bucket_key]
638
- when "bucketValue"
804
+ when :bucketValue
639
805
  evaluation[:bucket_value]
640
- when "ruleKey"
806
+ when :ruleKey
641
807
  evaluation[:rule_key]
642
- when "error"
808
+ when :error
643
809
  evaluation[:error]
644
- when "enabled"
810
+ when :enabled
645
811
  evaluation[:enabled]
646
- when "traffic"
812
+ when :traffic
647
813
  evaluation[:traffic]
648
- when "forceIndex"
814
+ when :forceIndex
649
815
  evaluation[:force_index]
650
- when "force"
816
+ when :force
651
817
  evaluation[:force]
652
- when "required"
818
+ when :required
653
819
  evaluation[:required]
654
- when "sticky"
820
+ when :sticky
655
821
  evaluation[:sticky]
656
- when "variation"
822
+ when :variation
657
823
  evaluation[:variation]
658
- when "variationValue"
824
+ when :variationValue
659
825
  evaluation[:variation_value]
660
- when "variableKey"
826
+ when :variableKey
661
827
  evaluation[:variable_key]
662
- when "variableValue"
828
+ when :variableValue
663
829
  evaluation[:variable_value]
664
- when "variableSchema"
830
+ when :variableSchema
665
831
  evaluation[:variable_schema]
832
+ when :variableOverrideIndex
833
+ evaluation[:variable_override_index]
666
834
  else
667
835
  nil
668
836
  end
@@ -765,16 +933,7 @@ module FeaturevisorCLI
765
933
  end
766
934
  end
767
935
 
768
- def symbolize_keys(obj)
769
- case obj
770
- when Hash
771
- obj.transform_keys(&:to_sym).transform_values { |v| symbolize_keys(v) }
772
- when Array
773
- obj.map { |v| symbolize_keys(v) }
774
- else
775
- obj
776
- end
777
- end
936
+
778
937
 
779
938
  def execute_command(command)
780
939
  stdout, stderr, status = Open3.capture3(command)