diff --git a/.rubocop.yml b/.rubocop.yml index 75ca911..a460242 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -37,6 +37,12 @@ Naming/FileName: Exclude: - lib/amplitude-experiment.rb # Gem name, added for easier Gemfile usage +Naming/AccessorMethodName: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + Metrics/ClassLength: Enabled: false diff --git a/amplitude-experiment.gemspec b/amplitude-experiment.gemspec index cede91c..0335e17 100644 --- a/amplitude-experiment.gemspec +++ b/amplitude-experiment.gemspec @@ -22,13 +22,13 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'concurrent-ruby', '~> 1.2.2' spec.add_development_dependency 'psych', '~> 4.0' spec.add_development_dependency 'rake', '~> 13.0' - spec.add_development_dependency 'rdoc', '= 6.4' + spec.add_development_dependency 'rdoc', '= 6.10' spec.add_development_dependency 'rspec', '~> 3.6' spec.add_development_dependency 'rubocop', '= 1.22.3' spec.add_development_dependency 'simplecov', '~> 0.21' spec.add_development_dependency 'webmock', '~> 3.14' spec.add_development_dependency 'yard', '~> 0.9' spec.add_development_dependency 'dotenv', '~> 2.8.1' + spec.add_development_dependency 'jar-dependencies', '= 0.4.1' spec.metadata['rubygems_mfa_required'] = 'false' - spec.add_runtime_dependency 'ffi', '~> 1.15' end diff --git a/lib/amplitude-experiment.rb b/lib/amplitude-experiment.rb index 490d9a2..60db186 100644 --- a/lib/amplitude-experiment.rb +++ b/lib/amplitude-experiment.rb @@ -14,7 +14,6 @@ require 'experiment/local/assignment/assignment_config' require 'experiment/util/lru_cache' require 'experiment/util/hash' -require 'experiment/util/topological_sort' require 'experiment/util/user' require 'experiment/util/variant' require 'experiment/error' @@ -28,6 +27,12 @@ require 'experiment/cohort/cohort_sync_config' require 'experiment/deployment/deployment_runner' require 'experiment/util/poller' +require 'experiment/evaluation/evaluation' +require 'experiment/evaluation/flag' +require 'experiment/evaluation/murmur3' +require 'experiment/evaluation/select' +require 'experiment/evaluation/semantic_version' +require 'experiment/evaluation/topological_sort' # Amplitude Experiment Module module AmplitudeExperiment diff --git a/lib/experiment/deployment/deployment_runner.rb b/lib/experiment/deployment/deployment_runner.rb index 2d27c22..fbdd6fb 100644 --- a/lib/experiment/deployment/deployment_runner.rb +++ b/lib/experiment/deployment/deployment_runner.rb @@ -60,9 +60,9 @@ def periodic_flag_update def update_flag_configs flags = @flag_config_fetcher.fetch_v2 - flag_configs = flags.each_with_object({}) { |flag, hash| hash[flag['key']] = flag } - flag_keys = flag_configs.values.map { |flag| flag['key'] }.to_set - @flag_config_storage.remove_if { |f| !flag_keys.include?(f['key']) } + flag_configs = flags.map { |f| [f.key, f] }.to_h + flag_keys = flag_configs.keys.to_set + @flag_config_storage.remove_if { |f| !flag_keys.include?(f.key) } unless @cohort_loader flag_configs.each do |flag_key, flag_config| diff --git a/lib/experiment/evaluation/evaluation.rb b/lib/experiment/evaluation/evaluation.rb new file mode 100644 index 0000000..9aa72c5 --- /dev/null +++ b/lib/experiment/evaluation/evaluation.rb @@ -0,0 +1,311 @@ +# frozen_string_literal: true + +module Evaluation + # Engine for evaluating feature flags based on context + class Engine + def evaluate(context, flags) + results = {} + target = { + 'context' => context, + 'result' => results + } + + flags.each do |flag| + variant = evaluate_flag(target, flag) + results[flag.key] = variant if variant + end + + results + end + + private + + def evaluate_flag(target, flag) + result = nil + flag.segments.each do |segment| + result = evaluate_segment(target, flag, segment) + next unless result + + # Merge all metadata into the result + metadata = {} + metadata.merge!(flag.metadata) if flag.metadata + metadata.merge!(segment.metadata) if segment.metadata + metadata.merge!(result.metadata) if result.metadata + result.metadata = metadata + break + end + result + end + + def evaluate_segment(target, flag, segment) + if segment.conditions + match = evaluate_conditions(target, segment.conditions) + if match + variant_key = bucket(target, segment) + variant_key ? flag.variants[variant_key] : nil + end + else + # Null conditions always match + variant_key = bucket(target, segment) + variant_key ? flag.variants[variant_key] : nil + end + end + + def evaluate_conditions(target, conditions) + # Outer list logic is "or" (||) + conditions.any? do |inner_conditions| + match = true + inner_conditions.each do |condition| + match = match_condition(target, condition) + break unless match + end + match + end + end + + def match_condition(target, condition) + prop_value = Evaluation.select(target, condition.selector) + # Special matching for null properties and set type prop values and operators + if !prop_value + match_null(condition.op, condition.values) + elsif set_operator?(condition.op) + prop_value_string_list = coerce_string_array(prop_value) + return false unless prop_value_string_list + + match_set(prop_value_string_list, condition.op, condition.values) + else + prop_value_string = coerce_string(prop_value) + if prop_value_string + match_string(prop_value_string, condition.op, condition.values) + else + false + end + end + end + + def get_hash(key) + Murmur3.hash32x86(key) + end + + def bucket(target, segment) + unless segment.bucket + # Null bucket means segment is fully rolled out + return segment.variant + end + + bucketing_value = coerce_string(Evaluation.select(target, segment.bucket.selector)) + if !bucketing_value || bucketing_value.empty? + # Null or empty bucketing value cannot be bucketed + return segment.variant + end + + key_to_hash = "#{segment.bucket.salt}/#{bucketing_value}" + hash = get_hash(key_to_hash) + allocation_value = hash % 100 + distribution_value = (hash / 100).floor + + segment.bucket.allocations.each do |allocation| + allocation_start = allocation.range[0] + allocation_end = allocation.range[1] + next unless allocation_value >= allocation_start && allocation_value < allocation_end + + allocation.distributions.each do |distribution| + distribution_start = distribution.range[0] + distribution_end = distribution.range[1] + return distribution.variant if distribution_value >= distribution_start && distribution_value < distribution_end + end + end + + segment.variant + end + + def match_null(op, filter_values) + contains_none = contains_none?(filter_values) + case op + when Operator::IS, Operator::CONTAINS, Operator::LESS_THAN, + Operator::LESS_THAN_EQUALS, Operator::GREATER_THAN, + Operator::GREATER_THAN_EQUALS, Operator::VERSION_LESS_THAN, + Operator::VERSION_LESS_THAN_EQUALS, Operator::VERSION_GREATER_THAN, + Operator::VERSION_GREATER_THAN_EQUALS, Operator::SET_IS, + Operator::SET_CONTAINS, Operator::SET_CONTAINS_ANY + contains_none + when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN, + Operator::SET_DOES_NOT_CONTAIN, Operator::SET_DOES_NOT_CONTAIN_ANY + !contains_none + else + false + end + end + + def match_set(prop_values, op, filter_values) + case op + when Operator::SET_IS + set_equals?(prop_values, filter_values) + when Operator::SET_IS_NOT + !set_equals?(prop_values, filter_values) + when Operator::SET_CONTAINS + matches_set_contains_all?(prop_values, filter_values) + when Operator::SET_DOES_NOT_CONTAIN + !matches_set_contains_all?(prop_values, filter_values) + when Operator::SET_CONTAINS_ANY + matches_set_contains_any?(prop_values, filter_values) + when Operator::SET_DOES_NOT_CONTAIN_ANY + !matches_set_contains_any?(prop_values, filter_values) + else + false + end + end + + def match_string(prop_value, op, filter_values) + case op + when Operator::IS + matches_is?(prop_value, filter_values) + when Operator::IS_NOT + !matches_is?(prop_value, filter_values) + when Operator::CONTAINS + matches_contains?(prop_value, filter_values) + when Operator::DOES_NOT_CONTAIN + !matches_contains?(prop_value, filter_values) + when Operator::LESS_THAN, Operator::LESS_THAN_EQUALS, + Operator::GREATER_THAN, Operator::GREATER_THAN_EQUALS + matches_comparable?(prop_value, op, filter_values, + method(:parse_number), + method(:comparator)) + when Operator::VERSION_LESS_THAN, Operator::VERSION_LESS_THAN_EQUALS, + Operator::VERSION_GREATER_THAN, Operator::VERSION_GREATER_THAN_EQUALS + matches_comparable?(prop_value, op, filter_values, + SemanticVersion.method(:parse), + method(:comparator)) + when Operator::REGEX_MATCH + matches_regex?(prop_value, filter_values) + when Operator::REGEX_DOES_NOT_MATCH + !matches_regex?(prop_value, filter_values) + else + false + end + end + + def matches_is?(prop_value, filter_values) + if contains_booleans?(filter_values) + lower = prop_value.downcase + return filter_values.any? { |value| value.downcase == lower } if %w[true false].include?(lower) + end + filter_values.any? { |value| prop_value == value } + end + + def matches_contains?(prop_value, filter_values) + filter_values.any? do |filter_value| + prop_value.downcase.include?(filter_value.downcase) + end + end + + def matches_comparable?(prop_value, op, filter_values, type_transformer, type_comparator) + prop_value_transformed = type_transformer.call(prop_value) + filter_values_transformed = filter_values + .map { |filter_value| type_transformer.call(filter_value) } + .compact + + if !prop_value_transformed || filter_values_transformed.empty? + filter_values.any? { |filter_value| comparator(prop_value, op, filter_value) } + else + filter_values_transformed.any? do |filter_value_transformed| + type_comparator.call(prop_value_transformed, op, filter_value_transformed) + end + end + end + + def comparator(prop_value, op, filter_value) + case op + when Operator::LESS_THAN, Operator::VERSION_LESS_THAN + prop_value < filter_value + when Operator::LESS_THAN_EQUALS, Operator::VERSION_LESS_THAN_EQUALS + prop_value <= filter_value + when Operator::GREATER_THAN, Operator::VERSION_GREATER_THAN + prop_value > filter_value + when Operator::GREATER_THAN_EQUALS, Operator::VERSION_GREATER_THAN_EQUALS + prop_value >= filter_value + else + false + end + end + + def matches_regex?(prop_value, filter_values) + filter_values.any? { |filter_value| !!(Regexp.new(filter_value) =~ prop_value) } + end + + def contains_none?(filter_values) + filter_values.any? { |filter_value| filter_value == '(none)' } + end + + def contains_booleans?(filter_values) + filter_values.any? do |filter_value| + case filter_value.downcase + when 'true', 'false' + true + else + false + end + end + end + + def parse_number(value) + Float(value) + rescue StandardError + nil + end + + def coerce_string(value) + return nil if value.nil? + return value.to_json if value.is_a?(Hash) + + value.to_s + end + + def coerce_string_array(value) + if value.is_a?(Array) + value.map { |e| coerce_string(e) }.compact + else + string_value = value.to_s + begin + parsed_value = JSON.parse(string_value) + if parsed_value.is_a?(Array) + parsed_value.map { |e| coerce_string(e) }.compact + else + s = coerce_string(string_value) + s ? [s] : nil + end + rescue JSON::ParserError + s = coerce_string(string_value) + s ? [s] : nil + end + end + end + + def set_operator?(op) + case op + when Operator::SET_IS, Operator::SET_IS_NOT, + Operator::SET_CONTAINS, Operator::SET_DOES_NOT_CONTAIN, + Operator::SET_CONTAINS_ANY, Operator::SET_DOES_NOT_CONTAIN_ANY + true + else + false + end + end + + def set_equals?(xa, ya) + xs = Set.new(xa) + ys = Set.new(ya) + xs.size == ys.size && ys.all? { |y| xs.include?(y) } + end + + def matches_set_contains_all?(prop_values, filter_values) + return false if prop_values.length < filter_values.length + + filter_values.all? { |filter_value| matches_is?(filter_value, prop_values) } + end + + def matches_set_contains_any?(prop_values, filter_values) + filter_values.any? { |filter_value| matches_is?(filter_value, prop_values) } + end + end +end diff --git a/lib/experiment/evaluation/flag.rb b/lib/experiment/evaluation/flag.rb new file mode 100644 index 0000000..5faa32f --- /dev/null +++ b/lib/experiment/evaluation/flag.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'json' + +module Evaluation + class Distribution + attr_accessor :variant, :range + + def self.from_hash(hash) + new.tap do |dist| + dist.variant = hash['variant'] + dist.range = hash['range'] + end + end + end + + class Allocation + attr_accessor :range, :distributions + + def self.from_hash(hash) + new.tap do |alloc| + alloc.range = hash['range'] + alloc.distributions = hash['distributions']&.map { |d| Distribution.from_hash(d) } + end + end + end + + class Condition + attr_accessor :selector, :op, :values + + def self.from_hash(hash) + new.tap do |cond| + cond.selector = hash['selector'] + cond.op = hash['op'] + cond.values = hash['values'] + end + end + end + + class Bucket + attr_accessor :selector, :salt, :allocations + + def self.from_hash(hash) + new.tap do |bucket| + bucket.selector = hash['selector'] + bucket.salt = hash['salt'] + bucket.allocations = hash['allocations']&.map { |a| Allocation.from_hash(a) } + end + end + end + + class Segment + attr_accessor :bucket, :conditions, :variant, :metadata + + def self.from_hash(hash) + new.tap do |segment| + segment.bucket = hash['bucket'] && Bucket.from_hash(hash['bucket']) + segment.conditions = hash['conditions']&.map { |c| c.map { |inner| Condition.from_hash(inner) } } + segment.variant = hash['variant'] + segment.metadata = hash['metadata'] + end + end + end + + class Variant + attr_accessor :key, :value, :payload, :metadata + + def [](key) + instance_variable_get("@#{key}") + end + + def self.from_hash(hash) + new.tap do |variant| + variant.key = hash['key'] + variant.value = hash['value'] + variant.payload = hash['payload'] + variant.metadata = hash['metadata'] + end + end + end + + class Flag + attr_accessor :key, :variants, :segments, :dependencies, :metadata + + def self.from_hash(hash) + new.tap do |flag| + flag.key = hash['key'] + flag.variants = hash['variants'].transform_values { |v| Variant.from_hash(v) } + flag.segments = hash['segments'].map { |s| Segment.from_hash(s) } + flag.dependencies = hash['dependencies'] + flag.metadata = hash['metadata'] + end + end + + # Used for testing + def ==(other) + key == other.key + end + end + + module Operator + IS = 'is' + IS_NOT = 'is not' + CONTAINS = 'contains' + DOES_NOT_CONTAIN = 'does not contain' + LESS_THAN = 'less' + LESS_THAN_EQUALS = 'less or equal' + GREATER_THAN = 'greater' + GREATER_THAN_EQUALS = 'greater or equal' + VERSION_LESS_THAN = 'version less' + VERSION_LESS_THAN_EQUALS = 'version less or equal' + VERSION_GREATER_THAN = 'version greater' + VERSION_GREATER_THAN_EQUALS = 'version greater or equal' + SET_IS = 'set is' + SET_IS_NOT = 'set is not' + SET_CONTAINS = 'set contains' + SET_DOES_NOT_CONTAIN = 'set does not contain' + SET_CONTAINS_ANY = 'set contains any' + SET_DOES_NOT_CONTAIN_ANY = 'set does not contain any' + REGEX_MATCH = 'regex match' + REGEX_DOES_NOT_MATCH = 'regex does not match' + end +end diff --git a/lib/experiment/evaluation/murmur3.rb b/lib/experiment/evaluation/murmur3.rb new file mode 100644 index 0000000..0ff5c8e --- /dev/null +++ b/lib/experiment/evaluation/murmur3.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +# Implements 32-bit x86 MurmurHash3 +class Murmur3 + C1_32 = -0x3361d2af + C2_32 = 0x1b873593 + R1_32 = 15 + R2_32 = 13 + M_32 = 5 + N_32 = -0x19ab949c + + class << self + def hash32x86(input, seed = 0) + data = string_to_utf8_bytes(input) + length = data.length + n_blocks = length >> 2 + hash = seed + + # Process body + n_blocks.times do |i| + index = i << 2 + k = read_int_le(data, index) + hash = mix32(k, hash) + end + + # Process tail + index = n_blocks << 2 + k1 = 0 + + case length - index + when 3 + k1 ^= data[index + 2] << 16 + k1 ^= data[index + 1] << 8 + k1 ^= data[index] + k1 = (k1 * C1_32) & 0xffffffff + k1 = rotate_left(k1, R1_32) + k1 = (k1 * C2_32) & 0xffffffff + hash ^= k1 + when 2 + k1 ^= data[index + 1] << 8 + k1 ^= data[index] + k1 = (k1 * C1_32) & 0xffffffff + k1 = rotate_left(k1, R1_32) + k1 = (k1 * C2_32) & 0xffffffff + hash ^= k1 + when 1 + k1 ^= data[index] + k1 = (k1 * C1_32) & 0xffffffff + k1 = rotate_left(k1, R1_32) + k1 = (k1 * C2_32) & 0xffffffff + hash ^= k1 + end + + hash ^= length + fmix32(hash) & 0xffffffff + end + + private + + def mix32(k, hash) + k = (k * C1_32) & 0xffffffff + k = rotate_left(k, R1_32) + k = (k * C2_32) & 0xffffffff + hash ^= k + hash = rotate_left(hash, R2_32) + ((hash * M_32) + N_32) & 0xffffffff + end + + def fmix32(hash) + hash ^= hash >> 16 + hash = (hash * -0x7a143595) & 0xffffffff + hash ^= hash >> 13 + hash = (hash * -0x3d4d51cb) & 0xffffffff + hash ^= hash >> 16 + hash + end + + def rotate_left(x, n, width = 32) + n = n % width if n > width + mask = (0xffffffff << (width - n)) & 0xffffffff + r = ((x & mask) >> (width - n)) & 0xffffffff + ((x << n) | r) & 0xffffffff + end + + def read_int_le(data, index = 0) + n = (data[index] << 24) | + (data[index + 1] << 16) | + (data[index + 2] << 8) | + data[index + 3] + reverse_bytes(n) + end + + def reverse_bytes(n) + ((n & -0x1000000) >> 24) | + ((n & 0x00ff0000) >> 8) | + ((n & 0x0000ff00) << 8) | + ((n & 0x000000ff) << 24) + end + + def string_to_utf8_bytes(str) + str.encode('UTF-8').bytes + end + end +end diff --git a/lib/experiment/evaluation/select.rb b/lib/experiment/evaluation/select.rb new file mode 100644 index 0000000..94e327b --- /dev/null +++ b/lib/experiment/evaluation/select.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Selects a value from a nested object using an array of selector keys +module Evaluation + def self.select(selectable, selector) + return nil if selector.nil? || selector.empty? + + selector.each do |selector_element| + return nil if selector_element.nil? || selectable.nil? + + selectable = selectable[selector_element] + end + + selectable.nil? ? nil : selectable + end +end diff --git a/lib/experiment/evaluation/semantic_version.rb b/lib/experiment/evaluation/semantic_version.rb new file mode 100644 index 0000000..5dc557f --- /dev/null +++ b/lib/experiment/evaluation/semantic_version.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class SemanticVersion + include Comparable + + attr_reader :major, :minor, :patch, :pre_release + + MAJOR_MINOR_REGEX = '(\d+)\.(\d+)' + PATCH_REGEX = '(\d+)' + PRERELEASE_REGEX = '(-(([-\w]+\.?)*))?' + VERSION_PATTERN = /^#{MAJOR_MINOR_REGEX}(\.#{PATCH_REGEX}#{PRERELEASE_REGEX})?$/.freeze + + def initialize(major, minor, patch, pre_release = nil) + @major = major + @minor = minor + @patch = patch + @pre_release = pre_release + end + + def self.parse(version) + return nil if version.nil? + + match = VERSION_PATTERN.match(version) + return nil unless match + + major = match[1].to_i + minor = match[2].to_i + patch = match[4]&.to_i || 0 + pre_release = match[5] + + new(major, minor, patch, pre_release) + end + + def <=>(other) + return nil unless other.is_a?(SemanticVersion) + + result = major <=> other.major + return result unless result.zero? + + result = minor <=> other.minor + return result unless result.zero? + + result = patch <=> other.patch + return result unless result.zero? + + return 1 if !pre_release && other.pre_release + return -1 if pre_release && !other.pre_release + return 0 if !pre_release && !other.pre_release + + pre_release <=> other.pre_release + end +end diff --git a/lib/experiment/evaluation/topological_sort.rb b/lib/experiment/evaluation/topological_sort.rb new file mode 100644 index 0000000..8c4cac1 --- /dev/null +++ b/lib/experiment/evaluation/topological_sort.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class CycleError < StandardError + attr_accessor :path + + def initialize(path) + super("Detected a cycle between flags #{path}") + self.path = path + end +end + +# Performs topological sorting of feature flags based on their dependencies +class TopologicalSort + # Sort flags topologically based on their dependencies + def self.sort(flags, flag_keys = nil) + available = flags.clone + result = [] + starting_keys = flag_keys.nil? || flag_keys.empty? ? flags.keys : flag_keys + + starting_keys.each do |flag_key| + traversal = parent_traversal(flag_key, available) + result.concat(traversal) if traversal + end + + result + end + + # Perform depth-first traversal of flag dependencies + def self.parent_traversal(flag_key, available, path = []) + flag = available[flag_key] + return nil unless flag + + # No dependencies - return flag and remove from available + if !flag.dependencies || flag.dependencies.empty? + available.delete(flag.key) + return [flag] + end + + # Check for cycles + path.push(flag.key) + result = [] + + flag.dependencies.each do |parent_key| + raise CycleError, path if path.any? { |p| p == parent_key } + + traversal = parent_traversal(parent_key, available, path) + result.concat(traversal) if traversal + end + + result.push(flag) + path.pop + available.delete(flag.key) + + result + end +end diff --git a/lib/experiment/flag/flag_config_fetcher.rb b/lib/experiment/flag/flag_config_fetcher.rb index 63278c7..1a26ecf 100644 --- a/lib/experiment/flag/flag_config_fetcher.rb +++ b/lib/experiment/flag/flag_config_fetcher.rb @@ -44,7 +44,7 @@ def fetch_v2 raise "flagConfigs - received error response: #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPOK) @logger.debug("[Experiment] Fetch flag configs: #{response.body}") - JSON.parse(response.body) + JSON.parse(response.body).map { |f| Evaluation::Flag.from_hash(f) } end # Fetch local evaluation mode flag configs from the Experiment API server. diff --git a/lib/experiment/flag/flag_config_storage.rb b/lib/experiment/flag/flag_config_storage.rb index 060fff0..512762d 100644 --- a/lib/experiment/flag/flag_config_storage.rb +++ b/lib/experiment/flag/flag_config_storage.rb @@ -40,7 +40,7 @@ def flag_configs def put_flag_config(flag_config) @flag_configs_lock.synchronize do - @flag_configs[flag_config['key']] = flag_config + @flag_configs[flag_config.key] = flag_config end end diff --git a/lib/experiment/local/client.rb b/lib/experiment/local/client.rb index 14ddc4f..26eb2de 100644 --- a/lib/experiment/local/client.rb +++ b/lib/experiment/local/client.rb @@ -12,7 +12,6 @@ class LocalEvaluationClient # @param [LocalEvaluationConfig] config The config object def initialize(api_key, config = nil) - require 'experiment/local/evaluation/evaluation' @api_key = api_key @config = config || LocalEvaluationConfig.new @flags = nil @@ -25,6 +24,8 @@ def initialize(api_key, config = nil) end raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty? + @engine = Evaluation::Engine.new + @assignment_service = nil @assignment_service = AssignmentService.new(AmplitudeAnalytics::Amplitude.new(config.assignment_config.api_key, configuration: config.assignment_config), AssignmentFilter.new(config.assignment_config.cache_capacity)) if config&.assignment_config @@ -67,15 +68,13 @@ def evaluate_v2(user, flag_keys = []) flags = @flag_config_storage.flag_configs return {} if flags.nil? - sorted_flags = AmplitudeExperiment.topological_sort(flags, flag_keys.to_set) + sorted_flags = TopologicalSort.sort(flags, flag_keys) required_cohorts_in_storage(sorted_flags) - flags_json = sorted_flags.to_json user = enrich_user_with_cohorts(user, flags) if @config.cohort_sync_config context = AmplitudeExperiment.user_to_evaluation_context(user) - context_json = context.to_json - @logger.debug("[Experiment] Evaluate: User: #{context_json} - Rules: #{flags}") if @config.debug - result = evaluation(flags_json, context_json) + @logger.debug("[Experiment] Evaluate: User: #{context} - Rules: #{flags}") if @config.debug + result = @engine.evaluate(context, sorted_flags) @logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result) @assignment_service&.track(Assignment.new(user, variants)) @@ -113,9 +112,9 @@ def required_cohorts_in_storage(flag_configs) missing_cohorts_str = "[#{missing_cohorts.map(&:to_s).join(', ')}]" message = if @config.cohort_sync_config - "Evaluating flag #{flag['key']} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage" + "Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage" else - "Evaluating flag #{flag['key']} dependent on cohorts #{cohort_ids_str} without cohort syncing configured" + "Evaluating flag #{flag.key} dependent on cohorts #{cohort_ids_str} without cohort syncing configured" end @logger.warn(message) diff --git a/lib/experiment/local/evaluation/evaluation.rb b/lib/experiment/local/evaluation/evaluation.rb deleted file mode 100644 index 5a4c201..0000000 --- a/lib/experiment/local/evaluation/evaluation.rb +++ /dev/null @@ -1,76 +0,0 @@ -# rubocop:disable all -require 'ffi' -require 'json' - -# The evaluation wrapper -module EvaluationInterop - extend FFI::Library - host_os = RbConfig::CONFIG['host_os'] - cpu = RbConfig::CONFIG['host_cpu'] - evaluation_dir = File.dirname(__FILE__) - ffi_lib ["#{evaluation_dir}/lib/macosX64/libevaluation_interop.dylib"] if host_os =~ /darwin|mac os/ && cpu =~ /x86_64/ - ffi_lib ["#{evaluation_dir}/lib/macosArm64/libevaluation_interop.dylib"] if host_os =~ /darwin|mac os/ && cpu =~ /arm64/ - ffi_lib ["#{evaluation_dir}/lib/linuxX64/libevaluation_interop.so"] if host_os =~ /linux/ && cpu =~ /x86_64/ - ffi_lib ["#{evaluation_dir}/lib/linuxArm64/libevaluation_interop.so"] if host_os =~ /linux/ && cpu =~ /arm64|aarch64/ - - class Root < FFI::Struct - layout :evaluate, callback([:string, :string], :pointer) - end - - class Kotlin < FFI::Struct - layout :root, Root - end - - class Libevaluation_interop_ExportedSymbols < FFI::Struct - layout :DisposeStablePointer, callback([:pointer], :void), - :DisposeString, callback([:pointer], :void), - :IsInstance, callback([:pointer, :string], :pointer), - :createNullableByte, callback([:string], :pointer), - :getNonNullValueOfByte, callback([:pointer], :pointer), - :createNullableShort, callback([:pointer], :pointer), - :getNonNullValueOfShort, callback([:pointer], :pointer), - :createNullableInt, callback([:pointer], :pointer), - :getNonNullValueOfInt, callback([:pointer], :pointer), - :createNullableLong, callback([:pointer], :pointer), - :getNonNullValueOfLong, callback([:pointer], :pointer), - :createNullableFloat, callback([:pointer], :pointer), - :getNonNullValueOfFloat, callback([:pointer], :pointer), - :createNullableDouble, callback([:pointer], :pointer), - :getNonNullValueOfDouble, callback([:pointer], :pointer), - :createNullableChar, callback([:pointer], :pointer), - :getNonNullValueOfChar, callback([:pointer], :pointer), - :createNullableBoolean, callback([:pointer], :pointer), - :getNonNullValueOfBoolean, callback([:pointer], :pointer), - :createNullableUnit, callback([], :pointer), - :createNullableUByte, callback([:pointer], :pointer), - :getNonNullValueOfUByte, callback([:pointer], :pointer), - :createNullableUShort, callback([:pointer], :pointer), - :getNonNullValueOfUShort, callback([:pointer], :pointer), - :createNullableUInt, callback([:pointer], :pointer), - :getNonNullValueOfUInt, callback([:pointer], :pointer), - :createNullableULong, callback([:pointer], :pointer), - :getNonNullValueOfULong, callback([:pointer], :pointer), - - :kotlin, Kotlin - end - - attach_function :libevaluation_interop_symbols, [], Libevaluation_interop_ExportedSymbols.by_ref -end - -def evaluation(rule_json, context_json) - lib = EvaluationInterop.libevaluation_interop_symbols() - evaluate = lib[:kotlin][:root][:evaluate] - dispose = lib[:DisposeString] - result_raw = evaluate.call(rule_json, context_json) - result_json = result_raw.read_string - result = JSON.parse(result_json) - dispose.call(result_raw) - - if result["error"] != nil - raise "#{result["error"]}" - elsif result["result"] == nil - raise "Evaluation result is nil." - end - result["result"] -end -# rubocop:disable all diff --git a/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so b/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so deleted file mode 100755 index 525c434..0000000 Binary files a/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop.so and /dev/null differ diff --git a/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h b/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h deleted file mode 100644 index 70485d1..0000000 --- a/lib/experiment/local/evaluation/lib/linuxArm64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* flags, const char* context); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so b/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so deleted file mode 100755 index 14a8f55..0000000 Binary files a/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop.so and /dev/null differ diff --git a/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h b/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h deleted file mode 100644 index 70485d1..0000000 --- a/lib/experiment/local/evaluation/lib/linuxX64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* flags, const char* context); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib b/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib deleted file mode 100755 index 35b8fe1..0000000 Binary files a/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop.dylib and /dev/null differ diff --git a/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h b/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h deleted file mode 100644 index 70485d1..0000000 --- a/lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* flags, const char* context); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib b/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib deleted file mode 100755 index 23d73f9..0000000 Binary files a/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib and /dev/null differ diff --git a/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h b/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h deleted file mode 100644 index 70485d1..0000000 --- a/lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h +++ /dev/null @@ -1,110 +0,0 @@ -#ifndef KONAN_LIBEVALUATION_INTEROP_H -#define KONAN_LIBEVALUATION_INTEROP_H -#ifdef __cplusplus -extern "C" { -#endif -#ifdef __cplusplus -typedef bool libevaluation_interop_KBoolean; -#else -typedef _Bool libevaluation_interop_KBoolean; -#endif -typedef unsigned short libevaluation_interop_KChar; -typedef signed char libevaluation_interop_KByte; -typedef short libevaluation_interop_KShort; -typedef int libevaluation_interop_KInt; -typedef long long libevaluation_interop_KLong; -typedef unsigned char libevaluation_interop_KUByte; -typedef unsigned short libevaluation_interop_KUShort; -typedef unsigned int libevaluation_interop_KUInt; -typedef unsigned long long libevaluation_interop_KULong; -typedef float libevaluation_interop_KFloat; -typedef double libevaluation_interop_KDouble; -typedef float __attribute__ ((__vector_size__ (16))) libevaluation_interop_KVector128; -typedef void* libevaluation_interop_KNativePtr; -struct libevaluation_interop_KType; -typedef struct libevaluation_interop_KType libevaluation_interop_KType; - -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Byte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Short; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Int; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Long; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Float; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Double; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Char; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Boolean; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_Unit; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UByte; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UShort; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_UInt; -typedef struct { - libevaluation_interop_KNativePtr pinned; -} libevaluation_interop_kref_kotlin_ULong; - - -typedef struct { - /* Service functions. */ - void (*DisposeStablePointer)(libevaluation_interop_KNativePtr ptr); - void (*DisposeString)(const char* string); - libevaluation_interop_KBoolean (*IsInstance)(libevaluation_interop_KNativePtr ref, const libevaluation_interop_KType* type); - libevaluation_interop_kref_kotlin_Byte (*createNullableByte)(libevaluation_interop_KByte); - libevaluation_interop_KByte (*getNonNullValueOfByte)(libevaluation_interop_kref_kotlin_Byte); - libevaluation_interop_kref_kotlin_Short (*createNullableShort)(libevaluation_interop_KShort); - libevaluation_interop_KShort (*getNonNullValueOfShort)(libevaluation_interop_kref_kotlin_Short); - libevaluation_interop_kref_kotlin_Int (*createNullableInt)(libevaluation_interop_KInt); - libevaluation_interop_KInt (*getNonNullValueOfInt)(libevaluation_interop_kref_kotlin_Int); - libevaluation_interop_kref_kotlin_Long (*createNullableLong)(libevaluation_interop_KLong); - libevaluation_interop_KLong (*getNonNullValueOfLong)(libevaluation_interop_kref_kotlin_Long); - libevaluation_interop_kref_kotlin_Float (*createNullableFloat)(libevaluation_interop_KFloat); - libevaluation_interop_KFloat (*getNonNullValueOfFloat)(libevaluation_interop_kref_kotlin_Float); - libevaluation_interop_kref_kotlin_Double (*createNullableDouble)(libevaluation_interop_KDouble); - libevaluation_interop_KDouble (*getNonNullValueOfDouble)(libevaluation_interop_kref_kotlin_Double); - libevaluation_interop_kref_kotlin_Char (*createNullableChar)(libevaluation_interop_KChar); - libevaluation_interop_KChar (*getNonNullValueOfChar)(libevaluation_interop_kref_kotlin_Char); - libevaluation_interop_kref_kotlin_Boolean (*createNullableBoolean)(libevaluation_interop_KBoolean); - libevaluation_interop_KBoolean (*getNonNullValueOfBoolean)(libevaluation_interop_kref_kotlin_Boolean); - libevaluation_interop_kref_kotlin_Unit (*createNullableUnit)(void); - libevaluation_interop_kref_kotlin_UByte (*createNullableUByte)(libevaluation_interop_KUByte); - libevaluation_interop_KUByte (*getNonNullValueOfUByte)(libevaluation_interop_kref_kotlin_UByte); - libevaluation_interop_kref_kotlin_UShort (*createNullableUShort)(libevaluation_interop_KUShort); - libevaluation_interop_KUShort (*getNonNullValueOfUShort)(libevaluation_interop_kref_kotlin_UShort); - libevaluation_interop_kref_kotlin_UInt (*createNullableUInt)(libevaluation_interop_KUInt); - libevaluation_interop_KUInt (*getNonNullValueOfUInt)(libevaluation_interop_kref_kotlin_UInt); - libevaluation_interop_kref_kotlin_ULong (*createNullableULong)(libevaluation_interop_KULong); - libevaluation_interop_KULong (*getNonNullValueOfULong)(libevaluation_interop_kref_kotlin_ULong); - - /* User functions. */ - struct { - struct { - const char* (*evaluate)(const char* flags, const char* context); - } root; - } kotlin; -} libevaluation_interop_ExportedSymbols; -extern libevaluation_interop_ExportedSymbols* libevaluation_interop_symbols(void); -#ifdef __cplusplus -} /* extern "C" */ -#endif -#endif /* KONAN_LIBEVALUATION_INTEROP_H */ diff --git a/lib/experiment/util/flag_config.rb b/lib/experiment/util/flag_config.rb index 507ade0..64b5844 100644 --- a/lib/experiment/util/flag_config.rb +++ b/lib/experiment/util/flag_config.rb @@ -1,35 +1,35 @@ module AmplitudeExperiment def self.cohort_filter?(condition) - ['set contains any', 'set does not contain any'].include?(condition['op']) && - condition['selector'] && - condition['selector'][-1] == 'cohort_ids' + ['set contains any', 'set does not contain any'].include?(condition.op) && + condition.selector && + condition.selector[-1] == 'cohort_ids' end def self.get_grouped_cohort_condition_ids(segment) cohort_ids = {} - conditions = segment['conditions'] || [] + conditions = segment.conditions || [] conditions.each do |condition| condition = condition[0] - next unless cohort_filter?(condition) && (condition['selector'][1].length > 2) + next unless cohort_filter?(condition) && (condition.selector[1].length > 2) - context_subtype = condition['selector'][1] + context_subtype = condition.selector[1] group_type = if context_subtype == 'user' USER_GROUP_TYPE - elsif condition['selector'].include?('groups') - condition['selector'][2] + elsif condition.selector.include?('groups') + condition.selector[2] else next end cohort_ids[group_type] ||= Set.new - cohort_ids[group_type].merge(condition['values']) + cohort_ids[group_type].merge(condition.values) end cohort_ids end def self.get_grouped_cohort_ids_from_flag(flag) cohort_ids = {} - segments = flag['segments'] || [] + segments = flag.segments || [] segments.each do |segment| get_grouped_cohort_condition_ids(segment).each do |key, values| cohort_ids[key] ||= Set.new diff --git a/lib/experiment/util/topological_sort.rb b/lib/experiment/util/topological_sort.rb deleted file mode 100644 index b7ed4a6..0000000 --- a/lib/experiment/util/topological_sort.rb +++ /dev/null @@ -1,39 +0,0 @@ -module AmplitudeExperiment - def self.topological_sort(flags, keys = nil, ordered: false) - available = flags.dup - result = [] - starting_keys = keys.nil? || keys.empty? ? flags.keys : keys - # Used for testing to ensure consistency. - starting_keys.sort! if ordered && (keys.nil? || keys.empty?) - - starting_keys.each do |flag_key| - traversal = parent_traversal(flag_key, available, Set.new) - result.concat(traversal) unless traversal.nil? - end - result - end - - def self.parent_traversal(flag_key, available, path) - flag = available[flag_key] - return nil if flag.nil? - - dependencies = flag['dependencies'] - if dependencies.nil? || dependencies.empty? - available.delete(flag_key) - return [flag] - end - - path.add(flag_key) - result = [] - dependencies.each do |parent_key| - raise CycleError, path if path.include?(parent_key) - - traversal = parent_traversal(parent_key, available, path) - result.concat(traversal) unless traversal.nil? - end - result << flag - path.delete(flag_key) - available.delete(flag_key) - result - end -end diff --git a/spec/experiment/deployment/deployment_runner_spec.rb b/spec/experiment/deployment/deployment_runner_spec.rb index c351f00..b2fe03f 100644 --- a/spec/experiment/deployment/deployment_runner_spec.rb +++ b/spec/experiment/deployment/deployment_runner_spec.rb @@ -3,23 +3,25 @@ module AmplitudeExperiment describe DeploymentRunner do let(:cohort_id) { '1234' } before(:each) do - @flag = { - 'key' => 'flag', - 'variants' => {}, - 'segments' => [ - { - 'conditions' => [ - [ - { - 'selector' => %w[context user cohort_ids], - 'op' => 'set contains any', - 'values' => [cohort_id] - } + @flag = Evaluation::Flag.from_hash( + { + 'key' => 'flag', + 'variants' => {}, + 'segments' => [ + { + 'conditions' => [ + [ + { + 'selector' => %w[context user cohort_ids], + 'op' => 'set contains any', + 'values' => [cohort_id] + } + ] ] - ] - } - ] - } + } + ] + } + ) end describe '#start' do diff --git a/spec/experiment/evaluation/evaluation_intgration_spec.rb b/spec/experiment/evaluation/evaluation_intgration_spec.rb new file mode 100644 index 0000000..57b4ab8 --- /dev/null +++ b/spec/experiment/evaluation/evaluation_intgration_spec.rb @@ -0,0 +1,495 @@ +# frozen_string_literal: true + +require 'net/http' +require 'json' + +describe Evaluation::Engine do + let(:deployment_key) { 'server-NgJxxvg8OGwwBsWVXqyxQbdiflbhvugy' } + let(:engine) { Evaluation::Engine.new } + let(:flags) { get_flags(deployment_key) } + + describe 'basic tests' do + it 'tests off' do + user = user_context('user_id', 'device_id') + result = engine.evaluate(user, flags)['test-off'] + expect(result.key).to eq('off') + end + + it 'tests on' do + user = user_context('user_id', 'device_id') + result = engine.evaluate(user, flags)['test-on'] + expect(result.key).to eq('on') + end + end + + describe 'opinionated segment tests' do + it 'tests individual inclusions match' do + # Match user ID + user = user_context('user_id') + result = engine.evaluate(user, flags)['test-individual-inclusions'] + expect(result.key).to eq('on') + expect(result.metadata['segmentName']).to eq('individual-inclusions') + + # Match device ID + user = user_context(nil, 'device_id') + result = engine.evaluate(user, flags)['test-individual-inclusions'] + expect(result.key).to eq('on') + expect(result.metadata['segmentName']).to eq('individual-inclusions') + + # Doesn't match user ID + user = user_context('not_user_id') + result = engine.evaluate(user, flags)['test-individual-inclusions'] + expect(result.key).to eq('off') + + # Doesn't match device ID + user = user_context(nil, 'not_device_id') + result = engine.evaluate(user, flags)['test-individual-inclusions'] + expect(result.key).to eq('off') + end + + it 'tests flag dependencies on' do + user = user_context('user_id', 'device_id') + result = engine.evaluate(user, flags)['test-flag-dependencies-on'] + expect(result.key).to eq('on') + end + + it 'tests flag dependencies off' do + user = user_context('user_id', 'device_id') + result = engine.evaluate(user, flags)['test-flag-dependencies-off'] + expect(result.key).to eq('off') + expect(result.metadata['segmentName']).to eq('flag-dependencies') + end + + it 'tests sticky bucketing' do + # On + user = user_context('user_id', 'device_id', nil, { + '[Experiment] test-sticky-bucketing' => 'on' + }) + + result = engine.evaluate(user, flags)['test-sticky-bucketing'] + expect(result.key).to eq('on') + expect(result.metadata['segmentName']).to eq('sticky-bucketing') + + # Off + user = user_context('user_id', 'device_id', nil, { + '[Experiment] test-sticky-bucketing' => 'off' + }) + result = engine.evaluate(user, flags)['test-sticky-bucketing'] + expect(result.key).to eq('off') + expect(result.metadata['segmentName']).to eq('All Other Users') + + # Non-variant + user = user_context('user_id', 'device_id', nil, { + '[Experiment] test-sticky-bucketing' => 'not-a-variant' + }) + result = engine.evaluate(user, flags)['test-sticky-bucketing'] + expect(result.key).to eq('off') + expect(result.metadata['segmentName']).to eq('All Other Users') + end + end + + describe 'experiment and flag segment tests' do + it 'tests experiment' do + user = user_context('user_id', 'device_id') + result = engine.evaluate(user, flags)['test-experiment'] + expect(result.key).to eq('on') + expect(result.metadata['experimentKey']).to eq('exp-1') + end + + it 'tests flag' do + user = user_context('user_id', 'device_id') + result = engine.evaluate(user, flags)['test-flag'] + expect(result.key).to eq('on') + expect(result.metadata['experimentKey']).to be_nil + end + end + + describe 'conditional logic tests' do + it 'tests multiple conditions and values' do + # All match + user = user_context('user_id', 'device_id', nil, { + 'key-1' => 'value-1', + 'key-2' => 'value-2', + 'key-3' => 'value-3' + }) + result = engine.evaluate(user, flags)['test-multiple-conditions-and-values'] + expect(result.key).to eq('on') + + # Some match + user = user_context('user_id', 'device_id', nil, { + 'key-1' => 'value-1', + 'key-2' => 'value-2' + }) + result = engine.evaluate(user, flags)['test-multiple-conditions-and-values'] + expect(result.key).to eq('off') + end + end + + describe 'conditional property targeting tests' do + it 'tests amplitude property targeting' do + user = user_context('user_id') + result = engine.evaluate(user, flags)['test-amplitude-property-targeting'] + expect(result.key).to eq('on') + end + + it 'tests cohort targeting' do + user = user_context(nil, nil, nil, nil, %w[u0qtvwla 12345678]) + result = engine.evaluate(user, flags)['test-cohort-targeting'] + expect(result.key).to eq('on') + + user = user_context(nil, nil, nil, nil, %w[12345678 87654321]) + result = engine.evaluate(user, flags)['test-cohort-targeting'] + expect(result.key).to eq('off') + end + + it 'tests group name targeting' do + user = group_context('org name', 'amplitude') + result = engine.evaluate(user, flags)['test-group-name-targeting'] + expect(result.key).to eq('on') + end + + it 'tests group property targeting' do + user = group_context('org name', 'amplitude', { 'org plan' => 'enterprise2' }) + result = engine.evaluate(user, flags)['test-group-property-targeting'] + expect(result.key).to eq('on') + end + end + + describe 'bucketing tests' do + it 'tests amplitude id bucketing' do + user = user_context(nil, nil, '1234567890') + result = engine.evaluate(user, flags)['test-amplitude-id-bucketing'] + expect(result.key).to eq('on') + end + + it 'tests user id bucketing' do + user = user_context('user_id') + result = engine.evaluate(user, flags)['test-user-id-bucketing'] + expect(result.key).to eq('on') + end + + it 'tests device id bucketing' do + user = user_context(nil, 'device_id') + result = engine.evaluate(user, flags)['test-device-id-bucketing'] + expect(result.key).to eq('on') + end + + it 'tests custom user property bucketing' do + user = user_context(nil, nil, nil, { 'key' => 'value' }) + result = engine.evaluate(user, flags)['test-custom-user-property-bucketing'] + expect(result.key).to eq('on') + end + + it 'tests group name bucketing' do + user = group_context('org name', 'amplitude') + result = engine.evaluate(user, flags)['test-group-name-bucketing'] + expect(result.key).to eq('on') + end + + it 'tests group property bucketing' do + user = group_context('org name', 'amplitude', { 'org plan' => 'enterprise2' }) + result = engine.evaluate(user, flags)['test-group-name-bucketing'] + expect(result.key).to eq('on') + end + end + + describe 'bucketing allocation tests' do + it 'tests 1 percent allocation' do + on_count = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-1-percent-allocation'] + on_count += 1 if result&.key == 'on' + end + expect(on_count).to eq(107) + end + + it 'tests 50 percent allocation' do + on_count = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-50-percent-allocation'] + on_count += 1 if result&.key == 'on' + end + expect(on_count).to eq(5009) + end + + it 'tests 99 percent allocation' do + on_count = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-99-percent-allocation'] + on_count += 1 if result&.key == 'on' + end + expect(on_count).to eq(9900) + end + end + + describe 'bucketing distribution tests' do + it 'tests 1 percent distribution' do + control = 0 + treatment = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-1-percent-distribution'] + case result&.key + when 'control' + control += 1 + when 'treatment' + treatment += 1 + end + end + expect(control).to eq(106) + expect(treatment).to eq(9894) + end + + it 'tests 50 percent distribution' do + control = 0 + treatment = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-50-percent-distribution'] + case result&.key + when 'control' + control += 1 + when 'treatment' + treatment += 1 + end + end + expect(control).to eq(4990) + expect(treatment).to eq(5010) + end + + it 'tests 99 percent distribution' do + control = 0 + treatment = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-99-percent-distribution'] + case result&.key + when 'control' + control += 1 + when 'treatment' + treatment += 1 + end + end + expect(control).to eq(9909) + expect(treatment).to eq(91) + end + + it 'tests multiple distributions' do + a = 0 + b = 0 + c = 0 + d = 0 + 10_000.times do |i| + user = user_context(nil, (i + 1).to_s) + result = engine.evaluate(user, flags)['test-multiple-distributions'] + case result&.key + when 'a' then a += 1 + when 'b' then b += 1 + when 'c' then c += 1 + when 'd' then d += 1 + end + end + expect(a).to eq(2444) + expect(b).to eq(2634) + expect(c).to eq(2447) + expect(d).to eq(2475) + end + end + + describe 'operator tests' do + it 'tests is' do + user = user_context(nil, nil, nil, { 'key' => 'value' }) + result = engine.evaluate(user, flags)['test-is'] + expect(result.key).to eq('on') + end + + it 'tests is not' do + user = user_context(nil, nil, nil, { 'key' => 'value' }) + result = engine.evaluate(user, flags)['test-is-not'] + expect(result.key).to eq('on') + end + + it 'tests contains' do + user = user_context(nil, nil, nil, { 'key' => 'value' }) + result = engine.evaluate(user, flags)['test-contains'] + expect(result.key).to eq('on') + end + + it 'tests does not contain' do + user = user_context(nil, nil, nil, { 'key' => 'value' }) + result = engine.evaluate(user, flags)['test-does-not-contain'] + expect(result.key).to eq('on') + end + + it 'tests less' do + user = user_context(nil, nil, nil, { 'key' => '-1' }) + result = engine.evaluate(user, flags)['test-less'] + expect(result.key).to eq('on') + end + + it 'tests less or equal' do + user = user_context(nil, nil, nil, { 'key' => '0' }) + result = engine.evaluate(user, flags.select { |f| f.key == 'test-less-or-equal' })['test-less-or-equal'] + expect(result.key).to eq('on') + end + + it 'tests greater' do + user = user_context(nil, nil, nil, { 'key' => '1' }) + result = engine.evaluate(user, flags)['test-greater'] + expect(result.key).to eq('on') + end + + it 'tests greater or equal' do + user = user_context(nil, nil, nil, { 'key' => '0' }) + result = engine.evaluate(user, flags)['test-greater-or-equal'] + expect(result.key).to eq('on') + end + + it 'tests version less' do + user = freeform_user_context({ 'version' => '1.9.0' }) + result = engine.evaluate(user, flags)['test-version-less'] + expect(result.key).to eq('on') + end + + it 'tests version less or equal' do + user = freeform_user_context({ 'version' => '1.10.0' }) + result = engine.evaluate(user, flags)['test-version-less-or-equal'] + expect(result.key).to eq('on') + end + + it 'tests version greater' do + user = freeform_user_context({ 'version' => '1.10.0' }) + result = engine.evaluate(user, flags)['test-version-greater'] + expect(result.key).to eq('on') + end + + it 'tests version greater or equal' do + user = freeform_user_context({ 'version' => '1.9.0' }) + result = engine.evaluate(user, flags)['test-version-greater-or-equal'] + expect(result.key).to eq('on') + end + + it 'tests set is' do + user = user_context(nil, nil, nil, { 'key' => %w[1 2 3] }) + result = engine.evaluate(user, flags)['test-set-is'] + expect(result.key).to eq('on') + end + + it 'tests set is not' do + user = user_context(nil, nil, nil, { 'key' => %w[1 2] }) + result = engine.evaluate(user, flags)['test-set-is-not'] + expect(result.key).to eq('on') + end + + it 'tests set contains' do + user = user_context(nil, nil, nil, { 'key' => %w[1 2 3 4] }) + result = engine.evaluate(user, flags)['test-set-contains'] + expect(result.key).to eq('on') + end + + it 'tests set does not contain' do + user = user_context(nil, nil, nil, { 'key' => %w[1 2 4] }) + result = engine.evaluate(user, flags)['test-set-does-not-contain'] + expect(result.key).to eq('on') + end + + it 'tests set contains any' do + user = user_context(nil, nil, nil, nil, %w[u0qtvwla 12345678]) + result = engine.evaluate(user, flags)['test-set-contains-any'] + expect(result.key).to eq('on') + end + + it 'tests set does not contain any' do + user = user_context(nil, nil, nil, nil, %w[12345678 87654321]) + result = engine.evaluate(user, flags)['test-set-does-not-contain-any'] + expect(result.key).to eq('on') + end + + it 'tests glob match' do + user = user_context(nil, nil, nil, { 'key' => '/path/1/2/3/end' }) + result = engine.evaluate(user, flags)['test-glob-match'] + expect(result.key).to eq('on') + end + + it 'tests glob does not match' do + user = user_context(nil, nil, nil, { 'key' => '/path/1/2/3' }) + result = engine.evaluate(user, flags)['test-glob-does-not-match'] + expect(result.key).to eq('on') + end + + it 'tests is with booleans' do + # Test with uppercase TRUE/FALSE + user = user_context(nil, nil, nil, { + 'true' => 'TRUE', + 'false' => 'FALSE' + }) + result = engine.evaluate(user, flags)['test-is-with-booleans'] + expect(result.key).to eq('on') + + # Test with title case True/False + user = user_context(nil, nil, nil, { + 'true' => 'True', + 'false' => 'False' + }) + result = engine.evaluate(user, flags)['test-is-with-booleans'] + expect(result.key).to eq('on') + + # Test with lowercase true/false + user = user_context(nil, nil, nil, { + 'true' => 'true', + 'false' => 'false' + }) + result = engine.evaluate(user, flags)['test-is-with-booleans'] + expect(result.key).to eq('on') + end + end + + # Helper methods + def user_context(user_id = nil, device_id = nil, amplitude_id = nil, user_properties = nil, cohort_ids = nil) + { + 'user' => { + 'user_id' => user_id, + 'device_id' => device_id, + 'amplitude_id' => amplitude_id, + 'user_properties' => user_properties, + 'cohort_ids' => cohort_ids + } + } + end + + def freeform_user_context(user) + { + 'user' => user + } + end + + def group_context(group_type, group_name, group_properties = nil) + { + 'groups' => { + group_type => { + 'group_name' => group_name, + 'group_properties' => group_properties + } + } + } + end + + def get_flags(deployment_key) + server_url = 'https://api.lab.amplitude.com' + uri = URI("#{server_url}/sdk/v2/flags?eval_mode=remote") + + request = Net::HTTP::Get.new(uri) + request['Authorization'] = "Api-Key #{deployment_key}" + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| + http.request(request) + end + + raise "Response error #{response.code}" unless response.code == '200' + + JSON.parse(response.body).map { |flag| Evaluation::Flag.from_hash(flag) } + end +end diff --git a/spec/experiment/evaluation/murmur3_spec.rb b/spec/experiment/evaluation/murmur3_spec.rb new file mode 100644 index 0000000..6cde098 --- /dev/null +++ b/spec/experiment/evaluation/murmur3_spec.rb @@ -0,0 +1,4033 @@ +# frozen_string_literal: true + +RSpec.describe Murmur3 do + let(:murmur_seed) { 0x7f3a21ea } + + describe '.hash32x86' do + it 'handles simple input' do + input = 'brian' + result = described_class.hash32x86(input, murmur_seed) + expect(result).to eq(3_948_467_465) + end + + it 'matches reference output for english words' do + inputs = ENGLISH_WORDS.split("\n") + outputs = MURMUR3_X86_32.split("\n") + + inputs.zip(outputs).each do |input, output| + result = described_class.hash32x86(input, murmur_seed) + expect(result).to eq(output.to_i) + end + end + + it 'handles unicode strings' do + expect(described_class.hash32x86('My hovercraft is full of eels.')).to eq(2_953_494_853) + expect(described_class.hash32x86('My 🚀 is full of 🦎.')).to eq(1_818_098_979) + expect(described_class.hash32x86('吉 星 高 照')).to eq(3_435_142_074) + end + end +end + +ENGLISH_WORDS = <<~WORDS + a + a-horizon + a-ok + aardvark + aardwolf + ab + aba + abaca + abacist + aback + abactinal + abacus + abaddon + abaft + abalienate + abalienation + abalone + abampere + abandon + abandoned + abandonment + abarticulation + abase + abased + abasement + abash + abashed + abashment + abasia + abasic + abate + abatement + abating + abatis + abatjour + abattis + abattoir + abaxial + abba + abbacy + abbatial + abbatical + abbatis + abbe + abbess + abbey + abbot + abbreviate + abbreviated + abbreviation + abbreviature + abc + abcoulomb + abdal + abderite + abdicable + abdicant + abdicate + abdication + abdicator + abditory + abditos + abdomen + abdominal + abdominocentesis + abdominoscope + abdominoscopy + abdominous + abdominousness + abdominovesical + abduce + abducent + abduct + abduction + abductive + abductor + abeam + abecedarian + abecedarius + abecedary + abed + abel + abelia + abelmoschus + abelmosk + abends + aber + aberdeen + aberdevine + aberrance + aberrant + aberration + abest + abet + abetalipoproteinemia + abetment + abettor + abeunt + abeyance + abeyant + abfarad + abhenry + abhor + abhorrence + abhorrent + abhorrer + abibis + abidance + abide + abiding + abidjan + abience + abient + abies + abigail + abiit + abilities + ability + abiogenesis + abiogenetic + abiogenist + abiotrophy + abito + abject + abjection + abjectly + abjectness + abjunction + abjuration + abjurationabjurement + abjure + abkari + ablactation + ablated + ablation + ablative + ablaut + ablaze + ablaze(p) + able + ablebodied + ablegate + ableism + ableness + ablepharia + ablepsia + ablepsy + abloom + ablude + ablution + ablutionary + abnaki + abnegation + abnegator + abnormal + abnormality + abnormalize + abnormally + abnormis + abnormity + abnormous + aboard + abocclusion + abode + abodement + aboding + abohm + aboideau + abois + aboiteau + abolengo + abolish + abolishable + abolishment + abolition + abolitionary + abolitionism + abolitionist + abolitionize + abomasal + abomasum + abominable + abominate + abomination + abominator + aborad + aboral + abord + aboriginal + aborigine + aborigines + aborning + abort + aborticide + abortifacient + abortion + abortionist + abortive + abortively + abortus + abound + abounding + about + about(p) + about-face + abouts + above + above-mentioned + aboveboard + aboveground + abovementioned + abovesaid + abovestairs + abra + abracadabra + abrachia + abrade + abraded + abrader + abraham + abramis + abranchiate + abrasion + abrasive + abreast + abrege + abreption + abridge + abridged + abridger + abridgment + abroach + abroad + abrocoma + abrocome + abrogate + abrogated + abrogation + abronia + abrupt + abruption + abruptly + abruptness + abruzzi + abscess + abscessed + abscind + abscision + abscissa + abscission + abscond + absconder + abscondment + absence + absens + absent + absentee + absenteeism + absently + absentminded + absentmindedness + absento + absents + absinth + absinthe + absolute + absolutely + absoluteness + absolution + absolutism + absolutist + absolve + absolved + absolver + absolvitory + absolvitur + absonant + absonous + absorb + absorbable + absorbate + absorbed + absorbefacient + absorbency + absorbent + absorber + absorbing + absorption + absorptivity + absquatulate + abstain + abstainer + abstemious + abstemiously + abstemiousness + abstention + absterge + abstergent + abstersion + abstersive + abstinence + abstinent + abstract + abstracted + abstractedly + abstractedness + abstraction + abstractionism + abstractionist + abstractive + abstractly + abstractness + abstractor + abstruse + abstrusely + absurd + absurdity + absurdly + absurdness + absurdum + abudefduf + abulia + abulic + abuna + abundance + abundant + abundanti + abundantly + abuse + abused + abuser + abusive + abusively + abut + abutilon + abutment + abuttal + abutter + abutting + abuzz + abvolt + abwatt + aby + abysm + abysmal + abyss + abyssal + abyssinian + ac + acacia + academia + academic + academical + academically + academician + academicianship + academist + academy + acadia + acadian + acalypha + acanthaceae + acanthisitta + acanthocephala + acanthocephalan + acanthocereus + acanthocybium + acanthocyte + acanthocytosis + acanthoid + acantholysis + acanthoma + acanthophis + acanthopterygii + acanthoscelides + acanthosis + acanthotic + acanthuridae + acanthurus + acanthus + acapnic + acapulco + acardia + acariasis + acariatre + acaricide + acarid + acaridae + acarina + acarine + acaritre + acarophobia + acarpelous + acarpous + acarus + acatalectic + acataphasia + acathexia + acathexis + acaudate + acaulescent + accedas + accede + accelerando + accelerate + accelerated + accelerating + acceleration + accelerative + accelerator + accelerometer + accension + accent + accented + accentor + accents + accentual + accentuate + accentuation + accept + accepta + acceptability + acceptable + acceptably + acceptance + acceptation + accepted + accepting + acception + acceptive + acceptor + access + accessible + accession + accessional + accessorial + accessory + acciaccatura + accidence + accident + accident-prone + accidental + accidentally + accidentalness + accidents + accipere + accipient + accipiter + accipitres + accipitridae + accipitriformes + accipitrine + acclaim + acclamate + acclamation + acclimatization + acclimatize + acclivitous + acclivity + acclivous + accloy + accolade + accommodate + accommodating + accommodation + accommodational + accommodative + accomodation + accompanied + accompaniment + accompanist + accompany + accompanying + accompli + accomplice + accomplish + accomplishable + accomplished + accomplishment + accomplishments + accompts + accord + accordance + accordant + accordian + according + accordingly + accordion + accordionist + accost + accouchement + accoucheur + accoucheuse + account + accountability + accountable + accountable(p) + accountableness + accountancy + accountant + accountantship + accounter + accounting + accounts + accouple + accouplement + accousente + accouter + accoutered + accouterment + accouterments + accoy + accra + accredit + accreditation + accredited + accretion + accretionary + accretive + accrimination + accroach + accrue + accrued + accrust + accubation + accueil + accultural + acculturation + acculturational + accumbent + accumulate + accumulated + accumulation + accumulative + accuracy + accurate + accurately + accurse + accursed + accusable + accusation + accusative + accusatorial + accusatory + accuse + accused + accuser + accusing + accusingly + accustom + accustomary + accustomed + ace + acebutolol + aceite + aceldama + acentric + acephalia + acephalous + acequia + acequiador + acequiamadre + acer + aceraceae + acerate + aceration + acerb + acerbate + acerbic + acerbity + acerola + acervate + acervatim + acervation + acervulus + acervus + acescent + acetabular + acetabulum + acetal + acetaldehyde + acetamide + acetaminophen + acetanilide + acetate + acetic + acetone + acetonic + acetophenetidin + acetose + acetous + acetyl + acetylcholine + acetylene + acetylenic + acetylic + achaean + achar + acharn + acharne + acharnement + achates + ache + achene + achenial + acheron + acheronian + acherontia + acherontis + acheta + achievability + achievable + achieve + achievement + achiever + achillea + achillean + achilles + achimenes + aching + achira + achivi + achlamydeous + achlorhydria + achlorhydric + achoerodus + acholia + achomawi + achondrite + achondritic + achondroplasia + achondroplastic + achras + achromatic + achromatin + achromatinic + achromatism + achromatize + achromatous + achromia + achromic + achylia + acicula + acicular + aciculate + acid + acid-fast + acid-forming + acid-loving + acidemia + acidic + acidification + acidify + acidimetric + acidimetry + acidity + acidophil + acidophilic + acidosis + acidotic + acidulate + acidulated + acidulous + acierta + aciform + acinaform + acinar + aciniform + acinonyx + acinos + acinus + acipenser + acipenseridae + ackee + acknowledge + acknowledgeable + acknowledged + acknowledgement + acknowledgment + acme + acne + acned + acneiform + acnidosporidia + acocanthera + acold + acology + acolothyst + acolyte + acolyth + acomia + aconcagua + aconite + aconitum + acoraceae + acorea + acorn + acorus + acousma + acoustic + acoustically + acoustician + acoustics + acquaint + acquaintance + acquainted + acquainted(p) + acquaintenace + acquaintend + acquainting + acquest + acquiesce + acquiescence + acquiescent + acquirable + acquire + acquired + acquirement + acquirements + acquirer + acquiring + acquirit + acquisition + acquisitions + acquisitive + acquisitiveness + acquit + acquitment + acquittal + acquittance + acquitted + acrasiomycetes + acre + acre-foot + acreage + acres + acrid + acrididae + acridity + acridotheres + acrilan + acrimonious + acrimony + acris + acritical + acritude + acroama + acroamatic + acroamatical + acroamatics + acroanesthesia + acroatic + acrobat + acrobates + acrobatic + acrobatics + acrocarp + acrocarpous + acrocarpus + acrocentric + acrocephalus + acroclinium + acrocomia + acrocyanosis + acrodont + acrogen + acrogenic + acromatic + acromegalic + acromegaly + acromicria + acromion + acromphalus + acromyotonia + acronym + acronymic + acropetal + acrophobia + acrophobic + acropolis + acropora + acrosome + acrospire + across + across-the-board + acrostic + acrostichum + acrylic + act + acta + actable + actaea + acted + acti + actias + actifed + actin + actinal + acting + acting(a) + actinia + actiniaria + actinic + actinidia + actinidiaceae + actiniopteris + actinism + actinium + actinoid + actinolite + actinomeris + actinometer + actinometric + actinometry + actinomorphic + actinomyces + actinomycetacaea + actinomycetal + actinomycetales + actinomycete + actinomycin + actinomycosis + actinomycotic + actinomyxidia + actinomyxidian + actinopod + actinopoda + action + actionable + actions + actitis + actium + activated + activating(a) + activation + activator + active + actively + activeness + activism + activist + activity + actomyosin + actor + actress + acts + actu + actual + actuality + actualized + actually + actuarial + actuary + actuateact + actuated + actuator + actum + actus + acu + acuate + acuity + aculea + aculeate + aculeated + aculeus + acumen + acuminate + acuminated + acumination + acun + acupressure + acupuncture + acute + acutely + acuteness + acyclic + acyclovir + ad + ad-lib + adactylia + adactylism + adactylous + adad + adaga + adage + adagio + adalia + adam + adamance + adamant + adamantean + adamantine + adamantly + adams + adansonia + adapa + adapid + adapt + adaptability + adaptable + adaptation + adaptational + adapted + adapter + adaption + adaptive + adar + adaxial + add + addable + addax + adde + added + addend + addendum + adder + addere + addesse + addict + addicted + addiction + addictive + adding + addison + additament + addition + additional + additionally + additive + additum + addle + addle-head + addlebrained + addled + addlehead + addlepated + address + addressable + addressed + addressee + addresses + adduce + adducent + adducing + adduction + adductive + adductor + ade + adeem + adel + adelaide + adelges + adelgid + adelgidae + adelie + adelig + adelomorphous + ademption + ademptum + aden + adenanthera + adenine + adenitis + adenium + adenocarcinoma + adenocarcinomatous + adenography + adenoid + adenoidal + adenoidectomy + adenoma + adenomegaly + adenopathy + adenosine + adenota + adenovirus + adeo + adeology + adept + adeptness + adequacy + adequate + adequately + adespotic + adhere + adherence + adherent + adhering + adhesion + adhesive + adhesiveness + adhibenda + adhibit + adhibition + adhortation + adiabatic + adiantaceae + adiantum + adiaphanous + adiathermancy + adience + adient + adieu + adige + adipocere + adipose + adiposity + adirondacks + adit + aditi + aditya + adj + adjacency + adjacent + adjection + adjectitious + adjectival + adjectivally + adjective + adjectively + adjectives + adjoin + adjoining + adjourn + adjournment + adjuc + adjudge + adjudicate + adjudication + adjudicative + adjudicator + adjunct + adjunctive + adjuration + adjuratory + adjure + adjust + adjustable + adjusted + adjuster + adjustive + adjustment + adjutage + adjutant + adjuvant + adjuvat + adlumia + admass + admeasurement + administer + administrable + administration + administrative + administratively + administrator + administrators + admirability + admirable + admirably + admiral + admiralty + admirari + admiration + admire + admired + admirer + admiring + admiringly + admissable + admissibility + admissible + admission + admissive + admit + admittable + admittance + admitted + admitted(a) + admitting + admixture + admlration + admonish + admonished + admonisher + admonition + admonitive + admonitory + adnate + adnexa + adnexal + adnoun + ado + adobe + adobo + adolescence + adolescent + adonic + adonis + adonize + adopt + adoptable + adopted + adoption + adoptive + adorability + adorable + adorably + adoration + adore + adored + adorer + adoring + adoringly + adorn + adorned + adorned(p) + adornment + adown + adrenal + adrenalectomy + adrenaline + adrenarche + adrenergic + adrenocortical + adrenocorticotropic + adrenosterone + adrenotrophin + adriatic + adrift + adrift(p) + adroit + adroitly + adroitness + adrolepsy + adscititious + adscript + adscriptus + adsorbable + adsorbate + adsorbed + adsorbent + adsorption + adulation + adulator + adulatory + adullam + adult + adulterant + adulterate + adulterated + adulterating + adulteration + adulterer + adulteress + adulterine + adulterous + adulterously + adultery + adulthood + adultism + adultness + adultress + adumbrate + adumbration + adumbrative + aduncated + aduncity + aduncous + adust + adustion + adv + advance + advance(a) + advanced + advanced(a) + advancement + advances + advancing + advantage + advantaged + advantageous + advection + advective + advene + advent + adventism + adventist + adventitial + adventitious + adventive + adventure + adventurer + adventures + adventuress + adventurism + adventuristic + adventurous + adventurousness + adverb + adverbial + adverbially + adverbs + adversaria + adversary + adversarys + adversative + adverse + adversely + adversis + adversity + adversitys + adversum + advert + advertence + advertency + advertent + advertise + advertised + advertisement + advertiser + advertising + advice + advisability + advisable + advise + advised + advisedly + advisee + advisemement + adviser + advisory + advocacy + advocate + advocation + advoutress + advoutry + advowson + adynamia + adynamic + adynamy + adytum + adz + adze + adzooks + aecial + aeciospore + aecium + aedes + aedile + aedipus + aeequa + aegean + aegiceras + aegilops + aegina + aegis + aegospotami + aegri + aegypiidae + aegypius + aegyptopithecus + aeneas + aeneid + aeneus + aeolian + aeolic + aeolis + aeolotropic + aeolus + aeon + aeonium + aepyceros + aepyornidae + aepyorniformes + aequa + aequam + aequat + aequis + aequo + aerated + aeration + aerator + aere + aerial + aerialist + aerially + aerides + aerie + aeriferous + aerifiction + aeriform + aerobacter + aerobe + aerobic + aerobics + aerobiosis + aerobiotic + aerodontalgia + aerodrome + aerodynamic + aerodynamics + aerography + aerolite + aerological + aerology + aerolytic + aeromancy + aeromechanic + aeromechanics + aeromedical + aeromedicine + aerometer + aerometry + aeronatics + aeronaut + aeronautic + aeronautical + aeronautics + aerophagia + aerophilatelic + aerophilately + aeroplane + aeroplanist + aeroscope + aeroscopy + aerosol + aerosolized + aerospace + aerosphere + aerostat + aerostatic + aerostatics + aerostation + aertex + aes + aeschylean + aeschylus + aeschynanthus + aesculapian + aesculapius + aesculus + aesir + aesop + aestas + aesthetic + aesthetically + aesthetics + aestival + aetas + aeterna + aeternum + aether + aethionema + aethusa + aetiology + aetobatus + aevi + afar + afeard + afeard(p) + afebrile + affability + affable + affably + affair + affaire + affaires + affairs + affect + affectation + affected + affected(p) + affectedly + affectedness + affectibility + affecting + affectingly + affection + affectional + affectionate + affectionateness + affectioned + affections + affector + affects + affenpinscher + afferent + affettuoso + affiance + affianced + affiche + afficher + affidation + affidavit + affiliated + affiliation + affinal + affined + affinity + affirm + affirmable + affirmance + affirmation + affirmative + affirmatively + affirmativeness + affix + affixal + affixation + affixed + afflation + afflatus + afflict + afflicted + afflicting + affliction + afflictions + afflictive + affluence + affluent + afflux + affluxion + afford + afforestation + affraid + affranchise + affranchisement + affray + affrayment + affricate + affrication + affriction + affright + affrightment + affront + affronted + affronterai + affuse + affusion + afghan + afghani + afghanistan + afibrinogenemia + afield + afire + aflak + aflame(p) + aflare + afloat + afloat(p) + aflutter + afoot + afoot(p) + afore + aforehand + aforementioned + aforenamed + aforesaid + aforesaid(a) + aforethought + aforethought(ip) + afoul + afoul(ip) + afraid + afraid(p) + aframomum + afreet + afresh + afric + africa + african + african-american + africander + afrikaans + afrikaner + afro + afro-asian + afro-wig + afroasiatic + afrocarpus + afropavo + aft + aft(a) + after + after(a) + after-hours + after-school(a) + after-shave + afterage + afterbirth + afterburden + afterburner + aftercare + afterclap + aftercome + aftercourse + aftercrop + afterdamp + afterdeck + afterdinner + aftereffect + aftergame + afterglow + aftergrowth + afterimage + afterlife + aftermath + aftermost + afternoon + afternoon(a) + afterpains + afterpart + afterpiece + aftershaft + aftershafted + aftershock + aftertaste + afterthought + afterwards + afterworld + aga + agacerie + again + agains + against + agalactia + agalloch + agallochium + agama + agamemnon + agamic + agamid + agamidae + agamist + agammaglobulinemia + agapanthus + agape + agape(p) + agapemone + agapornis + agar + agaric + agaricaceae + agaricales + agaricus + agas + agastache + agate + agateware + agathis + agavaceae + agave + agaze + agdistis + age + age-old + aged + aged(a) + agedness + ageism + agelaius + ageless + agelessness + agelong + agency + agenda + agendum + agenesis + agent + agential + agentive + agents + agentship + agerasia + ageratina + ageratum + ages + agglomerate + agglomeration + agglutinate + agglutination + agglutinative + agglutinin + agglutinogen + aggrandize + aggrandizement + aggravable + aggravate + aggravated + aggravating + aggravatingly + aggravation + aggregate + aggregation + aggression + aggressive + aggressively + aggressiveness + aggressor + aggrieve + aggrieved + aggro + aggroup + aghan + aghast + aghast(p) + agianst + agile + agilely + agility + agincourt + aging + agio + agiotage + agir + agis + agitate + agitated + agitation + agitative + agitator + agitur + agjus + agkistrodon + aglaomorpha + aglaonema + agleam + aglet + aglitter(p) + aglow + aglow(p) + agnate + agnatha + agnation + agni + agnition + agnize + agnomen + agnosia + agnostic + agnosticism + agnus + ago + agog + agoing + agonadal + agonal + agonidae + agonies + agonism + agonist + agonistic + agonize + agonized + agonizing + agonizingly + agonus + agony + agora + agoraphobia + agoraphobic + agostadero + agouti + agranulocytic + agranulocytosis + agrapha + agraphia + agraphic + agrarian + agree + agreeable + agreeableness + agreeably + agreed + agreeing + agreeing(a) + agreement + agrescit + agrestic + agribusiness + agricultor + agricultural + agriculture + agriculturist + agrimonia + agriocharis + agrippa + agrobacterium + agrobiologic + agrobiology + agrologic + agrology + agromania + agronomic + agronomist + agronomy + agropyron + agrostemma + agrostis + aground + aground(p) + agrypnia + agrypnotic + agua + aguardiente + ague + aguets + agueweed + aguish + agural + agurial + ah + aha + ahab + ahariolation + ahead + ahead(p) + ahorse + ahorse(p) + ahriman + ahuehuete + ahura + aid + aidance + aide + aide-memoire + aidedecamp + aidetoi + aiding + aidless + aids + aigrette + aiguille + aigulet + aikido + ail + ailanthus + aile + aileron + ailing + ailment + ailurophobia + ailuropoda + ailuropodidae + ailurus + aim + aimer + aimless + aimlessly + aimlessness + aingenium + aioli + air + air(a) + air-conditioned + air-conditioner + air-cooled + air-intake + air-to-air + air-to-surface + airborne + airbrake + airbrush + airbubble + airbuilt + airbus + aircraft + aircraftsman + aircrew + aircrewman + airdock + aire + aired + airedale + airfield + airflow + airfoil + airframe + airheaded + airhole + airiness + airing + airless + airlift + airline + airliner + airlock + airmail + airman + airmanship + airpipe + airplane + airport + airs + airship + airsick + airspace + airspeed + airstream + airstrip + airtight + airwind + airworthiness + airworthy + airy + aise + aisle + ait + aitch + aitchbone + aiunt + aix + aizoaceae + ajaia + ajar + ajar(p) + ajax + ajee + ajuga + ajutage + akan + akaryocyte + akee + akeridae + akimbo + akimbo(ip) + akin + akin(p) + akinesis + akkadian + akron + akwa'ala + al + ala + alabama + alabaman + alabaster + alack + alacran + alacritous + alacrity + alacritywant + aladdin + alalia + alameda + alamo + alanine + alar + alarm + alarmed + alarming + alarmingly + alarmism + alarmist + alarum + alas + alaska + alaskan + alate + alated + alauda + alaudidae + alaw + alb + alba + albacore + albania + albanian + albany + albata + albatrellus + albatross + albedo + albeit + alberca + albert + alberta + albetur + albification + albinal + albinism + albino + albite + albitic + albizzia + albuca + albuginaceae + albugo + albula + albulidae + album + albumen + albumin + albuminous + albuminuria + albuminuric + albuquerque + albuterol + alca + alcaeus + alcaic + alcaid + alcalde + alcazar + alcea + alcedinidae + alcedo + alcelaphus + alces + alchemic + alchemist + alchemistic + alchemy + alcidae + alcohol + alcoholic + alcoholism + alcoran + alcove + alcyonacea + alcyonaria + aldebaran + aldehyde + aldehydic + alder + alderfly + alderman + aldermanic + aldol + aldose + aldosterone + aldosteronism + aldrovanda + ale + alea + aleatory + alectis + alecto + alectoria + alectoris + alectoromancy + alectryomancy + alectura + alee + alehouse + alembic + alentours + aleph + aleph-null + alepisaurus + aleppo + alert + alertly + alertness + alerts + aletris + aleurites + aleuromancy + aleurone + aleuronic + aleut + alewife + alex + alexander + alexandria + alexandrian + alexandrine + alexandrite + alexic + alexipharmic + alexiteric + aleyrodes + aleyrodidae + alfalfa + alfardaws + alfilaria + alfresco + alga + algal + algarroba + algebra + algebraic + algebraically + algebraist + algebraize + algeria + algerian + algeripithecus + alget + algid + algiers + algin + algoid + algol + algolagnic + algology + algometer + algometric + algometry +WORDS + +MURMUR3_X86_32 = <<~OUTPUTS + 479943832 + 1667709009 + 821871466 + 3407736416 + 3096332942 + 3551383265 + 3482684574 + 2565507831 + 3854066542 + 2587107967 + 2808392652 + 3446716934 + 3502963659 + 1987448620 + 24285452 + 2738203305 + 4232420174 + 3862742673 + 1866007835 + 2260804683 + 3625386851 + 1454628382 + 3350035316 + 3809838383 + 2426510977 + 1193232102 + 2866675877 + 51648458 + 3306774988 + 1682563376 + 2965126635 + 941752146 + 2429220625 + 276371974 + 2583938337 + 59050478 + 2858339597 + 125225229 + 988317359 + 1113280942 + 3715449071 + 3444742227 + 2456188981 + 1442787282 + 2173228754 + 4045160086 + 2928355258 + 3381815694 + 3322818116 + 4182951506 + 1805745589 + 2101748366 + 3792465379 + 2370038907 + 4272752794 + 3326761741 + 3504238505 + 1406242349 + 711813597 + 1026919206 + 1162801996 + 3568847465 + 1793047661 + 1636371546 + 3932247933 + 1526356669 + 1225471874 + 3540638123 + 2635550809 + 4097763765 + 3112919846 + 3669823156 + 449299197 + 3482740039 + 2214560286 + 3573650952 + 965724450 + 22241655 + 3782283617 + 488558219 + 3722744581 + 308050588 + 1787755456 + 3635016399 + 2928539924 + 3674664861 + 3789510022 + 208914522 + 3357179249 + 1136002305 + 1223882462 + 1489673113 + 2616149414 + 4150564834 + 92689916 + 4292195407 + 1887956199 + 2573581345 + 119505784 + 3381225555 + 2127168897 + 1221576289 + 1799495839 + 3184250659 + 4252821245 + 926076625 + 1165334098 + 2738430625 + 1213272418 + 696817588 + 3828657519 + 432552516 + 1185187836 + 1469601454 + 3976428667 + 2221717097 + 273053635 + 3312990606 + 260262762 + 485774389 + 2161756208 + 2574222548 + 3971468958 + 3966418662 + 1402090407 + 3207319850 + 3510111757 + 1691226417 + 210010751 + 3167713523 + 808128695 + 1467135643 + 2098791062 + 2824528353 + 265455727 + 3817768194 + 2297434297 + 516437602 + 3429077782 + 1665722383 + 1725163699 + 3834356229 + 2308223238 + 3469654560 + 2591905186 + 2120020250 + 4084571501 + 1170031954 + 2890400796 + 716489704 + 2789972441 + 3641589098 + 1407711709 + 3034710729 + 1643798472 + 443810740 + 3510908902 + 266230063 + 3253401496 + 1154946843 + 2230277207 + 3441337261 + 3770208956 + 1517420149 + 4073671813 + 2617112832 + 3754202074 + 1642266108 + 3896133307 + 2674121734 + 3805252303 + 2868534446 + 2272665555 + 2615253321 + 355387517 + 1391279937 + 4183562935 + 1444477415 + 457354149 + 1368757089 + 1185932692 + 3956234285 + 815078461 + 2290455632 + 3866070370 + 1989844323 + 3553465460 + 119501113 + 2701783073 + 1892610665 + 559640549 + 2616771970 + 4005750476 + 3371912046 + 362801503 + 1324398509 + 1249204340 + 3548655625 + 1145187998 + 2118727525 + 1094336927 + 823799195 + 980604398 + 1580418341 + 2026811535 + 154690488 + 566787852 + 810057128 + 1707651442 + 3507190449 + 4237630584 + 2551055318 + 3515381994 + 1269743426 + 1812848471 + 80052581 + 2035725641 + 3049436605 + 2195212065 + 1318653802 + 2245741237 + 1363246427 + 2406124216 + 825666336 + 1651808082 + 1916646796 + 413795722 + 3851246225 + 3198226486 + 1221879996 + 1076061994 + 1592858141 + 2575708577 + 4185606507 + 3964661531 + 1077896895 + 274068358 + 642506002 + 976601532 + 2616288143 + 3692520296 + 66175194 + 1458620196 + 2769318157 + 3652412113 + 437404922 + 1245829761 + 621804297 + 2055065820 + 3137033121 + 683454299 + 2990393269 + 2077612862 + 326672952 + 700606360 + 3267867075 + 2436744631 + 732620276 + 4253335689 + 3488564445 + 612606850 + 827327274 + 3043504842 + 114477531 + 2356740879 + 4043028977 + 1452936829 + 2875196560 + 2072170996 + 7376391 + 1023612149 + 2713539077 + 3011952297 + 2810713799 + 840415664 + 3684492239 + 323846150 + 540782310 + 854433253 + 3646233015 + 3970433237 + 1489430183 + 1359641427 + 3297178217 + 2739708822 + 2930674002 + 3676755256 + 1693129065 + 1186344186 + 3614822403 + 3536297717 + 1693651274 + 2912984517 + 2427144686 + 39601588 + 447811838 + 896743038 + 3649085850 + 3544697351 + 1727638948 + 1459232538 + 3981007615 + 3343398595 + 45740861 + 1409220498 + 1554557133 + 385608624 + 2711897640 + 2556851370 + 31784056 + 3996459150 + 2236236113 + 3268559443 + 1676133402 + 4170716858 + 2961341127 + 3330387646 + 2032866238 + 2323779213 + 997562267 + 1163856300 + 2401130160 + 3534043939 + 1526460816 + 497953481 + 434404863 + 2598954397 + 1880049848 + 42687069 + 2115084235 + 1023314251 + 653116723 + 1575679949 + 2207026213 + 658496145 + 188454540 + 74773676 + 4079917818 + 2742180667 + 1718403919 + 3467778015 + 1667851559 + 3249898252 + 2894254320 + 1453970587 + 1871381956 + 2191200100 + 1923995608 + 1585402617 + 238905625 + 1186636312 + 1441092839 + 3976298986 + 1910009683 + 2013960270 + 4141896354 + 2598852332 + 2565770833 + 1152200988 + 376123365 + 2748082454 + 2806447596 + 1237627630 + 2456073301 + 18297902 + 962423124 + 311925022 + 3494690406 + 232125398 + 3768891634 + 425335007 + 2250940149 + 1286522375 + 3727250457 + 327768174 + 1131433808 + 776675303 + 806297962 + 2320481583 + 887634214 + 224927059 + 1071636533 + 897138920 + 2761180897 + 1877608917 + 211714391 + 1320958441 + 3305302506 + 1199978114 + 189712539 + 2338772098 + 4110073093 + 3543471284 + 2478815746 + 715330183 + 3887433828 + 1030680667 + 3826956930 + 1021197200 + 1969755147 + 746431212 + 3223225061 + 60100726 + 2766813346 + 3083524672 + 1979824401 + 2530020481 + 4013529374 + 3510419866 + 562322081 + 3480148065 + 3545814150 + 1475429168 + 2487392694 + 829033654 + 2684010907 + 3587737279 + 2832379853 + 818226767 + 999053687 + 3400064511 + 3069783881 + 2162543950 + 2248528731 + 949788089 + 4163943386 + 787990728 + 3962567055 + 2779550968 + 3558066911 + 2422164805 + 1943573869 + 1749987761 + 4144035084 + 2993565407 + 1935448595 + 3159612048 + 3685501735 + 540903897 + 2908439485 + 1914454572 + 1123490337 + 3742157185 + 3509001304 + 895732841 + 2368302610 + 2455105807 + 1399869929 + 3139440125 + 1293366076 + 3021024712 + 1144107802 + 2214280696 + 2007906238 + 1972503713 + 3497166630 + 2934945802 + 150226060 + 302290846 + 3507177779 + 1061057668 + 533727180 + 4125876014 + 2683883754 + 2057779400 + 3337988993 + 2527348240 + 704756384 + 2525168361 + 651613445 + 3039464093 + 2899175484 + 2515835901 + 2774434160 + 3993741290 + 1442643311 + 1663358189 + 1781180986 + 3347369800 + 3284181095 + 1076109161 + 3568507044 + 748054083 + 1548759950 + 3220435225 + 4236337430 + 3794294861 + 1219800664 + 1651064367 + 1363905096 + 3078546027 + 323289380 + 2010248514 + 66889553 + 860671828 + 3064373862 + 1380934452 + 1175812279 + 2009044307 + 3958328789 + 2154567647 + 591437262 + 1641738086 + 2288299256 + 501046143 + 1727265065 + 1279697279 + 945611776 + 530292284 + 1698572276 + 635126711 + 2904959450 + 880690436 + 2347156371 + 3918407210 + 946722844 + 3218210008 + 1399737189 + 923691871 + 1319128627 + 3245692737 + 3737964057 + 41826165 + 2027162914 + 486261897 + 2786253279 + 2947271480 + 4275611960 + 2233827989 + 46627886 + 1691985495 + 4181650768 + 2533017019 + 3161383587 + 2261811614 + 3565610549 + 3386521896 + 59132769 + 161670924 + 231621965 + 4045288910 + 1667179964 + 449187926 + 2344112010 + 3467753403 + 2305101156 + 2296694200 + 703532106 + 1142394369 + 1183945048 + 2021559822 + 4166415422 + 954072873 + 3847391202 + 288502854 + 1950400383 + 3614755555 + 3336012377 + 3776800563 + 3222044657 + 1376387809 + 3932743002 + 36146641 + 1752496937 + 2601521776 + 2164493091 + 1399432738 + 3934836427 + 3513047743 + 2652905190 + 814640578 + 2971907118 + 329978951 + 317251984 + 2105896398 + 3290391579 + 869559945 + 1662959374 + 2411343309 + 2183131922 + 2999665611 + 604211450 + 440203927 + 295572879 + 2657198930 + 437239755 + 1164192264 + 470866387 + 1755976844 + 1546694178 + 576928681 + 2761292588 + 3645714043 + 3507624825 + 395737289 + 3267650905 + 78838860 + 1626342422 + 2679402531 + 1406771551 + 3266226658 + 4214937957 + 1675437638 + 2216120497 + 3689082907 + 3186372108 + 2511915765 + 34467525 + 572511405 + 4157005326 + 95605688 + 2890105817 + 748779133 + 1097015041 + 789156156 + 1653603334 + 2247234555 + 601241297 + 3275544899 + 814209449 + 2570592879 + 1774098114 + 485685284 + 1409129655 + 2537644521 + 3870539782 + 847187015 + 592498244 + 2558396831 + 3521503035 + 614692122 + 2771658112 + 3577134905 + 2494141426 + 81162297 + 850121388 + 717096011 + 2205488185 + 3730476312 + 1974619798 + 3277992597 + 406033996 + 3119412164 + 2705821557 + 1651380651 + 2636665424 + 1953697251 + 3069299329 + 2792992905 + 1561856108 + 1993447863 + 4005826653 + 2134018641 + 3914961247 + 2728296686 + 808327910 + 2226535580 + 3241534487 + 2442681 + 391603768 + 2770601636 + 3626765907 + 1818824840 + 392719218 + 1367150609 + 1141794622 + 2701729042 + 1579716514 + 1860843867 + 1410241613 + 527241781 + 4213727616 + 3703459097 + 2178820185 + 1954003526 + 843783550 + 4113951527 + 1245752470 + 3207549252 + 1404850546 + 2635490741 + 1266204803 + 1857112478 + 4292414932 + 2186307714 + 2434532279 + 3935724308 + 2748740696 + 3505568062 + 3988374441 + 484081045 + 4005141205 + 2387494402 + 3608836999 + 4262213474 + 3106826283 + 445927826 + 4071803420 + 309867577 + 3500758064 + 4086941368 + 914164102 + 2668420338 + 4214615934 + 3118852435 + 3576373248 + 660900972 + 547390543 + 407033641 + 1706244219 + 3312283754 + 1992079545 + 921597021 + 558320745 + 4044558623 + 1341997166 + 3961356485 + 3181451102 + 3245996717 + 2365376705 + 2999917225 + 2593376121 + 2190092023 + 2965216209 + 2858388372 + 1460519790 + 3625826717 + 2108322133 + 1996335868 + 2715922682 + 917481412 + 1825463384 + 3005827365 + 399616526 + 3003198331 + 2887157916 + 1174558717 + 3583769212 + 1337811651 + 599654970 + 888127439 + 1769385198 + 1925858107 + 3982557209 + 1119883036 + 2714415471 + 611639245 + 429216215 + 2544646678 + 2962331678 + 4141379649 + 985486527 + 3184988813 + 4010129267 + 4214937903 + 4150689113 + 1028470775 + 673712797 + 675999988 + 3410367343 + 2422171229 + 855415704 + 1223295504 + 406655830 + 3691263914 + 290810063 + 891334188 + 3788267641 + 1513520347 + 3400181987 + 3478557428 + 4014487758 + 2802387961 + 4008154345 + 2929855991 + 3526840663 + 1230502518 + 3464132038 + 3920111497 + 2919289023 + 1172935864 + 2900249378 + 694797723 + 2313309466 + 4111021356 + 2136416877 + 2297524679 + 2134884023 + 1306711673 + 2284132675 + 1743220491 + 568139715 + 667478595 + 35168067 + 2673978838 + 3891576919 + 1528721841 + 3188075470 + 471272420 + 2303387459 + 1477779029 + 2744768577 + 1336378048 + 1516722101 + 222546532 + 2722583132 + 2774666788 + 3926184745 + 2020318314 + 3590544633 + 3282502994 + 2323276517 + 184250139 + 1241772204 + 2331745656 + 3205795611 + 3545623655 + 587550778 + 1407676934 + 2985409624 + 3746202431 + 351739541 + 2723800019 + 438558775 + 4185591131 + 3595960631 + 979814554 + 3866011097 + 2625949275 + 2440240515 + 1098356411 + 3387319666 + 1633713248 + 2967407103 + 1680699613 + 59554886 + 55943917 + 100484899 + 4157431707 + 1747851344 + 1537875403 + 994888934 + 2369930284 + 1687539712 + 4065105946 + 889552074 + 2837076628 + 2728509243 + 2160045407 + 3454231334 + 2561282408 + 1763619278 + 3053791223 + 3868618734 + 4123753028 + 3810266261 + 3163015507 + 3497096272 + 3414643063 + 1932823504 + 2923980959 + 3020589255 + 3263462340 + 1879262667 + 1534768220 + 3511781839 + 789785567 + 4015462982 + 476944116 + 2229113231 + 3248714722 + 2847014415 + 4109600768 + 142301427 + 3419338280 + 1241981257 + 186371752 + 3542744997 + 2262133025 + 2789463806 + 307379095 + 33445834 + 425913123 + 2918335140 + 1572219061 + 277483306 + 1509941403 + 4129723340 + 833257604 + 538618864 + 355461387 + 395930754 + 2493847794 + 2515787584 + 3938674219 + 580964118 + 2775418465 + 4259565641 + 2112179098 + 670029902 + 4017498764 + 610159687 + 1209888758 + 3561676319 + 1923604773 + 1332050469 + 3606455966 + 1638118900 + 2654529851 + 850904257 + 3614221178 + 1784516892 + 1815976611 + 2928419899 + 2862390384 + 1486753005 + 2556583923 + 3514695902 + 767761394 + 1360706234 + 3935506207 + 2249128585 + 2870327202 + 2395120415 + 2635356720 + 2162689222 + 4171841634 + 2654110505 + 1637499906 + 1427943040 + 1137510903 + 2142606313 + 3703386790 + 854945889 + 2379145327 + 2201184606 + 153547460 + 2856700316 + 3977951268 + 1342141581 + 2786874304 + 63019704 + 2865446926 + 4024771971 + 1886050815 + 221390908 + 3354946726 + 1140178746 + 1359337938 + 3218842266 + 2284383695 + 3153006133 + 415745063 + 3410515842 + 3642472434 + 1691128412 + 1054634102 + 3429592236 + 2485206145 + 707307215 + 2452249591 + 3273292331 + 645439352 + 4071259309 + 3342723182 + 3342715908 + 1284344423 + 4064608701 + 3987531496 + 4090725199 + 1801949766 + 494452007 + 436563778 + 2255276886 + 506399143 + 3009612576 + 192519482 + 3195083287 + 3942653198 + 1070423984 + 386566998 + 1539410374 + 2048669885 + 1863184759 + 2284272453 + 2291357642 + 3452504893 + 2621152825 + 2454668710 + 1817744667 + 1229920387 + 2826567630 + 279493146 + 697278789 + 304606287 + 3676687625 + 4143629353 + 4280417445 + 2353235155 + 1424653354 + 1643501461 + 341070130 + 3441010229 + 943904705 + 39125610 + 3717991064 + 764826915 + 997268883 + 92397327 + 1500599803 + 2763891793 + 2236582775 + 2585842595 + 550297128 + 1664834417 + 3511579147 + 4181711328 + 3408955670 + 3980832180 + 790711359 + 1841524075 + 818563632 + 1729224205 + 1696800603 + 3315539059 + 63074257 + 2894691303 + 3886896197 + 1639795350 + 3279628827 + 1815748138 + 3332036295 + 6327415 + 1537237437 + 4075416501 + 3823305462 + 1697015875 + 3073765873 + 268124703 + 1508317646 + 875693095 + 3607877509 + 3040642197 + 795701643 + 3006712200 + 234879086 + 4073562065 + 3726454173 + 2752425176 + 1407371816 + 1317740699 + 2829963686 + 957838546 + 2081254442 + 1999775920 + 3698637610 + 638993530 + 58967796 + 197513608 + 1179182019 + 2349668417 + 1600701465 + 3437964051 + 3434164759 + 3912892399 + 2040079264 + 587722307 + 1675152278 + 4145603677 + 487004433 + 2553814587 + 3363315843 + 326858577 + 3519712511 + 333992124 + 1549819933 + 3738961258 + 1218932097 + 2866062040 + 126077633 + 3356013417 + 3929310981 + 1983801482 + 975271763 + 2929375963 + 3270372260 + 158407809 + 3516263996 + 644671075 + 722951279 + 1851902713 + 1729762370 + 2327401085 + 2095734572 + 2745730413 + 1680140910 + 1155355083 + 253978657 + 1349532159 + 3424366335 + 2776829984 + 3615345609 + 2682918852 + 3614724695 + 4150087782 + 761495465 + 3173708930 + 225908670 + 2742411792 + 3998584912 + 1797616672 + 3672050669 + 2184908410 + 419323385 + 737179967 + 3288491367 + 3000789351 + 2080942491 + 2426882682 + 2864630608 + 3007863510 + 1038201506 + 1177581695 + 802540715 + 340214720 + 4030216418 + 84650719 + 3722626089 + 3458502957 + 3413434416 + 4225621861 + 1816025963 + 4002422034 + 435200476 + 3744837657 + 2212562978 + 3783848151 + 2213607735 + 2756425550 + 3015211131 + 1601337402 + 3394767977 + 766406004 + 1273713750 + 2134955252 + 3606732757 + 1171862239 + 125994686 + 2903294250 + 3561332848 + 1813177696 + 596487801 + 3433059340 + 960225991 + 1453114220 + 1151920528 + 355999712 + 1815934530 + 3882211934 + 1430273284 + 3594720355 + 3001119229 + 2856922809 + 3876128321 + 2601009847 + 210218186 + 2610150056 + 1788418876 + 2162426743 + 3024773148 + 773224368 + 2382669098 + 3180799825 + 15944152 + 3851502974 + 1495612368 + 3396463362 + 3691235621 + 2358018526 + 378618584 + 1129308550 + 3281570940 + 3718493827 + 3331985955 + 1217264672 + 2402032531 + 3671329047 + 2184596905 + 3264792477 + 2335424803 + 3668097440 + 1009432720 + 2601476522 + 3765270557 + 1039149214 + 1707426599 + 3314457281 + 2640196865 + 1366367044 + 1802265828 + 4251728133 + 674383950 + 2571949473 + 2187646062 + 3762426629 + 2649532698 + 268661045 + 236112017 + 1941226725 + 4236759496 + 128747019 + 338194262 + 158970807 + 937283909 + 2108738670 + 203588356 + 3772802675 + 360540841 + 2856698424 + 1425689232 + 2176664612 + 377847636 + 3372592368 + 894609819 + 1458677693 + 2738668183 + 3670423959 + 4098338025 + 3232472539 + 2035720620 + 3521099876 + 606978502 + 2811189443 + 4211087557 + 890044869 + 2021976803 + 3886867809 + 481285305 + 806615354 + 1625877641 + 1099718055 + 1372238067 + 3213299505 + 252376525 + 2546857989 + 2100410966 + 1342382724 + 4231306714 + 1848799132 + 3553469267 + 126734683 + 634198562 + 1581303695 + 3993513098 + 1286156135 + 668643917 + 4067859599 + 934928944 + 1780975974 + 3124648693 + 1079636000 + 2728852827 + 1712792386 + 3973717505 + 3730541706 + 1008622869 + 374215003 + 4076699729 + 3782065301 + 1277680220 + 3644582223 + 3230384581 + 4048329101 + 3972530603 + 1622651190 + 1780122149 + 1944730914 + 3072661266 + 842813796 + 3119053457 + 566527530 + 2260649084 + 755521347 + 2959631877 + 1924364446 + 3955166907 + 2415944365 + 1677072589 + 665468144 + 1188398722 + 2522488958 + 3552825967 + 1524289960 + 192430158 + 1584913193 + 1627176702 + 2638370594 + 210907863 + 4087932353 + 2366334891 + 3761589603 + 1069118981 + 949839095 + 87114663 + 1559735965 + 3586780570 + 4030358951 + 3277975437 + 1015573746 + 3470038195 + 2785670912 + 2603922433 + 1210947993 + 1106862462 + 929942164 + 568439440 + 3688432423 + 1868460156 + 3340533010 + 2816898687 + 1527607140 + 562649736 + 4168938871 + 1520941134 + 3398401661 + 811755035 + 681817209 + 708779615 + 1406782199 + 846994942 + 1294513990 + 1485919443 + 1227349508 + 1921181604 + 1113119277 + 2901205146 + 3561488788 + 1235874401 + 3563624094 + 1225137659 + 969981390 + 4192400661 + 489022838 + 3807783589 + 1884583315 + 614102067 + 2264070180 + 428942557 + 679805882 + 3441550586 + 4173809585 + 3171644380 + 1480825731 + 517848241 + 3105634246 + 2753417821 + 2346133695 + 824377742 + 1079509557 + 4068838714 + 2110392896 + 1144129374 + 4252557789 + 677996070 + 541231505 + 284261936 + 3056228629 + 3231332803 + 1847810301 + 3710688827 + 2483826767 + 3414140333 + 2131677643 + 2603079466 + 2499686130 + 554707642 + 473131486 + 673631503 + 1605096723 + 28256843 + 2895662654 + 4180761669 + 2920687099 + 2733091807 + 2170543968 + 2793464479 + 1899054144 + 3289616378 + 1804298063 + 355464343 + 1900616923 + 2045906425 + 4064075790 + 1025215720 + 1313403757 + 46741124 + 3966725132 + 600007132 + 2584269667 + 2996629167 + 26808252 + 2738683753 + 3246888369 + 1909686873 + 3597229537 + 2985911807 + 2483340995 + 945767497 + 1217341064 + 3881133795 + 3911348945 + 3415900629 + 721668741 + 3159840084 + 2429316508 + 1544943051 + 1372872204 + 308167994 + 1575461492 + 3915125143 + 3161878400 + 513043714 + 2781808939 + 4242240462 + 2779538599 + 409022199 + 3079444933 + 7181778 + 3847373973 + 2203279073 + 183517683 + 3600304109 + 1555821764 + 341763631 + 532312498 + 472624802 + 3651808920 + 3335899740 + 2060936333 + 1580548766 + 1493129880 + 369955197 + 3645011883 + 515387584 + 2505922842 + 239186087 + 1489160813 + 2224110516 + 1439646696 + 3488896105 + 2107308081 + 2738925067 + 3910177423 + 541285089 + 846127926 + 1304863982 + 3076541409 + 3316518439 + 813149638 + 2056294945 + 2553011997 + 2680708909 + 1929801903 + 1647656964 + 2660623807 + 3246111254 + 3367317527 + 1817046968 + 2207202227 + 2085598467 + 2785243057 + 1527692304 + 1937633278 + 2256643744 + 4206640071 + 1951874480 + 3522497273 + 191090235 + 1744568533 + 929420982 + 2543298498 + 3608150214 + 2183049692 + 4262986775 + 3029529525 + 1217983543 + 4039538404 + 1798133678 + 467170149 + 1225435672 + 1516038687 + 2179810027 + 440472117 + 620748452 + 4150289537 + 2753699450 + 1148611940 + 3982022074 + 3403224026 + 1210105084 + 2406481806 + 1662914097 + 1686746214 + 990347982 + 875010577 + 3561388222 + 4053700483 + 3725565174 + 1523304993 + 2929777979 + 2380320202 + 158443510 + 4127763946 + 298310148 + 1170708875 + 2838935908 + 730294034 + 726163556 + 1279994819 + 1969058109 + 1824644497 + 34245999 + 3531729984 + 4147203355 + 1588468981 + 1040270329 + 273816480 + 1241534256 + 2903884658 + 2419354760 + 1592350681 + 554237864 + 148356387 + 2750729997 + 984858271 + 391956606 + 1961325167 + 1924893885 + 2524574211 + 2911725517 + 4147790882 + 6007871 + 3577520231 + 529789211 + 310679183 + 1534876059 + 3172840515 + 1359826983 + 1084548478 + 1435338039 + 3093653778 + 2962571233 + 1696160738 + 4187396440 + 4195141931 + 3675622148 + 1200160064 + 3734161938 + 1394760307 + 2766952622 + 1621753974 + 3829848041 + 492998765 + 866657171 + 3366215792 + 3515460627 + 3519395627 + 1757409447 + 3425057988 + 3372287342 + 2196377313 + 1662872149 + 2586354745 + 1641599349 + 703701314 + 3522518384 + 2879428994 + 1481970611 + 2901064997 + 1990078781 + 3923791481 + 1669561050 + 1800708128 + 4118021958 + 2961152044 + 2480579794 + 1251125753 + 3914664591 + 1288677346 + 1782997871 + 1088718170 + 3861128759 + 560073703 + 3749549526 + 2813200110 + 4057688340 + 1653419509 + 2738885937 + 1072603126 + 2372054889 + 532421742 + 4012825124 + 3568864527 + 2379551214 + 2329039951 + 3515380036 + 3772623189 + 1167779284 + 3827115574 + 3450782787 + 3727098958 + 2446103738 + 888223339 + 818312784 + 1054260550 + 3212741400 + 1241077316 + 2726877516 + 1610829842 + 3231517301 + 2553846088 + 2122642582 + 3273752080 + 3706085302 + 561325561 + 2661189587 + 1862924332 + 3966062866 + 3724281329 + 3131534479 + 1374961168 + 2709850648 + 3794568567 + 4094193258 + 1117760822 + 2255524067 + 2142625777 + 3235413565 + 1585159601 + 4192698229 + 1703936686 + 3847675573 + 781200030 + 3694319770 + 2278214339 + 3416056679 + 2576275294 + 1399746260 + 4144663947 + 2526554209 + 1453205161 + 1283243237 + 2793221816 + 468818834 + 1883702388 + 4046169532 + 478491756 + 3517653914 + 2880043241 + 3106697039 + 3574137365 + 1331541081 + 2592823873 + 2766733359 + 2082174256 + 4121072913 + 1329291532 + 326859590 + 3538145657 + 1901401112 + 2748468818 + 3481925272 + 2310955076 + 2260966644 + 4007544712 + 2133163598 + 3131743632 + 773072117 + 2716164713 + 2423286762 + 3045868481 + 2784750273 + 4064018178 + 3849292388 + 3779311888 + 54866040 + 3188091291 + 541443232 + 983453024 + 1191086045 + 1564766768 + 3450656317 + 2635443018 + 2381165572 + 3101730823 + 3891348887 + 670249684 + 2782004831 + 3178682831 + 2283008334 + 622079136 + 1687813969 + 23914480 + 1301707473 + 3649043072 + 1801898607 + 2664738120 + 722003399 + 2032834454 + 1941892145 + 3566499739 + 1448307534 + 3833378326 + 3013802132 + 1079884306 + 4121291762 + 4015506573 + 766278917 + 1924471194 + 950741092 + 2182241048 + 4022867267 + 3039794966 + 3803660 + 1136813864 + 3794624705 + 2149473705 + 3797028109 + 3527363504 + 597193319 + 4150165033 + 212656849 + 2724927155 + 1479196699 + 2639151227 + 1917224994 + 932648234 + 137715978 + 1065322702 + 3790868789 + 1322394387 + 1410231372 + 551805591 + 584474655 + 345337368 + 2053314044 + 1345960108 + 2976710117 + 1223845776 + 2648892988 + 1672251520 + 4164656011 + 3659470034 + 1517319498 + 1430815721 + 170079433 + 3994576009 + 4257055185 + 3802527467 + 2406434495 + 1370723618 + 4141695210 + 2787082280 + 2795946050 + 4039467595 + 2986728267 + 1512055632 + 3878356470 + 2740489985 + 2725996043 + 1906150062 + 3133152306 + 1910728929 + 2966699050 + 2291373271 + 2130219933 + 1259913030 + 1442730330 + 3266959683 + 1263621027 + 2857138324 + 1777613223 + 2579031555 + 2638907916 + 218770162 + 1713901295 + 2064398102 + 3561393089 + 2027770387 + 4092020889 + 2485987707 + 1325175561 + 828434530 + 3226326012 + 2600004709 + 3903505396 + 1957504069 + 1097426595 + 80136267 + 2580174835 + 1510657164 + 1404811129 + 4167086127 + 3105578570 + 3284967354 + 473364371 + 888950103 + 2200755908 + 3700976274 + 3268168088 + 3909960036 + 1160266256 + 8493419 + 1573434170 + 2529587403 + 144521645 + 2180369265 + 1745395468 + 4181988875 + 860509949 + 3073653849 + 3068709047 + 3552941337 + 4066652781 + 251576741 + 3728212326 + 3936056760 + 2927413143 + 2803614169 + 2831827639 + 4194635233 + 3985850263 + 1143662519 + 3793731367 + 3504933439 + 4030732468 + 1679901271 + 2627247017 + 221321083 + 4230669443 + 412062344 + 1378498242 + 1405063105 + 810053778 + 165060875 + 3141103272 + 4035584215 + 3837409555 + 3477600288 + 1857053058 + 1412747569 + 3924680040 + 2517143636 + 3893580387 + 314489287 + 575612169 + 2301423839 + 672721763 + 379926221 + 761647549 + 717586402 + 430164438 + 2585102748 + 4197928827 + 3450588957 + 135630448 + 2552486054 + 1352955135 + 3505901640 + 2239863013 + 985319674 + 1888221102 + 146990110 + 3587682505 + 1387772658 + 100528453 + 1708549230 + 714145621 + 2686773158 + 1034251943 + 1542908012 + 2971108531 + 3175675256 + 3155812979 + 760745085 + 3681634288 + 3783077696 + 4217161467 + 2273014888 + 2451974032 + 264680448 + 53851393 + 3424479224 + 2739451217 + 1253261111 + 344934658 + 1804926748 + 3807665795 + 1151326556 + 1439068681 + 2685670378 + 2322855556 + 131047634 + 3402654236 + 3320531027 + 3004896517 + 1118091581 + 2741680934 + 396533897 + 491219183 + 1927402425 + 1057939946 + 380697219 + 2046587612 + 1569670525 + 360356901 + 2391290331 + 412315979 + 1220654423 + 3765059160 + 2701169740 + 1107272523 + 158622376 + 1520381348 + 835982829 + 4023089343 + 2290597368 + 1577291706 + 1189569986 + 2493558805 + 2142702960 + 2197415758 + 3948847365 + 264929504 + 667630022 + 2933655471 + 2766580877 + 1168943299 + 133611945 + 2486401355 + 4224587256 + 2367702403 + 2412800226 + 1646873382 + 3747956491 + 2744973553 + 3988038049 + 3595012406 + 1017451466 + 2649703220 + 3740838078 + 3105159101 + 2804436143 + 3127843949 + 2652285974 + 996252447 + 4002643122 + 2962869400 + 2938000538 + 4098624499 + 1043561429 + 1994115777 + 659054836 + 1134136174 + 838515121 + 3286292150 + 1925025503 + 1984754452 + 998454022 + 3385093023 + 2123911291 + 4090478485 + 2819407212 + 2085409569 + 2571076318 + 2571160046 + 2074098790 + 553659926 + 2284050650 + 2932204823 + 3170383113 + 3427056117 + 3089175458 + 3531552472 + 1536747422 + 2528019347 + 973549920 + 1665933403 + 3676633449 + 756429907 + 4099937919 + 4290541561 + 126302130 + 3928415825 + 1462292920 + 3801263493 + 2310690578 + 191970844 + 3128366957 +OUTPUTS diff --git a/spec/experiment/evaluation/select_spec.rb b/spec/experiment/evaluation/select_spec.rb new file mode 100644 index 0000000..cd7b8d9 --- /dev/null +++ b/spec/experiment/evaluation/select_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +RSpec.describe Evaluation do + let(:primitive_object) do + { + 'null' => nil, + 'string' => 'value', + 'number' => 13, + 'boolean' => true + } + end + + let(:nested_object) do + primitive_object.merge('object' => primitive_object) + end + + context '.select' do + it 'handles non-existent paths' do + expect(described_class.select(nested_object, %w[does not exist])).to be_nil + end + + it 'handles nil values' do + expect(described_class.select(nested_object, ['null'])).to be_nil + end + + it 'selects primitive values' do + expect(described_class.select(nested_object, ['string'])).to eq('value') + expect(described_class.select(nested_object, ['number'])).to eq(13) + expect(described_class.select(nested_object, ['boolean'])).to eq(true) + end + + it 'selects object values' do + expect(described_class.select(nested_object, ['object'])).to eq(primitive_object) + end + + it 'selects nested values' do + expect(described_class.select(nested_object, %w[object string])).to eq('value') + expect(described_class.select(nested_object, %w[object number])).to eq(13) + expect(described_class.select(nested_object, %w[object boolean])).to eq(true) + end + + it 'handles non-existent nested paths' do + expect(described_class.select(nested_object, %w[object does not exist])).to be_nil + end + end +end diff --git a/spec/experiment/evaluation/semantic_version_spec.rb b/spec/experiment/evaluation/semantic_version_spec.rb new file mode 100644 index 0000000..91af52f --- /dev/null +++ b/spec/experiment/evaluation/semantic_version_spec.rb @@ -0,0 +1,132 @@ +describe SemanticVersion do + def assert_invalid_version(version) + expect(SemanticVersion.parse(version)).to be_nil + end + + def assert_valid_version(version) + expect(SemanticVersion.parse(version)).not_to be_nil + end + + def assert_version_comparison(v1, op, v2) + sv1 = SemanticVersion.parse(v1) + sv2 = SemanticVersion.parse(v2) + expect(sv1).not_to be_nil + expect(sv2).not_to be_nil + return if sv1.nil? || sv2.nil? + + case op + when 'is' + expect(sv1 <=> sv2).to eq(0) + when 'is not' + expect(sv1 <=> sv2).not_to eq(0) + when 'version less' + expect(sv1 <=> sv2).to be < 0 + when 'version greater' + expect(sv1 <=> sv2).to be > 0 + end + end + + describe 'invalid versions' do + it 'rejects invalid version formats' do + # just major + assert_invalid_version('10') + + # trailing dots + assert_invalid_version('10.') + assert_invalid_version('10..') + assert_invalid_version('10.2.') + assert_invalid_version('10.2.33.') + + # dots in the middle + assert_invalid_version('10..2.33') + assert_invalid_version('102...33') + + # invalid characters + assert_invalid_version('a.2.3') + assert_invalid_version('23!') + assert_invalid_version('23.#5') + assert_invalid_version('') + assert_invalid_version(nil) + + # more numbers + assert_invalid_version('2.3.4.567') + assert_invalid_version('2.3.4.5.6.7') + + # prerelease if provided should always have major, minor, patch + assert_invalid_version('10.2.alpha') + assert_invalid_version('10.alpha') + assert_invalid_version('alpha-1.2.3') + + # prerelease should be separated by a hyphen after patch + assert_invalid_version('10.2.3alpha') + assert_invalid_version('10.2.3alpha-1.2.3') + + # negative numbers + assert_invalid_version('-10.1') + assert_invalid_version('10.-1') + end + end + + describe 'valid versions' do + it 'accepts valid version formats' do + assert_valid_version('100.2') + assert_valid_version('0.102.39') + assert_valid_version('0.0.0') + + # versions with leading 0s would be converted to int + assert_valid_version('01.02') + assert_valid_version('001.001100.000900') + + # prerelease tags + assert_valid_version('10.20.30-alpha') + assert_valid_version('10.20.30-1.x.y') + assert_valid_version('10.20.30-aslkjd') + assert_valid_version('10.20.30-b894') + assert_valid_version('10.20.30-b8c9') + end + end + + describe 'version comparison' do + it 'handles equality comparisons' do + assert_version_comparison('66.12.23', 'is', '66.12.23') + # patch if not specified equals 0 + assert_version_comparison('5.6', 'is', '5.6.0') + # leading 0s are not stored when parsed + assert_version_comparison('06.007.0008', 'is', '6.7.8') + # with pre-release + assert_version_comparison('1.23.4-b-1.x.y', 'is', '1.23.4-b-1.x.y') + end + + it 'handles inequality comparisons' do + assert_version_comparison('1.23.4-alpha-1.2', 'is not', '1.23.4-alpha-1') + # trailing 0s aren't stripped + assert_version_comparison('1.2.300', 'is not', '1.2.3') + assert_version_comparison('1.20.3', 'is not', '1.2.3') + end + + it 'handles less than comparisons' do + # patch of .1 makes it greater + assert_version_comparison('50.2', 'version less', '50.2.1') + # minor 9 < minor 20 + assert_version_comparison('20.9', 'version less', '20.20') + # same version with pre-release should be lesser + assert_version_comparison('20.9.4-alpha1', 'version less', '20.9.4') + # compare prerelease as strings + assert_version_comparison('20.9.4-a-1.2.3', 'version less', '20.9.4-a-1.3') + # since prerelease is compared as strings a1.23 < a1.5 because 2 < 5 + assert_version_comparison('20.9.4-a1.23', 'version less', '20.9.4-a1.5') + end + + it 'handles greater than comparisons' do + assert_version_comparison('12.30.2', 'version greater', '12.4.1') + # 100 > 1 + assert_version_comparison('7.100', 'version greater', '7.1') + # 10 > 9 + assert_version_comparison('7.10', 'version greater', '7.9') + # converts to 7.10.20 > 7.9.1 + assert_version_comparison('07.010.0020', 'version greater', '7.009.1') + # patch comparison comes first + assert_version_comparison('20.5.6-b1.2.x', 'version greater', '20.5.5') + end + end +end diff --git a/spec/experiment/evaluation/topological_sort_spec.rb b/spec/experiment/evaluation/topological_sort_spec.rb new file mode 100644 index 0000000..9cea294 --- /dev/null +++ b/spec/experiment/evaluation/topological_sort_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +RSpec.describe TopologicalSort do + def create_flag(key, dependencies = nil) + Evaluation::Flag.from_hash({ + 'key' => key.to_s, + 'variants' => {}, + 'segments' => [], + 'dependencies' => dependencies&.map(&:to_s) + }) + end + + describe '.sort' do + it 'handles empty flag list' do + expect(TopologicalSort.sort({})).to eq([]) + expect(TopologicalSort.sort({}, ['1'])).to eq([]) + end + + it 'handles single flag without dependencies' do + flags = { '1' => create_flag(1) } + expect(TopologicalSort.sort(flags)).to eq([create_flag(1)]) + expect(TopologicalSort.sort(flags, ['1'])).to eq([create_flag(1)]) + expect(TopologicalSort.sort(flags, ['999'])).to eq([]) + end + + it 'handles single flag with dependencies' do + flags = { '1' => create_flag(1, [2]) } + expect(TopologicalSort.sort(flags)).to eq([create_flag(1, [2])]) + expect(TopologicalSort.sort(flags, ['1'])).to eq([create_flag(1, [2])]) + expect(TopologicalSort.sort(flags, ['999'])).to eq([]) + end + + it 'handles multiple flags without dependencies' do + flags = { + '1' => create_flag(1), + '2' => create_flag(2) + } + expect(TopologicalSort.sort(flags)).to eq([create_flag(1), create_flag(2)]) + expect(TopologicalSort.sort(flags, %w[1 2])).to eq([create_flag(1), create_flag(2)]) + expect(TopologicalSort.sort(flags, %w[99 999])).to eq([]) + end + + it 'handles multiple flags with dependencies' do + flags = { + '1' => create_flag(1, [2]), + '2' => create_flag(2, [3]), + '3' => create_flag(3) + } + expected = [create_flag(3), create_flag(2, [3]), create_flag(1, [2])] + expect(TopologicalSort.sort(flags)).to eq(expected) + expect(TopologicalSort.sort(flags, %w[1 2])).to eq(expected) + expect(TopologicalSort.sort(flags, %w[99 999])).to eq([]) + end + + it 'detects single flag cycle' do + flags = { '1' => create_flag(1, [1]) } + expect { TopologicalSort.sort(flags) }.to raise_error(CycleError) { |e| expect(e.path).to eq ['1'] } + expect { TopologicalSort.sort(flags, ['1']) }.to raise_error(CycleError) { |e| expect(e.path).to eq ['1'] } + expect { TopologicalSort.sort(flags, ['999']) }.not_to raise_error + end + + it 'detects cycles between two flags' do + flags = { + '1' => create_flag(1, [2]), + '2' => create_flag(2, [1]) + } + expect { TopologicalSort.sort(flags) }.to raise_error(CycleError) { |e| expect(e.path).to eq %w[1 2] } + expect { TopologicalSort.sort(flags, ['2']) }.to raise_error(CycleError) { |e| expect(e.path).to eq %w[2 1] } + expect { TopologicalSort.sort(flags, ['999']) }.not_to raise_error + end + + it 'handles complex dependencies without cycles' do + flags = { + '8' => create_flag(8), + '7' => create_flag(7, [8]), + '4' => create_flag(4, [8, 7]), + '6' => create_flag(6, [7, 4]), + '3' => create_flag(3, [6]), + '1' => create_flag(1, [3]), + '2' => create_flag(2, [1]) + } + + expected = [ + create_flag(8), + create_flag(7, [8]), + create_flag(4, [8, 7]), + create_flag(6, [7, 4]), + create_flag(3, [6]), + create_flag(1, [3]), + create_flag(2, [1]) + ] + + expect(TopologicalSort.sort(flags)).to eq(expected) + end + end +end diff --git a/spec/experiment/local/client_cohort_spec.rb b/spec/experiment/local/client_cohort_spec.rb index a2f33aa..8a6767a 100644 --- a/spec/experiment/local/client_cohort_spec.rb +++ b/spec/experiment/local/client_cohort_spec.rb @@ -15,14 +15,6 @@ module AmplitudeExperiment let(:config) { LocalEvaluationConfig.new(cohort_sync_config: cohort_sync_config) } let(:config_eu) { LocalEvaluationConfig.new(cohort_sync_config: cohort_sync_config_eu, server_zone: ServerZone::EU) } - before(:each) do - WebMock.allow_net_connect! - end - - after(:all) do - WebMock.disable_net_connect!(allow_localhost: true) - end - describe '#evaluate with cohorts' do it 'evaluates targeted and non-targeted users' do client = LocalEvaluationClient.new(api_key, config) diff --git a/spec/experiment/local/flag_config_spec.rb b/spec/experiment/local/flag_config_spec.rb index 454a589..cec9cec 100644 --- a/spec/experiment/local/flag_config_spec.rb +++ b/spec/experiment/local/flag_config_spec.rb @@ -118,7 +118,7 @@ module AmplitudeExperiment } } } - } + }.transform_values { |f| Evaluation::Flag.from_hash(f) } end describe '#get_all_cohort_ids_from_flag' do diff --git a/spec/experiment/local/util/topological_sort_spec.rb b/spec/experiment/local/util/topological_sort_spec.rb deleted file mode 100644 index d254b47..0000000 --- a/spec/experiment/local/util/topological_sort_spec.rb +++ /dev/null @@ -1,235 +0,0 @@ -module AmplitudeExperiment - RSpec.describe 'TopologicalSort' do - def sort(flags, flag_keys = nil) - flag_keys_strings = flag_keys ? flag_keys.map(&:to_s) : [] - flags_dict = flags.each_with_object({}) do |flag, hash| - hash[flag['key']] = flag - end - AmplitudeExperiment.topological_sort(flags_dict, flag_keys_strings, ordered: true) - end - - def flag(key, dependencies) - { 'key' => key.to_s, 'dependencies' => dependencies.map(&:to_s) } - end - - it 'handles empty flags' do - flags = [] - # no flag keys - result = sort(flags) - expect(result).to eq([]) - # with flag keys - result = sort(flags, [1]) - expect(result).to eq([]) - end - - it 'handles single flag with no dependencies' do - flags = [flag(1, [])] - # no flag keys - result = sort(flags) - expect(result).to eq(flags) - # with flag keys - result = sort(flags, [1]) - expect(result).to eq(flags) - # with flag keys, no match - result = sort(flags, [999]) - expect(result).to eq([]) - end - - it 'handles single flag with dependencies' do - flags = [flag(1, [2])] - # no flag keys - result = sort(flags) - expect(result).to eq(flags) - # with flag keys - result = sort(flags, [1]) - expect(result).to eq(flags) - # with flag keys, no match - result = sort(flags, [999]) - expect(result).to eq([]) - end - - it 'handles multiple flags with no dependencies' do - flags = [flag(1, []), flag(2, [])] - # no flag keys - result = sort(flags) - expect(result).to eq(flags) - # with flag keys - result = sort(flags, [1, 2]) - expect(result).to eq(flags) - # with flag keys, no match - result = sort(flags, [99, 999]) - expect(result).to eq([]) - end - - it 'handles multiple flags with dependencies' do - flags = [flag(1, [2]), flag(2, [3]), flag(3, [])] - # no flag keys - result = sort(flags) - expect(result).to eq([flag(3, []), flag(2, [3]), flag(1, [2])]) - # with flag keys - result = sort(flags, [1, 2]) - expect(result).to eq([flag(3, []), flag(2, [3]), flag(1, [2])]) - # with flag keys, no match - result = sort(flags, [99, 999]) - expect(result).to eq([]) - end - - it 'handles single flag cycle' do - flags = [flag(1, [1])] - # no flag keys - expect do - sort(flags) - end.to raise_error(CycleError) { |e| expect(e.path).to eq(['1'].to_set) } - # with flag keys - expect do - sort(flags, [1]) - end.to raise_error(CycleError) { |e| expect(e.path).to eq(['1'].to_set) } - # with flag keys, no match - expect do - result = sort(flags, [999]) - expect(result).to eq([]) - end.not_to raise_error - end - - it 'handles two flag cycle' do - flags = [flag(1, [2]), flag(2, [1])] - # no flag keys - expect do - sort(flags) - end.to raise_error(CycleError) { |e| expect(e.path).to eq(%w[1 2].to_set) } - # with flag keys - expect do - sort(flags, [1, 2]) - end.to raise_error(CycleError) { |e| expect(e.path).to eq(%w[1 2].to_set) } - # with flag keys, no match - expect do - result = sort(flags, [999]) - expect(result).to eq([]) - end.not_to raise_error - end - - it 'handles multiple flags with complex cycle' do - flags = [ - flag(3, [1, 2]), - flag(1, []), - flag(4, [21, 3]), - flag(2, []), - flag(5, [3]), - flag(6, []), - flag(7, []), - flag(8, [9]), - flag(9, []), - flag(20, [4]), - flag(21, [20]) - ] - expect do - sort(flags, [3, 1, 4, 2, 5, 6, 7, 8, 9, 20, 21]) - end.to raise_error(CycleError) { |e| expect(e.path).to eq(%w[4 21 20].to_set) } - end - - it 'handles multiple flags with complex dependencies without cycle starting at leaf' do - flags = [ - flag(1, [6, 3]), - flag(2, [8, 5, 3, 1]), - flag(3, [6, 5]), - flag(4, [8, 7]), - flag(5, [10, 7]), - flag(7, [8]), - flag(6, [7, 4]), - flag(8, []), - flag(9, [10, 7, 5]), - flag(10, [7]), - flag(20, []), - flag(21, [20]), - flag(30, []) - ] - result = sort(flags, [1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 20, 21, 30]) - expected = [ - flag(8, []), - flag(7, [8]), - flag(4, [8, 7]), - flag(6, [7, 4]), - flag(10, [7]), - flag(5, [10, 7]), - flag(3, [6, 5]), - flag(1, [6, 3]), - flag(2, [8, 5, 3, 1]), - flag(9, [10, 7, 5]), - flag(20, []), - flag(21, [20]), - flag(30, []) - ] - expect(result).to eq(expected) - end - - it 'handles multiple flags with complex dependencies without cycle starting at middle' do - flags = [ - flag(6, [7, 4]), - flag(1, [6, 3]), - flag(2, [8, 5, 3, 1]), - flag(3, [6, 5]), - flag(4, [8, 7]), - flag(5, [10, 7]), - flag(7, [8]), - flag(8, []), - flag(9, [10, 7, 5]), - flag(10, [7]), - flag(20, []), - flag(21, [20]), - flag(30, []) - ] - result = sort(flags, [6, 1, 2, 3, 4, 5, 7, 8, 9, 10, 20, 21, 30]) - expected = [ - flag(8, []), - flag(7, [8]), - flag(4, [8, 7]), - flag(6, [7, 4]), - flag(10, [7]), - flag(5, [10, 7]), - flag(3, [6, 5]), - flag(1, [6, 3]), - flag(2, [8, 5, 3, 1]), - flag(9, [10, 7, 5]), - flag(20, []), - flag(21, [20]), - flag(30, []) - ] - expect(result).to eq(expected) - end - - it 'handles multiple flags with complex dependencies without cycle starting at root' do - flags = [ - flag(8, []), - flag(1, [6, 3]), - flag(2, [8, 5, 3, 1]), - flag(3, [6, 5]), - flag(4, [8, 7]), - flag(5, [10, 7]), - flag(6, [7, 4]), - flag(7, [8]), - flag(9, [10, 7, 5]), - flag(10, [7]), - flag(20, []), - flag(21, [20]), - flag(30, []) - ] - result = sort(flags, [8, 1, 2, 3, 4, 5, 6, 7, 9, 10, 20, 21, 30]) - expected = [ - flag(8, []), - flag(7, [8]), - flag(4, [8, 7]), - flag(6, [7, 4]), - flag(10, [7]), - flag(5, [10, 7]), - flag(3, [6, 5]), - flag(1, [6, 3]), - flag(2, [8, 5, 3, 1]), - flag(9, [10, 7, 5]), - flag(20, []), - flag(21, [20]), - flag(30, []) - ] - expect(result).to eq(expected) - end - end -end