Skip to content

Commit 0f030ab

Browse files
committed
updates
1 parent 9ed8e57 commit 0f030ab

File tree

5 files changed

+255
-20
lines changed

5 files changed

+255
-20
lines changed

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
4141
- [Close](#close)
4242
- [CLI usage](#cli-usage)
4343
- [Test](#test)
44-
- [Test against example-1](#test-against-example-1)
44+
- [Test against local monorepo's example-1](#test-against-local-monorepos-example-1)
4545
- [Benchmark](#benchmark)
4646
- [Assess distribution](#assess-distribution)
4747
- [Development](#development)
@@ -694,14 +694,17 @@ $ bundle exec featurevisor test \
694694

695695
`--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.
696696

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+
697699
For compatibility, camelCase aliases are also supported: `--withScopes` and `--withTags`.
698700

699-
### Test against example-1
701+
### Test against local monorepo's example-1
700702

701703
```bash
702704
$ cd /absolute/path/to/featurevisor-ruby
703-
$ bundle exec featurevisor test --projectDirectoryPath=/path/to/featurevisor/project
704-
$ bundle exec featurevisor test --projectDirectoryPath=/path/to/featurevisor/project --with-scopes
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
705708
```
706709

707710
### Benchmark

bin/commands/test.rb

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -300,25 +300,26 @@ def run_tests(tests, datafiles_by_key, segments_by_key, level, config)
300300
errors: " ✘ no datafile found for assertion scope/tag/environment combination\n",
301301
duration: 0
302302
}
303-
next
304303
end
305304

306-
instance = create_tester_instance(datafile, level, assertion)
307-
scope_context = {}
305+
if datafile
306+
instance = create_tester_instance(datafile, level, assertion)
307+
scope_context = {}
308308

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
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
313313

314-
# Show datafile if requested
315-
if @options.show_datafile
316-
puts ""
317-
puts JSON.pretty_generate(datafile)
318-
puts ""
319-
end
314+
# Show datafile if requested
315+
if @options.show_datafile
316+
puts ""
317+
puts JSON.pretty_generate(datafile)
318+
puts ""
319+
end
320320

321-
test_result = run_test_feature(assertion, test[:feature], instance, level, scope_context)
321+
test_result = run_test_feature(assertion, test[:feature], instance, level, scope_context)
322+
end
322323
elsif test[:segment]
323324
segment_key = test[:segment]
324325
segment = segments_by_key[segment_key]
@@ -608,6 +609,55 @@ def run_test_feature_child(assertion, feature_key, instance, level)
608609
end
609610
end
610611

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+
611661
duration = Time.now - start_time
612662

613663
{
@@ -707,6 +757,40 @@ def create_override_options(assertion)
707757
options
708758
end
709759

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+
710794
def get_evaluation_value(evaluation, key)
711795
case key
712796
when :type

lib/featurevisor/evaluate.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,8 @@ def self.evaluate(options)
386386

387387
required_variation_value = nil
388388

389-
if required_variation_evaluation[:variation_value]
390-
required_variation_value = required_variation_evaluation[:variation_value]
389+
if has_key?(required_variation_evaluation, :variation_value)
390+
required_variation_value = fetch_with_symbol_key(required_variation_evaluation, :variation_value)
391391
elsif required_variation_evaluation[:variation]
392392
required_variation_value = required_variation_evaluation[:variation][:value]
393393
end

spec/evaluate_spec.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,92 @@
319319
expect(result[:required]).to eq(["required-feature"])
320320
end
321321

322+
it "should treat required variation value false as a valid match" do
323+
feature = {
324+
key: "test-feature",
325+
required: [{ key: "required-feature", variation: false }]
326+
}
327+
328+
allow(datafile_reader).to receive(:get_feature).and_return(feature)
329+
allow(datafile_reader).to receive(:get_matched_force).and_return({
330+
force: nil,
331+
forceIndex: nil
332+
})
333+
allow(datafile_reader).to receive(:get_matched_traffic).and_return(nil)
334+
allow(Featurevisor::Bucketer).to receive(:get_bucket_key).and_return("test.123")
335+
allow(Featurevisor::Bucketer).to receive(:get_bucketed_number).and_return(50)
336+
337+
allow(Featurevisor::Evaluate).to receive(:evaluate).and_call_original
338+
allow(Featurevisor::Evaluate).to receive(:evaluate).with(
339+
hash_including(type: "flag", feature_key: "required-feature")
340+
).and_return({
341+
type: "flag",
342+
feature_key: "required-feature",
343+
enabled: true
344+
})
345+
allow(Featurevisor::Evaluate).to receive(:evaluate).with(
346+
hash_including(type: "variation", feature_key: "required-feature")
347+
).and_return({
348+
type: "variation",
349+
feature_key: "required-feature",
350+
variation_value: false
351+
})
352+
353+
result = Featurevisor::Evaluate.evaluate(
354+
type: "flag",
355+
feature_key: feature,
356+
context: {},
357+
logger: logger,
358+
hooks_manager: hooks_manager,
359+
datafile_reader: datafile_reader
360+
)
361+
362+
expect(result[:reason]).not_to eq(Featurevisor::EvaluationReason::REQUIRED)
363+
end
364+
365+
it "should treat required variation value 0 as a valid match" do
366+
feature = {
367+
key: "test-feature",
368+
required: [{ key: "required-feature", variation: 0 }]
369+
}
370+
371+
allow(datafile_reader).to receive(:get_feature).and_return(feature)
372+
allow(datafile_reader).to receive(:get_matched_force).and_return({
373+
force: nil,
374+
forceIndex: nil
375+
})
376+
allow(datafile_reader).to receive(:get_matched_traffic).and_return(nil)
377+
allow(Featurevisor::Bucketer).to receive(:get_bucket_key).and_return("test.123")
378+
allow(Featurevisor::Bucketer).to receive(:get_bucketed_number).and_return(50)
379+
380+
allow(Featurevisor::Evaluate).to receive(:evaluate).and_call_original
381+
allow(Featurevisor::Evaluate).to receive(:evaluate).with(
382+
hash_including(type: "flag", feature_key: "required-feature")
383+
).and_return({
384+
type: "flag",
385+
feature_key: "required-feature",
386+
enabled: true
387+
})
388+
allow(Featurevisor::Evaluate).to receive(:evaluate).with(
389+
hash_including(type: "variation", feature_key: "required-feature")
390+
).and_return({
391+
type: "variation",
392+
feature_key: "required-feature",
393+
variation_value: 0
394+
})
395+
396+
result = Featurevisor::Evaluate.evaluate(
397+
type: "flag",
398+
feature_key: feature,
399+
context: {},
400+
logger: logger,
401+
hooks_manager: hooks_manager,
402+
datafile_reader: datafile_reader
403+
)
404+
405+
expect(result[:reason]).not_to eq(Featurevisor::EvaluationReason::REQUIRED)
406+
end
407+
322408
it "should handle errors gracefully" do
323409
options = {
324410
type: "flag",

spec/test_command_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "featurevisor"
2+
require "stringio"
23

34
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "bin"))
45
require "cli"
@@ -79,4 +80,65 @@
7980
expect(datafile[:schemaVersion]).to eq("2")
8081
end
8182
end
83+
84+
describe "test execution behavior" do
85+
it "evaluates expectedEvaluations in child assertions" do
86+
command = described_class.new(options)
87+
instance = double("child-instance")
88+
89+
allow(instance).to receive(:is_enabled).and_return(true)
90+
allow(instance).to receive(:get_variation).and_return("control")
91+
allow(instance).to receive(:get_variable).and_return("v")
92+
allow(instance).to receive(:evaluate_flag).and_return({ type: "flag", enabled: false })
93+
allow(instance).to receive(:evaluate_variation).and_return({ type: "variation", variation_value: "control" })
94+
allow(instance).to receive(:evaluate_variable).and_return({ type: "variable", variable_key: "k", variable_value: "v" })
95+
96+
assertion = {
97+
expectedEvaluations: {
98+
flag: {
99+
enabled: true
100+
}
101+
}
102+
}
103+
104+
result = command.send(:run_test_feature_child, assertion, "myFeature", instance, "warn")
105+
expect(result[:has_error]).to be true
106+
expect(result[:errors]).to include("expectedEvaluations.flag.enabled")
107+
end
108+
109+
it "counts missing-datafile assertion as failed" do
110+
command = described_class.new(options)
111+
112+
allow(command).to receive(:exit).and_raise(SystemExit.new(1))
113+
114+
tests = [
115+
{
116+
key: "features/missing.spec",
117+
feature: "foo",
118+
assertions: [
119+
{
120+
description: "missing datafile assertion",
121+
environment: "production",
122+
scope: "browsers"
123+
}
124+
]
125+
}
126+
]
127+
128+
output = StringIO.new
129+
original_stdout = $stdout
130+
$stdout = output
131+
begin
132+
expect do
133+
command.send(:run_tests, tests, {}, {}, "warn", { scopes: [] })
134+
end.to raise_error(SystemExit)
135+
ensure
136+
$stdout = original_stdout
137+
end
138+
139+
expect(output.string).to include("no datafile found for assertion scope/tag/environment combination")
140+
expect(output.string).to include("Test specs: 0 passed, 1 failed")
141+
expect(output.string).to include("Assertions: 0 passed, 1 failed")
142+
end
143+
end
82144
end

0 commit comments

Comments
 (0)