From aaefc52c7ff75b8e4c1491af5f072dc63d0dfb97 Mon Sep 17 00:00:00 2001 From: Fahad Heylaal Date: Sun, 30 Aug 2026 22:28:15 +0200 Subject: [PATCH] feat: variables testing in Ruby --- Gemfile.lock | 2 +- README.md | 2 +- bin/commands/test.rb | 97 +++++++++++++++++++++++++------------ gemfiles/base.gemfile.lock | 2 +- lib/featurevisor/version.rb | 2 +- spec/test_command_spec.rb | 8 +++ 6 files changed, 79 insertions(+), 34 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2dedc92..56369a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - featurevisor (3.0.0) + featurevisor (3.1.0) benchmark (>= 0, < 1) GEM diff --git a/README.md b/README.md index 487e016..8b41090 100644 --- a/README.md +++ b/README.md @@ -946,7 +946,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS - Run `bundle install` - Push commit to `main` branch - Wait for CI to complete -- Tag the release with the same version number, for example `v3.0.0` +- Tag the release with the same version number, for example `v3.1.0` - The workflow verifies that the tag matches the shared version - The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature` diff --git a/bin/commands/test.rb b/bin/commands/test.rb index cdab652..468d0f6 100644 --- a/bin/commands/test.rb +++ b/bin/commands/test.rb @@ -223,18 +223,13 @@ def resolve_datafile_for_assertion(assertion, datafiles_by_key) environment = assertion.key?(:environment) ? assertion[:environment] : false environment = false if environment.nil? - target_key = assertion[:target] ? target_datafile_key(environment, assertion[:target]) : nil - base_key = base_datafile_key(environment) + return datafiles_by_key[target_datafile_key(environment, assertion[:target])] if assertion[:target] - if target_key && datafiles_by_key.key?(target_key) - return datafiles_by_key[target_key] - end - - datafiles_by_key[base_key] + datafiles_by_key[base_datafile_key(environment)] end def create_tester_instance(datafile, level, assertion) - sticky = parse_sticky(assertion[:sticky]) + sticky = parse_sticky(assertion[:stickyFeatures] || assertion[:sticky]) sticky_variables = assertion[:stickyVariables].is_a?(Hash) ? assertion[:stickyVariables] : {} Featurevisor.create_featurevisor( @@ -301,13 +296,26 @@ def run_tests(tests, datafiles_by_key, segments_by_key, level, config) puts "" end - test_result = run_test_feature(assertion, test[:feature], instance, level) + begin + test_result = run_test_feature(assertion, test[:feature], instance, level) + ensure + instance.close + end end elsif test[:variable] datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key) if datafile + if @options.show_datafile + puts "" + puts JSON.pretty_generate(datafile) + puts "" + end instance = create_tester_instance(datafile, level, assertion) - test_result = run_test_variable(assertion, test[:variable], instance) + begin + test_result = run_test_variable(assertion, test[:variable], instance) + ensure + instance.close + end else test_result = { has_error: true, errors: " ✘ no datafile found for assertion target/environment combination\n", duration: 0 } end @@ -358,28 +366,61 @@ def run_tests(tests, datafiles_by_key, segments_by_key, level, config) end def run_test_variable(assertion, variable_key, instance) - context = parse_context(assertion[:context]) + instance.set_context(parse_context(assertion[:context]), true) options = {} options[:default_variable_value] = assertion[:defaultVariableValue] if assertion.key?(:defaultVariableValue) started = Time.now errors = "" - if assertion.key?(:expectedValue) - actual = instance.get_variable(variable_key, context, options) - unless compare_values(actual, assertion[:expectedValue]) - errors += " ✘ expectedValue: expected #{assertion[:expectedValue].inspect} but received #{actual.inspect}\n" + errors += test_variable_expectation(assertion, variable_key, instance, options) + + Array(assertion[:children]).each_with_index do |child_assertion, child_index| + child = instance.spawn( + parse_context(child_assertion[:context]), + sticky_features: parse_sticky(child_assertion[:stickyFeatures]), + sticky_variables: child_assertion[:stickyVariables].is_a?(Hash) ? child_assertion[:stickyVariables] : {} + ) + begin + child_options = {} + if child_assertion.key?(:defaultVariableValue) + child_options[:default_variable_value] = child_assertion[:defaultVariableValue] + end + errors += test_variable_expectation( + child_assertion, + variable_key, + child, + child_options, + "children[#{child_index}]." + ) + ensure + child.close end end + { has_error: !errors.empty?, errors: errors, duration: Time.now - started } + end + + def test_variable_expectation(assertion, variable_key, evaluator, options, prefix = "") + evaluation = evaluator.evaluate_variable(variable_key, {}, options) + errors = "" + if assertion.key?(:expectedValue) && !compare_values(evaluation[:variable_value], assertion[:expectedValue]) + errors += " ✘ #{prefix}expectedValue: expected #{format_test_value(assertion[:expectedValue])} but received #{format_test_value(evaluation[:variable_value])}\n" + end if assertion[:expectedEvaluation].is_a?(Hash) - evaluation = instance.evaluate_variable(variable_key, context, options) assertion[:expectedEvaluation].each do |key, expected| actual = get_evaluation_value(evaluation, key) - errors += " ✘ expectedEvaluation.#{key}: expected #{expected.inspect} but received #{actual.inspect}\n" unless compare_values(actual, expected) + unless compare_values(actual, expected) + errors += " ✘ #{prefix}expectedEvaluation.#{key}: expected #{format_test_value(expected)} but received #{format_test_value(actual)}\n" + end end end + errors + end - { has_error: !errors.empty?, errors: errors, duration: Time.now - started } + def format_test_value(value) + JSON.generate(value) + rescue JSON::GeneratorError + value.inspect end def run_test_feature(assertion, feature_key, instance, level) @@ -518,20 +559,16 @@ def run_test_feature(assertion, feature_key, instance, level) child_context = parse_context(child[:context]) # Create override options for child with sticky values - child_override_options = create_override_options(child) - - # Pass sticky values to child instance - child_instance = instance.spawn(child_context, child_override_options) - - # Set sticky values for child if they exist - # Create a local copy to ensure it's never nil - child_sticky = sticky || {} - if !child_sticky.empty? - child_instance.set_sticky_features(child_sticky, false) + child_instance = instance.spawn( + child_context, + sticky_features: sticky || {} + ) + begin + child_result = run_test_feature_child(child, feature_key, child_instance, level) + ensure + child_instance.close end - child_result = run_test_feature_child(child, feature_key, child_instance, level) - if child_result[:has_error] has_error = true errors += child_result[:errors] diff --git a/gemfiles/base.gemfile.lock b/gemfiles/base.gemfile.lock index 55073cd..eebfb38 100644 --- a/gemfiles/base.gemfile.lock +++ b/gemfiles/base.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - featurevisor (3.0.0) + featurevisor (3.1.0) benchmark (>= 0, < 1) GEM diff --git a/lib/featurevisor/version.rb b/lib/featurevisor/version.rb index cf1748a..2ffd7ff 100644 --- a/lib/featurevisor/version.rb +++ b/lib/featurevisor/version.rb @@ -1,3 +1,3 @@ module Featurevisor - VERSION = "3.0.0" + VERSION = "3.1.0" end diff --git a/spec/test_command_spec.rb b/spec/test_command_spec.rb index 2f69af0..08da624 100644 --- a/spec/test_command_spec.rb +++ b/spec/test_command_spec.rb @@ -28,6 +28,14 @@ datafile = command.send(:resolve_datafile_for_assertion, assertion, datafiles_by_key) expect(datafile[:target]).to eq("checkout") end + + it "does not fall back when a target datafile is missing" do + command = described_class.new(options) + datafiles_by_key = { "production" => { schemaVersion: "2" } } + assertion = { environment: "production", target: "checkout" } + + expect(command.send(:resolve_datafile_for_assertion, assertion, datafiles_by_key)).to be_nil + end end describe "build command generation" do