split 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
- %w[array string].each do |f|
2
+ %w[string].each do |f|
3
3
  require "split/extensions/#{f}"
4
4
  end
@@ -22,7 +22,7 @@ module Split
22
22
 
23
23
  def save
24
24
  return false if @goals.nil?
25
- @goals.reverse.each { |goal| Split.redis.lpush(goals_key, goal) }
25
+ RedisInterface.new.persist_list(goals_key, @goals)
26
26
  end
27
27
 
28
28
  def validate!
@@ -0,0 +1,51 @@
1
+ module Split
2
+ # Simplifies the interface to Redis.
3
+ class RedisInterface
4
+ def initialize
5
+ self.redis = Split.redis
6
+ end
7
+
8
+ def persist_list(list_name, list_values)
9
+ max_index = list_length(list_name) - 1
10
+ list_values.each_with_index do |value, index|
11
+ if index > max_index
12
+ add_to_list(list_name, value)
13
+ else
14
+ set_list_index(list_name, index, value)
15
+ end
16
+ end
17
+ make_list_length(list_name, list_values.length)
18
+ list_values
19
+ end
20
+
21
+ def add_to_list(list_name, value)
22
+ redis.rpush(list_name, value)
23
+ end
24
+
25
+ def set_list_index(list_name, index, value)
26
+ redis.lset(list_name, index, value)
27
+ end
28
+
29
+ def list_length(list_name)
30
+ redis.llen(list_name)
31
+ end
32
+
33
+ def remove_last_item_from_list(list_name)
34
+ redis.rpop(list_name)
35
+ end
36
+
37
+ def make_list_length(list_name, new_length)
38
+ while list_length(list_name) > new_length
39
+ remove_last_item_from_list(list_name)
40
+ end
41
+ end
42
+
43
+ def add_to_set(set_name, value)
44
+ redis.sadd(set_name, value) unless redis.sismember(set_name, value)
45
+ end
46
+
47
+ private
48
+
49
+ attr_accessor :redis
50
+ end
51
+ end
@@ -21,7 +21,7 @@ module Split
21
21
  def max_experiments_reached?(experiment_key)
22
22
  if Split.configuration.allow_multiple_experiments == 'control'
23
23
  experiments = active_experiments
24
- count_control = experiments.values.count {|v| v == 'control'}
24
+ count_control = experiments.count {|k,v| k == experiment_key || v == 'control'}
25
25
  experiments.size > count_control
26
26
  else
27
27
  !Split.configuration.allow_multiple_experiments &&
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Split
3
3
  MAJOR = 2
4
- MINOR = 1
4
+ MINOR = 2
5
5
  PATCH = 0
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
@@ -212,20 +212,34 @@ describe Split::Configuration do
212
212
  expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
213
213
  end
214
214
 
215
- context "configuration URL" do
215
+ context 'redis_url configuration [DEPRECATED]' do
216
+ it 'should warn on set and assign to #redis' do
217
+ expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
218
+ @config.redis_url = 'example_url'
219
+ expect(@config.redis).to eq('example_url')
220
+ end
221
+
222
+ it 'should warn on get and return #redis' do
223
+ expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
224
+ @config.redis = 'example_url'
225
+ expect(@config.redis_url).to eq('example_url')
226
+ end
227
+ end
228
+
229
+ context "redis configuration" do
216
230
  it "should default to local redis server" do
217
- expect(@config.redis_url).to eq("localhost:6379")
231
+ expect(@config.redis).to eq("redis://localhost:6379")
218
232
  end
219
233
 
220
234
  it "should allow for redis url to be configured" do
221
- @config.redis_url = "custom_redis_url"
222
- expect(@config.redis_url).to eq("custom_redis_url")
235
+ @config.redis = "custom_redis_url"
236
+ expect(@config.redis).to eq("custom_redis_url")
223
237
  end
224
238
 
225
239
  context "provided REDIS_URL environment variable" do
226
240
  it "should use the ENV variable" do
227
241
  ENV['REDIS_URL'] = "env_redis_url"
228
- expect(Split::Configuration.new.redis_url).to eq("env_redis_url")
242
+ expect(Split::Configuration.new.redis).to eq("env_redis_url")
229
243
  end
230
244
  end
231
245
  end
@@ -73,6 +73,21 @@ describe Split::Dashboard do
73
73
  end
