fabes 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ module Fabes
2
+ module Helper
3
+ def fabes(name, control, *alternatives)
4
+ experiment = Fabes::Experiment.find_or_create(name, *([control] + alternatives))
5
+ if trackable_for experiment.name
6
+ alternative = experiment.select_alternative!
7
+ set_cookie_for experiment, alternative
8
+ alternative.increment_participants!
9
+ alternative.update_weight
10
+ else
11
+ alternative = current_alternative_for experiment
12
+ end
13
+
14
+ alternative.payload
15
+ rescue
16
+ control
17
+ end
18
+
19
+ def score!(name)
20
+ experiment = Fabes::Experiment.find name
21
+ alternative = current_alternative_for experiment
22
+ if scorable? experiment.name
23
+ alternative.increment_hits!
24
+ alternative.update_weight
25
+ mark_as_scored experiment.name
26
+ end
27
+ rescue
28
+ #Failed scoring, do nothin'
29
+ nil
30
+ end
31
+
32
+ private
33
+
34
+ def tracking
35
+ session[:fabes] ||= {}
36
+ end
37
+
38
+ def set_cookie_for(experiment, alternative)
39
+ tracking[experiment.name] = alternative.id
40
+ end
41
+
42
+ def current_alternative_for(experiment)
43
+ id = tracking[experiment.name]
44
+ experiment.find_alternative id
45
+ end
46
+
47
+ def trackable_for(name)
48
+ !has_cookie_for(name) && !marked_as_scored?(name)
49
+ end
50
+
51
+ def has_cookie_for(name)
52
+ !tracking[name].nil?
53
+ end
54
+
55
+ def scorable?(name)
56
+ has_cookie_for(name) && !marked_as_scored?(name)
57
+ end
58
+
59
+ def marked_as_scored?(name)
60
+ tracking[name] == :done
61
+ end
62
+
63
+ def mark_as_scored(name)
64
+ tracking[name] = :done
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,6 @@
1
+ #TODO: improve this
2
+ class Object
3
+ def log(msg)
4
+ puts msg
5
+ end
6
+ end
data/lib/fabes.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'fabes/utils'
2
+ require 'fabes/configuration'
3
+ require 'fabes/experiment'
4
+ require 'fabes/alternative'
5
+ require 'fabes/connection_handling'
6
+ require 'fabes/helper'
7
+ require 'fabes/admin'
8
+
9
+ module Fabes
10
+ extend self
11
+ attr_accessor :configuration
12
+
13
+ def configure
14
+ self.configuration ||= Configuration.new
15
+ yield(configuration) if block_given?
16
+ configuration
17
+ end
18
+
19
+ def db
20
+ @db ||= ConnectionHandling.establish_connection Fabes.configuration.db, Fabes.configuration.adapter
21
+ end
22
+ end
23
+
24
+ Fabes.configure
25
+
26
+ if defined? Rails
27
+ ActionController::Base.send :include, Fabes::Helper
28
+ ActionController::Base.helper Fabes::Helper
29
+ #TODO: Autoadd route to admin panel
30
+ #TODO: Railtie???
31
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter '/test/'
4
+ end
5
+
6
+ require 'test/unit'
7
+ require 'shoulda'
8
+ require 'mocha'
9
+
10
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
11
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
12
+
13
+ require 'fabes'
14
+
15
+ def session
16
+ @session ||= {}
17
+ end
18
+
19
+ class Test::Unit::TestCase
20
+ def setup
21
+ Redis.new.flushdb
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require 'helper'
2
+
3
+ class TestAbstractAdapter < Test::Unit::TestCase
4
+ context 'establish_connection' do
5
+ should 'raise if no suitable adapter' do
6
+ assert_raise RuntimeError do
7
+ adapter = 'caca'
8
+ db = ''
9
+ Fabes::ConnectionHandling.establish_connection(db, adapter)
10
+ end
11
+ end
12
+
13
+ should 'work if suitable adapter (redis)' do
14
+ adapter = 'redis'
15
+ db = ''
16
+ Fabes::ConnectionHandling.expects :redis_connection
17
+ Fabes::ConnectionHandling.establish_connection(db, adapter)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,92 @@
1
+ require 'helper'
2
+
3
+ class TestAlternative < Test::Unit::TestCase
4
+ context 'initialization' do
5
+ should 'have an id' do
6
+ alternative = Fabes::Alternative.new 'abc'
7
+ assert_not_nil alternative.id
8
+ end
9
+
10
+ should 'have a weight' do
11
+ alternative = Fabes::Alternative.new 'abc'
12
+ assert_not_nil alternative.weight
13
+ assert_equal alternative.weight, 0
14
+ end
15
+
16
+ should 'have a payload' do
17
+ alternative = Fabes::Alternative.new 'abc'
18
+ assert_not_nil alternative.payload
19
+ assert_equal alternative.payload, 'abc'
20
+ end
21
+
22
+ should 'have participants' do
23
+ alternative = Fabes::Alternative.new 'abc'
24
+ assert_not_nil alternative.participants
25
+ assert_equal alternative.participants, 0
26
+ end
27
+
28
+ should 'have hits' do
29
+ alternative = Fabes::Alternative.new 'abc'
30
+ assert_not_nil alternative.hits
31
+ assert_equal alternative.hits, 0
32
+ end
33
+ end
34
+
35
+ context 'increment_participants!' do
36
+ should 'be able to increment its participants' do
37
+ alternative = Fabes::Alternative.new 'abc'
38
+
39
+ assert_equal alternative.participants, 0
40
+ alternative.increment_participants!
41
+ assert_equal alternative.participants, 1
42
+ end
43
+ end
44
+
45
+ context 'increment_hits!' do
46
+ should 'be able to increment its hits' do
47
+ alternative = Fabes::Alternative.new 'abc'
48
+
49
+ assert_equal alternative.hits, 0
50
+ alternative.increment_hits!
51
+ assert_equal alternative.hits, 1
52
+ end
53
+ end
54
+
55
+ context 'update_weight' do
56
+ should 'be able to update the weight'
57
+
58
+ should 'calculate the new weight' do
59
+ alternative = Fabes::Alternative.new 'abc'
60
+ alternative.participants = 10
61
+ alternative.hits = 1
62
+ alternative.update_weight
63
+
64
+ assert_equal alternative.weight, 0.1
65
+ end
66
+ end
67
+
68
+ context 'create_from' do
69
+ should 'create a new alternative from the required data' do
70
+ hash = {payload: 1}
71
+ alternative = Fabes::Alternative.create_from(hash)
72
+ assert_not_nil alternative
73
+ assert_equal alternative.class, Fabes::Alternative
74
+ end
75
+
76
+ should 'create the new alternative with the given data' do
77
+ hash = {payload: 1, id: 'abc', weight: 0.5, participants: 9, hits: 2}
78
+ alternative = Fabes::Alternative.create_from(hash)
79
+ assert_equal alternative.payload, 1
80
+ assert_equal alternative.id, 'abc'
81
+ assert_equal alternative.weight, 0.5
82
+ assert_equal alternative.participants, 9
83
+ assert_equal alternative.hits, 2
84
+ end
85
+
86
+ should 'fail creating a new alternative if not enough data' do
87
+ assert_raise RuntimeError do
88
+ Fabes::Alternative.create_from(caca: 1)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,33 @@
1
+ require 'helper'
2
+
3
+ class TestConfiguration < Test::Unit::TestCase
4
+ context 'initialization' do
5
+ should 'have an adapter' do
6
+ configuration = Fabes::Configuration.new
7
+ assert configuration.respond_to? :adapter
8
+ end
9
+ end
10
+
11
+ context 'dsl' do
12
+ should 'have a use command' do
13
+ configuration = Fabes::Configuration.new
14
+ assert configuration.respond_to? :use
15
+ end
16
+
17
+ should 'set the adapter' do
18
+ configuration = Fabes.configure do |c|
19
+ c.use db: 'abc', adapter: :redis
20
+ end
21
+ assert_not_nil configuration.instance_variable_get :@adapter
22
+ assert_equal configuration.instance_variable_get(:@adapter), 'redis'
23
+ end
24
+
25
+ should 'set the db' do
26
+ configuration = Fabes.configure do |c|
27
+ c.use db: 'abc', adapter: :redis
28
+ end
29
+ assert_not_nil configuration.instance_variable_get(:@db)
30
+ assert_equal configuration.instance_variable_get(:@db), 'abc'
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,112 @@
1
+ require 'helper'
2
+
3
+ class TestExperiment < Test::Unit::TestCase
4
+
5
+ context 'initialization' do
6
+ should "have a name" do
7
+ experiment = Fabes::Experiment.new 'test', 'yay'
8
+ assert experiment.respond_to? :name
9
+ end
10
+
11
+ should "set a name" do
12
+ experiment = Fabes::Experiment.new 'test', 'yay'
13
+ experiment.name = 'Test name'
14
+ assert_equal experiment.name, 'Test name'
15
+ end
16
+
17
+ should "set a description" do
18
+ experiment = Fabes::Experiment.new 'test', 'yay'
19
+ experiment.description = 'yay'
20
+ assert_equal experiment.description, 'yay'
21
+ end
22
+
23
+ should "raise error if no name given" do
24
+ assert_raise ArgumentError do
25
+ experiment = Fabes::Experiment.new
26
+ end
27
+ end
28
+
29
+ should "be initialized with a name" do
30
+ experiment = Fabes::Experiment.new 'test', 'yay'
31
+ assert_not_nil experiment
32
+ assert_equal experiment.name, 'test'
33
+ end
34
+ end
35
+
36
+ context 'alternative' do
37
+ setup do
38
+ @experiment = Fabes::Experiment.new 'test', 'yay', 'yey'
39
+ end
40
+
41
+ should 'be able to select an alternative' do
42
+ assert @experiment.respond_to? :select_alternative!
43
+ end
44
+
45
+ should 'return an alternative' do
46
+ alternative = @experiment.select_alternative!
47
+ assert_equal alternative.class, Fabes::Alternative
48
+ end
49
+
50
+ should 'return a valid alternative' do
51
+ alternative = @experiment.select_alternative!
52
+ assert alternative.payload.match /y?y/
53
+ end
54
+
55
+ should 'shuffle when exploration'
56
+
57
+ should 'select the heaviest when exploitation' do
58
+ @experiment.alternatives.first.weight = 0.1
59
+ @experiment.stubs(:exploration?).returns(:false)
60
+ alternative = @experiment.select_alternative!
61
+ assert_not_equal alternative.id, @experiment.alternatives.first
62
+ end
63
+ end
64
+
65
+ context 'save' do
66
+ should 'save the experiment' do
67
+ experiment = Fabes::Experiment.new 'test', 'yay'
68
+ Fabes.stubs(db: mock(:save_experiment))
69
+
70
+ experiment.save
71
+ end
72
+ end
73
+
74
+ context 'find' do
75
+ should 'find the given experiment' do
76
+ Fabes::Experiment.new 'test', 'yay'
77
+ experiment = Fabes::Experiment.find 'test'
78
+ assert_not_nil experiment
79
+ assert_equal experiment.name, 'test'
80
+ end
81
+
82
+ should 'return nil when experiment not found' do
83
+ Fabes::Experiment.new 'test', 'yay'
84
+ experiment = Fabes::Experiment.find 'not_found'
85
+ assert_nil experiment
86
+ end
87
+ end
88
+
89
+ context 'all' do
90
+ setup do
91
+ Fabes::Experiment.new 'test', 'a', 'b', 'c'
92
+ Fabes::Experiment.new 'test2', 1, 2, 3
93
+ @experiments = Fabes::Experiment.all
94
+ end
95
+
96
+ should 'find all experiments (class)' do
97
+ @experiments.each do |exp|
98
+ assert_equal exp.class, Fabes::Experiment
99
+ end
100
+ end
101
+
102
+ should 'find all experiments (name)' do
103
+ @experiments.map(&:name).each do |name|
104
+ assert %w(test test2).include?(name)
105
+ end
106
+ end
107
+
108
+ should 'find all experiments (count)' do
109
+ assert_equal @experiments.count, 2
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,82 @@
1
+ require 'helper'
2
+
3
+ class TestHelper < Test::Unit::TestCase
4
+ include Fabes::Helper
5
+
6
+ def tracking
7
+ session[:fabes] ||= {}
8
+ end
9
+
10
+ context 'fabes' do
11
+ should 'be able to start an experiment' do
12
+ assert self.respond_to? :fabes
13
+ end
14
+
15
+ should 'return a valid alternative' do
16
+ alternative = fabes 'test', 1, 2, 3
17
+ assert_not_nil alternative
18
+ assert %w(1 2 3).include?(alternative.to_s)
19
+ end
20
+
21
+ should 'return control when error' do
22
+ Fabes::Experiment.expects(:find_or_create).then.raises(StandardError)
23
+ alternative = fabes 'test', 1, 2, 3
24
+ assert_not_nil alternative
25
+ assert_equal alternative, 1
26
+ end
27
+
28
+ should 'memorize options' do
29
+ alternative = fabes 'test', 'a', 'b', 'c'
30
+ second_alternative = fabes 'test', 'a', 'b', 'c'
31
+ assert_equal alternative, second_alternative
32
+ end
33
+
34
+ should 'increment the participant count if trackable' do
35
+ self.expects(:trackable_for).returns(true)
36
+ Fabes::Alternative.any_instance.expects :increment_participants!
37
+ alternative = fabes 'test', 1, 2, 3
38
+ end
39
+
40
+ should 'not increment the participant count if already participated' do
41
+ self.expects(:trackable_for).returns(false)
42
+ Fabes::Alternative.any_instance.expects(:increment_participants!).times(0)
43
+ alternative = fabes 'test', 1, 2, 3
44
+ end
45
+ end
46
+
47
+ context 'score' do
48
+ setup do
49
+ @experiment = Fabes::Experiment.new('test', 1, 2, 3)
50
+ @alternative = @experiment.alternatives.first
51
+ tracking['test'] = @alternative.id
52
+ end
53
+ should 'be able to finish an experiment' do
54
+ assert self.respond_to? :score!
55
+ end
56
+
57
+ should 'increment hits for an experiment' do
58
+ Fabes::Alternative.any_instance.expects :increment_hits!
59
+ score! 'test'
60
+ end
61
+
62
+ should 'not score twice the same person/experiment' do
63
+ Fabes::Alternative.any_instance.expects(:increment_hits!).once
64
+ score! 'test'
65
+ score! 'test'
66
+ end
67
+
68
+ should 'not score if not a participant' do
69
+ self.expects(:scorable?).returns(false)
70
+ Fabes::Alternative.any_instance.expects(:increment_hits!).times(0)
71
+ score! 'test'
72
+ end
73
+
74
+ should 'score two different experiments' do
75
+ @experiment2 = Fabes::Experiment.new('test2', 4, 5, 6)
76
+ tracking['test2'] = @experiment2.alternatives.first.id
77
+ Fabes::Alternative.any_instance.expects(:increment_hits!).twice
78
+ score! 'test'
79
+ score! 'test2'
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,67 @@
1
+ require 'helper'
2
+ require 'redis'
3
+
4
+ class TestRedisAdapter < Test::Unit::TestCase
5
+ context 'ConnectionHandling' do
6
+ context 'redis_connection' do
7
+ should 'create a new adapter' do
8
+ Fabes::ConnectionAdapters::RedisAdapter.expects :new
9
+ Fabes::ConnectionHandling.redis_connection({})
10
+ end
11
+ end
12
+ end
13
+
14
+ context 'ConnectionAdapters' do
15
+ setup do
16
+ @db = Redis.new
17
+ @adapter = Fabes::ConnectionAdapters::RedisAdapter.new(@db)
18
+ @experiment = Fabes::Experiment.new 'test', 'a', 'b', 'c'
19
+ @adapter.save_experiment(@experiment)
20
+ end
21
+
22
+ should 'clear the db' do
23
+ @db.set 'test', 'yay'
24
+ @adapter.clear!
25
+ assert_not_equal @db.get('test'), 'yay'
26
+ end
27
+
28
+ should 'save an experiment to the db' do
29
+ assert_equal @db.scard('fabes:experiments'), 1
30
+ assert @db.sismember 'fabes:experiments', 'test'
31
+ end
32
+
33
+ should 'save the alternatives to the db' do
34
+ @experiment.alternatives.each do |alt|
35
+ assert @db.exists "fabes:alternatives_pool:#{alt.id}"
36
+ data = @db.hgetall "fabes:alternatives_pool:#{alt.id}"
37
+ data.each do |field, value|
38
+ assert_equal alt.send(field).to_s, value
39
+ end
40
+ end
41
+ end
42
+
43
+ should 'not find an inexistant experiment' do
44
+ assert_nil @adapter.find_experiment('not_foundable')
45
+ end
46
+
47
+ should 'find an experiment' do
48
+ found_experiment = @adapter.find_experiment('test')
49
+ assert_not_nil found_experiment
50
+ assert_equal found_experiment.name, @experiment.name
51
+ end
52
+
53
+ should 'find an experiment with the correct data' do
54
+ found_experiment = @adapter.find_experiment('test')
55
+ found_experiment.alternatives.each do |alt|
56
+ %w(a b c).each do |expected_payload|
57
+ expected_payload.include? alt.payload
58
+ end
59
+ end
60
+ end
61
+
62
+ should 'find an experiment with the correct control' do
63
+ found_experiment = @adapter.find_experiment('test')
64
+ assert_equal found_experiment.control.payload, 'a'
65
+ end
66
+ end
67
+ end