split 2.1.0 → 2.2.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.
@@ -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