74
74
  end
75
75
 
76
+ describe "force alternative" do
77
+ let!(:user) do
78
+ Split::User.new(@app, { experiment.name => 'a' })
79
+ end
80
+
81
+ before do
82
+ allow(Split::User).to receive(:new).and_return(user)
83
+ end
84
+
85
+ it "should set current user's alternative" do
86
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "b"
87
+ expect(user[experiment.name]).to eq("b")
88
+ end
89
+ end
90
+
76
91
  describe "index page" do
77
92
  context "with winner" do
78
93
  before { experiment.winner = 'red' }
@@ -4,18 +4,49 @@ require 'spec_helper'
4
4
  describe Split::EncapsulatedHelper do
5
5
  include Split::EncapsulatedHelper
6
6
 
7
- before do
8
- allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
9
- .and_return(mock_user)
10
- end
11
7
 
12
8
  def params
13
9
  raise NoMethodError, 'This method is not really defined'
14
10
  end
15
11
 
16
12
  describe "ab_test" do
13
+ before do
14
+ allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
15
+ .and_return(mock_user)
16
+ end
17
+
17
18
  it "should not raise an error when params raises an error" do
19
+ expect{ params }.to raise_error(NoMethodError)
18
20
  expect(lambda { ab_test('link_color', 'blue', 'red') }).not_to raise_error
19
21
  end
22
+
23
+ it "calls the block with selected alternative" do
24
+ expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', nil)
25
+ end
26
+
27
+ context "inside a view" do
28
+
29
+ it "works inside ERB" do
30
+ require 'erb'
31
+ template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(' '), nil, "%")
32
+ foo <% ab_test(:foo, '1', '2') do |alt, meta| %>
33
+ static <%= alt %>
34
+ <% end %>
35
+ ERB
36
+ expect(template.result(binding)).to match /foo static \d/
37
+ end
38
+
39
+ end
40
+ end
41
+
42
+ describe "context" do
43
+ it 'is passed in shim' do
44
+ ctx = Class.new{
45
+ include Split::EncapsulatedHelper
46
+ public :session
47
+ }.new
48
+ expect(ctx).to receive(:session){{}}
49
+ expect{ ctx.ab_test('link_color', 'blue', 'red') }.not_to raise_error
50
+ end
20
51
  end
21
52
  end
@@ -13,9 +13,7 @@ describe Split::Experiment do
13
13
  Split::Alternative.new(color, 'link_color')
14
14
  end
15
15
 
16
- let(:experiment) {
17
- new_experiment
18
- }
16
+ let(:experiment) { new_experiment }
19
17
 
20
18
  let(:blue) { alternative("blue") }
21
19
  let(:green) { alternative("green") }
@@ -242,7 +240,15 @@ describe Split::Experiment do
242
240
  end
243
241
 
244
242
  describe 'reset' do
245
- before { experiment.save }
243
+ let(:reset_manually) { false }
244
+
245
+ before do
246
+ allow(Split.configuration).to receive(:reset_manually).and_return(reset_manually)
247
+ experiment.save
248
+ green.increment_participation
249
+ green.increment_participation
250
+ end
251
+
246
252
  it 'should reset all alternatives' do
247
253
  experiment.winner = 'green'
248
254
 
@@ -352,6 +358,34 @@ describe Split::Experiment do
352
358
  same_experiment_again = same_but_different_alternative
353
359
  expect(same_experiment_again.version).to eq(1)
354
360
  end
361
+
362
+ context 'when experiment configuration is changed' do
363
+ let(:reset_manually) { false }
364
+
365
+ before do
366
+ experiment.save
367
+ allow(Split.configuration).to receive(:reset_manually).and_return(reset_manually)
368
+ green.increment_participation
369
+ green.increment_participation
370
+ experiment.set_alternatives_and_options(alternatives: %w(blue red green zip),
371
+ goals: %w(purchase))
372
+ experiment.save
373
+ end
374
+
375
+ it 'resets all alternatives' do
376
+ expect(green.participant_count).to eq(0)
377
+ expect(green.completed_count).to eq(0)
378
+ end
379
+
380
+ context 'when reset_manually is set' do
381
+ let(:reset_manually) { true }
382
+
383
+ it 'does not reset alternatives' do
384
+ expect(green.participant_count).to eq(2)
385
+ expect(green.completed_count).to eq(0)
386
+ end
387
+ end
388
+ end
355
389
  end
