split 3.3.0 → 3.4.0
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.
- checksums.yaml +4 -4
- data/.eslintrc +1 -1
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +6 -1155
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +27 -20
- data/Appraisals +4 -0
- data/CHANGELOG.md +75 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/Gemfile +1 -0
- data/README.md +39 -22
- data/Rakefile +1 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/lib/split/algorithms/block_randomization.rb +1 -0
- data/lib/split/alternative.rb +3 -3
- data/lib/split/combined_experiments_helper.rb +2 -2
- data/lib/split/configuration.rb +9 -2
- data/lib/split/dashboard/helpers.rb +2 -2
- data/lib/split/dashboard/pagination_helpers.rb +3 -4
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +4 -1
- data/lib/split/engine.rb +6 -4
- data/lib/split/experiment.rb +29 -18
- data/lib/split/goals_collection.rb +1 -0
- data/lib/split/helper.rb +4 -3
- data/lib/split/persistence/dual_adapter.rb +54 -12
- data/lib/split/redis_interface.rb +1 -0
- data/lib/split/trial.rb +4 -6
- data/lib/split/user.rb +5 -1
- data/lib/split/version.rb +1 -1
- data/lib/split.rb +8 -1
- data/spec/dashboard/pagination_helpers_spec.rb +3 -1
- data/spec/dashboard_helpers_spec.rb +2 -2
- data/spec/dashboard_spec.rb +37 -16
- data/spec/encapsulated_helper_spec.rb +1 -1
- data/spec/experiment_spec.rb +44 -5
- data/spec/helper_spec.rb +123 -80
- data/spec/persistence/dual_adapter_spec.rb +160 -68
- data/spec/trial_spec.rb +20 -0
- data/spec/user_spec.rb +11 -0
- data/split.gemspec +3 -3
- metadata +25 -8
data/lib/split/experiment.rb
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
module Split
|
|
3
3
|
class Experiment
|
|
4
4
|
attr_accessor :name
|
|
5
|
-
attr_writer :algorithm
|
|
6
|
-
attr_accessor :resettable
|
|
7
5
|
attr_accessor :goals
|
|
8
|
-
attr_accessor :alternatives
|
|
9
6
|
attr_accessor :alternative_probabilities
|
|
10
7
|
attr_accessor :metadata
|
|
11
8
|
|
|
9
|
+
attr_reader :alternatives
|
|
10
|
+
attr_reader :resettable
|
|
11
|
+
|
|
12
12
|
DEFAULT_OPTIONS = {
|
|
13
13
|
:resettable => true
|
|
14
14
|
}
|
|
@@ -25,7 +25,7 @@ module Split
|
|
|
25
25
|
alternatives: load_alternatives_from_configuration,
|
|
26
26
|
goals: Split::GoalsCollection.new(@name).load_from_configuration,
|
|
27
27
|
metadata: load_metadata_from_configuration,
|
|
28
|
-
resettable: exp_config
|
|
28
|
+
resettable: exp_config.fetch(:resettable, true),
|
|
29
29
|
algorithm: exp_config[:algorithm]
|
|
30
30
|
}
|
|
31
31
|
else
|
|
@@ -62,7 +62,7 @@ module Split
|
|
|
62
62
|
alts = load_alternatives_from_configuration
|
|
63
63
|
options[:goals] = Split::GoalsCollection.new(@name).load_from_configuration
|
|
64
64
|
options[:metadata] = load_metadata_from_configuration
|
|
65
|
-
options[:resettable] = exp_config
|
|
65
|
+
options[:resettable] = exp_config.fetch(:resettable, true)
|
|
66
66
|
options[:algorithm] = exp_config[:algorithm]
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -81,12 +81,12 @@ module Split
|
|
|
81
81
|
|
|
82
82
|
if new_record?
|
|
83
83
|
start unless Split.configuration.start_manually
|
|
84
|
+
persist_experiment_configuration
|
|
84
85
|
elsif experiment_configuration_has_changed?
|
|
85
86
|
reset unless Split.configuration.reset_manually
|
|
87
|
+
persist_experiment_configuration
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
persist_experiment_configuration if new_record? || experiment_configuration_has_changed?
|
|
89
|
-
|
|
90
90
|
redis.hset(experiment_config_key, :resettable, resettable)
|
|
91
91
|
redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
|
|
92
92
|
self
|
|
@@ -144,11 +144,13 @@ module Split
|
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
def has_winner?
|
|
147
|
-
|
|
147
|
+
return @has_winner if defined? @has_winner
|
|
148
|
+
@has_winner = !winner.nil?
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
def winner=(winner_name)
|
|
151
152
|
redis.hset(:experiment_winner, name, winner_name.to_s)
|
|
153
|
+
@has_winner = true
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
def participant_count
|
|
@@ -161,6 +163,7 @@ module Split
|
|
|
161
163
|
|
|
162
164
|
def reset_winner
|
|
163
165
|
redis.hdel(:experiment_winner, name)
|
|
166
|
+
@has_winner = false
|
|
164
167
|
end
|
|
165
168
|
|
|
166
169
|
def start
|
|
@@ -420,14 +423,22 @@ module Split
|
|
|
420
423
|
end
|
|
421
424
|
|
|
422
425
|
def load_alternatives_from_redis
|
|
423
|
-
case redis.type(@name)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
alternatives = case redis.type(@name)
|
|
427
|
+
when 'set' # convert legacy sets to lists
|
|
428
|
+
alts = redis.smembers(@name)
|
|
429
|
+
redis.del(@name)
|
|
430
|
+
alts.reverse.each {|a| redis.lpush(@name, a) }
|
|
431
|
+
redis.lrange(@name, 0, -1)
|
|
432
|
+
else
|
|
433
|
+
redis.lrange(@name, 0, -1)
|
|
434
|
+
end
|
|
435
|
+
alternatives.map do |alt|
|
|
436
|
+
alt = begin
|
|
437
|
+
JSON.parse(alt)
|
|
438
|
+
rescue
|
|
439
|
+
alt
|
|
440
|
+
end
|
|
441
|
+
Split::Alternative.new(alt, @name)
|
|
431
442
|
end
|
|
432
443
|
end
|
|
433
444
|
|
|
@@ -443,7 +454,7 @@ module Split
|
|
|
443
454
|
|
|
444
455
|
def persist_experiment_configuration
|
|
445
456
|
redis_interface.add_to_set(:experiments, name)
|
|
446
|
-
redis_interface.persist_list(name, @alternatives.map
|
|
457
|
+
redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
|
|
447
458
|
goals_collection.save
|
|
448
459
|
redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
|
|
449
460
|
end
|
|
@@ -459,7 +470,7 @@ module Split
|
|
|
459
470
|
existing_alternatives = load_alternatives_from_redis
|
|
460
471
|
existing_goals = Split::GoalsCollection.new(@name).load_from_redis
|
|
461
472
|
existing_metadata = load_metadata_from_redis
|
|
462
|
-
existing_alternatives != @alternatives.map(&:
|
|
473
|
+
existing_alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
|
|
463
474
|
existing_goals != @goals ||
|
|
464
475
|
existing_metadata != @metadata
|
|
465
476
|
end
|
data/lib/split/helper.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Split
|
|
|
8
8
|
def ab_test(metric_descriptor, control = nil, *alternatives)
|
|
9
9
|
begin
|
|
10
10
|
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
|
|
11
|
-
alternative = if Split.configuration.enabled
|
|
11
|
+
alternative = if Split.configuration.enabled && !exclude_visitor?
|
|
12
12
|
experiment.save
|
|
13
13
|
raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
|
|
14
14
|
trial = Trial.new(:user => ab_user, :experiment => experiment,
|
|
@@ -44,6 +44,7 @@ module Split
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def finish_experiment(experiment, options = {:reset => true})
|
|
47
|
+
return false if active_experiments[experiment.name].nil?
|
|
47
48
|
return true if experiment.has_winner?
|
|
48
49
|
should_reset = experiment.resettable? && options[:reset]
|
|
49
50
|
if ab_user[experiment.finished_key] && !should_reset
|
|
@@ -79,7 +80,7 @@ module Split
|
|
|
79
80
|
|
|
80
81
|
def ab_record_extra_info(metric_descriptor, key, value = 1)
|
|
81
82
|
return if exclude_visitor? || Split.configuration.disabled?
|
|
82
|
-
metric_descriptor,
|
|
83
|
+
metric_descriptor, _ = normalize_metric(metric_descriptor)
|
|
83
84
|
experiments = Metric.possible_experiments(metric_descriptor)
|
|
84
85
|
|
|
85
86
|
if experiments.any?
|
|
@@ -122,7 +123,7 @@ module Split
|
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
def exclude_visitor?
|
|
125
|
-
instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?
|
|
126
|
+
defined?(request) && (instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?)
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def is_robot?
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'forwardable'
|
|
4
|
-
|
|
5
3
|
module Split
|
|
6
4
|
module Persistence
|
|
7
5
|
class DualAdapter
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
def self.with_config(options={})
|
|
7
|
+
self.config.merge!(options)
|
|
8
|
+
self
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.config
|
|
12
|
+
@config ||= {}
|
|
13
|
+
end
|
|
10
14
|
|
|
11
15
|
def initialize(context)
|
|
12
16
|
if logged_in = self.class.config[:logged_in]
|
|
@@ -22,22 +26,60 @@ module Split
|
|
|
22
26
|
raise "Please configure :logged_out_adapter"
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
@fallback_to_logged_out_adapter =
|
|
30
|
+
self.class.config[:fallback_to_logged_out_adapter] || false
|
|
31
|
+
@logged_in = logged_in.call(context)
|
|
32
|
+
@logged_in_adapter = logged_in_adapter.new(context)
|
|
33
|
+
@logged_out_adapter = logged_out_adapter.new(context)
|
|
34
|
+
@active_adapter = @logged_in ? @logged_in_adapter : @logged_out_adapter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def keys
|
|
38
|
+
if @fallback_to_logged_out_adapter
|
|
39
|
+
(@logged_in_adapter.keys + @logged_out_adapter.keys).uniq
|
|
27
40
|
else
|
|
28
|
-
@
|
|
41
|
+
@active_adapter.keys
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
def [](key)
|
|
46
|
+
if @fallback_to_logged_out_adapter
|
|
47
|
+
@logged_in && @logged_in_adapter[key] || @logged_out_adapter[key]
|
|
48
|
+
else
|
|
49
|
+
@active_adapter[key]
|
|
50
|
+
end
|
|
35
51
|
end
|
|
36
52
|
|
|
37
|
-
def
|
|
38
|
-
@
|
|
53
|
+
def []=(key, value)
|
|
54
|
+
if @fallback_to_logged_out_adapter
|
|
55
|
+
@logged_in_adapter[key] = value if @logged_in
|
|
56
|
+
old_value = @logged_out_adapter[key]
|
|
57
|
+
@logged_out_adapter[key] = value
|
|
58
|
+
|
|
59
|
+
decrement_participation(key, old_value) if decrement_participation?(old_value, value)
|
|
60
|
+
else
|
|
61
|
+
@active_adapter[key] = value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def delete(key)
|
|
66
|
+
if @fallback_to_logged_out_adapter
|
|
67
|
+
@logged_in_adapter.delete(key)
|
|
68
|
+
@logged_out_adapter.delete(key)
|
|
69
|
+
else
|
|
70
|
+
@active_adapter.delete(key)
|
|
71
|
+
end
|
|
39
72
|
end
|
|
40
73
|
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def decrement_participation?(old_value, value)
|
|
77
|
+
!old_value.nil? && !value.nil? && old_value != value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def decrement_participation(key, value)
|
|
81
|
+
Split.redis.hincrby("#{key}:#{value}", 'participant_count', -1)
|
|
82
|
+
end
|
|
41
83
|
end
|
|
42
84
|
end
|
|
43
85
|
end
|
data/lib/split/trial.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module Split
|
|
3
3
|
class Trial
|
|
4
4
|
attr_accessor :experiment
|
|
5
|
-
|
|
5
|
+
attr_writer :metadata
|
|
6
6
|
|
|
7
7
|
def initialize(attrs = {})
|
|
8
8
|
self.experiment = attrs.delete(:experiment)
|
|
@@ -68,10 +68,8 @@ module Split
|
|
|
68
68
|
if exclude_user?
|
|
69
69
|
self.alternative = @experiment.control
|
|
70
70
|
else
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
self.alternative = value
|
|
74
|
-
else
|
|
71
|
+
self.alternative = @user[@experiment.key]
|
|
72
|
+
if alternative.nil?
|
|
75
73
|
self.alternative = @experiment.next_alternative
|
|
76
74
|
|
|
77
75
|
# Increment the number of participants since we are actually choosing a new alternative
|
|
@@ -82,7 +80,7 @@ module Split
|
|
|
82
80
|
end
|
|
83
81
|
end
|
|
84
82
|
|
|
85
|
-
@user[@experiment.key] = alternative.name if should_store_alternative?
|
|
83
|
+
@user[@experiment.key] = alternative.name if !@experiment.has_winner? && should_store_alternative?
|
|
86
84
|
@alternative_choosen = true
|
|
87
85
|
run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled?
|
|
88
86
|
alternative
|
data/lib/split/user.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'forwardable'
|
|
2
3
|
|
|
3
4
|
module Split
|
|
@@ -8,9 +9,11 @@ module Split
|
|
|
8
9
|
|
|
9
10
|
def initialize(context, adapter=nil)
|
|
10
11
|
@user = adapter || Split::Persistence.adapter.new(context)
|
|
12
|
+
@cleaned_up = false
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def cleanup_old_experiments!
|
|
16
|
+
return if @cleaned_up
|
|
14
17
|
keys_without_finished(user.keys).each do |key|
|
|
15
18
|
experiment = ExperimentCatalog.find key_without_version(key)
|
|
16
19
|
if experiment.nil? || experiment.has_winner? || experiment.start_time.nil?
|
|
@@ -18,6 +21,7 @@ module Split
|
|
|
18
21
|
user.delete Experiment.finished_key(key)
|
|
19
22
|
end
|
|
20
23
|
end
|
|
24
|
+
@cleaned_up = true
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def max_experiments_reached?(experiment_key)
|
|
@@ -38,7 +42,7 @@ module Split
|
|
|
38
42
|
|
|
39
43
|
def active_experiments
|
|
40
44
|
experiment_pairs = {}
|
|
41
|
-
user.keys.each do |key|
|
|
45
|
+
keys_without_finished(user.keys).each do |key|
|
|
42
46
|
Metric.possible_experiments(key_without_version(key)).each do |experiment|
|
|
43
47
|
if !experiment.has_winner?
|
|
44
48
|
experiment_pairs[key_without_version(key)] = user[key]
|
data/lib/split/version.rb
CHANGED
data/lib/split.rb
CHANGED
|
@@ -66,4 +66,11 @@ module Split
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
# Check to see if being run in a Rails application. If so, wait until before_initialize to run configuration so Gems that create ENV variables have the chance to initialize first.
|
|
70
|
+
if defined?(::Rails)
|
|
71
|
+
class Railtie < Rails::Railtie
|
|
72
|
+
config.before_initialize { Split.configure {} }
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
Split.configure {}
|
|
76
|
+
end
|
|
@@ -10,7 +10,9 @@ describe Split::DashboardPaginationHelpers do
|
|
|
10
10
|
context 'when params empty' do
|
|
11
11
|
let(:params) { Hash[] }
|
|
12
12
|
|
|
13
|
-
it 'returns 10' do
|
|
13
|
+
it 'returns the default (10)' do
|
|
14
|
+
default_per_page = Split.configuration.dashboard_pagination_default_per_page
|
|
15
|
+
expect(pagination_per).to eql default_per_page
|
|
14
16
|
expect(pagination_per).to eql 10
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -27,11 +27,11 @@ describe Split::DashboardHelpers do
|
|
|
27
27
|
|
|
28
28
|
describe '#round' do
|
|
29
29
|
it 'can round number strings' do
|
|
30
|
-
expect(round('3.1415')).to eq BigDecimal
|
|
30
|
+
expect(round('3.1415')).to eq BigDecimal('3.14')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
it 'can round number strings for precsion' do
|
|
34
|
-
expect(round('3.1415', 1)).to eq BigDecimal
|
|
34
|
+
expect(round('3.1415', 1)).to eq BigDecimal('3.1')
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
it 'can handle invalid number strings' do
|
data/spec/dashboard_spec.rb
CHANGED
|
@@ -29,6 +29,10 @@ describe Split::Dashboard do
|
|
|
29
29
|
let(:red_link) { link("red") }
|
|
30
30
|
let(:blue_link) { link("blue") }
|
|
31
31
|
|
|
32
|
+
before(:each) do
|
|
33
|
+
Split.configuration.beta_probability_simulations = 1
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
it "should respond to /" do
|
|
33
37
|
get '/'
|
|
34
38
|
expect(last_response).to be_ok
|
|
@@ -74,17 +78,39 @@ describe Split::Dashboard do
|
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
describe "force alternative" do
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
context "initial version" do
|
|
82
|
+
let!(:user) do
|
|
83
|
+
Split::User.new(@app, { experiment.name => 'red' })
|
|
84
|
+
end
|
|
80
85
|
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
before do
|
|
87
|
+
allow(Split::User).to receive(:new).and_return(user)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "should set current user's alternative" do
|
|
91
|
+
blue_link.participant_count = 7
|
|
92
|
+
post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
|
|
93
|
+
expect(user[experiment.key]).to eq("blue")
|
|
94
|
+
expect(blue_link.participant_count).to eq(8)
|
|
95
|
+
end
|
|
83
96
|
end
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
context "incremented version" do
|
|
99
|
+
let!(:user) do
|
|
100
|
+
experiment.increment_version
|
|
101
|
+
Split::User.new(@app, { "#{experiment.name}:#{experiment.version}" => 'red' })
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
before do
|
|
105
|
+
allow(Split::User).to receive(:new).and_return(user)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "should set current user's alternative" do
|
|
109
|
+
blue_link.participant_count = 7
|
|
110
|
+
post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
|
|
111
|
+
expect(user[experiment.key]).to eq("blue")
|
|
112
|
+
expect(blue_link.participant_count).to eq(8)
|
|
113
|
+
end
|
|
88
114
|
end
|
|
89
115
|
end
|
|
90
116
|
|
|
@@ -120,7 +146,7 @@ describe Split::Dashboard do
|
|
|
120
146
|
it "removes winner" do
|
|
121
147
|
post "/reopen?experiment=#{experiment.name}"
|
|
122
148
|
|
|
123
|
-
expect(experiment).to_not have_winner
|
|
149
|
+
expect(Split::ExperimentCatalog.find(experiment.name)).to_not have_winner
|
|
124
150
|
end
|
|
125
151
|
|
|
126
152
|
it "keeps existing stats" do
|
|
@@ -167,19 +193,14 @@ describe Split::Dashboard do
|
|
|
167
193
|
end
|
|
168
194
|
|
|
169
195
|
it "should display the start date" do
|
|
170
|
-
|
|
171
|
-
expect(Time).to receive(:now).at_least(:once).and_return(experiment_start_time)
|
|
172
|
-
experiment
|
|
196
|
+
experiment.start
|
|
173
197
|
|
|
174
198
|
get '/'
|
|
175
199
|
|
|
176
|
-
expect(last_response.body).to include(
|
|
200
|
+
expect(last_response.body).to include("<small>#{experiment.start_time.strftime('%Y-%m-%d')}</small>")
|
|
177
201
|
end
|
|
178
202
|
|
|
179
203
|
it "should handle experiments without a start date" do
|
|
180
|
-
experiment_start_time = Time.parse('2011-07-07')
|
|
181
|
-
expect(Time).to receive(:now).at_least(:once).and_return(experiment_start_time)
|
|
182
|
-
|
|
183
204
|
Split.redis.hdel(:experiment_start_times, experiment.name)
|
|
184
205
|
|
|
185
206
|
get '/'
|
data/spec/experiment_spec.rb
CHANGED
|
@@ -35,6 +35,12 @@ describe Split::Experiment do
|
|
|
35
35
|
expect(experiment.resettable).to be_truthy
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
it "should be resettable when loading from configuration" do
|
|
39
|
+
allow(Split.configuration).to receive(:experiment_for).with('some_experiment') { { alternatives: %w(a b) } }
|
|
40
|
+
|
|
41
|
+
expect(Split::Experiment.new('some_experiment')).to be_resettable
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
it "should save to redis" do
|
|
39
45
|
experiment.save
|
|
40
46
|
expect(Split.redis.exists('basket_text')).to be true
|
|
@@ -86,7 +92,7 @@ describe Split::Experiment do
|
|
|
86
92
|
experiment.save
|
|
87
93
|
experiment.save
|
|
88
94
|
expect(Split.redis.exists('basket_text')).to be true
|
|
89
|
-
expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['Basket', "Cart"])
|
|
95
|
+
expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['{"Basket":1}', '{"Cart":1}'])
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
describe 'new record?' do
|
|
@@ -213,12 +219,41 @@ describe Split::Experiment do
|
|
|
213
219
|
it "should have no winner initially" do
|
|
214
220
|
expect(experiment.winner).to be_nil
|
|
215
221
|
end
|
|
222
|
+
end
|
|
216
223
|
|
|
224
|
+
describe 'winner=' do
|
|
217
225
|
it "should allow you to specify a winner" do
|
|
218
226
|
experiment.save
|
|
219
227
|
experiment.winner = 'red'
|
|
220
228
|
expect(experiment.winner.name).to eq('red')
|
|
221
229
|
end
|
|
230
|
+
|
|
231
|
+
context 'when has_winner state is memoized' do
|
|
232
|
+
before { expect(experiment).to_not have_winner }
|
|
233
|
+
|
|
234
|
+
it 'should keep has_winner state consistent' do
|
|
235
|
+
experiment.winner = 'red'
|
|
236
|
+
expect(experiment).to have_winner
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe 'reset_winner' do
|
|
242
|
+
before { experiment.winner = 'green' }
|
|
243
|
+
|
|
244
|
+
it 'should reset the winner' do
|
|
245
|
+
experiment.reset_winner
|
|
246
|
+
expect(experiment.winner).to be_nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
context 'when has_winner state is memoized' do
|
|
250
|
+
before { expect(experiment).to have_winner }
|
|
251
|
+
|
|
252
|
+
it 'should keep has_winner state consistent' do
|
|
253
|
+
experiment.reset_winner
|
|
254
|
+
expect(experiment).to_not have_winner
|
|
255
|
+
end
|
|
256
|
+
end
|
|
222
257
|
end
|
|
223
258
|
|
|
224
259
|
describe 'has_winner?' do
|
|
@@ -235,6 +270,12 @@ describe Split::Experiment do
|
|
|
235
270
|
expect(experiment).to_not have_winner
|
|
236
271
|
end
|
|
237
272
|
end
|
|
273
|
+
|
|
274
|
+
it 'memoizes has_winner state' do
|
|
275
|
+
expect(experiment).to receive(:winner).once
|
|
276
|
+
expect(experiment).to_not have_winner
|
|
277
|
+
expect(experiment).to_not have_winner
|
|
278
|
+
end
|
|
238
279
|
end
|
|
239
280
|
|
|
240
281
|
describe 'reset' do
|
|
@@ -414,9 +455,7 @@ describe Split::Experiment do
|
|
|
414
455
|
}
|
|
415
456
|
|
|
416
457
|
context "saving experiment" do
|
|
417
|
-
|
|
418
|
-
Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green')
|
|
419
|
-
end
|
|
458
|
+
let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green') }
|
|
420
459
|
|
|
421
460
|
before { experiment.save }
|
|
422
461
|
|
|
@@ -425,7 +464,7 @@ describe Split::Experiment do
|
|
|
425
464
|
end
|
|
426
465
|
|
|
427
466
|
it "should reset an experiment if it is loaded with different goals" do
|
|
428
|
-
|
|
467
|
+
same_but_different_goals
|
|
429
468
|
expect(Split::ExperimentCatalog.find("link_color").goals).to eq(["purchase", "refund"])
|
|
430
469
|
end
|
|
431
470
|
|