featurevisor 0.2.0 → 1.0.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,18 @@ module FeaturevisorCLI
19
20
 
20
21
  # Get project configuration
21
22
  config = get_config
22
- environments = config[:environments] || []
23
- segments_by_key = get_segments
24
-
25
- # Use CLI schemaVersion option or fallback to config
26
- schema_version = @options.schema_version
27
- if schema_version.nil? || schema_version.empty?
28
- schema_version = config[:schemaVersion]
23
+ environments = get_environments(config)
24
+ available_targets = get_targets
25
+ unknown_target = @options.targets.find { |target| !available_targets.include?(target) }
26
+ if unknown_target
27
+ puts "Unknown target \"#{unknown_target}\". Available targets: #{available_targets.empty? ? "none" : available_targets.join(", ")}."
28
+ exit 1
29
29
  end
30
+ targets = @options.targets.empty? ? available_targets : @options.targets
31
+ segments_by_key = get_segments
30
32
 
31
- # Build datafiles for all environments
32
- datafiles_by_environment = build_datafiles(environments, schema_version, @options.inflate)
33
+ # Build base and Target datafiles for all environments.
34
+ datafiles_by_key = build_datafiles(environments, targets, @options.inflate)
33
35
 
34
36
  puts ""
35
37
 
@@ -42,18 +44,15 @@ module FeaturevisorCLI
42
44
  return
43
45
  end
44
46
 
45
- # Create SDK instances for each environment
46
- sdk_instances_by_environment = create_sdk_instances(environments, datafiles_by_environment, level)
47
-
48
47
  # Run tests
49
- run_tests(tests, sdk_instances_by_environment, datafiles_by_environment, segments_by_key, level)
48
+ run_tests(tests, datafiles_by_key, segments_by_key, level, config)
50
49
  end
51
50
 
52
51
  private
53
52
 
54
53
  def get_config
55
54
  puts "Getting config..."
56
- command = "(cd #{@project_path} && npx featurevisor config --json)"
55
+ command = "(cd #{Shellwords.escape(@project_path)} && npx featurevisor config --json)"
57
56
  config_output = execute_command(command)
58
57
 
59
58
  begin
@@ -67,7 +66,7 @@ module FeaturevisorCLI
67
66
 
68
67
  def get_segments
69
68
  puts "Getting segments..."
70
- command = "(cd #{@project_path} && npx featurevisor list --segments --json)"
69
+ command = "(cd #{Shellwords.escape(@project_path)} && npx featurevisor list --segments --json)"
71
70
  segments_output = execute_command(command)
72
71
 
73
72
  begin
@@ -86,40 +85,105 @@ module FeaturevisorCLI
86
85
  end
87
86
  end
88
87
 
89
- def build_datafiles(environments, schema_version, inflate)
90
- datafiles_by_environment = {}
88
+ def get_targets
89
+ puts "Getting targets..."
90
+ command = "(cd #{Shellwords.escape(@project_path)} && npx featurevisor list --targets --json)"
91
+ targets_output = execute_command(command)
92
+
93
+ begin
94
+ targets = JSON.parse(targets_output, symbolize_names: true)
95
+
96
+ if targets.is_a?(Array)
97
+ targets.filter_map do |target|
98
+ if target.is_a?(Hash)
99
+ target[:name] || target[:key]
100
+ else
101
+ target
102
+ end
103
+ end
104
+ elsif targets.is_a?(Hash)
105
+ targets.keys
106
+ else
107
+ []
108
+ end
109
+ rescue JSON::ParserError => e
110
+ puts "Error: Failed to parse targets JSON: #{e.message}"
111
+ puts "Command output: #{targets_output}"
112
+ exit 1
113
+ end
114
+ end
115
+
116
+ def get_environments(config)
117
+ environments = config[:environments]
118
+
119
+ return [false] if environments == false
120
+ return environments if environments.is_a?(Array) && !environments.empty?
121
+
122
+ [false]
123
+ end
124
+
125
+ def base_datafile_key(environment)
126
+ environment == false ? false : environment
127
+ end
128
+
129
+ def target_datafile_key(environment, target)
130
+ if environment == false
131
+ "false-target-#{target}"
132
+ else
133
+ "#{environment}-target-#{target}"
134
+ end
135
+ end
136
+
137
+ def build_datafiles(environments, targets, inflate)
138
+ datafiles_by_key = {}
91
139
 
