featurevisor 0.3.0 → 1.1.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 +234 -88
- data/bin/cli.rb +9 -4
- data/bin/commands/assess_distribution.rb +34 -15
- data/bin/commands/benchmark.rb +65 -63
- data/bin/commands/test.rb +74 -96
- data/lib/featurevisor/child_instance.rb +22 -24
- data/lib/featurevisor/datafile_reader.rb +18 -13
- data/lib/featurevisor/emitter.rb +2 -2
- data/lib/featurevisor/evaluate.rb +16 -23
- data/lib/featurevisor/events.rb +5 -4
- data/lib/featurevisor/instance.rb +225 -39
- data/lib/featurevisor/logger.rb +0 -7
- data/lib/featurevisor/modules.rb +184 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +4 -1
- metadata +28 -3
- data/lib/featurevisor/hooks.rb +0 -159
|
@@ -29,13 +29,21 @@ module FeaturevisorCLI
|
|
|
29
29
|
exit 1
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
puts ""
|
|
33
|
-
puts "Assessing distribution for feature: \"#{@options.feature}\"..."
|
|
34
|
-
puts ""
|
|
35
|
-
|
|
36
|
-
# Parse context if provided
|
|
37
32
|
context = parse_context
|
|
38
33
|
|
|
34
|
+
targets = resolve_targets
|
|
35
|
+
(targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run_for_target(context, target)
|
|
41
|
+
puts "\nAssess Featurevisor distribution"
|
|
42
|
+
puts " Feature: #{@options.feature}"
|
|
43
|
+
puts " Environment: #{@options.environment}"
|
|
44
|
+
puts " Target: #{target}" if target
|
|
45
|
+
puts " Iterations: #{@options.n}"
|
|
46
|
+
|
|
39
47
|
# Print context information
|
|
40
48
|
if @options.context
|
|
41
49
|
puts "Against context: #{@options.context}"
|
|
@@ -47,10 +55,10 @@ module FeaturevisorCLI
|
|
|
47
55
|
puts ""
|
|
48
56
|
|
|
49
57
|
# Build datafile
|
|
50
|
-
datafile = build_datafile(@options.environment)
|
|
58
|
+
datafile = build_datafile(@options.environment, target)
|
|
51
59
|
|
|
52
60
|
# Create SDK instance
|
|
53
|
-
instance =
|
|
61
|
+
instance = create_featurevisor(datafile)
|
|
54
62
|
|
|
55
63
|
# Check if feature has variations
|
|
56
64
|
feature = instance.get_feature(@options.feature)
|
|
@@ -104,7 +112,21 @@ module FeaturevisorCLI
|
|
|
104
112
|
end
|
|
105
113
|
end
|
|
106
114
|
|
|
107
|
-
|
|
115
|
+
def resolve_targets
|
|
116
|
+
return [] if @options.targets.empty?
|
|
117
|
+
stdout, stderr, status = Open3.capture3("npx", "featurevisor", "list", "--targets", "--json", chdir: @project_path)
|
|
118
|
+
unless status.success?
|
|
119
|
+
puts stderr
|
|
120
|
+
exit 1
|
|
121
|
+
end
|
|
122
|
+
available = JSON.parse(stdout).map { |item| item.is_a?(Hash) ? (item["key"] || item["name"]) : item }
|
|
123
|
+
unknown = @options.targets.find { |target| !available.include?(target) }
|
|
124
|
+
if unknown
|
|
125
|
+
puts "Unknown target \"#{unknown}\". Available targets: #{available.empty? ? "none" : available.join(", ")}."
|
|
126
|
+
exit 1
|
|
127
|
+
end
|
|
128
|
+
@options.targets
|
|
129
|
+
end
|
|
108
130
|
|
|
109
131
|
def parse_context
|
|
110
132
|
if @options.context
|
|
@@ -121,15 +143,12 @@ module FeaturevisorCLI
|
|
|
121
143
|
end
|
|
122
144
|
end
|
|
123
145
|
|
|
124
|
-
def build_datafile(environment)
|
|
146
|
+
def build_datafile(environment, target = nil)
|
|
125
147
|
puts "Building datafile for environment: #{environment}..."
|
|
126
148
|
|
|
127
149
|
# Build the command similar to Go implementation
|
|
128
150
|
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
|
|
129
|
-
|
|
130
|
-
if @options.schema_version
|
|
131
|
-
command_parts << "--schemaVersion=#{@options.schema_version}"
|
|
132
|
-
end
|
|
151
|
+
command_parts << "--target=#{target}" if target
|
|
133
152
|
|
|
134
153
|
if @options.inflate
|
|
135
154
|
command_parts << "--inflate=#{@options.inflate}"
|
|
@@ -159,9 +178,9 @@ module FeaturevisorCLI
|
|
|
159
178
|
[stdout, stderr, exit_status.exitstatus]
|
|
160
179
|
end
|
|
161
180
|
|
|
162
|
-
def
|
|
181
|
+
def create_featurevisor(datafile)
|
|
163
182
|
# Create SDK instance
|
|
164
|
-
Featurevisor.
|
|
183
|
+
Featurevisor.create_featurevisor(
|
|
165
184
|
datafile: datafile,
|
|
166
185
|
log_level: get_logger_level
|
|
167
186
|
)
|
data/bin/commands/benchmark.rb
CHANGED
|
@@ -27,29 +27,32 @@ module FeaturevisorCLI
|
|
|
27
27
|
exit 1
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
puts ""
|
|
31
|
-
puts "Running benchmark for feature \"#{@options.feature}\"..."
|
|
32
|
-
puts ""
|
|
33
|
-
|
|
34
|
-
# Parse context if provided
|
|
35
30
|
context = parse_context
|
|
31
|
+
targets = resolve_targets
|
|
32
|
+
(targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
|
|
33
|
+
end
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
datafile_build_start = Time.now
|
|
35
|
+
private
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
def run_for_target(context, target)
|
|
38
|
+
datafile_build_start = Time.now
|
|
39
|
+
datafile = build_datafile(@options.environment, target)
|
|
42
40
|
datafile_build_duration = Time.now - datafile_build_start
|
|
43
41
|
datafile_build_duration_ms = (datafile_build_duration * 1000).round
|
|
44
42
|
|
|
45
|
-
puts "
|
|
43
|
+
puts "\nBenchmark Featurevisor feature"
|
|
44
|
+
puts " Feature: #{@options.feature}"
|
|
45
|
+
puts " Environment: #{@options.environment}"
|
|
46
|
+
puts " Target: #{target}" if target
|
|
47
|
+
puts " Iterations: #{@options.n}"
|
|
48
|
+
puts " Build duration: #{datafile_build_duration_ms}ms"
|
|
46
49
|
|
|
47
50
|
# Calculate datafile size
|
|
48
51
|
datafile_size = datafile.to_json.bytesize
|
|
49
52
|
puts "Datafile size: #{(datafile_size / 1024.0).round(2)} kB"
|
|
50
53
|
|
|
51
54
|
# Create SDK instance with the datafile
|
|
52
|
-
instance =
|
|
55
|
+
instance = create_featurevisor(datafile)
|
|
53
56
|
puts "...SDK initialized"
|
|
54
57
|
|
|
55
58
|
puts ""
|
|
@@ -73,10 +76,26 @@ module FeaturevisorCLI
|
|
|
73
76
|
value_output = format_value(output[:value])
|
|
74
77
|
puts "Evaluated value : #{value_output}"
|
|
75
78
|
puts "Total duration : #{pretty_duration(output[:duration])}"
|
|
76
|
-
puts "
|
|
79
|
+
puts "Minimum duration: #{format_duration_ms(output[:min_duration])}"
|
|
80
|
+
puts "Average duration: #{format_duration_ms(output[:average_duration])}"
|
|
81
|
+
puts "Maximum duration: #{format_duration_ms(output[:max_duration])}"
|
|
77
82
|
end
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
def resolve_targets
|
|
85
|
+
return [] if @options.targets.empty?
|
|
86
|
+
stdout, stderr, status = Open3.capture3("npx", "featurevisor", "list", "--targets", "--json", chdir: @project_path)
|
|
87
|
+
unless status.success?
|
|
88
|
+
puts stderr
|
|
89
|
+
exit 1
|
|
90
|
+
end
|
|
91
|
+
available = JSON.parse(stdout).map { |target| target.is_a?(Hash) ? (target["key"] || target["name"]) : target }
|
|
92
|
+
unknown = @options.targets.find { |target| !available.include?(target) }
|
|
93
|
+
if unknown
|
|
94
|
+
puts "Unknown target \"#{unknown}\". Available targets: #{available.empty? ? "none" : available.join(", ")}."
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
@options.targets
|
|
98
|
+
end
|
|
80
99
|
|
|
81
100
|
def parse_context
|
|
82
101
|
if @options.context
|
|
@@ -93,24 +112,15 @@ module FeaturevisorCLI
|
|
|
93
112
|
end
|
|
94
113
|
end
|
|
95
114
|
|
|
96
|
-
def build_datafile(environment)
|
|
115
|
+
def build_datafile(environment, target = nil)
|
|
97
116
|
puts "Building datafile for environment: #{environment}..."
|
|
98
117
|
|
|
99
|
-
# Build the command similar to Go implementation
|
|
100
118
|
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
|
|
101
|
-
|
|
102
|
-
# Add schema version if specified
|
|
103
|
-
if @options.schema_version && !@options.schema_version.empty?
|
|
104
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{@options.schema_version}", "--json"]
|
|
105
|
-
end
|
|
119
|
+
command_parts << "--target=#{target}" if target
|
|
106
120
|
|
|
107
121
|
# Add inflate if specified
|
|
108
122
|
if @options.inflate && @options.inflate > 0
|
|
109
|
-
|
|
110
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--schemaVersion=#{@options.schema_version}", "--inflate=#{@options.inflate}", "--json"]
|
|
111
|
-
else
|
|
112
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--inflate=#{@options.inflate}", "--json"]
|
|
113
|
-
end
|
|
123
|
+
command_parts << "--inflate=#{@options.inflate}"
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
command = command_parts.join(" ")
|
|
@@ -142,9 +152,9 @@ module FeaturevisorCLI
|
|
|
142
152
|
stdout
|
|
143
153
|
end
|
|
144
154
|
|
|
145
|
-
def
|
|
155
|
+
def create_featurevisor(datafile)
|
|
146
156
|
# Create a real Featurevisor instance
|
|
147
|
-
instance = Featurevisor.
|
|
157
|
+
instance = Featurevisor.create_featurevisor(
|
|
148
158
|
log_level: get_logger_level
|
|
149
159
|
)
|
|
150
160
|
|
|
@@ -164,61 +174,53 @@ module FeaturevisorCLI
|
|
|
164
174
|
end
|
|
165
175
|
end
|
|
166
176
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
def benchmark_evaluation(n)
|
|
178
|
+
value = nil
|
|
179
|
+
total_duration_ns = 0
|
|
180
|
+
min_duration_ns = nil
|
|
181
|
+
max_duration_ns = 0
|
|
172
182
|
|
|
173
|
-
# Benchmark the evaluation
|
|
174
183
|
n.times do
|
|
175
|
-
|
|
184
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
185
|
+
value = yield
|
|
186
|
+
duration_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start_time
|
|
187
|
+
|
|
188
|
+
total_duration_ns += duration_ns
|
|
189
|
+
min_duration_ns = duration_ns if min_duration_ns.nil? || duration_ns < min_duration_ns
|
|
190
|
+
max_duration_ns = duration_ns if duration_ns > max_duration_ns
|
|
176
191
|
end
|
|
177
192
|
|
|
178
|
-
duration =
|
|
193
|
+
duration = total_duration_ns / 1_000_000_000.0
|
|
179
194
|
|
|
180
195
|
{
|
|
181
196
|
value: value,
|
|
182
|
-
duration: duration
|
|
197
|
+
duration: duration,
|
|
198
|
+
min_duration: (min_duration_ns || 0) / 1_000_000_000.0,
|
|
199
|
+
average_duration: duration / n,
|
|
200
|
+
max_duration: max_duration_ns / 1_000_000_000.0
|
|
183
201
|
}
|
|
184
202
|
end
|
|
185
203
|
|
|
186
|
-
def
|
|
187
|
-
|
|
204
|
+
def format_duration_ms(duration)
|
|
205
|
+
format("%.6fms", duration * 1000)
|
|
206
|
+
end
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
|
|
208
|
+
def benchmark_feature_flag(instance, feature_key, context, n)
|
|
209
|
+
benchmark_evaluation(n) do
|
|
210
|
+
instance.is_enabled(feature_key, context)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
n
|
|
214
|
+
def benchmark_feature_variation(instance, feature_key, context, n)
|
|
215
|
+
benchmark_evaluation(n) do
|
|
194
216
|
instance.get_variation(feature_key, context)
|
|
195
217
|
end
|
|
196
|
-
|
|
197
|
-
duration = Time.now - start_time
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
value: value,
|
|
201
|
-
duration: duration
|
|
202
|
-
}
|
|
203
218
|
end
|
|
204
219
|
|
|
205
220
|
def benchmark_feature_variable(instance, feature_key, variable_key, context, n)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# Get the actual variable value from the SDK
|
|
209
|
-
value = instance.get_variable(feature_key, variable_key, context)
|
|
210
|
-
|
|
211
|
-
# Benchmark the evaluation
|
|
212
|
-
n.times do
|
|
221
|
+
benchmark_evaluation(n) do
|
|
213
222
|
instance.get_variable(feature_key, variable_key, context)
|
|
214
223
|
end
|
|
215
|
-
|
|
216
|
-
duration = Time.now - start_time
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
value: value,
|
|
220
|
-
duration: duration
|
|
221
|
-
}
|
|
222
224
|
end
|
|
223
225
|
|
|
224
226
|
def format_value(value)
|
data/bin/commands/test.rb
CHANGED
|
@@ -21,16 +21,17 @@ module FeaturevisorCLI
|
|
|
21
21
|
# Get project configuration
|
|
22
22
|
config = get_config
|
|
23
23
|
environments = get_environments(config)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
schema_version = config[:schemaVersion]
|
|
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
|
|
30
29
|
end
|
|
30
|
+
targets = @options.targets.empty? ? available_targets : @options.targets
|
|
31
|
+
segments_by_key = get_segments
|
|
31
32
|
|
|
32
|
-
# Build datafiles for all environments
|
|
33
|
-
datafiles_by_key = build_datafiles(
|
|
33
|
+
# Build base and Target datafiles for all environments.
|
|
34
|
+
datafiles_by_key = build_datafiles(environments, targets, @options.inflate)
|
|
34
35
|
|
|
35
36
|
puts ""
|
|
36
37
|
|
|
@@ -84,6 +85,34 @@ module FeaturevisorCLI
|
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
|
|
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
|
+
|
|
87
116
|
def get_environments(config)
|
|
88
117
|
environments = config[:environments]
|
|
89
118
|
|
|
@@ -97,23 +126,15 @@ module FeaturevisorCLI
|
|
|
97
126
|
environment == false ? false : environment
|
|
98
127
|
end
|
|
99
128
|
|
|
100
|
-
def
|
|
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)
|
|
129
|
+
def target_datafile_key(environment, target)
|
|
109
130
|
if environment == false
|
|
110
|
-
"
|
|
131
|
+
"false-target-#{target}"
|
|
111
132
|
else
|
|
112
|
-
"#{environment}-
|
|
133
|
+
"#{environment}-target-#{target}"
|
|
113
134
|
end
|
|
114
135
|
end
|
|
115
136
|
|
|
116
|
-
def build_datafiles(
|
|
137
|
+
def build_datafiles(environments, targets, inflate)
|
|
117
138
|
datafiles_by_key = {}
|
|
118
139
|
|
|
119
140
|
environments.each do |environment|
|
|
@@ -122,61 +143,35 @@ module FeaturevisorCLI
|
|
|
122
143
|
|
|
123
144
|
datafiles_by_key[base_datafile_key(environment)] = build_single_datafile(
|
|
124
145
|
environment: environment,
|
|
125
|
-
schema_version: schema_version,
|
|
126
146
|
inflate: inflate
|
|
127
147
|
)
|
|
128
148
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
schema_version: schema_version,
|
|
137
|
-
inflate: inflate,
|
|
138
|
-
scope: scope[:name]
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
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
|
-
)
|
|
152
|
-
end
|
|
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
|
+
)
|
|
153
156
|
end
|
|
154
157
|
end
|
|
155
158
|
|
|
156
159
|
datafiles_by_key
|
|
157
160
|
end
|
|
158
161
|
|
|
159
|
-
def build_single_datafile(environment:,
|
|
162
|
+
def build_single_datafile(environment:, inflate:, target: nil)
|
|
160
163
|
command_parts = ["npx", "featurevisor", "build", "--json"]
|
|
161
164
|
|
|
162
165
|
if environment != false && !environment.nil?
|
|
163
166
|
command_parts << "--environment=#{Shellwords.escape(environment.to_s)}"
|
|
164
167
|
end
|
|
165
168
|
|
|
166
|
-
if schema_version && !schema_version.empty?
|
|
167
|
-
command_parts << "--schemaVersion=#{Shellwords.escape(schema_version.to_s)}"
|
|
168
|
-
end
|
|
169
|
-
|
|
170
169
|
if inflate && inflate > 0
|
|
171
170
|
command_parts << "--inflate=#{inflate}"
|
|
172
171
|
end
|
|
173
172
|
|
|
174
|
-
if
|
|
175
|
-
command_parts << "--
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
if tag
|
|
179
|
-
command_parts << "--tag=#{Shellwords.escape(tag.to_s)}"
|
|
173
|
+
if target
|
|
174
|
+
command_parts << "--target=#{Shellwords.escape(target.to_s)}"
|
|
180
175
|
end
|
|
181
176
|
|
|
182
177
|
command = "(cd #{Shellwords.escape(@project_path)} && #{command_parts.join(' ')})"
|
|
@@ -224,29 +219,15 @@ module FeaturevisorCLI
|
|
|
224
219
|
end
|
|
225
220
|
end
|
|
226
221
|
|
|
227
|
-
def get_scope_context(config, scope_name)
|
|
228
|
-
return {} unless scope_name && config[:scopes].is_a?(Array)
|
|
229
|
-
|
|
230
|
-
scope = config[:scopes].find { |s| s[:name] == scope_name }
|
|
231
|
-
return {} unless scope && scope[:context].is_a?(Hash)
|
|
232
|
-
|
|
233
|
-
parse_context(scope[:context])
|
|
234
|
-
end
|
|
235
|
-
|
|
236
222
|
def resolve_datafile_for_assertion(assertion, datafiles_by_key)
|
|
237
223
|
environment = assertion.key?(:environment) ? assertion[:environment] : false
|
|
238
224
|
environment = false if environment.nil?
|
|
239
225
|
|
|
240
|
-
|
|
241
|
-
tagged_key = assertion[:tag] ? tagged_datafile_key(environment, assertion[:tag]) : nil
|
|
226
|
+
target_key = assertion[:target] ? target_datafile_key(environment, assertion[:target]) : nil
|
|
242
227
|
base_key = base_datafile_key(environment)
|
|
243
228
|
|
|
244
|
-
if
|
|
245
|
-
return datafiles_by_key[
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
if tagged_key && datafiles_by_key.key?(tagged_key)
|
|
249
|
-
return datafiles_by_key[tagged_key]
|
|
229
|
+
if target_key && datafiles_by_key.key?(target_key)
|
|
230
|
+
return datafiles_by_key[target_key]
|
|
250
231
|
end
|
|
251
232
|
|
|
252
233
|
datafiles_by_key[base_key]
|
|
@@ -255,19 +236,19 @@ module FeaturevisorCLI
|
|
|
255
236
|
def create_tester_instance(datafile, level, assertion)
|
|
256
237
|
sticky = parse_sticky(assertion[:sticky])
|
|
257
238
|
|
|
258
|
-
Featurevisor.
|
|
239
|
+
Featurevisor.create_featurevisor(
|
|
259
240
|
datafile: datafile,
|
|
260
241
|
sticky: sticky,
|
|
261
242
|
log_level: level,
|
|
262
|
-
|
|
243
|
+
modules: [
|
|
263
244
|
{
|
|
264
|
-
name: "
|
|
245
|
+
name: "test-module",
|
|
265
246
|
bucket_value: ->(options) do
|
|
266
247
|
at = assertion[:at]
|
|
267
248
|
if at.is_a?(Numeric)
|
|
268
249
|
(at * 1000).to_i
|
|
269
250
|
else
|
|
270
|
-
options
|
|
251
|
+
options[:bucket_value]
|
|
271
252
|
end
|
|
272
253
|
end
|
|
273
254
|
}
|
|
@@ -284,6 +265,12 @@ module FeaturevisorCLI
|
|
|
284
265
|
tests.each do |test|
|
|
285
266
|
test_key = test[:key]
|
|
286
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
|
|
287
274
|
results = ""
|
|
288
275
|
test_has_error = false
|
|
289
276
|
test_duration = 0.0
|
|
@@ -297,19 +284,13 @@ module FeaturevisorCLI
|
|
|
297
284
|
if datafile.nil?
|
|
298
285
|
test_result = {
|
|
299
286
|
has_error: true,
|
|
300
|
-
errors: " ✘ no datafile found for assertion
|
|
287
|
+
errors: " ✘ no datafile found for assertion target/environment combination\n",
|
|
301
288
|
duration: 0
|
|
302
289
|
}
|
|
303
290
|
end
|
|
304
291
|
|
|
305
292
|
if datafile
|
|
306
293
|
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
|
|
313
294
|
|
|
314
295
|
# Show datafile if requested
|
|
315
296
|
if @options.show_datafile
|
|
@@ -318,7 +299,7 @@ module FeaturevisorCLI
|
|
|
318
299
|
puts ""
|
|
319
300
|
end
|
|
320
301
|
|
|
321
|
-
test_result = run_test_feature(assertion, test[:feature], instance, level
|
|
302
|
+
test_result = run_test_feature(assertion, test[:feature], instance, level)
|
|
322
303
|
end
|
|
323
304
|
elsif test[:segment]
|
|
324
305
|
segment_key = test[:segment]
|
|
@@ -366,9 +347,8 @@ module FeaturevisorCLI
|
|
|
366
347
|
end
|
|
367
348
|
end
|
|
368
349
|
|
|
369
|
-
def run_test_feature(assertion, feature_key, instance, level
|
|
350
|
+
def run_test_feature(assertion, feature_key, instance, level)
|
|
370
351
|
context = parse_context(assertion[:context])
|
|
371
|
-
context = { **scope_context, **context } if scope_context && !scope_context.empty?
|
|
372
352
|
sticky = parse_sticky(assertion[:sticky])
|
|
373
353
|
|
|
374
354
|
# Set context and sticky for this assertion
|
|
@@ -680,7 +660,7 @@ module FeaturevisorCLI
|
|
|
680
660
|
}
|
|
681
661
|
|
|
682
662
|
# Create SDK instance for segment testing
|
|
683
|
-
instance = Featurevisor.
|
|
663
|
+
instance = Featurevisor.create_featurevisor(
|
|
684
664
|
datafile: datafile,
|
|
685
665
|
log_level: level
|
|
686
666
|
)
|
|
@@ -768,16 +748,14 @@ module FeaturevisorCLI
|
|
|
768
748
|
return instance.send(method_name, feature_key, variable_key, context, override_options)
|
|
769
749
|
end
|
|
770
750
|
|
|
771
|
-
if instance.
|
|
772
|
-
parent = instance.parent
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
context
|
|
777
|
-
end
|
|
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 }
|
|
778
756
|
|
|
779
757
|
combined_options = {
|
|
780
|
-
|
|
758
|
+
__featurevisor_child_sticky: child_sticky,
|
|
781
759
|
**override_options
|
|
782
760
|
}
|
|
783
761
|
|