356
390
 
357
391
  describe 'alternatives passed as non-strings' do
@@ -448,5 +482,4 @@ describe Split::Experiment do
448
482
  expect(experiment.calc_winning_alternatives).to be nil
449
483
  end
450
484
  end
451
-
452
485
  end
@@ -197,24 +197,51 @@ describe Split::Helper do
197
197
  expect(button_size_alt.participant_count).to eq(1)
198
198
  end
199
199
 
200
- it "should let a user participate in many experiment with one non-'control' alternative with allow_multiple_experiments = 'control'" do
201
- Split.configure do |config|
202
- config.allow_multiple_experiments = 'control'
203
- end
204
- groups = []
205
- (0..100).each do |n|
206
- alt = ab_test("test#{n}".to_sym, {'control' => (100 - n)}, {'test#{n}-alt' => n})
207
- groups << alt
200
+ context "with allow_multiple_experiments = 'control'" do
201
+ it "should let a user participate in many experiment with one non-'control' alternative" do
202
+ Split.configure do |config|
203
+ config.allow_multiple_experiments = 'control'
204
+ end
205
+ groups = 100.times.map do |n|
206
+ ab_test("test#{n}".to_sym, {'control' => (100 - n)}, {"test#{n}-alt" => n})
207
+ end
208
+
209
+ experiments = ab_user.active_experiments
210
+ expect(experiments.size).to be > 1
211
+
212
+ count_control = experiments.values.count { |g| g == 'control' }
213
+ expect(count_control).to eq(experiments.size - 1)
214
+
215
+ count_alts = groups.count { |g| g != 'control' }
216
+ expect(count_alts).to eq(1)
208
217
  end
209
218
 
210
- experiments = ab_user.active_experiments
211
- expect(experiments.size).to be > 1
219
+ context "when user already has experiment" do
220
+ let(:mock_user){ Split::User.new(self, {'test_0' => 'test-alt'}) }
221
+ before{
222
+ Split.configure do |config|
223
+ config.allow_multiple_experiments = 'control'
224
+ end
225
+ Split::ExperimentCatalog.find_or_initialize('test_0', 'control', 'test-alt').save
226
+ Split::ExperimentCatalog.find_or_initialize('test_1', 'control', 'test-alt').save
227
+ }
212
228
 
213
- count_control = experiments.values.count { |g| g == 'control' }
214
- expect(count_control).to eq(experiments.size - 1)
229
+ it "should restore previously selected alternative" do
230
+ expect(ab_user.active_experiments.size).to eq 1
231
+ expect(ab_test(:test_0, {'control' => 100}, {"test-alt" => 1})).to eq 'test-alt'
232
+ expect(ab_test(:test_0, {'control' => 1}, {"test-alt" => 100})).to eq 'test-alt'
233
+ end
234
+
235
+ it "lets override existing choice" do
236
+ pending "this requires user store reset on first call not depending on whelther it is current trial"
237
+ @params = { 'ab_test' => { 'test_1' => 'test-alt' } }
238
+
239
+ expect(ab_test(:test_0, {'control' => 0}, {"test-alt" => 100})).to eq 'control'
240
+ expect(ab_test(:test_1, {'control' => 100}, {"test-alt" => 1})).to eq 'test-alt'
241
+ end
242
+
243
+ end
215
244
 
216
- count_alts = groups.count { |g| g != 'control' }
217
- expect(count_alts).to eq(1)
218
245
  end
219
246
 
220
247
  it "should not over-write a finished key when an experiment is on a later version" do
@@ -642,6 +669,22 @@ describe Split::Helper do
642
669
 
643
670
  it_behaves_like "a disabled test"
644
671
  end
672
+
673
+ context "when ignored other address" do
674
+ before do
675
+ @request = OpenStruct.new(:ip => '1.1.1.1')
676
+ Split.configure do |c|
677
+ c.ignore_ip_addresses << '81.19.48.130'
678
+ end
679
+ end
680
+
681
+ it "works as usual" do
682
+ alternative_name = ab_test('link_color', 'red', 'blue')
683
+ expect{
684
+ ab_finished('link_color')
685
+ }.to change(Split::Alternative.new(alternative_name, 'link_color'), :completed_count).by(1)
686
+ end
687
+ end
645
688
  end
646
689
 