92
140
  environments.each do |environment|
93
- puts "Building datafile for environment: #{environment}..."
141
+ environment_label = environment == false ? "default (no environment)" : environment
142
+ puts "Building datafile for environment: #{environment_label}..."
94
143
 
95
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
144
+ datafiles_by_key[base_datafile_key(environment)] = build_single_datafile(
145
+ environment: environment,
146
+ inflate: inflate
147
+ )
96
148
 
97
- if schema_version && !schema_version.empty?
98
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{schema_version}", "--json"]
149
+ targets.each do |target|
150
+ puts "Building datafile for target: #{target}..."
151
+ datafiles_by_key[target_datafile_key(environment, target)] = build_single_datafile(
152
+ environment: environment,
153
+ inflate: inflate,
154
+ target: target
155
+ )
99
156
  end
157
+ end
100
158
 
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"]
106
- end
107
- end
159
+ datafiles_by_key
160
+ end
108
161
 
109
- command = command_parts.join(" ")
110
- datafile_output = execute_command(command)
162
+ def build_single_datafile(environment:, inflate:, target: nil)
163
+ command_parts = ["npx", "featurevisor", "build", "--json"]
111
164
 
112
- begin
113
- datafile = JSON.parse(datafile_output, symbolize_names: true)
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
165
+ if environment != false && !environment.nil?
166
+ command_parts << "--environment=#{Shellwords.escape(environment.to_s)}"
167
+ end
168
+
169
+ if inflate && inflate > 0
170
+ command_parts << "--inflate=#{inflate}"
171
+ end
172
+
173
+ if target
174
+ command_parts << "--target=#{Shellwords.escape(target.to_s)}"
120
175
  end
121
176
 
122
- datafiles_by_environment
177
+ command = "(cd #{Shellwords.escape(@project_path)} && #{command_parts.join(' ')})"
178
+ datafile_output = execute_command(command)
179
+
180
+ begin
181
+ JSON.parse(datafile_output, symbolize_names: true)
182
+ rescue JSON::ParserError => e
183
+ puts "Error: Failed to parse datafile JSON: #{e.message}"
184
+ puts "Command output: #{datafile_output}"
185
+ exit 1
186
+ end
123
187
  end
124
188
 
125
189
  def get_logger_level
@@ -133,17 +197,17 @@ module FeaturevisorCLI
133
197
  end
134
198
 
135
199
  def get_tests
136
- command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "list", "--tests", "--applyMatrix", "--json"]
200
+ command_parts = ["npx", "featurevisor", "list", "--tests", "--applyMatrix", "--json"]
137
201
 
138
202
  if @options.key_pattern && !@options.key_pattern.empty?
139
- command_parts << "--keyPattern=#{@options.key_pattern}"
203
+ command_parts << "--keyPattern=#{Shellwords.escape(@options.key_pattern)}"
140
204
  end
141
205
 
142
206
  if @options.assertion_pattern && !@options.assertion_pattern.empty?
143
- command_parts << "--assertionPattern=#{@options.assertion_pattern}"
207
+ command_parts << "--assertionPattern=#{Shellwords.escape(@options.assertion_pattern)}"
144
208
  end
145
209
 
146
- command = command_parts.join(" ")
210
+ command = "(cd #{Shellwords.escape(@project_path)} && #{command_parts.join(' ')})"
147
211
  tests_output = execute_command(command)
148
212
 
149
213
  begin
@@ -155,31 +219,44 @@ module FeaturevisorCLI
155
219
  end
156
220
  end
157
221
 
158
- def create_sdk_instances(environments, datafiles_by_environment, level)
159
- sdk_instances_by_environment = {}
222
+ def resolve_datafile_for_assertion(assertion, datafiles_by_key)
223
+ environment = assertion.key?(:environment) ? assertion[:environment] : false
224
+ environment = false if environment.nil?
160
225
 
161
- environments.each do |environment|
162
- datafile = datafiles_by_environment[environment]
163
-
164
- # Create SDK instance
165
- instance = Featurevisor.create_instance(
166
- datafile: datafile,
167
- log_level: level,
168
- hooks: [
169
- {
170
- name: "tester-hook",
171
- bucket_value: ->(options) { options.bucket_value }
172
- }
173
- ]
174
- )
226
+ target_key = assertion[:target] ? target_datafile_key(environment, assertion[:target]) : nil
227
+ base_key = base_datafile_key(environment)
175
228
 
