Permalink
Browse files

1) Adding tests. 2) Massive refactoring. 3) Can now use conversion na…

…me as option to Abingo.test, then Abingo.bingo(conversion) rather than doing it via test name.
  • Loading branch information...
1 parent 4409c39 commit abf98be7063d0460fd3f0038f405a96ab113a69b @invalid-email-address invalid-email-address committed Feb 11, 2010
Showing with 161 additions and 20 deletions.
  1. +54 −16 lib/abingo.rb
  2. +8 −1 lib/abingo/experiment.rb
  3. +13 −0 lib/abingo/rails/controller/dashboard.rb
  4. +86 −3 test/abingo_test.rb
View
@@ -57,9 +57,13 @@ def self.flip(test_name)
end
#This is the meat of A/Bingo.
+ #options accepts
+ # :multiple_participation (true or false)
+ # :conversion name of conversion to listen for (alias: conversion_name)
def self.test(test_name, alternatives, options = {})
unless Abingo::Experiment.exists?(test_name)
- Abingo::Experiment.start_experiment!(test_name, self.parse_alternatives(alternatives))
+ conversion_name = options[:conversion] || options[:conversion_name]
+ Abingo::Experiment.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name)
end
choice = self.find_alternative_for_user(test_name, alternatives)
@@ -82,34 +86,52 @@ def self.test(test_name, alternatives, options = {})
end
- def Abingo.bingo!(test_name_or_array = nil, options = {})
- if test_name_or_array.kind_of? Array
- test_name_or_array.map do |single_test|
+ #Scores conversions for tests.
+ #test_name_or_array supports three types of input:
+ #
+ #A conversion name: scores a conversion for any test the user is participating in which
+ # is listening to the specified conversion.
+ #
+ #A test name: scores a conversion for the named test if the user is participating in it.
+ #
+ #An array of either of the above: for each element of the array, process as above.
+ #
+ #nil: score a conversion for every test the u
+ def Abingo.bingo!(name = nil, options = {})
+ if name.kind_of? Array
+ name.map do |single_test|
self.bingo!(single_test, options)
end
else
- participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
- if test_name_or_array.nil?
+ if name.nil?
+ #Score all participating tests
+ participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
participating_tests.each do |participating_test|
self.bingo!(participating_test, options)
end
- else #Individual, non-nil test is named
- test_name_str = test_name_or_array.to_s
- if options[:assume_participation] || participating_tests.include?(test_name_str)
- cache_key = "Abingo::conversions(#{Abingo.identity},#{test_name_str}"
- if options[:multiple_conversions] || !Abingo.cache.read(cache_key)
- Abingo::Alternative.score_conversion(test_name_str)
- if Abingo.cache.exist?(cache_key)
- Abingo.cache.increment(cache_key)
- else
- Abingo.cache.write(cache_key, 1)
+ else #Could be a test name or conversion name.
+ conversion_name = name.gsub(" ", "_")
+ tests_listening_to_conversion = Abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}")
+ if tests_listening_to_conversion
+ if tests_listening_to_conversion.size > 1
+ tests_listening_to_conversion.map do |individual_test|
+ self.score_conversion!(individual_test.to_s)
end
+ elsif tests_listening_to_conversion.size == 1
+ test_name_str = tests_listening_to_conversion.first.to_s
+ self.score_conversion!(test_name_str)
end
+ else
+ #No tests listening for this conversion. Assume it is just a test name.
+ test_name_str = name.to_s
+ self.score_conversion!(test_name_str)
end
end
end
end
+ protected
+
#For programmer convenience, we allow you to specify what the alternatives for
#an experiment are in a few ways. Thus, we need to actually be able to handle
#all of them. We fire this parser very infrequently (once per test, typically)
@@ -164,4 +186,20 @@ def self.modulo_choice(test_name, choices_count)
Digest::MD5.hexdigest(Abingo.salt.to_s + test_name + self.identity.to_s).to_i(16) % choices_count
end
+ def self.score_conversion!(test_name)
+ test_name.gsub!(" ", "_")
+ participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
+ if options[:assume_participation] || participating_tests.include?(test_name)
+ cache_key = "Abingo::conversions(#{Abingo.identity},#{test_name}"
+ if options[:multiple_conversions] || !Abingo.cache.read(cache_key)
+ Abingo::Alternative.score_conversion(test_name)
+ if Abingo.cache.exist?(cache_key)
+ Abingo.cache.increment(cache_key)
+ else
+ Abingo.cache.write(cache_key, 1)
+ end
+ end
+ end
+ end
+
end
View
@@ -48,7 +48,8 @@ def self.alternatives_for_test(test_name)
end
end
- def self.start_experiment!(test_name, alternatives_array)
+ def self.start_experiment!(test_name, alternatives_array, conversion_name = nil)
+ conversion_name ||= test_name
cloned_alternatives_array = alternatives_array.clone
ActiveRecord::Base.transaction do
experiment = Abingo::Experiment.new(:test_name => test_name)
@@ -61,6 +62,12 @@ def self.start_experiment!(test_name, alternatives_array)
end
experiment.save!
Abingo.cache.write("Abingo::Experiment::exists(#{test_name})".gsub(" ", ""), 1)
+
+ #This might have issues in very, very high concurrency environments...
+ tests_listening_to_conversion = Abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}") || []
+ tests_listening_to_conversion << test_name unless tests_listening_to_conversion.include? test_name
+ Abingo.cache.write("Abingo::tests_listening_to_conversion#{conversion_name}", tests_listening_to_conversion)
+
experiment
end
end
@@ -0,0 +1,13 @@
+module Abingo
+ module Rails
+ module Controller
+ module Dashboard
+
+ def index
+ @experiments = Abingo::Experiment.all
+ end
+
+ end
+ end
+ end
+end
View
@@ -1,8 +1,91 @@
require 'test_helper'
class AbingoTest < ActiveSupport::TestCase
- # Replace this with your real tests.
- test "the truth" do
- assert true
+
+ #Wipes cache, D/B prior to doing a test run.
+ Abingo.cache.clear
+ Abingo::Experiment.delete_all
+ Abingo::Alternative.delete_all
+
+ test "identity automatically assigned" do
+ assert Abingo.identity != nil
+ end
+
+ test "alternative parsing" do
+ array = %w{a b c}
+ assert_equal array, Abingo.parse_alternatives(array)
+ assert_equal 65, Abingo.parse_alternatives(65).size
+ assert_equal 4, Abingo.parse_alternatives(2..5).size
+ assert !(Abingo.parse_alternatives(2..5).include? 1)
+ end
+
+ test "experiment creation" do
+ assert_equal 0, Abingo::Experiment.count
+ assert_equal 0, Abingo::Alternative.count
+ alternatives = %w{A B}
+ alternative_selected = Abingo.test("unit_test_sample_A", alternatives)
+ assert_equal 1, Abingo::Experiment.count
+ assert_equal 2, Abingo::Alternative.count
+ assert alternatives.include?(alternative_selected)
+ end
+
+ test "alternatives picked consistently" do
+ alternative_picked = Abingo.test("consistency_test", 1..100)
+ 100.times do
+ assert_equal alternative_picked, Abingo.test("consistency_test", 1..100)
+ end
+ end
+
+ test "participation works" do
+ new_tests = %w{participationA participationB participationC}
+ new_tests.map do |test_name|
+ Abingo.test(test_name, 1..5)
+ end
+
+ participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
+
+ new_tests.map do |test_name|
+ assert participating_tests.include? test_name
+ end
+ end
+
+ test "participants counted" do
+ test_name = "participants_counted_test"
+ alternative = Abingo.test(test_name, %w{a b c})
+
+ ex = Abingo::Experiment.find_by_test_name(test_name)
+ lookup = Abingo::Alternative.calculate_lookup(test_name, alternative)
+ chosen_alt = Abingo::Alternative.find_by_lookup(lookup)
+ assert_equal 1, ex.participants
+ assert_equal 1, chosen_alt.participants
+ end
+
+ test "conversion tracking by test name" do
+ test_name = "conversion_test_by_name"
+ alternative = Abingo.test(test_name, %w{a b c})
+ Abingo.bingo!(test_name)
+ ex = Abingo::Experiment.find_by_test_name(test_name)
+ lookup = Abingo::Alternative.calculate_lookup(test_name, alternative)
+ chosen_alt = Abingo::Alternative.find_by_lookup(lookup)
+ assert_equal 1, ex.conversions
+ assert_equal 1, chosen_alt.conversions
+ Abingo.bingo!(test_name)
+
+ #Should still only have one because this conversion should not be double counted.
+ #We haven't specified that in the test options.
+ assert_equal 1, Abingo::Experiment.find_by_test_name(test_name).conversions
+ end
+
+ test "conversion tracking by conversion name" do
+ conversion_name = "purchase"
+ tests = %w{conversionTrackingByConversionNameA conversionTrackingByConversionNameB conversionTrackingByConversionNameC}
+ tests.map do |test_name|
+ Abingo.test(test_name, %w{A B}, :conversion => conversion_name)
+ end
+
+ Abingo.bingo!(conversion_name)
+ tests.map do |test_name|
+ assert_equal 1, Abingo::Experiment.find_by_test_name(test_name).conversions
+ end
end
end

0 comments on commit abf98be

Please sign in to comment.