featurevisor 0.2.0 → 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 +25 -3
- data/bin/cli.rb +9 -1
- data/bin/commands/test.rb +264 -92
- data/lib/featurevisor/evaluate.rb +90 -77
- data/lib/featurevisor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42c8ec3e5bf6293eeae189365b0383588a8cf7fdd5a5ee249a6fd44a2676403c
|
|
4
|
+
data.tar.gz: ce5794865b3dd5a057b9e0841814dab1c276b52237f230e23da6317f8dea3f70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41aefa056806a97bc1f5bf13d847007fd4b38fb176f97d1a09868434644fda2ed8cd58698ad9f5002a6ef5d30f7fc105accf7d505eb41fb71abe28a5bda6e47c
|
|
7
|
+
data.tar.gz: e4376d46699b8fe730d4dc4f73890fd43c6e879c446bc484dc6a47cc6b2c31f715af41b83b5f6189f9b8eeed3fd61b48213ec589855d909fe7ebccb702c04303
|
data/README.md
CHANGED
|
@@ -41,6 +41,7 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
|
|
|
41
41
|
- [Close](#close)
|
|
42
42
|
- [CLI usage](#cli-usage)
|
|
43
43
|
- [Test](#test)
|
|
44
|
+
- [Test against local monorepo's example-1](#test-against-local-monorepos-example-1)
|
|
44
45
|
- [Benchmark](#benchmark)
|
|
45
46
|
- [Assess distribution](#assess-distribution)
|
|
46
47
|
- [Development](#development)
|
|
@@ -49,6 +50,8 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
|
|
|
49
50
|
- [Releasing](#releasing)
|
|
50
51
|
- [License](#license)
|
|
51
52
|
|
|
53
|
+
<!-- FEATUREVISOR_DOCS_BEGIN -->
|
|
54
|
+
|
|
52
55
|
## Installation
|
|
53
56
|
|
|
54
57
|
Add this line to your application's Gemfile:
|
|
@@ -381,7 +384,7 @@ require 'json'
|
|
|
381
384
|
def update_datafile(f, datafile_url)
|
|
382
385
|
loop do
|
|
383
386
|
sleep(5 * 60) # 5 minutes
|
|
384
|
-
|
|
387
|
+
|
|
385
388
|
begin
|
|
386
389
|
response = Net::HTTP.get_response(URI(datafile_url))
|
|
387
390
|
datafile_content = JSON.parse(response.body)
|
|
@@ -681,10 +684,27 @@ Additional options that are available:
|
|
|
681
684
|
```bash
|
|
682
685
|
$ bundle exec featurevisor test \
|
|
683
686
|
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
684
|
-
--quiet
|
|
687
|
+
--quiet|--verbose \
|
|
685
688
|
--onlyFailures \
|
|
686
689
|
--keyPattern="myFeatureKey" \
|
|
687
|
-
--assertionPattern="#1"
|
|
690
|
+
--assertionPattern="#1" \
|
|
691
|
+
--with-scopes \
|
|
692
|
+
--with-tags
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
`--with-scopes` and `--with-tags` make the Ruby test runner build scoped/tagged datafiles in memory (via `npx featurevisor build --json`) and evaluate matching assertions against those exact datafiles.
|
|
696
|
+
|
|
697
|
+
If an assertion references `scope` and `--with-scopes` is not provided, the runner still evaluates the assertion by merging that scope's configured context into the assertion context (without building scoped datafiles).
|
|
698
|
+
|
|
699
|
+
For compatibility, camelCase aliases are also supported: `--withScopes` and `--withTags`.
|
|
700
|
+
|
|
701
|
+
### Test against local monorepo's example-1
|
|
702
|
+
|
|
703
|
+
```bash
|
|
704
|
+
$ cd /absolute/path/to/featurevisor-ruby
|
|
705
|
+
$ bundle exec featurevisor test --projectDirectoryPath=./monorepo/examples/example-1
|
|
706
|
+
$ bundle exec featurevisor test --projectDirectoryPath=./monorepo/examples/example-1 --with-scopes
|
|
707
|
+
$ bundle exec featurevisor test --projectDirectoryPath=./monorepo/examples/example-1 --with-tags
|
|
688
708
|
```
|
|
689
709
|
|
|
690
710
|
### Benchmark
|
|
@@ -716,6 +736,8 @@ $ bundle exec featurevisor assess-distribution \
|
|
|
716
736
|
--n=1000
|
|
717
737
|
```
|
|
718
738
|
|
|
739
|
+
<!-- FEATUREVISOR_DOCS_END -->
|
|
740
|
+
|
|
719
741
|
## Development
|
|
720
742
|
|
|
721
743
|
### Setting up
|
data/bin/cli.rb
CHANGED
|
@@ -7,7 +7,7 @@ module FeaturevisorCLI
|
|
|
7
7
|
attr_accessor :command, :assertion_pattern, :context, :environment, :feature,
|
|
8
8
|
:key_pattern, :n, :only_failures, :quiet, :variable, :variation,
|
|
9
9
|
:verbose, :inflate, :show_datafile, :schema_version, :project_directory_path,
|
|
10
|
-
:populate_uuid
|
|
10
|
+
:populate_uuid, :with_scopes, :with_tags
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@n = 1000
|
|
@@ -86,6 +86,14 @@ module FeaturevisorCLI
|
|
|
86
86
|
options.schema_version = v
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
opts.on("--with-scopes", "--withScopes", "Test scoped assertions against scoped datafiles") do
|
|
90
|
+
options.with_scopes = true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
opts.on("--with-tags", "--withTags", "Test tagged assertions against tagged datafiles") do
|
|
94
|
+
options.with_tags = true
|
|
95
|
+
end
|
|
96
|
+
|
|
89
97
|
opts.on("--projectDirectoryPath=PATH", "Project directory path") do |v|
|
|
90
98
|
options.project_directory_path = v
|
|
91
99
|
end
|
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,7 +20,7 @@ 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
|
|
@@ -28,8 +29,8 @@ module FeaturevisorCLI
|
|
|
28
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,18 +43,15 @@ 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
|
|
@@ -67,7 +65,7 @@ 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
|
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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)}"
|
|
164
|
+
end
|
|
165
|
+
|
|
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)}"
|
|
120
180
|
end
|
|
121
181
|
|
|
122
|
-
|
|
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,17 +202,17 @@ 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
|
|
@@ -155,31 +224,58 @@ 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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
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
|
|
175
247
|
|
|
176
|
-
|
|
248
|
+
if tagged_key && datafiles_by_key.key?(tagged_key)
|
|
249
|
+
return datafiles_by_key[tagged_key]
|
|
177
250
|
end
|
|
178
251
|
|
|
179
|
-
|
|
252
|
+
datafiles_by_key[base_key]
|
|
180
253
|
end
|
|
181
254
|
|
|
182
|
-
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)
|
|
183
279
|
passed_tests_count = 0
|
|
184
280
|
failed_tests_count = 0
|
|
185
281
|
passed_assertions_count = 0
|
|
@@ -197,43 +293,33 @@ module FeaturevisorCLI
|
|
|
197
293
|
test_result = nil
|
|
198
294
|
|
|
199
295
|
if test[:feature]
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
puts JSON.pretty_generate(datafile)
|
|
208
|
-
puts ""
|
|
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
|
+
}
|
|
209
303
|
end
|
|
210
304
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
(at * 1000).to_i
|
|
227
|
-
else
|
|
228
|
-
options.bucket_value
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
}
|
|
232
|
-
]
|
|
233
|
-
)
|
|
234
|
-
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
|
|
313
|
+
|
|
314
|
+
# Show datafile if requested
|
|
315
|
+
if @options.show_datafile
|
|
316
|
+
puts ""
|
|
317
|
+
puts JSON.pretty_generate(datafile)
|
|
318
|
+
puts ""
|
|
319
|
+
end
|
|
235
320
|
|
|
236
|
-
|
|
321
|
+
test_result = run_test_feature(assertion, test[:feature], instance, level, scope_context)
|
|
322
|
+
end
|
|
237
323
|
elsif test[:segment]
|
|
238
324
|
segment_key = test[:segment]
|
|
239
325
|
segment = segments_by_key[segment_key]
|
|
@@ -280,8 +366,9 @@ module FeaturevisorCLI
|
|
|
280
366
|
end
|
|
281
367
|
end
|
|
282
368
|
|
|
283
|
-
def run_test_feature(assertion, feature_key, instance, level)
|
|
369
|
+
def run_test_feature(assertion, feature_key, instance, level, scope_context = {})
|
|
284
370
|
context = parse_context(assertion[:context])
|
|
371
|
+
context = { **scope_context, **context } if scope_context && !scope_context.empty?
|
|
285
372
|
sticky = parse_sticky(assertion[:sticky])
|
|
286
373
|
|
|
287
374
|
# Set context and sticky for this assertion
|
|
@@ -325,7 +412,7 @@ module FeaturevisorCLI
|
|
|
325
412
|
expected_variables = assertion[:expectedVariables]
|
|
326
413
|
expected_variables.each do |variable_key, expected_value|
|
|
327
414
|
# Set default variable value for this specific variable
|
|
328
|
-
if assertion[:defaultVariableValues] && assertion[:defaultVariableValues]
|
|
415
|
+
if assertion[:defaultVariableValues].is_a?(Hash) && assertion[:defaultVariableValues].key?(variable_key)
|
|
329
416
|
override_options[:default_variable_value] = assertion[:defaultVariableValues][variable_key]
|
|
330
417
|
end
|
|
331
418
|
|
|
@@ -483,7 +570,7 @@ module FeaturevisorCLI
|
|
|
483
570
|
expected_variables = assertion[:expectedVariables]
|
|
484
571
|
expected_variables.each do |variable_key, expected_value|
|
|
485
572
|
# Set default variable value for this specific variable
|
|
486
|
-
if assertion[:defaultVariableValues] && assertion[:defaultVariableValues]
|
|
573
|
+
if assertion[:defaultVariableValues].is_a?(Hash) && assertion[:defaultVariableValues].key?(variable_key)
|
|
487
574
|
override_options[:default_variable_value] = assertion[:defaultVariableValues][variable_key]
|
|
488
575
|
end
|
|
489
576
|
|
|
@@ -522,6 +609,55 @@ module FeaturevisorCLI
|
|
|
522
609
|
end
|
|
523
610
|
end
|
|
524
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
|
+
|
|
525
661
|
duration = Time.now - start_time
|
|
526
662
|
|
|
527
663
|
{
|
|
@@ -614,13 +750,47 @@ module FeaturevisorCLI
|
|
|
614
750
|
def create_override_options(assertion)
|
|
615
751
|
options = {}
|
|
616
752
|
|
|
617
|
-
if assertion
|
|
753
|
+
if assertion.key?(:defaultVariationValue)
|
|
618
754
|
options[:default_variation_value] = assertion[:defaultVariationValue]
|
|
619
755
|
end
|
|
620
756
|
|
|
621
757
|
options
|
|
622
758
|
end
|
|
623
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
|
+
|
|
624
794
|
def get_evaluation_value(evaluation, key)
|
|
625
795
|
case key
|
|
626
796
|
when :type
|
|
@@ -659,6 +829,8 @@ module FeaturevisorCLI
|
|
|
659
829
|
evaluation[:variable_value]
|
|
660
830
|
when :variableSchema
|
|
661
831
|
evaluation[:variable_schema]
|
|
832
|
+
when :variableOverrideIndex
|
|
833
|
+
evaluation[:variable_override_index]
|
|
662
834
|
else
|
|
663
835
|
nil
|
|
664
836
|
end
|
|
@@ -17,7 +17,8 @@ module Featurevisor
|
|
|
17
17
|
VARIABLE_NOT_FOUND = "variable_not_found" # variable's schema is not defined in the feature
|
|
18
18
|
VARIABLE_DEFAULT = "variable_default" # default variable value used
|
|
19
19
|
VARIABLE_DISABLED = "variable_disabled" # feature is disabled, and variable's disabledValue is used
|
|
20
|
-
|
|
20
|
+
VARIABLE_OVERRIDE_VARIATION = "variable_override_variation" # variable overridden from inside a variation
|
|
21
|
+
VARIABLE_OVERRIDE_RULE = "variable_override_rule" # variable overridden from inside a rule
|
|
21
22
|
|
|
22
23
|
# Common
|
|
23
24
|
NO_MATCH = "no_match" # no rules matched
|
|
@@ -55,14 +56,14 @@ module Featurevisor
|
|
|
55
56
|
evaluation = evaluate(result_options)
|
|
56
57
|
|
|
57
58
|
# Default: variation
|
|
58
|
-
if options
|
|
59
|
+
if options.key?(:default_variation_value) &&
|
|
59
60
|
evaluation[:type] == "variation" &&
|
|
60
61
|
evaluation[:variation_value].nil?
|
|
61
62
|
evaluation[:variation_value] = options[:default_variation_value]
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
# Default: variable
|
|
65
|
-
if options
|
|
66
|
+
if options.key?(:default_variable_value) &&
|
|
66
67
|
evaluation[:type] == "variable" &&
|
|
67
68
|
evaluation[:variable_value].nil?
|
|
68
69
|
evaluation[:variable_value] = options[:default_variable_value]
|
|
@@ -132,10 +133,10 @@ module Featurevisor
|
|
|
132
133
|
if type == "variable"
|
|
133
134
|
if feature && variable_key &&
|
|
134
135
|
feature[:variablesSchema] &&
|
|
135
|
-
(feature[:variablesSchema]
|
|
136
|
-
variable_schema = feature[:variablesSchema]
|
|
136
|
+
has_key?(feature[:variablesSchema], variable_key)
|
|
137
|
+
variable_schema = fetch_with_symbol_key(feature[:variablesSchema], variable_key)
|
|
137
138
|
|
|
138
|
-
if variable_schema
|
|
139
|
+
if variable_schema.key?(:disabledValue)
|
|
139
140
|
# disabledValue: <value>
|
|
140
141
|
evaluation = {
|
|
141
142
|
type: type,
|
|
@@ -179,8 +180,8 @@ module Featurevisor
|
|
|
179
180
|
end
|
|
180
181
|
|
|
181
182
|
# Sticky
|
|
182
|
-
if sticky && (sticky
|
|
183
|
-
sticky_feature = sticky
|
|
183
|
+
if sticky && has_key?(sticky, feature_key)
|
|
184
|
+
sticky_feature = fetch_with_symbol_key(sticky, feature_key)
|
|
184
185
|
|
|
185
186
|
# flag
|
|
186
187
|
if type == "flag" && sticky_feature.key?(:enabled)
|
|
@@ -201,7 +202,7 @@ module Featurevisor
|
|
|
201
202
|
if type == "variation"
|
|
202
203
|
variation_value = sticky_feature[:variation]
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
unless variation_value.nil?
|
|
205
206
|
evaluation = {
|
|
206
207
|
type: type,
|
|
207
208
|
feature_key: feature_key,
|
|
@@ -219,8 +220,8 @@ module Featurevisor
|
|
|
219
220
|
if type == "variable" && variable_key
|
|
220
221
|
variables = sticky_feature[:variables]
|
|
221
222
|
|
|
222
|
-
if variables && (variables
|
|
223
|
-
variable_value = variables
|
|
223
|
+
if variables && has_key?(variables, variable_key)
|
|
224
|
+
variable_value = fetch_with_symbol_key(variables, variable_key)
|
|
224
225
|
evaluation = {
|
|
225
226
|
type: type,
|
|
226
227
|
feature_key: feature_key,
|
|
@@ -261,8 +262,8 @@ module Featurevisor
|
|
|
261
262
|
variable_schema = nil
|
|
262
263
|
|
|
263
264
|
if variable_key
|
|
264
|
-
if feature[:variablesSchema] && (feature[:variablesSchema]
|
|
265
|
-
variable_schema = feature[:variablesSchema]
|
|
265
|
+
if feature[:variablesSchema] && has_key?(feature[:variablesSchema], variable_key)
|
|
266
|
+
variable_schema = fetch_with_symbol_key(feature[:variablesSchema], variable_key)
|
|
266
267
|
end
|
|
267
268
|
|
|
268
269
|
# variable schema not found
|
|
@@ -343,8 +344,8 @@ module Featurevisor
|
|
|
343
344
|
end
|
|
344
345
|
|
|
345
346
|
# variable
|
|
346
|
-
if variable_key && force[:variables] && (force[:variables]
|
|
347
|
-
variable_value = force[:variables]
|
|
347
|
+
if variable_key && force[:variables] && has_key?(force[:variables], variable_key)
|
|
348
|
+
variable_value = fetch_with_symbol_key(force[:variables], variable_key)
|
|
348
349
|
evaluation = {
|
|
349
350
|
type: type,
|
|
350
351
|
feature_key: feature_key,
|
|
@@ -385,8 +386,8 @@ module Featurevisor
|
|
|
385
386
|
|
|
386
387
|
required_variation_value = nil
|
|
387
388
|
|
|
388
|
-
if required_variation_evaluation
|
|
389
|
-
required_variation_value = required_variation_evaluation
|
|
389
|
+
if has_key?(required_variation_evaluation, :variation_value)
|
|
390
|
+
required_variation_value = fetch_with_symbol_key(required_variation_evaluation, :variation_value)
|
|
390
391
|
elsif required_variation_evaluation[:variation]
|
|
391
392
|
required_variation_value = required_variation_evaluation[:variation][:value]
|
|
392
393
|
end
|
|
@@ -601,10 +602,50 @@ module Featurevisor
|
|
|
601
602
|
# variable
|
|
602
603
|
if type == "variable" && variable_key
|
|
603
604
|
# override from rule
|
|
605
|
+
if matched_traffic &&
|
|
606
|
+
matched_traffic[:variableOverrides] &&
|
|
607
|
+
has_key?(matched_traffic[:variableOverrides], variable_key)
|
|
608
|
+
overrides = fetch_with_symbol_key(matched_traffic[:variableOverrides], variable_key)
|
|
609
|
+
|
|
610
|
+
override_index = overrides.find_index do |o|
|
|
611
|
+
if o[:conditions]
|
|
612
|
+
conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
|
|
613
|
+
datafile_reader.all_conditions_are_matched(conditions, context)
|
|
614
|
+
elsif o[:segments]
|
|
615
|
+
segments = datafile_reader.parse_segments_if_stringified(o[:segments])
|
|
616
|
+
datafile_reader.all_segments_are_matched(segments, context)
|
|
617
|
+
else
|
|
618
|
+
false
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
unless override_index.nil?
|
|
623
|
+
override = overrides[override_index]
|
|
624
|
+
|
|
625
|
+
evaluation = {
|
|
626
|
+
type: type,
|
|
627
|
+
feature_key: feature_key,
|
|
628
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_RULE,
|
|
629
|
+
bucket_key: bucket_key,
|
|
630
|
+
bucket_value: bucket_value,
|
|
631
|
+
rule_key: matched_traffic[:key],
|
|
632
|
+
traffic: matched_traffic,
|
|
633
|
+
variable_key: variable_key,
|
|
634
|
+
variable_schema: variable_schema,
|
|
635
|
+
variable_value: override[:value],
|
|
636
|
+
variable_override_index: override_index
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
logger.debug("variable override from rule", evaluation)
|
|
640
|
+
|
|
641
|
+
return evaluation
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
604
645
|
if matched_traffic &&
|
|
605
646
|
matched_traffic[:variables] &&
|
|
606
|
-
(matched_traffic[:variables]
|
|
607
|
-
variable_value = matched_traffic[:variables]
|
|
647
|
+
has_key?(matched_traffic[:variables], variable_key)
|
|
648
|
+
variable_value = fetch_with_symbol_key(matched_traffic[:variables], variable_key)
|
|
608
649
|
evaluation = {
|
|
609
650
|
type: type,
|
|
610
651
|
feature_key: feature_key,
|
|
@@ -637,81 +678,38 @@ module Featurevisor
|
|
|
637
678
|
if variation_value && feature[:variations].is_a?(Array)
|
|
638
679
|
variation = feature[:variations].find { |v| v[:value] == variation_value }
|
|
639
680
|
|
|
640
|
-
if variation && variation[:variableOverrides] && (variation[:variableOverrides]
|
|
641
|
-
overrides = variation[:variableOverrides]
|
|
681
|
+
if variation && variation[:variableOverrides] && has_key?(variation[:variableOverrides], variable_key)
|
|
682
|
+
overrides = fetch_with_symbol_key(variation[:variableOverrides], variable_key)
|
|
642
683
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
context: context
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
override = overrides.find do |o|
|
|
651
|
-
logger.debug("evaluating override", {
|
|
652
|
-
feature_key: feature_key,
|
|
653
|
-
variable_key: variable_key,
|
|
654
|
-
override: o,
|
|
655
|
-
context: context
|
|
656
|
-
})
|
|
657
|
-
|
|
658
|
-
result = if o[:conditions]
|
|
659
|
-
matched = datafile_reader.all_conditions_are_matched(
|
|
660
|
-
o[:conditions].is_a?(String) && o[:conditions] != "*" ?
|
|
661
|
-
JSON.parse(o[:conditions]) : o[:conditions],
|
|
662
|
-
context
|
|
663
|
-
)
|
|
664
|
-
logger.debug("conditions match result", {
|
|
665
|
-
feature_key: feature_key,
|
|
666
|
-
variable_key: variable_key,
|
|
667
|
-
conditions: o[:conditions],
|
|
668
|
-
matched: matched
|
|
669
|
-
})
|
|
670
|
-
matched
|
|
684
|
+
override_index = overrides.find_index do |o|
|
|
685
|
+
if o[:conditions]
|
|
686
|
+
conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
|
|
687
|
+
datafile_reader.all_conditions_are_matched(conditions, context)
|
|
671
688
|
elsif o[:segments]
|
|
672
689
|
segments = datafile_reader.parse_segments_if_stringified(o[:segments])
|
|
673
|
-
|
|
674
|
-
logger.debug("segments match result", {
|
|
675
|
-
feature_key: feature_key,
|
|
676
|
-
variable_key: variable_key,
|
|
677
|
-
segments: o[:segments],
|
|
678
|
-
parsed_segments: segments,
|
|
679
|
-
matched: matched
|
|
680
|
-
})
|
|
681
|
-
matched
|
|
690
|
+
datafile_reader.all_segments_are_matched(segments, context)
|
|
682
691
|
else
|
|
683
|
-
logger.debug("override has no conditions or segments", {
|
|
684
|
-
feature_key: feature_key,
|
|
685
|
-
variable_key: variable_key,
|
|
686
|
-
override: o
|
|
687
|
-
})
|
|
688
692
|
false
|
|
689
693
|
end
|
|
690
|
-
|
|
691
|
-
logger.debug("override evaluation result", {
|
|
692
|
-
feature_key: feature_key,
|
|
693
|
-
variable_key: variable_key,
|
|
694
|
-
result: result
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
result
|
|
698
694
|
end
|
|
699
695
|
|
|
700
|
-
|
|
696
|
+
unless override_index.nil?
|
|
697
|
+
override = overrides[override_index]
|
|
701
698
|
evaluation = {
|
|
702
699
|
type: type,
|
|
703
700
|
feature_key: feature_key,
|
|
704
|
-
reason: Featurevisor::EvaluationReason::
|
|
701
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_VARIATION,
|
|
705
702
|
bucket_key: bucket_key,
|
|
706
703
|
bucket_value: bucket_value,
|
|
707
704
|
rule_key: matched_traffic&.[](:key),
|
|
708
705
|
traffic: matched_traffic,
|
|
709
706
|
variable_key: variable_key,
|
|
710
707
|
variable_schema: variable_schema,
|
|
711
|
-
variable_value: override[:value]
|
|
708
|
+
variable_value: override[:value],
|
|
709
|
+
variable_override_index: override_index
|
|
712
710
|
}
|
|
713
711
|
|
|
714
|
-
logger.debug("variable override", evaluation)
|
|
712
|
+
logger.debug("variable override from variation", evaluation)
|
|
715
713
|
|
|
716
714
|
return evaluation
|
|
717
715
|
end
|
|
@@ -719,8 +717,8 @@ module Featurevisor
|
|
|
719
717
|
|
|
720
718
|
if variation &&
|
|
721
719
|
variation[:variables] &&
|
|
722
|
-
(variation[:variables]
|
|
723
|
-
variable_value = variation[:variables]
|
|
720
|
+
has_key?(variation[:variables], variable_key)
|
|
721
|
+
variable_value = fetch_with_symbol_key(variation[:variables], variable_key)
|
|
724
722
|
evaluation = {
|
|
725
723
|
type: type,
|
|
726
724
|
feature_key: feature_key,
|
|
@@ -814,5 +812,20 @@ module Featurevisor
|
|
|
814
812
|
evaluation
|
|
815
813
|
end
|
|
816
814
|
end
|
|
815
|
+
|
|
816
|
+
def self.fetch_with_symbol_key(obj, key)
|
|
817
|
+
return obj[key] if obj.is_a?(Hash) && obj.key?(key)
|
|
818
|
+
|
|
819
|
+
symbol_key = key.to_sym
|
|
820
|
+
return obj[symbol_key] if obj.is_a?(Hash) && obj.key?(symbol_key)
|
|
821
|
+
|
|
822
|
+
nil
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def self.has_key?(obj, key)
|
|
826
|
+
return false unless obj.is_a?(Hash)
|
|
827
|
+
|
|
828
|
+
obj.key?(key) || obj.key?(key.to_sym)
|
|
829
|
+
end
|
|
817
830
|
end
|
|
818
831
|
end
|
data/lib/featurevisor/version.rb
CHANGED