176
- sdk_instances_by_environment[environment] = instance
229
+ if target_key && datafiles_by_key.key?(target_key)
230
+ return datafiles_by_key[target_key]
177
231
  end
178
232
 
179
- sdk_instances_by_environment
233
+ datafiles_by_key[base_key]
180
234
  end
181
235
 
182
- def run_tests(tests, sdk_instances_by_environment, datafiles_by_environment, segments_by_key, level)
236
+ def create_tester_instance(datafile, level, assertion)
237
+ sticky = parse_sticky(assertion[:sticky])
238
+
239
+ Featurevisor.create_featurevisor(
240
+ datafile: datafile,
241
+ sticky: sticky,
242
+ log_level: level,
243
+ modules: [
244
+ {
245
+ name: "test-module",
246
+ bucket_value: ->(options) do
247
+ at = assertion[:at]
248
+ if at.is_a?(Numeric)
249
+ (at * 1000).to_i
250
+ else
251
+ options[:bucket_value]
252
+ end
253
+ end
254
+ }
255
+ ]
256
+ )
257
+ end
258
+
259
+ def run_tests(tests, datafiles_by_key, segments_by_key, level, config)
183
260
  passed_tests_count = 0
184
261
  failed_tests_count = 0
185
262
  passed_assertions_count = 0
@@ -188,6 +265,12 @@ module FeaturevisorCLI
188
265
  tests.each do |test|
189
266
  test_key = test[:key]
190
267
  assertions = test[:assertions] || []
268
+ if test[:feature] && !@options.targets.empty?
269
+ assertions = assertions.select do |assertion|
270
+ assertion[:target].nil? || @options.targets.include?(assertion[:target])
271
+ end
272
+ next if assertions.empty?
273
+ end
191
274
  results = ""
192
275
  test_has_error = false
193
276
  test_duration = 0.0
@@ -197,43 +280,27 @@ module FeaturevisorCLI
197
280
  test_result = nil
198
281
 
199
282
  if test[:feature]
200
- environment = assertion[:environment]
201
- instance = sdk_instances_by_environment[environment]
202
-
203
- # Show datafile if requested
204
- if @options.show_datafile
205
- datafile = datafiles_by_environment[environment]
206
- puts ""
207
- puts JSON.pretty_generate(datafile)
208
- puts ""
283
+ datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
284
+ if datafile.nil?
285
+ test_result = {
286
+ has_error: true,
287
+ errors: " ✘ no datafile found for assertion target/environment combination\n",
288
+ duration: 0
289
+ }
209
290
  end
210
291
 
211
- # If "at" parameter is provided, create a new instance with the specific hook
212
- if assertion[:at]
213
- datafile = datafiles_by_environment[environment]
214
-
215
- instance = Featurevisor.create_instance(
216
- datafile: datafile,
217
- log_level: level,
218
- hooks: [
219
- {
220
- name: "tester-hook",
221
- bucket_value: ->(options) do
222
- # Match JavaScript implementation: assertion.at * (MAX_BUCKETED_NUMBER / 100)
223
- # MAX_BUCKETED_NUMBER is 100000, so this becomes assertion.at * 1000
224
- at = assertion[:at]
225
- if at.is_a?(Numeric)
226
- (at * 1000).to_i
227
- else
228
- options.bucket_value
229
- end
230
- end
231
- }
232
- ]
233
- )
234
- end
292
+ if datafile
293
+ instance = create_tester_instance(datafile, level, assertion)
235
294
 
236
- test_result = run_test_feature(assertion, test[:feature], instance, level)
295
+ # Show datafile if requested
296
+ if @options.show_datafile
297
+ puts ""
298
+ puts JSON.pretty_generate(datafile)
299
+ puts ""
300
+ end
301
+
302
+ test_result = run_test_feature(assertion, test[:feature], instance, level)
303
+ end
237
304
  elsif test[:segment]
238
305
  segment_key = test[:segment]
239
306
  segment = segments_by_key[segment_key]
@@ -325,7 +392,7 @@ module FeaturevisorCLI
325
392
  expected_variables = assertion[:expectedVariables]
326
393
  expected_variables.each do |variable_key, expected_value|
327
394
  # Set default variable value for this specific variable
328
- if assertion[:defaultVariableValues] && assertion[:defaultVariableValues][variable_key]
395
+ if assertion[:defaultVariableValues].is_a?(Hash) && assertion[:defaultVariableValues].key?(variable_key)
329
396
  override_options[:default_variable_value] = assertion[:defaultVariableValues][variable_key]