647
690
  describe 'versioned experiments' do
@@ -774,7 +817,7 @@ describe Split::Helper do
774
817
  end
775
818
  end
776
819
 
777
- expect(Split.configuration.db_failover_on_db_error).to receive(:call)
820
+ expect(Split.configuration.db_failover_on_db_error).to receive(:call).and_call_original
778
821
  ab_test('link_color', 'blue', 'red')
779
822
  end
780
823
 
@@ -833,7 +876,7 @@ describe Split::Helper do
833
876
  end
834
877
  end
835
878
 
836
- expect(Split.configuration.db_failover_on_db_error).to receive(:call)
879
+ expect(Split.configuration.db_failover_on_db_error).to receive(:call).and_call_original
837
880
  ab_finished('link_color')
838
881
  end
839
882
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe Split::Persistence::DualAdapter do
5
+
6
+ let(:context){ "some context" }
7
+
8
+ let(:just_adapter){ Class.new }
9
+ let(:selected_adapter_instance){ double }
10
+ let(:selected_adapter){
11
+ c = Class.new
12
+ expect(c).to receive(:new){ selected_adapter_instance }
13
+ c
14
+ }
15
+ let(:not_selected_adapter){
16
+ c = Class.new
17
+ expect(c).not_to receive(:new)
18
+ c
19
+ }
20
+
21
+ shared_examples_for "forwarding calls" do
22
+ it "#[]=" do
23
+ expect(selected_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
24
+ expect_any_instance_of(not_selected_adapter).not_to receive(:[]=)
25
+ subject["my_key"] = "my_value"
26
+ end
27
+
28
+ it "#[]" do
29
+ expect(selected_adapter_instance).to receive(:[]).with('my_key'){'my_value'}
30
+ expect_any_instance_of(not_selected_adapter).not_to receive(:[])
31
+ expect(subject["my_key"]).to eq('my_value')
32
+ end
33
+
34
+ it "#delete" do
35
+ expect(selected_adapter_instance).to receive(:delete).with('my_key'){'my_value'}
36
+ expect_any_instance_of(not_selected_adapter).not_to receive(:delete)
37
+ expect(subject.delete("my_key")).to eq('my_value')
38
+ end
39
+
40
+ it "#keys" do
41
+ expect(selected_adapter_instance).to receive(:keys){'my_value'}
42
+ expect_any_instance_of(not_selected_adapter).not_to receive(:keys)
43
+ expect(subject.keys).to eq('my_value')
44
+ end
45
+ end
46
+
47
+ context "when logged in" do
48
+ subject {
49
+ described_class.with_config(
50
+ logged_in: -> (context) { true },
51
+ logged_in_adapter: selected_adapter,
52
+ logged_out_adapter: not_selected_adapter
53
+ ).new(context)
54
+ }
55
+
56
+ it_should_behave_like "forwarding calls"
57
+ end
58
+
59
+ context "when not logged in" do
60
+ subject {
61
+ described_class.with_config(
62
+ logged_in: -> (context) { false },
63
+ logged_in_adapter: not_selected_adapter,
64
+ logged_out_adapter: selected_adapter
65
+ ).new(context)
66
+ }
67
+
68
+ it_should_behave_like "forwarding calls"
69
+ end
70
+
71
+ describe "when errors in config" do
72
+ before{
73
+ described_class.config.clear
74
+ }
75
+ let(:some_proc){ ->{} }
76
+ it "when no logged in adapter" do
77
+ expect{
78
+ described_class.with_config(
79
+ logged_in: some_proc,
80
+ logged_out_adapter: just_adapter
81
+ ).new(context)
82
+ }.to raise_error(StandardError, /:logged_in_adapter/)
83
+ end
84
+ it "when no logged out adapter" do
85
+ expect{
86
+ described_class.with_config(
87
+ logged_in: some_proc,
88
+ logged_in_adapter: just_adapter
89
+ ).new(context)
90
+ }.to raise_error(StandardError, /:logged_out_adapter/)
91
+ end
92
+ it "when no logged in detector" do
93
+ expect{
94
+ described_class.with_config(
95
+ logged_in_adapter: just_adapter,
96
+ logged_out_adapter: just_adapter
97
+ ).new(context)
98
+ }.to raise_error(StandardError, /:logged_in$/)
99
+ end
100
+ end
101
+
102
+ end