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.
- checksums.yaml +4 -4
- data/README.md +49 -5
- data/bin/cli.rb +9 -1
- data/bin/commands/assess_distribution.rb +6 -20
- data/bin/commands/benchmark.rb +2 -16
- data/bin/commands/test.rb +335 -176
- data/lib/featurevisor/conditions.rb +2 -2
- data/lib/featurevisor/datafile_reader.rb +4 -0
- data/lib/featurevisor/evaluate.rb +90 -77
- data/lib/featurevisor/instance.rb +2 -2
- data/lib/featurevisor/version.rb +1 -1
- metadata +1 -1
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
|
|
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[
|
|
29
|
+
schema_version = config[:schemaVersion]
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
# Build datafiles for all environments
|
|
32
|
-
|
|
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,
|
|
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[
|
|
78
|
-
segments_by_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
|
|
90
|
-
|
|
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
|
-
|
|
120
|
+
environment_label = environment == false ? "default (no environment)" : environment
|
|
121
|
+
puts "Building datafile for environment: #{environment_label}..."
|
|
94
122
|
|
|
95
|
-
|
|
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
|
|
98
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
156
|
+
datafiles_by_key
|
|
157
|
+
end
|
|
111
158
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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 = ["
|
|
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
|
|
159
|
-
|
|
227
|
+
def get_scope_context(config, scope_name)
|
|
228
|
+
return {} unless scope_name && config[:scopes].is_a?(Array)
|
|
160
229
|
|
|
161
|
-
|
|
162
|
-
|
|
230
|
+
scope = config[:scopes].find { |s| s[:name] == scope_name }
|
|
231
|
+
return {} unless scope && scope[:context].is_a?(Hash)
|
|
163
232
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
248
|
+
if tagged_key && datafiles_by_key.key?(tagged_key)
|
|
249
|
+
return datafiles_by_key[tagged_key]
|
|
180
250
|
end
|
|
181
251
|
|
|
182
|
-
|
|
252
|
+
datafiles_by_key[base_key]
|
|
183
253
|
end
|
|
184
254
|
|
|
185
|
-
def
|
|
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[
|
|
193
|
-
assertions = test[
|
|
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[
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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[
|
|
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[
|
|
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[
|
|
289
|
-
|
|
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?(
|
|
306
|
-
expected_to_be_enabled = assertion[
|
|
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?(
|
|
317
|
-
expected_variation = assertion[
|
|
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[
|
|
329
|
-
expected_variables = assertion[
|
|
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[
|
|
333
|
-
override_options[:default_variable_value] = assertion[
|
|
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[
|
|
373
|
-
expected_evaluations = assertion[
|
|
455
|
+
if assertion[:expectedEvaluations]
|
|
456
|
+
expected_evaluations = assertion[:expectedEvaluations]
|
|
374
457
|
|
|
375
458
|
# Test flag evaluations
|
|
376
|
-
if expected_evaluations[
|
|
459
|
+
if expected_evaluations[:flag]
|
|
377
460
|
evaluation = instance.evaluate_flag(feature_key, context, override_options)
|
|
378
|
-
expected_evaluations[
|
|
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[
|
|
471
|
+
if expected_evaluations[:variation]
|
|
389
472
|
evaluation = instance.evaluate_variation(feature_key, context, override_options)
|
|
390
|
-
expected_evaluations[
|
|
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[
|
|
401
|
-
expected_evaluations[
|
|
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[
|
|
418
|
-
assertion[
|
|
500
|
+
if assertion[:children]
|
|
501
|
+
assertion[:children].each do |child|
|
|
419
502
|
if child.is_a?(Hash)
|
|
420
|
-
child_context = parse_context(child[
|
|
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[
|
|
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?(
|
|
464
|
-
expected_to_be_enabled = assertion[
|
|
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?(
|
|
475
|
-
expected_variation = assertion[
|
|
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[
|
|
487
|
-
expected_variables = assertion[
|
|
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[
|
|
491
|
-
override_options[:default_variable_value] = assertion[
|
|
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[
|
|
540
|
-
conditions = segment[
|
|
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:
|
|
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?(
|
|
561
|
-
expected_to_match = assertion[
|
|
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?(
|
|
597
|
-
evaluated_feature[:enabled] = value[
|
|
728
|
+
if value.key?(:enabled)
|
|
729
|
+
evaluated_feature[:enabled] = value[:enabled]
|
|
598
730
|
end
|
|
599
731
|
|
|
600
|
-
if value.key?(
|
|
601
|
-
evaluated_feature[:variation] = value[
|
|
732
|
+
if value.key?(:variation)
|
|
733
|
+
evaluated_feature[:variation] = value[:variation]
|
|
602
734
|
end
|
|
603
735
|
|
|
604
|
-
if value[
|
|
605
|
-
evaluated_feature[:variables] = value[
|
|
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
|
|
622
|
-
options[:default_variation_value] = assertion[
|
|
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
|
|
796
|
+
when :type
|
|
631
797
|
evaluation[:type]
|
|
632
|
-
when
|
|
798
|
+
when :featureKey
|
|
633
799
|
evaluation[:feature_key]
|
|
634
|
-
when
|
|
800
|
+
when :reason
|
|
635
801
|
evaluation[:reason]
|
|
636
|
-
when
|
|
802
|
+
when :bucketKey
|
|
637
803
|
evaluation[:bucket_key]
|
|
638
|
-
when
|
|
804
|
+
when :bucketValue
|
|
639
805
|
evaluation[:bucket_value]
|
|
640
|
-
when
|
|
806
|
+
when :ruleKey
|
|
641
807
|
evaluation[:rule_key]
|
|
642
|
-
when
|
|
808
|
+
when :error
|
|
643
809
|
evaluation[:error]
|
|
644
|
-
when
|
|
810
|
+
when :enabled
|
|
645
811
|
evaluation[:enabled]
|
|
646
|
-
when
|
|
812
|
+
when :traffic
|
|
647
813
|
evaluation[:traffic]
|
|
648
|
-
when
|
|
814
|
+
when :forceIndex
|
|
649
815
|
evaluation[:force_index]
|
|
650
|
-
when
|
|
816
|
+
when :force
|
|
651
817
|
evaluation[:force]
|
|
652
|
-
when
|
|
818
|
+
when :required
|
|
653
819
|
evaluation[:required]
|
|
654
|
-
when
|
|
820
|
+
when :sticky
|
|
655
821
|
evaluation[:sticky]
|
|
656
|
-
when
|
|
822
|
+
when :variation
|
|
657
823
|
evaluation[:variation]
|
|
658
|
-
when
|
|
824
|
+
when :variationValue
|
|
659
825
|
evaluation[:variation_value]
|
|
660
|
-
when
|
|
826
|
+
when :variableKey
|
|
661
827
|
evaluation[:variable_key]
|
|
662
|
-
when
|
|
828
|
+
when :variableValue
|
|
663
829
|
evaluation[:variable_value]
|
|
664
|
-
when
|
|
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
|
-
|
|
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)
|