abongo 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.
- data/MIT-LICENSE +20 -0
- data/README +1 -0
- data/lib/abongo.rb +292 -0
- data/lib/abongo.rb~ +282 -0
- data/lib/abongo/controller/dashboard.rb +24 -0
- data/lib/abongo/statistics.rb +93 -0
- data/lib/abongo/views/dashboard/_experiment.html.erb +43 -0
- data/lib/abongo/views/dashboard/index.html.erb +20 -0
- data/lib/abongo_sugar.rb +51 -0
- data/lib/abongo_view_helper.rb +43 -0
- data/rails/init.rb +2 -0
- data/rails/init.rb~ +2 -0
- data/test/test_abongo.rb +514 -0
- data/test/test_abongo.rb~ +472 -0
- metadata +103 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
class Abongo
|
2
|
+
module Controller
|
3
|
+
module Dashboard
|
4
|
+
|
5
|
+
if Rails::VERSION::MAJOR <= 2
|
6
|
+
ActionController::Base.view_paths.unshift File.join(File.dirname(__FILE__), "../views")
|
7
|
+
else
|
8
|
+
ActionController::Base.prepend_view_path File.join(File.dirname(__FILE__), "../views")
|
9
|
+
end
|
10
|
+
|
11
|
+
def index
|
12
|
+
@experiments = Abongo.all_tests.map{|e| {'participants' => 0, 'conversions' => 0}.merge(e)}
|
13
|
+
render :template => 'dashboard/index'
|
14
|
+
end
|
15
|
+
|
16
|
+
def end_experiment
|
17
|
+
@alternative = Abongo.get_alternative(params[:id])
|
18
|
+
@experiment = Abongo.get_test(@alternative['test'])
|
19
|
+
Abongo.end_experiment! @experiment['name'], @alternative['content']
|
20
|
+
redirect_to :back
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Abongo::Statistics
|
2
|
+
|
3
|
+
HANDY_Z_SCORE_CHEATSHEET = [[0.10, 1.29], [0.05, 1.65], [0.01, 2.33], [0.001, 3.08]]
|
4
|
+
|
5
|
+
PERCENTAGES = {0.10 => '90%', 0.05 => '95%', 0.01 => '99%', 0.001 => '99.9%'}
|
6
|
+
|
7
|
+
DESCRIPTION_IN_WORDS = {0.10 => 'fairly confident', 0.05 => 'confident',
|
8
|
+
0.01 => 'very confident', 0.001 => 'extremely confident'}
|
9
|
+
|
10
|
+
def self.zscore(alternatives)
|
11
|
+
if alternatives.size != 2
|
12
|
+
raise "Sorry, can't currently automatically calculate statistics for A/B tests with > 2 alternatives."
|
13
|
+
end
|
14
|
+
|
15
|
+
if (alternatives[0]['participants'] == 0) || (alternatives[1]['participants'] == 0)
|
16
|
+
raise "Can't calculate the z score if either of the alternatives lacks participants."
|
17
|
+
end
|
18
|
+
|
19
|
+
cr1 = conversion_rate(alternatives[0])
|
20
|
+
cr2 = conversion_rate(alternatives[1])
|
21
|
+
|
22
|
+
n1 = alternatives[0]['participants']
|
23
|
+
n2 = alternatives[1]['participants']
|
24
|
+
|
25
|
+
numerator = cr1 - cr2
|
26
|
+
frac1 = cr1 * (1 - cr1) / n1
|
27
|
+
frac2 = cr2 * (1 - cr2) / n2
|
28
|
+
|
29
|
+
numerator / ((frac1 + frac2) ** 0.5)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.p_value(alternatives)
|
33
|
+
index = 0
|
34
|
+
z = zscore(alternatives)
|
35
|
+
z = z.abs
|
36
|
+
found_p = nil
|
37
|
+
while index < HANDY_Z_SCORE_CHEATSHEET.size do
|
38
|
+
if (z > HANDY_Z_SCORE_CHEATSHEET[index][1])
|
39
|
+
found_p = HANDY_Z_SCORE_CHEATSHEET[index][0]
|
40
|
+
end
|
41
|
+
index += 1
|
42
|
+
end
|
43
|
+
found_p
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.is_statistically_significant?(p = 0.05)
|
47
|
+
p_value <= p
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.conversion_rate(exp)
|
51
|
+
1.0 * exp['conversions'] / exp['participants']
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.pretty_conversion_rate(exp)
|
55
|
+
sprintf("%4.2f%%", conversion_rate(exp) * 100)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.describe_result_in_words(experiment, alternatives)
|
59
|
+
begin
|
60
|
+
z = zscore(alternatives)
|
61
|
+
rescue
|
62
|
+
return "Could not execute the significance test because one or more of the alternatives has not been seen yet."
|
63
|
+
end
|
64
|
+
p = p_value(alternatives)
|
65
|
+
|
66
|
+
words = ""
|
67
|
+
if (alternatives[0]['participants'] < 10) || (alternatives[1]['participants'] < 10)
|
68
|
+
words += "Take these results with a grain of salt since your samples are so small: "
|
69
|
+
end
|
70
|
+
|
71
|
+
best_alternative = alternatives.max{|a, b| conversion_rate(a) <=> conversion_rate(b)}
|
72
|
+
alts = alternatives - [best_alternative]
|
73
|
+
worst_alternative = alts.first
|
74
|
+
|
75
|
+
words += "The best alternative you have is: [#{best_alternative['content']}], which had "
|
76
|
+
words += "#{best_alternative['conversions']} conversions from #{best_alternative['participants']} participants "
|
77
|
+
words += "(#{pretty_conversion_rate(best_alternative)}). The other alternative was [#{worst_alternative['content']}], "
|
78
|
+
words += "which had #{worst_alternative['conversions']} conversions from #{worst_alternative['participants']} participants "
|
79
|
+
words += "(#{pretty_conversion_rate(worst_alternative)}). "
|
80
|
+
|
81
|
+
if (p.nil?)
|
82
|
+
words += "However, this difference is not statistically significant."
|
83
|
+
else
|
84
|
+
words += "This difference is #{PERCENTAGES[p]} likely to be statistically significant, which means you can be "
|
85
|
+
words += "#{DESCRIPTION_IN_WORDS[p]} that it is the result of your alternatives actually mattering, rather than "
|
86
|
+
words += "being due to random chance. However, this statistical test can't measure how likely the currently "
|
87
|
+
words += "observed magnitude of the difference is to be accurate or not. It only says \"better\", not \"better "
|
88
|
+
words += "by so much\"."
|
89
|
+
end
|
90
|
+
words
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<h3><%= experiment['name'].titleize %> <%= %Q|(Test completed)| if experiment['final'] %> </h3>
|
2
|
+
<table class="experiment" style="border: 1px black;">
|
3
|
+
<tr class="header_row">
|
4
|
+
<th>Name</th>
|
5
|
+
<th>Participants</th>
|
6
|
+
<th>Conversions</th>
|
7
|
+
<th>Notes</th>
|
8
|
+
</tr>
|
9
|
+
<tr class="experiment_row">
|
10
|
+
<td>Experiment Total: </td>
|
11
|
+
<td><%= experiment['participants'] %> </td>
|
12
|
+
<td><%= experiment['conversions'] %> (<%= Abongo::Statistics.pretty_conversion_rate(experiment) %>)</td>
|
13
|
+
<td></td>
|
14
|
+
</tr>
|
15
|
+
<% alternatives = Abongo.get_alternatives(experiment['_id']).to_a %>
|
16
|
+
<% alternatives.each do |alternative| %>
|
17
|
+
<tr class="alternative_row">
|
18
|
+
<td>
|
19
|
+
<%= h alternative['content'] %>
|
20
|
+
</td>
|
21
|
+
<td><%= alternative['participants'] %></td>
|
22
|
+
<td><%= alternative['conversions'] %> (<%= Abongo::Statistics.pretty_conversion_rate(alternative) %>)</td>
|
23
|
+
<td>
|
24
|
+
<% unless experiment['final'] %>
|
25
|
+
<%= link_to("End experiment, picking this.", url_for(:id => alternative['_id'],
|
26
|
+
:action => "end_experiment"),
|
27
|
+
:method => :post,
|
28
|
+
:confirm => "Are you sure you want to terminate this experiment? This is not reversible."
|
29
|
+
) %>
|
30
|
+
<% else %>
|
31
|
+
<% if alternative['content'] == experiment['final'] %>
|
32
|
+
<b>(All users seeing this.)</b>
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
35
|
+
</td>
|
36
|
+
</tr>
|
37
|
+
<% end %>
|
38
|
+
<tr>
|
39
|
+
<td colspan="4">
|
40
|
+
<b>Significance test results: </b><%= Abongo::Statistics.describe_result_in_words(experiment, alternatives) %>
|
41
|
+
</td>
|
42
|
+
</tr>
|
43
|
+
</table>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<div id="abongo_dashboard">
|
2
|
+
<p><h1>Welcome to your A/Bongo dashboard!</h1>
|
3
|
+
|
4
|
+
See <a href="http://www.bongocardcreator.com/abongo">the official site</a> for documentation.
|
5
|
+
I encourage you to customize this page to fit your needs. See /vendor/plugins/abongo/views for
|
6
|
+
the view templates. Hack them to pieces -- please!
|
7
|
+
</p>
|
8
|
+
|
9
|
+
<p>
|
10
|
+
<% if flash[:notice] %>
|
11
|
+
<span style="color: green;"><%= flash[:notice] %> </span>
|
12
|
+
<% end %>
|
13
|
+
<h2>All Experiments</h2>
|
14
|
+
|
15
|
+
<% @experiments.each do |experiment| %>
|
16
|
+
<%= render :partial => "dashboard/experiment", :locals => {:experiment => experiment} %>
|
17
|
+
<br/>
|
18
|
+
<% end %>
|
19
|
+
</p>
|
20
|
+
</div>
|
data/lib/abongo_sugar.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#This module exists entirely to save finger strain for programmers.
|
2
|
+
#It is designed to be included in your ApplicationController.
|
3
|
+
#
|
4
|
+
#See abongo.rb for descriptions of what these do.
|
5
|
+
|
6
|
+
module AbongoSugar
|
7
|
+
|
8
|
+
def ab_test(test_name, alternatives = nil, options = {})
|
9
|
+
if (Abongo.options[:enable_specification] && !params[test_name].nil?)
|
10
|
+
choice = params[test_name]
|
11
|
+
elsif (Abongo.options[:enable_override_in_session] && !session[test_name].nil?)
|
12
|
+
choice = session[test_name]
|
13
|
+
elsif (Abongo.options[:enable_selection] && !params[test_name].nil?)
|
14
|
+
choice = alternatives[params[test_name].to_i]
|
15
|
+
elsif (alternatives.nil?)
|
16
|
+
choice = Abongo.flip(test_name, options)
|
17
|
+
else
|
18
|
+
choice = Abongo.test(test_name, alternatives, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
if block_given?
|
22
|
+
yield(choice)
|
23
|
+
else
|
24
|
+
choice
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def bongo!(test_name, options = {})
|
29
|
+
Abongo.bongo!(test_name, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
#Mark the user as a human.
|
33
|
+
def abongo_mark_human
|
34
|
+
textual_result = "1"
|
35
|
+
begin
|
36
|
+
a = params[:a].to_i
|
37
|
+
b = params[:b].to_i
|
38
|
+
c = params[:c].to_i
|
39
|
+
if (request.method == :post && (a + b == c))
|
40
|
+
Abongo.human!
|
41
|
+
else
|
42
|
+
textual_result = "0"
|
43
|
+
end
|
44
|
+
rescue #If a bot doesn't pass a, b, or c, to_i will fail. This scarfs up the exception, to save it from polluting our logs.
|
45
|
+
textual_result = "0"
|
46
|
+
end
|
47
|
+
render :text => textual_result, :layout => false #Not actually used by browser
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#Gives you easy syntax to use ABongo in your views.
|
2
|
+
|
3
|
+
module AbongoViewHelper
|
4
|
+
|
5
|
+
def ab_test(test_name, alternatives = nil, options = {}, &block)
|
6
|
+
|
7
|
+
if (Abongo.options[:enable_specification] && !params[test_name].nil?)
|
8
|
+
choice = params[test_name]
|
9
|
+
elsif (Abongo.options[:enable_override_in_session] && !session[test_name].nil?)
|
10
|
+
choice = session[test_name]
|
11
|
+
elsif (Abongo.options[:enable_selection] && !params[test_name].nil?)
|
12
|
+
choice = alternatives[params[test_name].to_i]
|
13
|
+
elsif (alternatives.nil?)
|
14
|
+
choice = Abongo.flip(test_name, options)
|
15
|
+
else
|
16
|
+
choice = Abongo.test(test_name, alternatives, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
if block
|
20
|
+
content_tag = capture(choice, &block)
|
21
|
+
Rails::VERSION::MAJOR <= 2 and block_called_from_erb?(block) ? concat(content_tag) : content_tag
|
22
|
+
else
|
23
|
+
choice
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def bongo!(test_name, options = {})
|
28
|
+
Abongo.bongo!(test_name, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
#This causes an AJAX post against the URL. That URL should call Abongo.human!
|
32
|
+
#This guarantees that anyone calling Abongo.human! is capable of at least minimal Javascript execution, and thus is (probably) not a robot.
|
33
|
+
def include_humanizing_javascript(url = "/abongo_mark_human", style = :prototype)
|
34
|
+
script = nil
|
35
|
+
if (style == :prototype)
|
36
|
+
script = "var a=Math.floor(Math.random()*11); var b=Math.floor(Math.random()*11);var x=new Ajax.Request('#{url}', {parameters:{a: a, b: b, c: a+b}})"
|
37
|
+
elsif (style == :jquery)
|
38
|
+
script = "var a=Math.floor(Math.random()*11); var b=Math.floor(Math.random()*11);var x=jQuery.post('#{url}', {a: a, b: b, c: a+b})"
|
39
|
+
end
|
40
|
+
script.nil? ? "" : %Q|<script type="text/javascript">#{script}</script>|.html_safe
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/rails/init.rb
ADDED
data/rails/init.rb~
ADDED
data/test/test_abongo.rb
ADDED
@@ -0,0 +1,514 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'mongo'
|
4
|
+
require 'abongo'
|
5
|
+
|
6
|
+
class TestAbongo < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def setup
|
9
|
+
conn = Mongo::Connection.new
|
10
|
+
db = conn['abongo_test']
|
11
|
+
Abongo.db = db
|
12
|
+
Abongo.options = {}
|
13
|
+
Abongo.identity = nil
|
14
|
+
Abongo.salt = 'Not really necessary.'
|
15
|
+
Abongo.participants.drop
|
16
|
+
Abongo.alternatives.drop
|
17
|
+
Abongo.conversions.drop
|
18
|
+
Abongo.experiments.drop
|
19
|
+
end
|
20
|
+
|
21
|
+
def teardown
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_experiment_creation
|
25
|
+
experiment = Abongo.start_experiment!('test_test', ['alt1', 'alt2'])
|
26
|
+
assert_equal('test_test', experiment['name'])
|
27
|
+
assert_equal(['alt1', 'alt2'], experiment['alternatives'])
|
28
|
+
assert(Abongo.tests_listening_to_conversion('test_test').include?(experiment['_id']))
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_experiment_creation_occurs_once
|
32
|
+
experiment1 = Abongo.start_experiment!('test_test', ['alt1', 'alt2'])
|
33
|
+
experiment2 = Abongo.start_experiment!('test_test', ['alt1', 'alt2'])
|
34
|
+
assert_equal(experiment1, experiment2)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_experiment_creation_with_conversion
|
38
|
+
Abongo.start_experiment!('test_test', ['alt1', 'alt2'], 'convert')
|
39
|
+
experiment = Abongo.get_test('test_test')
|
40
|
+
assert_equal('test_test', experiment['name'])
|
41
|
+
assert_equal(['alt1', 'alt2'], experiment['alternatives'])
|
42
|
+
assert(Abongo.tests_listening_to_conversion('convert').include?(experiment['_id']))
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_default_identity
|
46
|
+
assert 0 <= Abongo.identity.to_i
|
47
|
+
assert 10**10 >= Abongo.identity.to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_add_participation
|
51
|
+
Abongo.add_participation('ident', 'test1')
|
52
|
+
assert_equal(['test1'], Abongo.find_participant('ident')['tests'])
|
53
|
+
Abongo.add_participation('ident', 'test2')
|
54
|
+
assert_equal(['test1', 'test2'], Abongo.find_participant('ident')['tests'])
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_count_participation
|
58
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
59
|
+
experiment = Abongo.get_test 'test_test'
|
60
|
+
assert_equal 1, experiment['participants']
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_add_conversions
|
64
|
+
Abongo.add_conversion('ident', 'test1')
|
65
|
+
assert_equal(['test1'], Abongo.find_participant('ident')['conversions'])
|
66
|
+
Abongo.add_conversion('ident', 'test2')
|
67
|
+
assert_equal(['test1', 'test2'], Abongo.find_participant('ident')['conversions'])
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_find_alternative_for_user
|
71
|
+
Abongo.identity = 'ident'
|
72
|
+
experiment = Abongo.start_experiment!('test_test', ['alt1', 'alt2'])
|
73
|
+
assert_equal('alt1', Abongo.find_alternative_for_user('ident', experiment))
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_test
|
77
|
+
Abongo.identity = 'ident'
|
78
|
+
assert_equal('alt1', Abongo.test('test_test', ['alt1', 'alt2']))
|
79
|
+
experiment = Abongo.get_test('test_test')
|
80
|
+
assert_equal('test_test', experiment['name'])
|
81
|
+
assert_equal(['alt1', 'alt2'], experiment['alternatives'])
|
82
|
+
assert_equal([experiment['_id']], Abongo.find_participant('ident')['tests'])
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_test_with_block
|
86
|
+
Abongo.identity = 'ident'
|
87
|
+
Abongo.test('test_test', ['alt1', 'alt2']){|alt|
|
88
|
+
assert_equal('alt1', alt)
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_flip
|
93
|
+
Abongo.identity = 'ident'
|
94
|
+
assert_equal(true, Abongo.flip('test_test'))
|
95
|
+
experiment = Abongo.get_test('test_test')
|
96
|
+
assert_equal('test_test', experiment['name'])
|
97
|
+
assert_equal([true, false], experiment['alternatives'])
|
98
|
+
assert_equal([experiment['_id']], Abongo.find_participant('ident')['tests'])
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_flip_with_block
|
102
|
+
Abongo.identity = 'ident'
|
103
|
+
Abongo.flip('test_test'){|alt|
|
104
|
+
assert_equal(true, alt)
|
105
|
+
}
|
106
|
+
experiment = Abongo.get_test('test_test')
|
107
|
+
assert_equal([true, false], experiment['alternatives'])
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_flip_with_options
|
111
|
+
Abongo.identity = 'ident'
|
112
|
+
Abongo.flip('test_test', :conversion => 'test_conversions')
|
113
|
+
experiment = Abongo.get_test('test_test')
|
114
|
+
assert_equal([experiment['_id']], Abongo.tests_listening_to_conversion('test_conversions'))
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_flip_with_options_and_block
|
118
|
+
Abongo.identity = 'ident'
|
119
|
+
Abongo.flip('test_test', :conversion => 'test_conversions') do |alt|
|
120
|
+
# do nothing
|
121
|
+
end
|
122
|
+
experiment = Abongo.get_test('test_test')
|
123
|
+
assert_equal([experiment['_id']], Abongo.tests_listening_to_conversion('test_conversions'))
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_test_short_circuit
|
127
|
+
Abongo.identity = 'ident'
|
128
|
+
assert_equal('alt1', Abongo.test('test_test', ['alt1', 'alt2']))
|
129
|
+
Abongo.end_experiment!('test_test', 'alt2')
|
130
|
+
assert_equal('alt2', Abongo.test('test_test', ['alt1', 'alt2']))
|
131
|
+
Abongo.end_experiment!('test_test', 'alt3')
|
132
|
+
assert_equal('alt3', Abongo.test('test_test', ['alt1', 'alt2']))
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_ensure_one_participation_per_participant
|
136
|
+
Abongo.identity = 'ident'
|
137
|
+
10.times do
|
138
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
139
|
+
participant = Abongo.find_participant('ident')
|
140
|
+
assert_equal(1, participant['tests'].size)
|
141
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
142
|
+
assert_equal(1, alternative['participants'])
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_ensure_multiple_participation
|
147
|
+
Abongo.identity = 'ident'
|
148
|
+
10.times do |num|
|
149
|
+
Abongo.test('test_test', ['alt1', 'alt2'], {:multiple_participation => true})
|
150
|
+
participant = Abongo.find_participant('ident')
|
151
|
+
assert_equal(1, participant['tests'].size)
|
152
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
153
|
+
assert_equal(num+1, alternative['participants'])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_score_conversion
|
158
|
+
Abongo.identity = 'ident'
|
159
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
160
|
+
participant = Abongo.find_participant('ident')
|
161
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
162
|
+
assert_equal(1, alternative['participants'])
|
163
|
+
assert_equal(0, alternative['conversions'])
|
164
|
+
experiment = Abongo.get_test('test_test')
|
165
|
+
Abongo.score_conversion!(experiment['_id'])
|
166
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
167
|
+
assert_equal(1, alternative['conversions'])
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_count_participation
|
171
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
172
|
+
experiment = Abongo.get_test 'test_test'
|
173
|
+
assert_equal 0, experiment['conversions'] || 0
|
174
|
+
Abongo.score_conversion!(experiment['_id'])
|
175
|
+
experiment = Abongo.get_test 'test_test'
|
176
|
+
assert_equal 1, experiment['conversions'] || 0
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_score_conversion_with_name
|
180
|
+
Abongo.identity = 'ident'
|
181
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
182
|
+
participant = Abongo.find_participant('ident')
|
183
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
184
|
+
assert_equal(1, alternative['participants'])
|
185
|
+
assert_equal(0, alternative['conversions'])
|
186
|
+
experiment = Abongo.get_test('test_test')
|
187
|
+
Abongo.score_conversion!('test_test')
|
188
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
189
|
+
assert_equal(1, alternative['conversions'])
|
190
|
+
end
|
191
|
+
|
192
|
+
def test_score_conversion_only_once_per_participant
|
193
|
+
Abongo.identity = 'ident'
|
194
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
195
|
+
participant = Abongo.find_participant('ident')
|
196
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
197
|
+
assert_equal(1, alternative['participants'])
|
198
|
+
assert_equal(0, alternative['conversions'])
|
199
|
+
experiment = Abongo.get_test('test_test')
|
200
|
+
10.times do
|
201
|
+
Abongo.score_conversion!(experiment['_id'])
|
202
|
+
alternative = Abongo.alternatives.find_one(:test => participant['tests'].first, :content => 'alt1')
|
203
|
+
assert_equal(1, alternative['conversions'])
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_score_conversion_with_multiple_conversions
|
208
|
+
Abongo.identity = 'ident'
|
209
|
+
Abongo.options[:multiple_conversions] = true
|
210
|
+
Abongo.test('test_test', ['alt1', 'alt2'])
|
211
|
+
experiment = Abongo.get_test('test_test')
|
212
|
+
alternative = Abongo.alternatives.find_one(:test => experiment["_id"], :content => 'alt1')
|
213
|
+
assert_equal(1, alternative['participants'])
|
214
|
+
assert_equal(0, alternative['conversions'])
|
215
|
+
10.times do |num|
|
216
|
+
Abongo.score_conversion!(experiment['_id'])
|
217
|
+
alternative = Abongo.alternatives.find_one(:test => experiment["_id"], :content => 'alt1')
|
218
|
+
assert_equal(num+1, alternative['conversions'])
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_salt
|
223
|
+
Abongo.identity = 'ident'
|
224
|
+
assert_equal('alt1', Abongo.test('test_test', ['alt1', 'alt2']))
|
225
|
+
Abongo.salt = "This will change the result"
|
226
|
+
assert_equal('alt2', Abongo.test('test_test', ['alt1', 'alt2']))
|
227
|
+
end
|
228
|
+
|
229
|
+
def test_count_humans_only
|
230
|
+
Abongo.identity = 'ident'
|
231
|
+
Abongo.options[:count_humans_only] = true
|
232
|
+
assert_equal("alt1", Abongo.test('test_test', ['alt1', 'alt2']))
|
233
|
+
|
234
|
+
experiment = Abongo.get_test('test_test')
|
235
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
236
|
+
assert_equal(0, experiment['participants'])
|
237
|
+
assert_equal(0, experiment['conversions'])
|
238
|
+
assert_equal(0, alternative['participants'])
|
239
|
+
assert_equal(0, alternative['conversions'])
|
240
|
+
|
241
|
+
Abongo.score_conversion!('test_test')
|
242
|
+
experiment = Abongo.get_test('test_test')
|
243
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
244
|
+
assert_equal(0, experiment['participants'])
|
245
|
+
assert_equal(0, experiment['conversions'])
|
246
|
+
assert_equal(0, alternative['participants'])
|
247
|
+
assert_equal(0, alternative['conversions'])
|
248
|
+
|
249
|
+
Abongo.human!
|
250
|
+
experiment = Abongo.get_test('test_test')
|
251
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
252
|
+
assert_equal(1, experiment['participants'])
|
253
|
+
assert_equal(1, experiment['conversions'])
|
254
|
+
assert_equal(1, alternative['participants'])
|
255
|
+
assert_equal(1, alternative['conversions'])
|
256
|
+
|
257
|
+
|
258
|
+
assert_equal("alt1", Abongo.test('test2', ['alt1', 'alt2']))
|
259
|
+
|
260
|
+
experiment = Abongo.get_test('test2')
|
261
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
262
|
+
assert_equal(1, experiment['participants'])
|
263
|
+
assert_equal(0, experiment['conversions'])
|
264
|
+
assert_equal(1, alternative['participants'])
|
265
|
+
assert_equal(0, alternative['conversions'])
|
266
|
+
|
267
|
+
Abongo.score_conversion!('test2')
|
268
|
+
experiment = Abongo.get_test('test2')
|
269
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
270
|
+
assert_equal(1, experiment['participants'])
|
271
|
+
assert_equal(1, experiment['conversions'])
|
272
|
+
assert_equal(1, alternative['participants'])
|
273
|
+
assert_equal(1, alternative['conversions'])
|
274
|
+
|
275
|
+
Abongo.human!
|
276
|
+
experiment = Abongo.get_test('test2')
|
277
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
278
|
+
assert_equal(1, experiment['participants'])
|
279
|
+
assert_equal(1, experiment['conversions'])
|
280
|
+
assert_equal(1, alternative['participants'])
|
281
|
+
assert_equal(1, alternative['conversions'])
|
282
|
+
end
|
283
|
+
|
284
|
+
def test_count_humans_only_without_conversion_before_marked_human
|
285
|
+
Abongo.identity = 'ident'
|
286
|
+
Abongo.options[:count_humans_only] = true
|
287
|
+
assert_equal("alt1", Abongo.test('test_test', ['alt1', 'alt2']))
|
288
|
+
|
289
|
+
experiment = Abongo.get_test('test_test')
|
290
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
291
|
+
assert_equal(0, alternative['participants'])
|
292
|
+
assert_equal(0, alternative['conversions'])
|
293
|
+
|
294
|
+
Abongo.human!
|
295
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
296
|
+
assert_equal(1, alternative['participants'])
|
297
|
+
assert_equal(0, alternative['conversions'])
|
298
|
+
|
299
|
+
Abongo.score_conversion!('test_test')
|
300
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
301
|
+
assert_equal(1, alternative['participants'])
|
302
|
+
assert_equal(1, alternative['conversions'])
|
303
|
+
end
|
304
|
+
|
305
|
+
def test_parse_alternatives_array
|
306
|
+
assert_equal([1, 5, 2, 4, true], Abongo.parse_alternatives([1, 5, 2, 4, true]))
|
307
|
+
end
|
308
|
+
|
309
|
+
def test_parse_alternatives_integer
|
310
|
+
assert_equal([1, 2, 3, 4, 5], Abongo.parse_alternatives(5))
|
311
|
+
end
|
312
|
+
|
313
|
+
def test_parse_alternatives_range
|
314
|
+
assert_equal([2, 3, 4, 5], Abongo.parse_alternatives(2..5))
|
315
|
+
end
|
316
|
+
|
317
|
+
def test_parse_alternatives_hash
|
318
|
+
assert_equal(["three", "three", "three", "one"], Abongo.parse_alternatives({"three" => 3, "one" => 1}))
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_parse_alternatives_hash_invalid_value
|
322
|
+
assert_raise RuntimeError do
|
323
|
+
Abongo.parse_alternatives({"three" => "bob"})
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def test_parse_alternatives_invalid_type
|
328
|
+
assert_raise RuntimeError do
|
329
|
+
Abongo.parse_alternatives(Abongo.new)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def test_bongo_array
|
334
|
+
Abongo.identity = 'ident'
|
335
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
336
|
+
test2 = Abongo.start_experiment!('test2', ['alt1', 'alt2'])
|
337
|
+
test3 = Abongo.start_experiment!('test3', ['alt1', 'alt2'])
|
338
|
+
test4 = Abongo.start_experiment!('test3', ['alt1', 'alt2'])
|
339
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
340
|
+
Abongo.test('test2', ['alt1', 'alt2'])
|
341
|
+
Abongo.test('test3', ['alt1', 'alt2'])
|
342
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
343
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
344
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test3), :test => test3['_id']})['conversions'])
|
345
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test4), :test => test4['_id']})['conversions'])
|
346
|
+
|
347
|
+
Abongo.bongo!(['test1', 'test2'])
|
348
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
349
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
350
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test3), :test => test3['_id']})['conversions'])
|
351
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test4), :test => test4['_id']})['conversions'])
|
352
|
+
end
|
353
|
+
|
354
|
+
def test_bongo_nil
|
355
|
+
Abongo.identity = 'ident'
|
356
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
357
|
+
test2 = Abongo.start_experiment!('test2', ['alt1', 'alt2'])
|
358
|
+
test3 = Abongo.start_experiment!('test3', ['alt1', 'alt2'])
|
359
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
360
|
+
Abongo.test('test2', ['alt1', 'alt2'])
|
361
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
362
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
363
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test3), :test => test3['_id']})['conversions'])
|
364
|
+
|
365
|
+
Abongo.bongo!
|
366
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
367
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
368
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test3), :test => test3['_id']})['conversions'])
|
369
|
+
end
|
370
|
+
|
371
|
+
def test_bongo_with_alternative_conversion
|
372
|
+
Abongo.identity = 'ident'
|
373
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'], "convert!")
|
374
|
+
test2 = Abongo.start_experiment!('test2', ['alt1', 'alt2'], "convert!")
|
375
|
+
test3 = Abongo.start_experiment!('test3', ['alt1', 'alt2'], "dontconvert!")
|
376
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
377
|
+
Abongo.test('test2', ['alt1', 'alt2'])
|
378
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
379
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
380
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test3), :test => test3['_id']})['conversions'])
|
381
|
+
|
382
|
+
Abongo.bongo!('convert!')
|
383
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
384
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
385
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test3), :test => test3['_id']})['conversions'])
|
386
|
+
end
|
387
|
+
|
388
|
+
def test_bongo_with_test_name
|
389
|
+
Abongo.identity = 'ident'
|
390
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
391
|
+
test2 = Abongo.start_experiment!('test2', ['alt1', 'alt2'])
|
392
|
+
test3 = Abongo.start_experiment!('test3', ['alt1', 'alt2'])
|
393
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
394
|
+
Abongo.test('test2', ['alt1', 'alt2'])
|
395
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
396
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
397
|
+
Abongo.bongo!('test1')
|
398
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
399
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test2), :test => test2['_id']})['conversions'])
|
400
|
+
end
|
401
|
+
|
402
|
+
def test_bongo_doesnt_assume_participation
|
403
|
+
Abongo.identity = 'ident'
|
404
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
405
|
+
Abongo.bongo!('test1')
|
406
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
407
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
408
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
409
|
+
Abongo.bongo!('test1')
|
410
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
411
|
+
end
|
412
|
+
|
413
|
+
def test_bongo_with_assume_participation
|
414
|
+
Abongo.identity = 'ident'
|
415
|
+
Abongo.options[:assume_participation] = true
|
416
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
417
|
+
assert_equal(0, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
418
|
+
Abongo.bongo!('test1')
|
419
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
420
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
421
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
422
|
+
Abongo.bongo!('test1')
|
423
|
+
assert_equal(1, Abongo.alternatives.find_one({:content => Abongo.find_alternative_for_user(Abongo.identity, test1), :test => test1['_id']})['conversions'])
|
424
|
+
end
|
425
|
+
|
426
|
+
def test_participating_tests
|
427
|
+
Abongo.identity = 'ident'
|
428
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
429
|
+
test2 = Abongo.start_experiment!('test2', ['alt1', 'alt2'])
|
430
|
+
test3 = Abongo.start_experiment!('test3', ['alt1', 'alt2'])
|
431
|
+
assert_equal({}, Abongo.participating_tests)
|
432
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
433
|
+
assert_equal({'test1' => 'alt1'}, Abongo.participating_tests)
|
434
|
+
Abongo.test('test2', ['alt1', 'alt2'])
|
435
|
+
assert_equal({'test1' => 'alt1', 'test2' => 'alt1'}, Abongo.participating_tests)
|
436
|
+
Abongo.end_experiment!('test1', 'alt1')
|
437
|
+
assert_equal({'test2' => 'alt1'}, Abongo.participating_tests)
|
438
|
+
Abongo.end_experiment!('test2', 'alt1')
|
439
|
+
assert_equal({}, Abongo.participating_tests)
|
440
|
+
end
|
441
|
+
|
442
|
+
def test_participating_tests_with_noncurrent
|
443
|
+
Abongo.identity = 'ident'
|
444
|
+
test1 = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
445
|
+
test2 = Abongo.start_experiment!('test2', ['alt1', 'alt2'])
|
446
|
+
test3 = Abongo.start_experiment!('test3', ['alt1', 'alt2'])
|
447
|
+
assert_equal({}, Abongo.participating_tests)
|
448
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
449
|
+
assert_equal({'test1' => 'alt1'}, Abongo.participating_tests(false))
|
450
|
+
Abongo.test('test2', ['alt1', 'alt2'])
|
451
|
+
assert_equal({'test1' => 'alt1', 'test2' => 'alt1'}, Abongo.participating_tests(false))
|
452
|
+
Abongo.end_experiment!('test1', 'alt1')
|
453
|
+
assert_equal({'test1' => 'alt1', 'test2' => 'alt1'}, Abongo.participating_tests(false))
|
454
|
+
Abongo.end_experiment!('test2', 'alt1')
|
455
|
+
assert_equal({'test1' => 'alt1', 'test2' => 'alt1'}, Abongo.participating_tests(false))
|
456
|
+
end
|
457
|
+
|
458
|
+
def test_expires_in
|
459
|
+
Abongo.identity = 'ident'
|
460
|
+
Abongo.options[:expires_in] = 1
|
461
|
+
experiment = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
462
|
+
|
463
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
464
|
+
assert_equal(0, alternative['participants'])
|
465
|
+
assert_equal(0, alternative['conversions'])
|
466
|
+
|
467
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
468
|
+
Abongo.bongo!
|
469
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
470
|
+
assert_equal(1, alternative['participants'])
|
471
|
+
assert_equal(1, alternative['conversions'])
|
472
|
+
|
473
|
+
sleep(1)
|
474
|
+
|
475
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
476
|
+
Abongo.bongo!
|
477
|
+
alternative = Abongo.alternatives.find_one(:test => experiment['_id'], :content => 'alt1')
|
478
|
+
assert_equal(2, alternative['participants'])
|
479
|
+
assert_equal(2, alternative['conversions'])
|
480
|
+
end
|
481
|
+
|
482
|
+
def test_expires_in_for_bots
|
483
|
+
Abongo.identity = 'ident'
|
484
|
+
Abongo.options[:count_humans_only] = true
|
485
|
+
Abongo.options[:expires_in] = 1000
|
486
|
+
Abongo.options[:expires_in_for_bots] = 1
|
487
|
+
experiment = Abongo.start_experiment!('test1', ['alt1', 'alt2'])
|
488
|
+
|
489
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
490
|
+
participant = Abongo.find_participant(Abongo.identity)
|
491
|
+
assert(participant['expires'] < Time.now+1)
|
492
|
+
|
493
|
+
Abongo.human!
|
494
|
+
participant = Abongo.find_participant(Abongo.identity)
|
495
|
+
assert(participant['expires'] > Time.now+1)
|
496
|
+
|
497
|
+
Abongo.identity = 'ident2'
|
498
|
+
Abongo.human!
|
499
|
+
Abongo.test('test1', ['alt1', 'alt2'])
|
500
|
+
participant = Abongo.find_participant(Abongo.identity)
|
501
|
+
assert(participant['expires'] > Time.now+1)
|
502
|
+
end
|
503
|
+
|
504
|
+
def test_get_test_accepts_name
|
505
|
+
experiment = Abongo.start_experiment!('test_test', ['alt1', 'alt2'])
|
506
|
+
assert_equal(experiment, Abongo.get_test('test_test'))
|
507
|
+
end
|
508
|
+
|
509
|
+
def test_get_test_accepts_id
|
510
|
+
experiment = Abongo.start_experiment!('test_test', ['alt1', 'alt2'])
|
511
|
+
assert_equal(experiment, Abongo.get_test(experiment['_id']))
|
512
|
+
end
|
513
|
+
|
514
|
+
end
|