330
397
  end
331
398
 
@@ -483,7 +550,7 @@ module FeaturevisorCLI
483
550
  expected_variables = assertion[:expectedVariables]
484
551
  expected_variables.each do |variable_key, expected_value|
485
552
  # Set default variable value for this specific variable
486
- if assertion[:defaultVariableValues] && assertion[:defaultVariableValues][variable_key]
553
+ if assertion[:defaultVariableValues].is_a?(Hash) && assertion[:defaultVariableValues].key?(variable_key)
487
554
  override_options[:default_variable_value] = assertion[:defaultVariableValues][variable_key]
488
555
  end
489
556
 
@@ -522,6 +589,55 @@ module FeaturevisorCLI
522
589
  end
523
590
  end
524
591
 
592
+ # Test expectedEvaluations
593
+ if assertion[:expectedEvaluations]
594
+ expected_evaluations = assertion[:expectedEvaluations]
595
+
596
+ if expected_evaluations[:flag]
597
+ evaluation = evaluate_from_instance(instance, :flag, feature_key, context, override_options)
598
+ expected_evaluations[:flag].each do |key, expected_value|
599
+ actual_value = get_evaluation_value(evaluation, key)
600
+ if !compare_values(actual_value, expected_value)
601
+ has_error = true
602
+ errors += " ✘ expectedEvaluations.flag.#{key}: expected #{expected_value} but received #{actual_value}\n"
603
+ end
604
+ end
605
+ end
606
+
607
+ if expected_evaluations[:variation]
608
+ evaluation = evaluate_from_instance(instance, :variation, feature_key, context, override_options)
609
+ expected_evaluations[:variation].each do |key, expected_value|
610
+ actual_value = get_evaluation_value(evaluation, key)
611
+ if !compare_values(actual_value, expected_value)
612
+ has_error = true
613
+ errors += " ✘ expectedEvaluations.variation.#{key}: expected #{expected_value} but received #{actual_value}\n"
614
+ end
615
+ end
616
+ end
617
+
618
+ if expected_evaluations[:variables]
619
+ expected_evaluations[:variables].each do |variable_key, expected_eval|
620
+ if expected_eval.is_a?(Hash)
621
+ evaluation = evaluate_from_instance(
622
+ instance,
623
+ :variable,
624
+ feature_key,
625
+ context,
626
+ override_options,
627
+ variable_key
628
+ )
629
+ expected_eval.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.variables.#{variable_key}.#{key}: expected #{expected_value} but received #{actual_value}\n"
634
+ end
635
+ end
636
+ end
637
+ end
638
+ end
639
+ end
640
+
525
641
  duration = Time.now - start_time
526
642
 
527
643
  {
@@ -544,7 +660,7 @@ module FeaturevisorCLI
544
660
  }
545
661
 
546
662
  # Create SDK instance for segment testing
547
- instance = Featurevisor.create_instance(
663
+ instance = Featurevisor.create_featurevisor(
548
664
  datafile: datafile,
549
665
  log_level: level
550
666
  )
@@ -614,13 +730,45 @@ module FeaturevisorCLI
614
730
  def create_override_options(assertion)
615
731
  options = {}
616
732
 
617
- if assertion[:defaultVariationValue]
733
+ if assertion.key?(:defaultVariationValue)
618
734
  options[:default_variation_value] = assertion[:defaultVariationValue]
619
735
  end
620
736
 
621
737
  options
622
738
  end
623
739
 
740
+ def evaluate_from_instance(instance, type, feature_key, context, override_options, variable_key = nil)
741
+ method_name = :"evaluate_#{type}"
742
+
743
+ if instance.respond_to?(method_name)
744
+ if variable_key.nil?
745
+ return instance.send(method_name, feature_key, context, override_options)
746
+ end
747
+
748
+ return instance.send(method_name, feature_key, variable_key, context, override_options)
749
+ end
750
+
751
+ if instance.is_a?(Featurevisor::ChildInstance)
752
+ parent = instance.instance_variable_get(:@parent)
753
+ child_context = instance.instance_variable_get(:@context) || {}
754
+ child_sticky = instance.instance_variable_get(:@sticky)
755
+ combined_context = { **child_context, **context }
756
+
757
+ combined_options = {
758
+ __featurevisor_child_sticky: child_sticky,
759
+ **override_options
760
+ }
761
+
762
+ if variable_key.nil?
763
+ return parent.send(method_name, feature_key, combined_context, combined_options)
764
+ end
765
+
766
+ return parent.send(method_name, feature_key, variable_key, combined_context, combined_options)
767
+ end
768
+
769
+ {}
770
+ end
771
+
624
772
  def get_evaluation_value(evaluation, key)
625
773
  case key
626
774
  when :type
@@ -659,6 +807,8 @@ module FeaturevisorCLI
659
807
  evaluation[:variable_value]
660
808
  when :variableSchema
661
809
  evaluation[:variable_schema]
810
+ when :variableOverrideIndex
811
+ evaluation[:variable_override_index]
662
812
  else
663
813
  nil
664
814
  end
@@ -3,8 +3,6 @@
3
3
  module Featurevisor
4
4
  # Child instance class for managing child contexts and sticky features
5
5
  class ChildInstance
6
- attr_reader :parent, :context, :sticky, :emitter
7
-
8
6
  # Initialize a new child instance
9
7
  # @param options [Hash] Child instance options
10
8
  # @option options [Instance] :parent Parent instance
@@ -94,8 +92,8 @@ module Featurevisor
94
92
  **context
95
93
  },
