diff --git a/lib/optimizely/audience.rb b/lib/optimizely/audience.rb index 994f5b27..853feb6d 100644 --- a/lib/optimizely/audience.rb +++ b/lib/optimizely/audience.rb @@ -18,6 +18,7 @@ require 'json' require_relative './custom_attribute_condition_evaluator' require_relative 'condition_tree_evaluator' +require_relative 'helpers/constants' module Optimizely module Audience @@ -35,12 +36,31 @@ def user_in_experiment?(config, experiment, attributes) audience_conditions = experiment['audienceConditions'] || experiment['audienceIds'] + config.logger.log( + Logger::DEBUG, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCES_COMBINED'], + experiment['key'], + audience_conditions + ) + ) + # Return true if there are no audiences - return true if audience_conditions.empty? + if audience_conditions.empty? + config.logger.log( + Logger::INFO, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'], + experiment['key'], + 'TRUE' + ) + ) + return true + end attributes ||= {} - custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes) + custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes, config.logger) evaluate_custom_attr = lambda do |condition| return custom_attr_condition_evaluator.evaluate(condition) @@ -51,13 +71,39 @@ def user_in_experiment?(config, experiment, attributes) return nil unless audience audience_conditions = audience['conditions'] + config.logger.log( + Logger::DEBUG, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['EVALUATING_AUDIENCE'], + audience_id, + audience_conditions + ) + ) + audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String) - return ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr) + result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr) + result_str = result.nil? ? 'UNKNOWN' : result.to_s.upcase + config.logger.log( + Logger::INFO, + format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT'], audience_id, result_str) + ) + result end - return true if ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience) + eval_result = ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience) + + eval_result ||= false + + config.logger.log( + Logger::INFO, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['AUDIENCE_EVALUATION_RESULT_COMBINED'], + experiment['key'], + eval_result.to_s.upcase + ) + ) - false + eval_result end end end diff --git a/lib/optimizely/custom_attribute_condition_evaluator.rb b/lib/optimizely/custom_attribute_condition_evaluator.rb index dbcd4435..d1bd0230 100644 --- a/lib/optimizely/custom_attribute_condition_evaluator.rb +++ b/lib/optimizely/custom_attribute_condition_evaluator.rb @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +require_relative 'helpers/constants' require_relative 'helpers/validator' module Optimizely @@ -38,8 +39,9 @@ class CustomAttributeConditionEvaluator attr_reader :user_attributes - def initialize(user_attributes) + def initialize(user_attributes, logger) @user_attributes = user_attributes + @logger = logger end def evaluate(leaf_condition) @@ -51,11 +53,47 @@ def evaluate(leaf_condition) # Returns boolean if the given user attributes match/don't match the given conditions, # nil if the given conditions can't be evaluated. - return nil unless leaf_condition['type'] == CUSTOM_ATTRIBUTE_CONDITION_TYPE + unless leaf_condition['type'] == CUSTOM_ATTRIBUTE_CONDITION_TYPE + @logger.log( + Logger::WARN, + format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_TYPE'], leaf_condition) + ) + return nil + end condition_match = leaf_condition['match'] || EXACT_MATCH_TYPE - return nil unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match) + if !@user_attributes.key?(leaf_condition['name']) && condition_match != EXISTS_MATCH_TYPE + @logger.log( + Logger::DEBUG, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['MISSING_ATTRIBUTE_VALUE'], + leaf_condition, + leaf_condition['name'] + ) + ) + return nil + end + + if @user_attributes[leaf_condition['name']].nil? && condition_match != EXISTS_MATCH_TYPE + @logger.log( + Logger::DEBUG, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['NULL_ATTRIBUTE_VALUE'], + leaf_condition, + leaf_condition['name'] + ) + ) + return nil + end + + unless EVALUATORS_BY_MATCH_TYPE.include?(condition_match) + @logger.log( + Logger::WARN, + format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_MATCH_TYPE'], leaf_condition) + ) + return nil + end send(EVALUATORS_BY_MATCH_TYPE[condition_match], leaf_condition) end @@ -73,13 +111,40 @@ def exact_evaluator(condition) user_provided_value = @user_attributes[condition['name']] - if user_provided_value.is_a?(Numeric) && condition_value.is_a?(Numeric) - return true if condition_value.to_f == user_provided_value.to_f + if !value_type_valid_for_exact_conditions?(condition_value) || + (condition_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(condition_value)) + @logger.log( + Logger::WARN, + format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition) + ) + return nil end - return nil if !value_valid_for_exact_conditions?(user_provided_value) || - !value_valid_for_exact_conditions?(condition_value) || - !Helpers::Validator.same_types?(condition_value, user_provided_value) + if !value_type_valid_for_exact_conditions?(user_provided_value) || + !Helpers::Validator.same_types?(condition_value, user_provided_value) + @logger.log( + Logger::WARN, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'], + condition, + user_provided_value.class, + condition['name'] + ) + ) + return nil + end + + if user_provided_value.is_a?(Numeric) && !Helpers::Validator.finite_number?(user_provided_value) + @logger.log( + Logger::WARN, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'], + condition, + condition['name'] + ) + ) + return nil + end condition_value == user_provided_value end @@ -103,8 +168,7 @@ def greater_than_evaluator(condition) condition_value = condition['value'] user_provided_value = @user_attributes[condition['name']] - return nil if !Helpers::Validator.finite_number?(user_provided_value) || - !Helpers::Validator.finite_number?(condition_value) + return nil unless valid_numeric_values?(user_provided_value, condition_value, condition) user_provided_value > condition_value end @@ -118,8 +182,7 @@ def less_than_evaluator(condition) condition_value = condition['value'] user_provided_value = @user_attributes[condition['name']] - return nil if !Helpers::Validator.finite_number?(user_provided_value) || - !Helpers::Validator.finite_number?(condition_value) + return nil unless valid_numeric_values?(user_provided_value, condition_value, condition) user_provided_value < condition_value end @@ -133,20 +196,78 @@ def substring_evaluator(condition) condition_value = condition['value'] user_provided_value = @user_attributes[condition['name']] - return nil unless user_provided_value.is_a?(String) && condition_value.is_a?(String) + unless condition_value.is_a?(String) + @logger.log( + Logger::WARN, + format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition) + ) + return nil + end + + unless user_provided_value.is_a?(String) + @logger.log( + Logger::WARN, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'], + condition, + user_provided_value.class, + condition['name'] + ) + ) + return nil + end user_provided_value.include? condition_value end private - def value_valid_for_exact_conditions?(value) - # Returns true if the value is valid for exact conditions. Valid values include - # strings, booleans, and numbers that aren't NaN, -Infinity, or Infinity. + def valid_numeric_values?(user_value, condition_value, condition) + # Returns true if user and condition values are valid numeric. + # false otherwise. + + unless Helpers::Validator.finite_number?(condition_value) + @logger.log( + Logger::WARN, + format(Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNKNOWN_CONDITION_VALUE'], condition) + ) + return false + end + + unless user_value.is_a?(Numeric) + @logger.log( + Logger::WARN, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['UNEXPECTED_TYPE'], + condition, + user_value.class, + condition['name'] + ) + ) + return false + end - return Helpers::Validator.finite_number?(value) if value.is_a? Numeric + unless Helpers::Validator.finite_number?(user_value) + @logger.log( + Logger::WARN, + format( + Helpers::Constants::AUDIENCE_EVALUATION_LOGS['INFINITE_ATTRIBUTE_VALUE'], + condition, + condition['name'] + ) + ) + return false + end + + true + end + + def value_type_valid_for_exact_conditions?(value) + # Returns true if the value is valid for exact conditions. Valid values include + # strings or booleans or is a number. + # false otherwise. - (Helpers::Validator.boolean? value) || (value.is_a? String) + (Helpers::Validator.boolean? value) || (value.is_a? String) || value.is_a?(Numeric) end end end diff --git a/lib/optimizely/helpers/constants.rb b/lib/optimizely/helpers/constants.rb index 2d17165d..73e70b81 100644 --- a/lib/optimizely/helpers/constants.rb +++ b/lib/optimizely/helpers/constants.rb @@ -331,6 +331,27 @@ module Constants ATTRIBUTE_VALID_TYPES = [FalseClass, Float, Integer, String, TrueClass].freeze FINITE_NUMBER_LIMIT = 2**53 + + AUDIENCE_EVALUATION_LOGS = { + 'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.", + 'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.", + 'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.", + 'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.", + 'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \ + "for user attribute '%s' is not in the range [-2^53, +2^53].", + 'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \ + "was passed for user attribute '%s'.", + 'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \ + "for user attribute '%s'.", + 'UNEXPECTED_TYPE' => "Audience condition %s evaluated as UNKNOWN because a value of type '%s' " \ + "was passed for user attribute '%s'.", + 'UNKNOWN_CONDITION_TYPE' => 'Audience condition %s uses an unknown condition type. You may need ' \ + 'to upgrade to a newer release of the Optimizely SDK.', + 'UNKNOWN_CONDITION_VALUE' => 'Audience condition %s has an unsupported condition value. You may need ' \ + 'to upgrade to a newer release of the Optimizely SDK.', + 'UNKNOWN_MATCH_TYPE' => 'Audience condition %s uses an unknown match type. You may need ' \ + 'to upgrade to a newer release of the Optimizely SDK.' + }.freeze end end end diff --git a/lib/optimizely/helpers/validator.rb b/lib/optimizely/helpers/validator.rb index e992fadb..dc05f64e 100644 --- a/lib/optimizely/helpers/validator.rb +++ b/lib/optimizely/helpers/validator.rb @@ -153,9 +153,11 @@ def boolean?(value) def same_types?(value_1, value_2) # Returns true if given values are of same types. # false otherwise. + # Numeric values are considered as same type. + + return true if value_1.is_a?(Numeric) && value_2.is_a?(Numeric) return true if boolean?(value_1) && boolean?(value_2) - return true if value_1.is_a?(Integer) && value_2.is_a?(Integer) value_1.class == value_2.class end diff --git a/spec/audience_spec.rb b/spec/audience_spec.rb index 19e01ab9..ed310e6d 100644 --- a/spec/audience_spec.rb +++ b/spec/audience_spec.rb @@ -16,130 +16,125 @@ # limitations under the License. # require 'spec_helper' - describe Optimizely::Audience do - before(:context) do - @config_body = OptimizelySpec::VALID_CONFIG_BODY - @config_body_json = OptimizelySpec::VALID_CONFIG_BODY_JSON - @config_typed_audience_json = JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES) - end - - before(:example) do - @project_instance = Optimizely::Project.new(@config_body_json) - @project_typed_audience_instance = Optimizely::Project.new(@config_typed_audience_json) - end + let(:config_body_JSON) { OptimizelySpec::VALID_CONFIG_BODY_JSON } + let(:config_typed_audience_JSON) { JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES) } + let(:error_handler) { Optimizely::NoOpErrorHandler.new } + let(:spy_logger) { spy('logger') } + let(:config) { Optimizely::ProjectConfig.new(config_body_JSON, spy_logger, error_handler) } + let(:typed_audience_config) { Optimizely::ProjectConfig.new(config_typed_audience_JSON, spy_logger, error_handler) } it 'should return true for user_in_experiment? when experiment is using no audience' do user_attributes = {} # Both Audience Ids and Conditions are Empty - experiment = @project_instance.config.experiment_key_map['test_experiment'] + experiment = config.experiment_key_map['test_experiment'] experiment['audienceIds'] = [] experiment['audienceConditions'] = [] - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes)).to be true # Audience Ids exist but Audience Conditions is Empty - experiment = @project_instance.config.experiment_key_map['test_experiment'] + experiment = config.experiment_key_map['test_experiment'] experiment['audienceIds'] = ['11154'] experiment['audienceConditions'] = [] - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes)).to be true # Audience Ids is Empty and Audience Conditions is nil - experiment = @project_instance.config.experiment_key_map['test_experiment'] + experiment = config.experiment_key_map['test_experiment'] experiment['audienceIds'] = [] experiment['audienceConditions'] = nil - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes)).to be true end it 'should pass conditions when audience conditions exist else audienceIds are passed' do user_attributes = {'test_attribute' => 'test_value_1'} - experiment = @project_instance.config.experiment_key_map['test_experiment'] + experiment = config.experiment_key_map['test_experiment'] experiment['audienceIds'] = ['11154'] allow(Optimizely::ConditionTreeEvaluator).to receive(:evaluate) # Both Audience Ids and Conditions exist experiment['audienceConditions'] = ['and', %w[or 3468206642 3988293898], %w[or 3988293899 3468206646 3468206647 3468206644 3468206643]] - Optimizely::Audience.user_in_experiment?(@project_instance.config, + Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes) expect(Optimizely::ConditionTreeEvaluator).to have_received(:evaluate).with(experiment['audienceConditions'], any_args).once # Audience Ids exist but Audience Conditions is nil experiment['audienceConditions'] = nil - Optimizely::Audience.user_in_experiment?(@project_instance.config, + Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes) expect(Optimizely::ConditionTreeEvaluator).to have_received(:evaluate).with(experiment['audienceIds'], any_args).once end it 'should return false for user_in_experiment? if there are audiences but nil or empty attributes' do - experiment = @project_instance.config.experiment_key_map['test_experiment_with_audience'] + experiment = config.experiment_key_map['test_experiment_with_audience'] allow(Optimizely::CustomAttributeConditionEvaluator).to receive(:new).and_call_original # attributes set to empty dict - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, {})).to be false # attributes set to nil - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, nil)).to be false # asserts nil attributes default to empty dict - expect(Optimizely::CustomAttributeConditionEvaluator).to have_received(:new).with({}).twice + expect(Optimizely::CustomAttributeConditionEvaluator).to have_received(:new).with({}, config.logger).twice end it 'should return true for user_in_experiment? when condition tree evaluator returns true' do - experiment = @project_instance.config.experiment_key_map['test_experiment'] + experiment = config.experiment_key_map['test_experiment'] user_attributes = { 'test_attribute' => 'test_value_1' } allow(Optimizely::ConditionTreeEvaluator).to receive(:evaluate).and_return(true) - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes)).to be true end it 'should return false for user_in_experiment? when condition tree evaluator returns false or nil' do - experiment = @project_instance.config.experiment_key_map['test_experiment_with_audience'] + experiment = config.experiment_key_map['test_experiment_with_audience'] user_attributes = { 'browser_type' => 'firefox' } # condition tree evaluator returns nil allow(Optimizely::ConditionTreeEvaluator).to receive(:evaluate).and_return(nil) - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes)).to be false # condition tree evaluator returns false allow(Optimizely::ConditionTreeEvaluator).to receive(:evaluate).and_return(false) - expect(Optimizely::Audience.user_in_experiment?(@project_instance.config, + expect(Optimizely::Audience.user_in_experiment?(config, experiment, user_attributes)).to be false end it 'should correctly evaluate audience Ids and call custom attribute evaluator for leaf nodes' do - experiment = @project_instance.config.experiment_key_map['test_experiment_with_audience'] + experiment = config.experiment_key_map['test_experiment_with_audience'] user_attributes = { 'browser_type' => 'firefox' } experiment['audienceIds'] = %w[11154 11155] experiment['audienceConditions'] = nil - audience_11154 = @project_instance.config.get_audience_from_id('11154') - audience_11155 = @project_instance.config.get_audience_from_id('11155') + audience_11154 = config.get_audience_from_id('11154') + audience_11155 = config.get_audience_from_id('11155') audience_11154_condition = JSON.parse(audience_11154['conditions'])[1][1][1] audience_11155_condition = JSON.parse(audience_11155['conditions'])[1][1][1] - customer_attr = Optimizely::CustomAttributeConditionEvaluator.new(user_attributes) + customer_attr = Optimizely::CustomAttributeConditionEvaluator.new(user_attributes, spy_logger) allow(customer_attr).to receive(:exact_evaluator) customer_attr.evaluate(audience_11154_condition) customer_attr.evaluate(audience_11155_condition) @@ -149,21 +144,25 @@ end it 'should correctly evaluate audienceConditions and call custom attribute evaluator for leaf nodes' do - experiment = @project_typed_audience_instance.config.get_experiment_from_key('audience_combinations_experiment') + user_attributes = { + 'house' => 'Gryffindor', + 'lasers' => 45.5 + } + experiment = typed_audience_config.get_experiment_from_key('audience_combinations_experiment') experiment['audienceIds'] = [] experiment['audienceConditions'] = ['or', %w[or 3468206642 3988293898], %w[or 3988293899 3468206646]] - audience_3468206642 = @project_typed_audience_instance.config.get_audience_from_id('3468206642') - audience_3988293898 = @project_typed_audience_instance.config.get_audience_from_id('3988293898') - audience_3988293899 = @project_typed_audience_instance.config.get_audience_from_id('3988293899') - audience_3468206646 = @project_typed_audience_instance.config.get_audience_from_id('3468206646') + audience_3468206642 = typed_audience_config.get_audience_from_id('3468206642') + audience_3988293898 = typed_audience_config.get_audience_from_id('3988293898') + audience_3988293899 = typed_audience_config.get_audience_from_id('3988293899') + audience_3468206646 = typed_audience_config.get_audience_from_id('3468206646') audience_3468206642_condition = JSON.parse(audience_3468206642['conditions'])[1][1][1] audience_3988293898_condition = audience_3988293898['conditions'][1][1][1] audience_3988293899_condition = audience_3988293899['conditions'][1][1][1] audience_3468206646_condition = audience_3468206646['conditions'][1][1][1] - customer_attr = Optimizely::CustomAttributeConditionEvaluator.new({}) + customer_attr = Optimizely::CustomAttributeConditionEvaluator.new(user_attributes, spy_logger) allow(customer_attr).to receive(:exact_evaluator) allow(customer_attr).to receive(:substring_evaluator) allow(customer_attr).to receive(:exists_evaluator) @@ -179,11 +178,14 @@ end it 'should correctly evaluate leaf node in audienceConditions' do - experiment = @project_typed_audience_instance.config.get_experiment_from_key('audience_combinations_experiment') + user_attributes = { + 'browser' => 'chrome' + } + experiment = typed_audience_config.get_experiment_from_key('audience_combinations_experiment') experiment['audienceConditions'] = '3468206645' - customer_attr = Optimizely::CustomAttributeConditionEvaluator.new({}) + customer_attr = Optimizely::CustomAttributeConditionEvaluator.new(user_attributes, spy_logger) - audience_3468206645 = @project_typed_audience_instance.config.get_audience_from_id('3468206645') + audience_3468206645 = typed_audience_config.get_audience_from_id('3468206645') audience_3468206645_condition1 = audience_3468206645['conditions'][1][1][1] audience_3468206645_condition2 = audience_3468206645['conditions'][1][1][2] allow(customer_attr).to receive(:exact_evaluator) @@ -193,4 +195,129 @@ expect(customer_attr).to have_received(:exact_evaluator).with(audience_3468206645_condition1).once expect(customer_attr).to have_received(:exact_evaluator).with(audience_3468206645_condition2).once end + + it 'should return nil when audience not found' do + experiment = config.experiment_key_map['test_experiment_with_audience'] + user_attributes = { + 'browser_type' => 5.5 + } + experiment['audienceIds'] = %w[11110] + + expect(Optimizely::Audience.user_in_experiment?(config, + experiment, + user_attributes)).to be false + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Evaluating audiences for experiment 'test_experiment_with_audience': " + '["11110"].' + ) + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audiences for experiment 'test_experiment_with_audience' collectively evaluated to FALSE." + ) + end + + it 'should log and return false for user_in_experiment? evaluates audienceIds' do + experiment = config.experiment_key_map['test_experiment_with_audience'] + user_attributes = { + 'browser_type' => 5.5 + } + experiment['audienceIds'] = %w[11154 11155] + experiment['audienceConditions'] = nil + + expect(Optimizely::Audience.user_in_experiment?(config, + experiment, + user_attributes)).to be false + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Evaluating audiences for experiment 'test_experiment_with_audience': " + '["11154", "11155"].' + ) + + # audience_11154 + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Starting to evaluate audience '11154' with conditions: "\ + '["and", ["or", ["or", {"name": "browser_type", "type": "custom_attribute", "value": "firefox"}]]].' + ) + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audience '11154' evaluated to UNKNOWN." + ) + + # audience_11155 + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Starting to evaluate audience '11155' with conditions: "\ + '["and", ["or", ["or", {"name": "browser_type", "type": "custom_attribute", "value": "chrome"}]]].' + ) + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audience '11155' evaluated to UNKNOWN." + ) + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audiences for experiment 'test_experiment_with_audience' collectively evaluated to FALSE." + ) + end + + it 'should log and return true for user_in_experiment? evaluates audienceConditions' do + user_attributes = { + 'lasers' => 45.5 + } + experiment = typed_audience_config.get_experiment_from_key('audience_combinations_experiment') + experiment['audienceIds'] = [] + experiment['audienceConditions'] = ['or', %w[or 3468206647 3988293898 3468206646]] + + Optimizely::Audience.user_in_experiment?(typed_audience_config, experiment, user_attributes) + + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Evaluating audiences for experiment 'audience_combinations_experiment': "\ + '["or", ["or", "3468206647", "3988293898", "3468206646"]].' + ).ordered # Order: 0 + + # audience_3468206647 + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Starting to evaluate audience '3468206647' with conditions: "\ + '["and", ["or", ["or", {"name"=>"lasers", "type"=>"custom_attribute", "match"=>"gt", "value"=>70}]]].' + ).ordered # Order: 1 + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audience '3468206647' evaluated to FALSE." + ).ordered # Order: 2 + + # audience_3988293898 + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Starting to evaluate audience '3988293898' with conditions: "\ + '["and", ["or", ["or", {"name"=>"house", "type"=>"custom_attribute", "match"=>"substring", "value"=>"Slytherin"}]]].' + ).ordered # Order: 3 + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audience '3988293898' evaluated to UNKNOWN." + ).ordered # Order: 4 + + # audience_3468206646 + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Starting to evaluate audience '3468206646' with conditions: "\ + '["and", ["or", ["or", {"name"=>"lasers", "type"=>"custom_attribute", "match"=>"exact", "value"=>45.5}]]].' + ).ordered # Order: 5 + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audience '3468206646' evaluated to TRUE." + ).ordered # Order: 6 + + expect(spy_logger).to have_received(:log).once.with( + Logger::INFO, + "Audiences for experiment 'audience_combinations_experiment' collectively evaluated to TRUE." + ).ordered # Order: 7 + end end diff --git a/spec/custom_attribute_condition_evaluator_spec.rb b/spec/custom_attribute_condition_evaluator_spec.rb index c21c901a..6ff097c7 100644 --- a/spec/custom_attribute_condition_evaluator_spec.rb +++ b/spec/custom_attribute_condition_evaluator_spec.rb @@ -18,15 +18,18 @@ require 'json' require 'spec_helper' require 'optimizely/helpers/validator' +require 'optimizely/logger' describe Optimizely::CustomAttributeConditionEvaluator do + let(:spy_logger) { spy('logger') } + it 'should return true when the attributes pass the audience conditions and no match type is provided' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('browser_type' => 'safari') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'browser_type' => 'safari'}, spy_logger) expect(condition_evaluator.evaluate('name' => 'browser_type', 'type' => 'custom_attribute', 'value' => 'safari')).to be true end it 'should return false when the attributes pass the audience conditions and no match type is provided' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('browser_type' => 'firefox') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'browser_type' => 'firefox'}, spy_logger) expect(condition_evaluator.evaluate('name' => 'browser_type', 'type' => 'custom_attribute', 'value' => 'safari')).to be false end @@ -37,7 +40,7 @@ 'num_users' => 10, 'pi_value' => 3.14 } - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new(user_attributes) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new(user_attributes, spy_logger) expect(condition_evaluator.evaluate('name' => 'browser_type', 'type' => 'custom_attribute', 'value' => 'safari')).to be true expect(condition_evaluator.evaluate('name' => 'is_firefox', 'type' => 'custom_attribute', 'value' => true)).to be true @@ -45,19 +48,39 @@ expect(condition_evaluator.evaluate('name' => 'pi_value', 'type' => 'custom_attribute', 'value' => 3.14)).to be true end - it 'should return nil when condition has an invalid type property' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('weird_condition' => 'bye') - expect(condition_evaluator.evaluate('match' => 'exact', 'name' => 'weird_condition', 'type' => 'weird', 'value' => 'hi')).to eq(nil) + it 'should log and return nil when condition has an invalid type property' do + condition = {'match' => 'exact', 'name' => 'weird_condition', 'type' => 'weird', 'value' => 'hi'} + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'weird_condition' => 'bye'}, spy_logger) + expect(condition_evaluator.evaluate(condition)).to eq(nil) + expect(spy_logger).to have_received(:log).exactly(1).times + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{condition} uses an unknown condition type. You may need to upgrade to a newer release of " \ + 'the Optimizely SDK.' + ) end - it 'should return nil when condition has no type property' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('weird_condition' => 'bye') - expect(condition_evaluator.evaluate('match' => 'exact', 'name' => 'weird_condition', 'value' => 'hi')).to eq(nil) + it 'should log and return nil when condition has no type property' do + condition = {'match' => 'exact', 'name' => 'weird_condition', 'value' => 'hi'} + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'weird_condition' => 'bye'}, spy_logger) + expect(condition_evaluator.evaluate(condition)).to eq(nil) + expect(spy_logger).to have_received(:log).exactly(1).times + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{condition} uses an unknown condition type. You may need to upgrade to a newer release of " \ + 'the Optimizely SDK.' + ) end - it 'should return nil when condition has an invalid match property' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('weird_condition' => 'bye') - expect(condition_evaluator.evaluate('match' => 'invalid', 'name' => 'weird_condition', 'type' => 'custom_attribute', 'value' => 'bye')).to eq(nil) + it 'should log and return nil when condition has an invalid match property' do + condition = {'match' => 'invalid', 'name' => 'browser_type', 'type' => 'custom_attribute', 'value' => 'chrome'} + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'browser_type' => 'chrome'}, spy_logger) + expect(condition_evaluator.evaluate(condition)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{condition} uses an unknown match type. You may need to upgrade to a newer release " \ + 'of the Optimizely SDK.' + ) end describe 'exists match type' do @@ -66,30 +89,31 @@ end it 'should return false if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be false + expect(spy_logger).not_to have_received(:log) end it 'should return false if the user-provided value is nil' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => nil) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => nil}, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be false end it 'should return true if the user-provided value is a string' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 'test') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 'test'}, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be true end it 'should return true if the user-provided value is a number' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 10}, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be true - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 10.0}, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be true end it 'should return true if the user-provided value is a boolean' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => false) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => false}, spy_logger) expect(condition_evaluator.evaluate(@exists_conditions)).to be true end end @@ -101,23 +125,51 @@ end it 'should return true if the user-provided value is equal to the condition value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('location' => 'san francisco') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'location' => 'san francisco'}, spy_logger) expect(condition_evaluator.evaluate(@exact_string_conditions)).to be true end it 'should return false if the user-provided value is not equal to the condition value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('location' => 'new york') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'location' => 'new york'}, spy_logger) expect(condition_evaluator.evaluate(@exact_string_conditions)).to be false end - it 'should return nil if the user-provided value is of a different type than the condition value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('location' => false) + it 'should log and return nil if the user-provided value is of a different type than the condition value' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'location' => false}, spy_logger) expect(condition_evaluator.evaluate(@exact_string_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@exact_string_conditions} evaluated as UNKNOWN because a value of type '#{false.class}' was passed for user attribute 'location'." + ) end - it 'should return nil if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + it 'should log and return nil if there is no user-provided value' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@exact_string_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@exact_string_conditions} evaluated as UNKNOWN because no value was passed for user attribute 'location'." + ) + end + + it 'should log and return nil if the user-provided value is of a unexpected type' do + # attribute value: nil + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'location' => []}, spy_logger) + expect(condition_evaluator.evaluate(@exact_string_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@exact_string_conditions} evaluated as UNKNOWN because a value of type 'Array' was " \ + "passed for user attribute 'location'." + ) + + # attribute value: empty hash + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'location' => {}}, spy_logger) + expect(condition_evaluator.evaluate(@exact_string_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@exact_string_conditions} evaluated as UNKNOWN because a value of type 'Hash' was " \ + "passed for user attribute 'location'." + ) end end @@ -129,68 +181,54 @@ it 'should return true if the user-provided value is equal to the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 100) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => 100}, spy_logger) expect(condition_evaluator.evaluate(@exact_integer_conditions)).to be true expect(condition_evaluator.evaluate(@exact_float_conditions)).to be true # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 100.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => 100.0}, spy_logger) expect(condition_evaluator.evaluate(@exact_integer_conditions)).to be true expect(condition_evaluator.evaluate(@exact_float_conditions)).to be true end it 'should return false if the user-provided value is not equal to the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 101) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => 101}, spy_logger) expect(condition_evaluator.evaluate(@exact_integer_conditions)).to be false # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 100.1) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => 100.1}, spy_logger) expect(condition_evaluator.evaluate(@exact_float_conditions)).to be false end it 'should return nil if the user-provided value is of a different type than the condition value' do - # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 101) - expect(condition_evaluator.evaluate(@exact_float_conditions)).to eq(nil) - - # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 100.1) - expect(condition_evaluator.evaluate(@exact_integer_conditions)).to eq(nil) - # user-provided boolean value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => false) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => false}, spy_logger) expect(condition_evaluator.evaluate(@exact_integer_conditions)).to eq(nil) expect(condition_evaluator.evaluate(@exact_float_conditions)).to eq(nil) end it 'should return nil if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@exact_integer_conditions)).to eq(nil) expect(condition_evaluator.evaluate(@exact_float_conditions)).to eq(nil) end - it 'should return nil when finite_number? returns false for provided arguments' do - # Returns false for user attribute value - allow(Optimizely::Helpers::Validator).to receive(:finite_number?).once.with(10).and_return(false) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 10) - expect(condition_evaluator.evaluate(@exact_integer_conditions)).to be nil - # finite_number? should not be called with condition value as user attribute value is failed - expect(Optimizely::Helpers::Validator).not_to have_received(:finite_number?).with(100) - - # Returns false for condition value - @exact_integer_conditions['value'] = 101 - allow(Optimizely::Helpers::Validator).to receive(:finite_number?).twice.and_return(true, false) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 100) - expect(condition_evaluator.evaluate(@exact_integer_conditions)).to be nil - # finite_number? should be called with condition value as it returns true for user attribute value - expect(Optimizely::Helpers::Validator).to have_received(:finite_number?).with(101) + it 'should return nil when user-provided value is infinite' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => 1 / 0.0}, spy_logger) + expect(condition_evaluator.evaluate(@exact_float_conditions)).to be nil + + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@exact_float_conditions} evaluated to UNKNOWN because the number value for " \ + "user attribute 'sum' is not in the range [-2^53, +2^53]." + ) end it 'should not return nil when finite_number? returns true for provided arguments' do @exact_integer_conditions['value'] = 10 allow(Optimizely::Helpers::Validator).to receive(:finite_number?).twice.and_return(true, true) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('sum' => 10) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'sum' => 10}, spy_logger) expect(condition_evaluator.evaluate(@exact_integer_conditions)).not_to be_nil end end @@ -201,27 +239,27 @@ end it 'should return true if the user-provided value is equal to the condition value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('boolean' => false) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'boolean' => false}, spy_logger) expect(condition_evaluator.evaluate(@exact_boolean_conditions)).to be true end it 'should return false if the user-provided value is not equal to the condition value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('boolean' => true) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'boolean' => true}, spy_logger) expect(condition_evaluator.evaluate(@exact_boolean_conditions)).to be false end it 'should return nil if the user-provided value is of a different type than the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('boolean' => 10) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'boolean' => 10}, spy_logger) expect(condition_evaluator.evaluate(@exact_boolean_conditions)).to eq(nil) # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('boolean' => 10.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'boolean' => 10.0}, spy_logger) expect(condition_evaluator.evaluate(@exact_boolean_conditions)).to eq(nil) end it 'should return nil if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@exact_boolean_conditions)).to eq(nil) end end @@ -233,28 +271,62 @@ end it 'should return true if the condition value is a substring of the user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('text' => 'This is a test message!') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => 'This is a test message!'}, spy_logger) expect(condition_evaluator.evaluate(@substring_conditions)).to be true end it 'should return false if the user-provided value is not a substring of the condition value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('text' => 'Not found!') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => 'Not found!'}, spy_logger) expect(condition_evaluator.evaluate(@substring_conditions)).to be false end it 'should return nil if the user-provided value is not a string' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('text' => 10) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => 10}, spy_logger) expect(condition_evaluator.evaluate(@substring_conditions)).to eq(nil) # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('text' => 10.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => 10.0}, spy_logger) expect(condition_evaluator.evaluate(@substring_conditions)).to eq(nil) end - it 'should return nil if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + it 'should log and return nil if there is no user-provided value' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@substring_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@substring_conditions} evaluated as UNKNOWN because no value was passed for user attribute 'text'." + ) + end + + it 'should log and return nil if there user-provided value is of a unexpected type' do + # attribute value: nil + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => nil}, spy_logger) + expect(condition_evaluator.evaluate(@substring_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@substring_conditions} evaluated to UNKNOWN because a nil value was passed for user attribute 'text'." + ) + + # attribute value: empty hash + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => {}}, spy_logger) + expect(condition_evaluator.evaluate(@substring_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@substring_conditions} evaluated as UNKNOWN because a value of type 'Hash' was " \ + "passed for user attribute 'text'." + ) + end + + it 'should log and return nil when condition value is invalid' do + @substring_conditions['value'] = 5 + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'text' => 'This is a test message!'}, spy_logger) + expect(condition_evaluator.evaluate(@substring_conditions)).to be_nil + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@substring_conditions} has an unsupported condition value. You may need to upgrade "\ + 'to a newer release of the Optimizely SDK.' + ) end end @@ -266,75 +338,108 @@ it 'should return true if the user-provided value is greater than the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 12) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 12}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be true expect(condition_evaluator.evaluate(@gt_float_conditions)).to be true # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 12.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 12.0}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be true expect(condition_evaluator.evaluate(@gt_float_conditions)).to be true end it 'should return false if the user-provided value is equal to condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 10}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@gt_float_conditions)).to be false # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 10.0}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@gt_float_conditions)).to be false end it 'should return true if the user-provided value is less than the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 8) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 8}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@gt_float_conditions)).to be false # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 8.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 8.0}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@gt_float_conditions)).to be false end it 'should return nil if the user-provided value is not a number' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 'test') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 'test'}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to eq(nil) expect(condition_evaluator.evaluate(@gt_float_conditions)).to eq(nil) end - it 'should return nil if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + it 'should log and return nil if there is no user-provided value' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to eq(nil) expect(condition_evaluator.evaluate(@gt_float_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@gt_integer_conditions} evaluated as UNKNOWN because no value was passed for user attribute 'input_value'." + ) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@gt_float_conditions} evaluated as UNKNOWN because no value was passed for user attribute 'input_value'." + ) end - it 'should return nil when finite_number? returns false for provided arguments' do - # Returns false for user attribute value - allow(Optimizely::Helpers::Validator).to receive(:finite_number?).once.with(5).and_return(false) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 5) - expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be nil - # finite_number? should not be called with condition value as user attribute value is failed - expect(Optimizely::Helpers::Validator).not_to have_received(:finite_number?).with(10) + it 'should log and return nil if there user-provided value is of a unexpected type' do + # attribute value: nil + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => nil}, spy_logger) + expect(condition_evaluator.evaluate(@gt_integer_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@gt_integer_conditions} evaluated to UNKNOWN because a nil value was passed for " \ + "user attribute 'input_value'." + ) + + # attribute value: empty hash + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => {}}, spy_logger) + expect(condition_evaluator.evaluate(@gt_integer_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@gt_integer_conditions} evaluated as UNKNOWN because a value of type 'Hash' was " \ + "passed for user attribute 'input_value'." + ) + end - # Returns false for condition value - @gt_integer_conditions['value'] = 95 - allow(Optimizely::Helpers::Validator).to receive(:finite_number?).twice.and_return(true, false) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10) + it 'should return nil when user-provided value is infinite' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 1 / 0.0}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be nil - # finite_number? should be called with condition value as it returns true for user attribute value - expect(Optimizely::Helpers::Validator).to have_received(:finite_number?).with(95) + + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@gt_integer_conditions} evaluated to UNKNOWN because the number value for " \ + "user attribute 'input_value' is not in the range [-2^53, +2^53]." + ) end it 'should not return nil when finite_number? returns true for provided arguments' do @gt_integer_conditions['value'] = 81 allow(Optimizely::Helpers::Validator).to receive(:finite_number?).twice.and_return(true, true) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 51) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 51}, spy_logger) expect(condition_evaluator.evaluate(@gt_integer_conditions)).not_to be_nil end + + it 'should log and return nil when condition value is infinite' do + @gt_integer_conditions['value'] = 1 / 0.0 + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 51}, spy_logger) + expect(condition_evaluator.evaluate(@gt_integer_conditions)).to be_nil + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@gt_integer_conditions} has an unsupported condition value. You may need to upgrade "\ + 'to a newer release of the Optimizely SDK.' + ) + end end describe 'less than match type' do @@ -345,74 +450,107 @@ it 'should return true if the user-provided value is less than the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 8) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 8}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be true expect(condition_evaluator.evaluate(@lt_float_conditions)).to be true # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 8.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 8.0}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be true expect(condition_evaluator.evaluate(@lt_float_conditions)).to be true end it 'should return false if the user-provided value is equal to condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 10}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@lt_float_conditions)).to be false # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 10.0}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@lt_float_conditions)).to be false end it 'should return false if the user-provided value is greater than the condition value' do # user-provided integer value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 12) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 12}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@lt_float_conditions)).to be false # user-provided float value - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 12.0) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 12.0}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be false expect(condition_evaluator.evaluate(@lt_float_conditions)).to be false end it 'should return nil if the user-provided value is not a number' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 'test') + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 'test'}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to eq(nil) expect(condition_evaluator.evaluate(@lt_float_conditions)).to eq(nil) end - it 'should return nil if there is no user-provided value' do - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}) + it 'should log and return nil if there is no user-provided value' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to eq(nil) expect(condition_evaluator.evaluate(@lt_float_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@lt_integer_conditions} evaluated as UNKNOWN because no value was passed for user attribute 'input_value'." + ) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@lt_float_conditions} evaluated as UNKNOWN because no value was passed for user attribute 'input_value'." + ) end - it 'should return nil when finite_number? returns false for provided arguments' do - # Returns false for user attribute value - allow(Optimizely::Helpers::Validator).to receive(:finite_number?).once.with(15).and_return(false) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 15) - expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be nil - # finite_number? should not be called with condition value as user attribute value is failed - expect(Optimizely::Helpers::Validator).not_to have_received(:finite_number?).with(10) + it 'should log and return nil if there user-provided value is of a unexpected type' do + # attribute value: nil + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => nil}, spy_logger) + expect(condition_evaluator.evaluate(@lt_integer_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::DEBUG, + "Audience condition #{@lt_integer_conditions} evaluated to UNKNOWN because a nil value was passed for " \ + "user attribute 'input_value'." + ) + + # attribute value: empty hash + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => {}}, spy_logger) + expect(condition_evaluator.evaluate(@lt_integer_conditions)).to eq(nil) + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@lt_integer_conditions} evaluated as UNKNOWN because a value of type 'Hash' was " \ + "passed for user attribute 'input_value'." + ) + end - # Returns false for condition value - @lt_integer_conditions['value'] = 25 - allow(Optimizely::Helpers::Validator).to receive(:finite_number?).twice.and_return(true, false) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 10) + it 'should return nil when user-provided value is infinite' do + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 1 / 0.0}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be nil - # finite_number? should be called with condition value as it returns true for user attribute value - expect(Optimizely::Helpers::Validator).to have_received(:finite_number?).with(25) + + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@lt_integer_conditions} evaluated to UNKNOWN because the number value for " \ + "user attribute 'input_value' is not in the range [-2^53, +2^53]." + ) end it 'should not return nil when finite_number? returns true for provided arguments' do @lt_integer_conditions['value'] = 65 allow(Optimizely::Helpers::Validator).to receive(:finite_number?).twice.and_return(true, true) - condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new('input_value' => 75) + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 75}, spy_logger) expect(condition_evaluator.evaluate(@lt_integer_conditions)).not_to be_nil end + + it 'should log and return nil when condition value is infinite' do + @lt_integer_conditions['value'] = 1 / 0.0 + condition_evaluator = Optimizely::CustomAttributeConditionEvaluator.new({'input_value' => 51}, spy_logger) + expect(condition_evaluator.evaluate(@lt_integer_conditions)).to be_nil + expect(spy_logger).to have_received(:log).once.with( + Logger::WARN, + "Audience condition #{@lt_integer_conditions} has an unsupported condition value. You may need to upgrade "\ + 'to a newer release of the Optimizely SDK.' + ) + end end end diff --git a/spec/validator_helper_spec.rb b/spec/validator_helper_spec.rb index 4626681e..b5d50b5e 100644 --- a/spec/validator_helper_spec.rb +++ b/spec/validator_helper_spec.rb @@ -102,6 +102,8 @@ expect(Optimizely::Helpers::Validator.same_types?('', 'test')).to eq(true) expect(Optimizely::Helpers::Validator.same_types?([], [])).to eq(true) expect(Optimizely::Helpers::Validator.same_types?({}, {})).to eq(true) + # Treat integers and floats as same type. + expect(Optimizely::Helpers::Validator.same_types?(10, 8.5)).to eq(true) # Fixnum and Bignum expect(Optimizely::Helpers::Validator.same_types?(10, 10_000_000_000_000_000_000)).to eq(true) end @@ -109,7 +111,6 @@ it 'should return false when passed values are of different types' do expect(Optimizely::Helpers::Validator.same_types?(true, 1)).to eq(false) expect(Optimizely::Helpers::Validator.same_types?(0, false)).to eq(false) - expect(Optimizely::Helpers::Validator.same_types?(0, 0.0)).to eq(false) expect(Optimizely::Helpers::Validator.same_types?(0, '0.0')).to eq(false) expect(Optimizely::Helpers::Validator.same_types?({}, [])).to eq(false) end