featurevisor 2.0.0 → 3.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 +67 -29
- data/bin/cli.rb +2 -2
- data/bin/commands/benchmark.rb +14 -4
- data/bin/commands/test.rb +102 -24
- data/lib/featurevisor/child_instance.rb +64 -289
- data/lib/featurevisor/emitter.rb +1 -1
- data/lib/featurevisor/evaluate.rb +59 -61
- data/lib/featurevisor/evaluation_data_provider.rb +15 -3
- data/lib/featurevisor/events.rb +118 -2
- data/lib/featurevisor/instance.rb +153 -39
- data/lib/featurevisor/modules.rb +51 -6
- 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: 2cc7d95c6c897b09f4092c105b73b9776c6e4b6d1851dad7c2108ea8cced6b2b
|
|
4
|
+
data.tar.gz: 9b06c4ac0e499345af18967d161ef102b5636edd82dbf2013e15ae790ad7e832
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2df8d3afe705ea850e4a77458c17d8090f4bf3b0ec27da761ef3447f8952ba0e2f970d0b1f75962c0c3402f01a2c8fe10b341a694eda2d17d5e004730f3aa51
|
|
7
|
+
data.tar.gz: ebd5698bbd941fbd2778350e6b7602375912a8455f0bd6af8ecd3034ca56d97b23c5803089c57a3a956fb638062838e65adeb3d38126bf4e72f5468512a54634
|
data/README.md
CHANGED
|
@@ -19,8 +19,9 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
|
|
|
19
19
|
- [Getting variation](#getting-variation)
|
|
20
20
|
- [Getting variables](#getting-variables)
|
|
21
21
|
- [Type specific methods](#type-specific-methods)
|
|
22
|
-
- [Getting
|
|
23
|
-
- [
|
|
22
|
+
- [Getting global variables](#getting-global-variables)
|
|
23
|
+
- [Getting aggregate evaluations](#getting-aggregate-evaluations)
|
|
24
|
+
- [Sticky features and variables](#sticky-features-and-variables)
|
|
24
25
|
- [Initialize with sticky](#initialize-with-sticky)
|
|
25
26
|
- [Set sticky afterwards](#set-sticky-afterwards)
|
|
26
27
|
- [Setting datafile](#setting-datafile)
|
|
@@ -36,7 +37,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
|
|
|
36
37
|
- [Events](#events)
|
|
37
38
|
- [`datafile_set`](#datafile_set)
|
|
38
39
|
- [`context_set`](#context_set)
|
|
39
|
-
- [`
|
|
40
|
+
- [`sticky_features_set` and `sticky_variables_set`](#sticky_features_set-and-sticky_variables_set)
|
|
40
41
|
- [`error`](#error)
|
|
41
42
|
- [Modules](#modules)
|
|
42
43
|
- [Defining a module](#defining-a-module)
|
|
@@ -89,7 +90,7 @@ f = Featurevisor.create_featurevisor(
|
|
|
89
90
|
|
|
90
91
|
Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory.
|
|
91
92
|
|
|
92
|
-
Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `
|
|
93
|
+
Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `set_sticky_features`, `set_sticky_variables`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
|
|
93
94
|
|
|
94
95
|
## Initialization
|
|
95
96
|
|
|
@@ -295,12 +296,24 @@ f.get_variable_json(feature_key, variable_key, context = {})
|
|
|
295
296
|
|
|
296
297
|
Type specific methods do not coerce values. `get_variable_integer` returns `nil` for the string `"1"`, and boolean getters return `nil` for non-boolean values.
|
|
297
298
|
|
|
298
|
-
## Getting
|
|
299
|
+
## Getting global variables
|
|
300
|
+
|
|
301
|
+
Global variables use the same methods with a single variable key:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
message = f.get_variable_string('welcomeMessage', context)
|
|
305
|
+
value = f.get_variable('checkoutSettings', context)
|
|
306
|
+
evaluation = f.evaluate_variable('checkoutSettings', context)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Global variables resolve sticky values first, then required features, then the first matching override, and finally their default value.
|
|
310
|
+
|
|
311
|
+
## Getting aggregate evaluations
|
|
299
312
|
|
|
300
313
|
You can get evaluations of all features available in the SDK instance:
|
|
301
314
|
|
|
302
315
|
```ruby
|
|
303
|
-
all_evaluations = f.
|
|
316
|
+
all_evaluations = f.get_feature_evaluations({})
|
|
304
317
|
|
|
305
318
|
puts all_evaluations
|
|
306
319
|
# {
|
|
@@ -321,11 +334,17 @@ puts all_evaluations
|
|
|
321
334
|
|
|
322
335
|
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
|
|
323
336
|
|
|
324
|
-
|
|
337
|
+
Global variable values are available separately:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
global_variables = f.get_variable_evaluations({})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Sticky features and variables
|
|
325
344
|
|
|
326
345
|
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
|
|
327
346
|
|
|
328
|
-
Sticky values belong to an SDK or child instance.
|
|
347
|
+
Sticky values belong to an SDK or child instance. Feature and global variable sticky values are independent.
|
|
329
348
|
|
|
330
349
|
### Initialize with sticky
|
|
331
350
|
|
|
@@ -333,7 +352,7 @@ Sticky values belong to an SDK or child instance. Evaluation options do not acce
|
|
|
333
352
|
require 'featurevisor'
|
|
334
353
|
|
|
335
354
|
f = Featurevisor.create_featurevisor(
|
|
336
|
-
|
|
355
|
+
sticky_features: {
|
|
337
356
|
myFeatureKey: {
|
|
338
357
|
enabled: true,
|
|
339
358
|
# optional
|
|
@@ -345,6 +364,9 @@ f = Featurevisor.create_featurevisor(
|
|
|
345
364
|
anotherFeatureKey: {
|
|
346
365
|
enabled: false
|
|
347
366
|
}
|
|
367
|
+
},
|
|
368
|
+
sticky_variables: {
|
|
369
|
+
welcomeMessage: 'Welcome back'
|
|
348
370
|
}
|
|
349
371
|
)
|
|
350
372
|
```
|
|
@@ -356,7 +378,7 @@ Once initialized with sticky features, the SDK will look for values there first
|
|
|
356
378
|
You can also set sticky features after the SDK is initialized:
|
|
357
379
|
|
|
358
380
|
```ruby
|
|
359
|
-
f.
|
|
381
|
+
f.set_sticky_features({
|
|
360
382
|
myFeatureKey: {
|
|
361
383
|
enabled: true,
|
|
362
384
|
variation: 'treatment',
|
|
@@ -368,6 +390,8 @@ f.set_sticky({
|
|
|
368
390
|
enabled: false
|
|
369
391
|
}
|
|
370
392
|
}, true) # replace existing sticky features (false by default)
|
|
393
|
+
|
|
394
|
+
f.set_sticky_variables({ welcomeMessage: 'Welcome back' }, true)
|
|
371
395
|
```
|
|
372
396
|
|
|
373
397
|
## Setting datafile
|
|
@@ -393,7 +417,7 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
|
|
|
393
417
|
- `segments` are merged, with incoming entries overriding existing ones
|
|
394
418
|
- `features` are merged, with incoming entries overriding existing ones
|
|
395
419
|
|
|
396
|
-
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and
|
|
420
|
+
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features, segments, and global variables together.
|
|
397
421
|
|
|
398
422
|
### Replacing
|
|
399
423
|
|
|
@@ -430,7 +454,7 @@ load_datafile(f, "checkout")
|
|
|
430
454
|
|
|
431
455
|
### Updating datafile
|
|
432
456
|
|
|
433
|
-
You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#
|
|
457
|
+
You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly.
|
|
434
458
|
|
|
435
459
|
The triggers for setting the datafile again can be:
|
|
436
460
|
|
|
@@ -496,7 +520,7 @@ f = Featurevisor.create_featurevisor(
|
|
|
496
520
|
|
|
497
521
|
Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:details` hash. Optional `:module`, `:moduleName`, and `:originalError` fields describe provenance. Evaluation metadata belongs in `:details`.
|
|
498
522
|
|
|
499
|
-
Diagnostic handlers are isolated from SDK
|
|
523
|
+
Diagnostic handlers are isolated from SDK behaviour. An exception in a handler does not stop other handlers or evaluations.
|
|
500
524
|
|
|
501
525
|
|
|
502
526
|
## Events
|
|
@@ -545,15 +569,19 @@ unsubscribe = f.on('context_set') do |event|
|
|
|
545
569
|
end
|
|
546
570
|
```
|
|
547
571
|
|
|
548
|
-
### `
|
|
572
|
+
### `sticky_features_set` and `sticky_variables_set`
|
|
549
573
|
|
|
550
574
|
```ruby
|
|
551
|
-
|
|
575
|
+
feature_unsubscribe = f.on('sticky_features_set') do |event|
|
|
552
576
|
replaced = event[:replaced] # true if sticky features got replaced
|
|
553
577
|
features = event[:features] # list of all affected feature keys
|
|
554
578
|
|
|
555
579
|
puts 'Sticky features set'
|
|
556
580
|
end
|
|
581
|
+
|
|
582
|
+
variable_unsubscribe = f.on('sticky_variables_set') do |event|
|
|
583
|
+
variables = event[:variables]
|
|
584
|
+
end
|
|
557
585
|
```
|
|
558
586
|
|
|
559
587
|
### `error`
|
|
@@ -583,11 +611,14 @@ evaluation = f.evaluate_variation(feature_key, context = {})
|
|
|
583
611
|
|
|
584
612
|
# variable
|
|
585
613
|
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
|
|
614
|
+
|
|
615
|
+
# global variable
|
|
616
|
+
global_evaluation = f.evaluate_variable(variable_key, context = {})
|
|
586
617
|
```
|
|
587
618
|
|
|
588
619
|
The returned object will always contain the following properties:
|
|
589
620
|
|
|
590
|
-
- `feature_key`: the feature key
|
|
621
|
+
- `feature_key`: the feature key when evaluating a feature
|
|
591
622
|
- `reason`: the reason how the value was evaluated
|
|
592
623
|
|
|
593
624
|
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
|
|
@@ -604,7 +635,11 @@ And optionally these properties depending on whether you are evaluating a featur
|
|
|
604
635
|
|
|
605
636
|
## Modules
|
|
606
637
|
|
|
607
|
-
Modules allow you to intercept the evaluation process and customize SDK
|
|
638
|
+
Modules allow you to intercept the evaluation process and customize SDK behaviour.
|
|
639
|
+
|
|
640
|
+
For feature evaluations, all `before` callbacks run in registration order, followed by all `before_evaluation` callbacks. After evaluation and caller defaults, all `after_evaluation` callbacks run, followed by all `after` callbacks. Global variable evaluations use only `before_evaluation` and `after_evaluation`. Required feature checks run through the complete module pipeline, and transformed defaults are preserved.
|
|
641
|
+
|
|
642
|
+
`before` and `after` remain available as deprecated feature-only compatibility callbacks. Use `before_evaluation` and `after_evaluation` for new modules so the same callbacks can handle feature and global variable evaluations.
|
|
608
643
|
|
|
609
644
|
### Defining a module
|
|
610
645
|
|
|
@@ -630,8 +665,8 @@ my_custom_module = {
|
|
|
630
665
|
})
|
|
631
666
|
},
|
|
632
667
|
|
|
633
|
-
# before evaluation
|
|
634
|
-
|
|
668
|
+
# before feature or global variable evaluation
|
|
669
|
+
before_evaluation: ->(options) {
|
|
635
670
|
# update context before evaluation
|
|
636
671
|
options[:context] = options[:context].merge({
|
|
637
672
|
someAdditionalAttribute: 'value'
|
|
@@ -639,13 +674,13 @@ my_custom_module = {
|
|
|
639
674
|
options
|
|
640
675
|
},
|
|
641
676
|
|
|
642
|
-
# after evaluation
|
|
643
|
-
|
|
677
|
+
# after feature or global variable evaluation
|
|
678
|
+
after_evaluation: ->(evaluation, options) {
|
|
644
679
|
reason = evaluation[:reason]
|
|
645
680
|
if reason == 'error'
|
|
646
681
|
# log error
|
|
647
|
-
return
|
|
648
682
|
end
|
|
683
|
+
evaluation
|
|
649
684
|
},
|
|
650
685
|
|
|
651
686
|
# configure bucket key
|
|
@@ -720,12 +755,14 @@ Now you can pass the child instance where your individual request is being handl
|
|
|
720
755
|
is_enabled = child_f.is_enabled('my_feature')
|
|
721
756
|
variation = child_f.get_variation('my_feature')
|
|
722
757
|
variable_value = child_f.get_variable('my_feature', 'my_variable')
|
|
758
|
+
global_value = child_f.get_variable('welcomeMessage')
|
|
723
759
|
```
|
|
724
760
|
|
|
725
761
|
Similar to parent SDK, child instances also support several additional methods:
|
|
726
762
|
|
|
727
763
|
- `set_context`
|
|
728
|
-
- `
|
|
764
|
+
- `set_sticky_features`
|
|
765
|
+
- `set_sticky_variables`
|
|
729
766
|
- `evaluate_flag`
|
|
730
767
|
- `is_enabled`
|
|
731
768
|
- `evaluate_variation`
|
|
@@ -739,7 +776,8 @@ Similar to parent SDK, child instances also support several additional methods:
|
|
|
739
776
|
- `get_variable_array`
|
|
740
777
|
- `get_variable_object`
|
|
741
778
|
- `get_variable_json`
|
|
742
|
-
- `
|
|
779
|
+
- `get_feature_evaluations`
|
|
780
|
+
- `get_variable_evaluations`
|
|
743
781
|
- `on`
|
|
744
782
|
- `close`
|
|
745
783
|
|
|
@@ -780,7 +818,7 @@ $ bundle exec featurevisor test \
|
|
|
780
818
|
|
|
781
819
|
The Ruby test runner builds base datafiles and Target datafiles in memory via `npx featurevisor build --json`. When an assertion contains `target`, it is evaluated against the matching Target datafile.
|
|
782
820
|
|
|
783
|
-
All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide
|
|
821
|
+
All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behaviour is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
|
|
784
822
|
|
|
785
823
|
### Test against local monorepo's example-1
|
|
786
824
|
|
|
@@ -828,7 +866,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
|
|
|
828
866
|
Install the provider:
|
|
829
867
|
|
|
830
868
|
```ruby
|
|
831
|
-
gem "featurevisor-openfeature", "~>
|
|
869
|
+
gem "featurevisor-openfeature", "~> 3.0"
|
|
832
870
|
```
|
|
833
871
|
|
|
834
872
|
It installs the matching `featurevisor` gem and the official `openfeature-sdk` dependency. The provider and base SDK deliberately share the same version, and the provider requires that exact Featurevisor version.
|
|
@@ -852,9 +890,9 @@ enabled = client.fetch_boolean_value(
|
|
|
852
890
|
)
|
|
853
891
|
```
|
|
854
892
|
|
|
855
|
-
Use `checkout` for a flag, `checkout:variation` for its variation,
|
|
893
|
+
Use `checkout` for a flag, `checkout:variation` for its variation, `checkout:title` for its `title` variable, and `variable:welcomeMessage` for a global variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
|
|
856
894
|
|
|
857
|
-
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `
|
|
895
|
+
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, `variation_key`, and `global_variable_prefix` can customize the mapping. The global variable prefix defaults to `variable` and cannot contain the separator.
|
|
858
896
|
|
|
859
897
|
You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
|
|
860
898
|
|
|
@@ -908,7 +946,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
|
|
|
908
946
|
- Run `bundle install`
|
|
909
947
|
- Push commit to `main` branch
|
|
910
948
|
- Wait for CI to complete
|
|
911
|
-
- Tag the release with the same version number, for example `
|
|
949
|
+
- Tag the release with the same version number, for example `v3.1.0`
|
|
912
950
|
- The workflow verifies that the tag matches the shared version
|
|
913
951
|
- The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
|
|
914
952
|
|
data/bin/cli.rb
CHANGED
|
@@ -43,7 +43,7 @@ module FeaturevisorCLI
|
|
|
43
43
|
options.environment = v
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
opts.on("--feature=FEATURE", "Feature key
|
|
46
|
+
opts.on("--feature=FEATURE", "Feature key") do |v|
|
|
47
47
|
options.feature = v
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -150,6 +150,6 @@ module FeaturevisorCLI
|
|
|
150
150
|
puts " featurevisor benchmark --feature=myFeature --environment=dev --n=10000"
|
|
151
151
|
puts " featurevisor assess-distribution --feature=myFeature --n=10000"
|
|
152
152
|
puts ""
|
|
153
|
-
puts "Note: benchmark
|
|
153
|
+
puts "Note: benchmark requires --environment and either --feature or --variable"
|
|
154
154
|
end
|
|
155
155
|
end
|
data/bin/commands/benchmark.rb
CHANGED
|
@@ -22,8 +22,8 @@ module FeaturevisorCLI
|
|
|
22
22
|
exit 1
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
unless @options.feature
|
|
26
|
-
puts "Error: --feature is required for benchmark command"
|
|
25
|
+
unless @options.feature || @options.variable
|
|
26
|
+
puts "Error: --feature or --variable is required for benchmark command"
|
|
27
27
|
exit 1
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -41,7 +41,8 @@ module FeaturevisorCLI
|
|
|
41
41
|
datafile_build_duration_ms = (datafile_build_duration * 1000).round
|
|
42
42
|
|
|
43
43
|
puts "\nBenchmark Featurevisor feature"
|
|
44
|
-
puts " Feature: #{@options.feature}"
|
|
44
|
+
puts " Feature: #{@options.feature}" if @options.feature
|
|
45
|
+
puts " Global variable: #{@options.variable}" if @options.variable && !@options.feature
|
|
45
46
|
puts " Environment: #{@options.environment}"
|
|
46
47
|
puts " Target: #{target}" if target
|
|
47
48
|
puts " Iterations: #{@options.n}"
|
|
@@ -59,7 +60,10 @@ module FeaturevisorCLI
|
|
|
59
60
|
puts "Against context: #{context.to_json}"
|
|
60
61
|
|
|
61
62
|
# Run the appropriate benchmark
|
|
62
|
-
if @options.
|
|
63
|
+
if @options.variable && !@options.feature
|
|
64
|
+
puts "Evaluating global variable \"#{@options.variable}\" #{@options.n} times..."
|
|
65
|
+
output = benchmark_global_variable(instance, @options.variable, context, @options.n)
|
|
66
|
+
elsif @options.variation
|
|
63
67
|
puts "Evaluating variation #{@options.n} times..."
|
|
64
68
|
output = benchmark_feature_variation(instance, @options.feature, context, @options.n)
|
|
65
69
|
elsif @options.variable
|
|
@@ -223,6 +227,12 @@ module FeaturevisorCLI
|
|
|
223
227
|
end
|
|
224
228
|
end
|
|
225
229
|
|
|
230
|
+
def benchmark_global_variable(instance, variable_key, context, n)
|
|
231
|
+
benchmark_evaluation(n) do
|
|
232
|
+
instance.get_variable(variable_key, context)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
226
236
|
def format_value(value)
|
|
227
237
|
if value.nil?
|
|
228
238
|
"null"
|
data/bin/commands/test.rb
CHANGED
|
@@ -223,22 +223,19 @@ module FeaturevisorCLI
|
|
|
223
223
|
environment = assertion.key?(:environment) ? assertion[:environment] : false
|
|
224
224
|
environment = false if environment.nil?
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
base_key = base_datafile_key(environment)
|
|
226
|
+
return datafiles_by_key[target_datafile_key(environment, assertion[:target])] if assertion[:target]
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
return datafiles_by_key[target_key]
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
datafiles_by_key[base_key]
|
|
228
|
+
datafiles_by_key[base_datafile_key(environment)]
|
|
234
229
|
end
|
|
235
230
|
|
|
236
231
|
def create_tester_instance(datafile, level, assertion)
|
|
237
|
-
sticky = parse_sticky(assertion[:sticky])
|
|
232
|
+
sticky = parse_sticky(assertion[:stickyFeatures] || assertion[:sticky])
|
|
233
|
+
sticky_variables = assertion[:stickyVariables].is_a?(Hash) ? assertion[:stickyVariables] : {}
|
|
238
234
|
|
|
239
235
|
Featurevisor.create_featurevisor(
|
|
240
236
|
datafile: datafile,
|
|
241
|
-
|
|
237
|
+
sticky_features: sticky,
|
|
238
|
+
sticky_variables: sticky_variables,
|
|
242
239
|
log_level: level,
|
|
243
240
|
modules: [
|
|
244
241
|
{
|
|
@@ -265,7 +262,7 @@ module FeaturevisorCLI
|
|
|
265
262
|
tests.each do |test|
|
|
266
263
|
test_key = test[:key]
|
|
267
264
|
assertions = test[:assertions] || []
|
|
268
|
-
if test[:feature] && !@options.targets.empty?
|
|
265
|
+
if (test[:feature] || test[:variable]) && !@options.targets.empty?
|
|
269
266
|
assertions = assertions.select do |assertion|
|
|
270
267
|
assertion[:target].nil? || @options.targets.include?(assertion[:target])
|
|
271
268
|
end
|
|
@@ -299,7 +296,28 @@ module FeaturevisorCLI
|
|
|
299
296
|
puts ""
|
|
300
297
|
end
|
|
301
298
|
|
|
302
|
-
|
|
299
|
+
begin
|
|
300
|
+
test_result = run_test_feature(assertion, test[:feature], instance, level)
|
|
301
|
+
ensure
|
|
302
|
+
instance.close
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
elsif test[:variable]
|
|
306
|
+
datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
|
|
307
|
+
if datafile
|
|
308
|
+
if @options.show_datafile
|
|
309
|
+
puts ""
|
|
310
|
+
puts JSON.pretty_generate(datafile)
|
|
311
|
+
puts ""
|
|
312
|
+
end
|
|
313
|
+
instance = create_tester_instance(datafile, level, assertion)
|
|
314
|
+
begin
|
|
315
|
+
test_result = run_test_variable(assertion, test[:variable], instance)
|
|
316
|
+
ensure
|
|
317
|
+
instance.close
|
|
318
|
+
end
|
|
319
|
+
else
|
|
320
|
+
test_result = { has_error: true, errors: " ✘ no datafile found for assertion target/environment combination\n", duration: 0 }
|
|
303
321
|
end
|
|
304
322
|
elsif test[:segment]
|
|
305
323
|
segment_key = test[:segment]
|
|
@@ -347,6 +365,64 @@ module FeaturevisorCLI
|
|
|
347
365
|
end
|
|
348
366
|
end
|
|
349
367
|
|
|
368
|
+
def run_test_variable(assertion, variable_key, instance)
|
|
369
|
+
instance.set_context(parse_context(assertion[:context]), true)
|
|
370
|
+
options = {}
|
|
371
|
+
options[:default_variable_value] = assertion[:defaultVariableValue] if assertion.key?(:defaultVariableValue)
|
|
372
|
+
started = Time.now
|
|
373
|
+
errors = ""
|
|
374
|
+
|
|
375
|
+
errors += test_variable_expectation(assertion, variable_key, instance, options)
|
|
376
|
+
|
|
377
|
+
Array(assertion[:children]).each_with_index do |child_assertion, child_index|
|
|
378
|
+
child = instance.spawn(
|
|
379
|
+
parse_context(child_assertion[:context]),
|
|
380
|
+
sticky_features: parse_sticky(child_assertion[:stickyFeatures]),
|
|
381
|
+
sticky_variables: child_assertion[:stickyVariables].is_a?(Hash) ? child_assertion[:stickyVariables] : {}
|
|
382
|
+
)
|
|
383
|
+
begin
|
|
384
|
+
child_options = {}
|
|
385
|
+
if child_assertion.key?(:defaultVariableValue)
|
|
386
|
+
child_options[:default_variable_value] = child_assertion[:defaultVariableValue]
|
|
387
|
+
end
|
|
388
|
+
errors += test_variable_expectation(
|
|
389
|
+
child_assertion,
|
|
390
|
+
variable_key,
|
|
391
|
+
child,
|
|
392
|
+
child_options,
|
|
393
|
+
"children[#{child_index}]."
|
|
394
|
+
)
|
|
395
|
+
ensure
|
|
396
|
+
child.close
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
{ has_error: !errors.empty?, errors: errors, duration: Time.now - started }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def test_variable_expectation(assertion, variable_key, evaluator, options, prefix = "")
|
|
404
|
+
evaluation = evaluator.evaluate_variable(variable_key, {}, options)
|
|
405
|
+
errors = ""
|
|
406
|
+
if assertion.key?(:expectedValue) && !compare_values(evaluation[:variable_value], assertion[:expectedValue])
|
|
407
|
+
errors += " ✘ #{prefix}expectedValue: expected #{format_test_value(assertion[:expectedValue])} but received #{format_test_value(evaluation[:variable_value])}\n"
|
|
408
|
+
end
|
|
409
|
+
if assertion[:expectedEvaluation].is_a?(Hash)
|
|
410
|
+
assertion[:expectedEvaluation].each do |key, expected|
|
|
411
|
+
actual = get_evaluation_value(evaluation, key)
|
|
412
|
+
unless compare_values(actual, expected)
|
|
413
|
+
errors += " ✘ #{prefix}expectedEvaluation.#{key}: expected #{format_test_value(expected)} but received #{format_test_value(actual)}\n"
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
errors
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def format_test_value(value)
|
|
421
|
+
JSON.generate(value)
|
|
422
|
+
rescue JSON::GeneratorError
|
|
423
|
+
value.inspect
|
|
424
|
+
end
|
|
425
|
+
|
|
350
426
|
def run_test_feature(assertion, feature_key, instance, level)
|
|
351
427
|
context = parse_context(assertion[:context])
|
|
352
428
|
sticky = parse_sticky(assertion[:sticky])
|
|
@@ -354,7 +430,7 @@ module FeaturevisorCLI
|
|
|
354
430
|
# Set context and sticky for this assertion
|
|
355
431
|
instance.set_context(context, false)
|
|
356
432
|
if sticky && !sticky.empty?
|
|
357
|
-
instance.
|
|
433
|
+
instance.set_sticky_features(sticky, false)
|
|
358
434
|
end
|
|
359
435
|
|
|
360
436
|
# Create override options
|
|
@@ -483,20 +559,16 @@ module FeaturevisorCLI
|
|
|
483
559
|
child_context = parse_context(child[:context])
|
|
484
560
|
|
|
485
561
|
# Create override options for child with sticky values
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if !child_sticky.empty?
|
|
495
|
-
child_instance.set_sticky(child_sticky, false)
|
|
562
|
+
child_instance = instance.spawn(
|
|
563
|
+
child_context,
|
|
564
|
+
sticky_features: sticky || {}
|
|
565
|
+
)
|
|
566
|
+
begin
|
|
567
|
+
child_result = run_test_feature_child(child, feature_key, child_instance, level)
|
|
568
|
+
ensure
|
|
569
|
+
child_instance.close
|
|
496
570
|
end
|
|
497
571
|
|
|
498
|
-
child_result = run_test_feature_child(child, feature_key, child_instance, level)
|
|
499
|
-
|
|
500
572
|
if child_result[:has_error]
|
|
501
573
|
has_error = true
|
|
502
574
|
errors += child_result[:errors]
|
|
@@ -795,6 +867,8 @@ module FeaturevisorCLI
|
|
|
795
867
|
evaluation[:force]
|
|
796
868
|
when :required
|
|
797
869
|
evaluation[:required]
|
|
870
|
+
when :requiredFeatures
|
|
871
|
+
evaluation[:required_features]
|
|
798
872
|
when :sticky
|
|
799
873
|
evaluation[:sticky]
|
|
800
874
|
when :variation
|
|
@@ -809,6 +883,10 @@ module FeaturevisorCLI
|
|
|
809
883
|
evaluation[:variable_schema]
|
|
810
884
|
when :variableOverrideIndex
|
|
811
885
|
evaluation[:variable_override_index]
|
|
886
|
+
when :variableOverrideKey
|
|
887
|
+
evaluation[:variable_override_key]
|
|
888
|
+
when :variableOverridePath
|
|
889
|
+
evaluation[:variable_override_path]
|
|
812
890
|
else
|
|
813
891
|
nil
|
|
814
892
|
end
|