96
94
  {
97
- sticky: @sticky,
98
- **options
95
+ **options,
96
+ __featurevisor_child_sticky: @sticky
99
97
  }
100
98
  )
101
99
  end
@@ -113,8 +111,8 @@ module Featurevisor
113
111
  **context
114
112
  },
115
113
  {
116
- sticky: @sticky,
117
- **options
114
+ **options,
115
+ __featurevisor_child_sticky: @sticky
118
116
  }
119
117
  )
120
118
  end
@@ -134,8 +132,8 @@ module Featurevisor
134
132
  **context
135
133
  },
136
134
  {
137
- sticky: @sticky,
138
- **options
135
+ **options,
136
+ __featurevisor_child_sticky: @sticky
139
137
  }
140
138
  )
141
139
  end
@@ -155,8 +153,8 @@ module Featurevisor
155
153
  **context
156
154
  },
157
155
  {
158
- sticky: @sticky,
159
- **options
156
+ **options,
157
+ __featurevisor_child_sticky: @sticky
160
158
  }
161
159
  )
162
160
  end
@@ -176,8 +174,8 @@ module Featurevisor
176
174
  **context
177
175
  },
178
176
  {
179
- sticky: @sticky,
180
- **options
177
+ **options,
178
+ __featurevisor_child_sticky: @sticky
181
179
  }
182
180
  )
183
181
  end
@@ -197,8 +195,8 @@ module Featurevisor
197
195
  **context
198
196
  },
199
197
  {
200
- sticky: @sticky,
201
- **options
198
+ **options,
199
+ __featurevisor_child_sticky: @sticky
202
200
  }
203
201
  )
204
202
  end
@@ -218,8 +216,8 @@ module Featurevisor
218
216
  **context
219
217
  },
220
218
  {
221
- sticky: @sticky,
222
- **options
219
+ **options,
220
+ __featurevisor_child_sticky: @sticky
223
221
  }
224
222
  )
225
223
  end
@@ -239,8 +237,8 @@ module Featurevisor
239
237
  **context
240
238
  },
241
239
  {
242
- sticky: @sticky,
243
- **options
240
+ **options,
241
+ __featurevisor_child_sticky: @sticky
244
242
  }
245
243
  )
246
244
  end
@@ -260,8 +258,8 @@ module Featurevisor
260
258
  **context
261
259
  },
262
260
  {
263
- sticky: @sticky,
264
- **options
261
+ **options,
262
+ __featurevisor_child_sticky: @sticky
265
263
  }
266
264
  )
267
265
  end
@@ -281,8 +279,8 @@ module Featurevisor
281
279
  **context
282
280
  },
283
281
  {
284
- sticky: @sticky,
285
- **options
282
+ **options,
283
+ __featurevisor_child_sticky: @sticky
286
284
  }
287
285
  )
288
286
  end
@@ -300,8 +298,8 @@ module Featurevisor
300
298
  },
301
299
  feature_keys,
302
300
  {
303
- sticky: @sticky,
304
- **options
301
+ **options,
302
+ __featurevisor_child_sticky: @sticky
305
303
  }
306
304
  )
307
305
  end