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/spec/helper_spec.rb
CHANGED
|
@@ -183,8 +183,7 @@ describe Split::Helper do
|
|
|
183
183
|
ab_test('link_color', {'blue' => 0.01}, 'red' => 0.2)
|
|
184
184
|
experiment = Split::ExperimentCatalog.find('link_color')
|
|
185
185
|
expect(experiment.alternatives.map(&:name)).to eq(['blue', 'red'])
|
|
186
|
-
|
|
187
|
-
# expect(experiment.alternatives.collect{|a| a.weight}).to eq([0.01, 0.2])
|
|
186
|
+
expect(experiment.alternatives.collect{|a| a.weight}).to match_array([0.01, 0.2])
|
|
188
187
|
end
|
|
189
188
|
|
|
190
189
|
it "should only let a user participate in one experiment at a time" do
|
|
@@ -297,98 +296,126 @@ describe Split::Helper do
|
|
|
297
296
|
end
|
|
298
297
|
|
|
299
298
|
describe 'ab_finished' do
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
299
|
+
context 'for an experiment that the user participates in' do
|
|
300
|
+
before(:each) do
|
|
301
|
+
@experiment_name = 'link_color'
|
|
302
|
+
@alternatives = ['blue', 'red']
|
|
303
|
+
@experiment = Split::ExperimentCatalog.find_or_create(@experiment_name, *@alternatives)
|
|
304
|
+
@alternative_name = ab_test(@experiment_name, *@alternatives)
|
|
305
|
+
@previous_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
|
|
306
|
+
end
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
it 'should increment the counter for the completed alternative' do
|
|
309
|
+
ab_finished(@experiment_name)
|
|
310
|
+
new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
|
|
311
|
+
expect(new_completion_count).to eq(@previous_completion_count + 1)
|
|
312
|
+
end
|
|
313
313
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
it "should set experiment's finished key if reset is false" do
|
|
315
|
+
ab_finished(@experiment_name, {:reset => false})
|
|
316
|
+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
317
|
+
expect(ab_user[@experiment.finished_key]).to eq(true)
|
|
318
|
+
end
|
|
319
319
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
320
|
+
it 'should not increment the counter if reset is false and the experiment has been already finished' do
|
|
321
|
+
2.times { ab_finished(@experiment_name, {:reset => false}) }
|
|
322
|
+
new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
|
|
323
|
+
expect(new_completion_count).to eq(@previous_completion_count + 1)
|
|
324
|
+
end
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
it 'should not increment the counter for an ended experiment' do
|
|
327
|
+
e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
|
|
328
|
+
e.winner = 'small'
|
|
329
|
+
a = ab_test('button_size', 'small', 'big')
|
|
330
|
+
expect(a).to eq('small')
|
|
331
|
+
expect(lambda {
|
|
332
|
+
ab_finished('button_size')
|
|
333
|
+
}).not_to change { Split::Alternative.new(a, 'button_size').completed_count }
|
|
334
|
+
end
|
|
328
335
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
ab_finished('button_size')
|
|
335
|
-
}).not_to change { Split::Alternative.new('small', 'button_size').completed_count }
|
|
336
|
-
end
|
|
336
|
+
it "should clear out the user's participation from their session" do
|
|
337
|
+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
338
|
+
ab_finished(@experiment_name)
|
|
339
|
+
expect(ab_user.keys).to be_empty
|
|
340
|
+
end
|
|
337
341
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
ab_finished('button_size')
|
|
345
|
-
}).not_to change { Split::Alternative.new(a, 'button_size').completed_count }
|
|
346
|
-
end
|
|
342
|
+
it "should not clear out the users session if reset is false" do
|
|
343
|
+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
344
|
+
ab_finished(@experiment_name, {:reset => false})
|
|
345
|
+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
346
|
+
expect(ab_user[@experiment.finished_key]).to eq(true)
|
|
347
|
+
end
|
|
347
348
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
it "should reset the users session when experiment is not versioned" do
|
|
350
|
+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
351
|
+
ab_finished(@experiment_name)
|
|
352
|
+
expect(ab_user.keys).to be_empty
|
|
353
|
+
end
|
|
353
354
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
358
|
-
expect(ab_user[@experiment.finished_key]).to eq(true)
|
|
359
|
-
end
|
|
355
|
+
it "should reset the users session when experiment is versioned" do
|
|
356
|
+
@experiment.increment_version
|
|
357
|
+
@alternative_name = ab_test(@experiment_name, *@alternatives)
|
|
360
358
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
end
|
|
359
|
+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
|
|
360
|
+
ab_finished(@experiment_name)
|
|
361
|
+
expect(ab_user.keys).to be_empty
|
|
362
|
+
end
|
|
366
363
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
context "when on_trial_complete is set" do
|
|
365
|
+
before { Split.configuration.on_trial_complete = :some_method }
|
|
366
|
+
it "should call the method" do
|
|
367
|
+
expect(self).to receive(:some_method)
|
|
368
|
+
ab_finished(@experiment_name)
|
|
369
|
+
end
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
it "should not call the method without alternative" do
|
|
372
|
+
ab_user[@experiment.key] = nil
|
|
373
|
+
expect(self).not_to receive(:some_method)
|
|
374
|
+
ab_finished(@experiment_name)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
374
377
|
end
|
|
375
378
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
+
context 'for an experiment that the user is excluded from' do
|
|
380
|
+
before do
|
|
381
|
+
alternative = ab_test('link_color', 'blue', 'red')
|
|
382
|
+
expect(Split::Alternative.new(alternative, 'link_color').participant_count).to eq(1)
|
|
383
|
+
alternative = ab_test('button_size', 'small', 'big')
|
|
384
|
+
expect(Split::Alternative.new(alternative, 'button_size').participant_count).to eq(0)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it 'should not increment the completed counter' do
|
|
388
|
+
# So, user should be participating in the link_color experiment and
|
|
389
|
+
# receive the control for button_size. As the user is not participating in
|
|
390
|
+
# the button size experiment, finishing it should not increase the
|
|
391
|
+
# completion count for that alternative.
|
|
392
|
+
expect(lambda {
|
|
393
|
+
ab_finished('button_size')
|
|
394
|
+
}).not_to change { Split::Alternative.new('small', 'button_size').completed_count }
|
|
395
|
+
end
|
|
379
396
|
end
|
|
380
397
|
|
|
381
|
-
context
|
|
382
|
-
before
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
398
|
+
context 'for an experiment that the user does not participate in' do
|
|
399
|
+
before do
|
|
400
|
+
Split::ExperimentCatalog.find_or_create(:not_started_experiment, 'control', 'alt')
|
|
401
|
+
end
|
|
402
|
+
it 'should not raise an exception' do
|
|
403
|
+
expect { ab_finished(:not_started_experiment) }.not_to raise_exception
|
|
386
404
|
end
|
|
387
405
|
|
|
388
|
-
it
|
|
389
|
-
ab_user[
|
|
390
|
-
|
|
391
|
-
|
|
406
|
+
it 'should not change the user state when reset is false' do
|
|
407
|
+
expect { ab_finished(:not_started_experiment, reset: false) }.not_to change { ab_user.keys}.from([])
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it 'should not change the user state when reset is true' do
|
|
411
|
+
expect(self).not_to receive(:reset!)
|
|
412
|
+
ab_finished(:not_started_experiment)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it 'should not increment the completed counter' do
|
|
416
|
+
ab_finished(:not_started_experiment)
|
|
417
|
+
expect(Split::Alternative.new('control', :not_started_experiment).completed_count).to eq(0)
|
|
418
|
+
expect(Split::Alternative.new('alt', :not_started_experiment).completed_count).to eq(0)
|
|
392
419
|
end
|
|
393
420
|
end
|
|
394
421
|
end
|
|
@@ -528,6 +555,17 @@ describe Split::Helper do
|
|
|
528
555
|
expect(active_experiments.first[0]).to eq "link_color"
|
|
529
556
|
end
|
|
530
557
|
|
|
558
|
+
it 'should show versioned tests properly' do
|
|
559
|
+
10.times { experiment.reset }
|
|
560
|
+
|
|
561
|
+
alternative = ab_test(experiment.name, 'blue', 'red')
|
|
562
|
+
ab_finished(experiment.name, reset: false)
|
|
563
|
+
|
|
564
|
+
expect(experiment.version).to eq(10)
|
|
565
|
+
expect(active_experiments.count).to eq 1
|
|
566
|
+
expect(active_experiments).to eq({'link_color' => alternative })
|
|
567
|
+
end
|
|
568
|
+
|
|
531
569
|
it 'should show multiple tests' do
|
|
532
570
|
Split.configure do |config|
|
|
533
571
|
config.allow_multiple_experiments = true
|
|
@@ -545,7 +583,7 @@ describe Split::Helper do
|
|
|
545
583
|
end
|
|
546
584
|
e = Split::ExperimentCatalog.find_or_create('def', '4', '5', '6')
|
|
547
585
|
e.winner = '4'
|
|
548
|
-
|
|
586
|
+
ab_test('def', '4', '5', '6')
|
|
549
587
|
another_alternative = ab_test('ghi', '7', '8', '9')
|
|
550
588
|
expect(active_experiments.count).to eq 1
|
|
551
589
|
expect(active_experiments.first[0]).to eq "ghi"
|
|
@@ -564,6 +602,11 @@ describe Split::Helper do
|
|
|
564
602
|
expect(alternative).to eq experiment.control.name
|
|
565
603
|
end
|
|
566
604
|
|
|
605
|
+
it 'should not create a experiment' do
|
|
606
|
+
ab_test('link_color', 'blue', 'red')
|
|
607
|
+
expect(Split::Experiment.new('link_color')).to be_a_new_record
|
|
608
|
+
end
|
|
609
|
+
|
|
567
610
|
it "should not increment the participation count" do
|
|
568
611
|
|
|
569
612
|
previous_red_count = Split::Alternative.new('red', 'link_color').participant_count
|
|
@@ -1016,8 +1059,8 @@ describe Split::Helper do
|
|
|
1016
1059
|
|
|
1017
1060
|
it 'should handle multiple experiments correctly' do
|
|
1018
1061
|
experiment2 = Split::ExperimentCatalog.find_or_create('link_color2', 'blue', 'red')
|
|
1019
|
-
|
|
1020
|
-
|
|
1062
|
+
ab_test('link_color', 'blue', 'red')
|
|
1063
|
+
ab_test('link_color2', 'blue', 'red')
|
|
1021
1064
|
ab_finished('link_color2')
|
|
1022
1065
|
|
|
1023
1066
|
experiment2.alternatives.each do |alt|
|
|
@@ -1,102 +1,194 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
3
4
|
|
|
4
5
|
describe Split::Persistence::DualAdapter do
|
|
6
|
+
let(:context) { 'some context' }
|
|
5
7
|
|
|
6
|
-
let(:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let(:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
8
|
+
let(:logged_in_adapter_instance) { double }
|
|
9
|
+
let(:logged_in_adapter) do
|
|
10
|
+
Class.new.tap { |c| allow(c).to receive(:new) { logged_in_adapter_instance } }
|
|
11
|
+
end
|
|
12
|
+
let(:logged_out_adapter_instance) { double }
|
|
13
|
+
let(:logged_out_adapter) do
|
|
14
|
+
Class.new.tap { |c| allow(c).to receive(:new) { logged_out_adapter_instance } }
|
|
15
|
+
end
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
context 'when fallback_to_logged_out_adapter is false' do
|
|
18
|
+
context 'when logged in' do
|
|
19
|
+
subject do
|
|
20
|
+
described_class.with_config(
|
|
21
|
+
logged_in: lambda { |context| true },
|
|
22
|
+
logged_in_adapter: logged_in_adapter,
|
|
23
|
+
logged_out_adapter: logged_out_adapter,
|
|
24
|
+
fallback_to_logged_out_adapter: false
|
|
25
|
+
).new(context)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it '#[]=' do
|
|
29
|
+
expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
|
|
30
|
+
expect_any_instance_of(logged_out_adapter).not_to receive(:[]=)
|
|
31
|
+
subject['my_key'] = 'my_value'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it '#[]' do
|
|
35
|
+
expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
|
|
36
|
+
expect_any_instance_of(logged_out_adapter).not_to receive(:[])
|
|
37
|
+
expect(subject['my_key']).to eq('my_value')
|
|
38
|
+
end
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
it '#delete' do
|
|
41
|
+
expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
|
|
42
|
+
expect_any_instance_of(logged_out_adapter).not_to receive(:delete)
|
|
43
|
+
expect(subject.delete('my_key')).to eq('my_value')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it '#keys' do
|
|
47
|
+
expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
|
|
48
|
+
expect_any_instance_of(logged_out_adapter).not_to receive(:keys)
|
|
49
|
+
expect(subject.keys).to eq(['my_value'])
|
|
50
|
+
end
|
|
38
51
|
end
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
context 'when logged out' do
|
|
54
|
+
subject do
|
|
55
|
+
described_class.with_config(
|
|
56
|
+
logged_in: lambda { |context| false },
|
|
57
|
+
logged_in_adapter: logged_in_adapter,
|
|
58
|
+
logged_out_adapter: logged_out_adapter,
|
|
59
|
+
fallback_to_logged_out_adapter: false
|
|
60
|
+
).new(context)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it '#[]=' do
|
|
64
|
+
expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
|
|
65
|
+
expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
|
|
66
|
+
subject['my_key'] = 'my_value'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it '#[]' do
|
|
70
|
+
expect_any_instance_of(logged_in_adapter).not_to receive(:[])
|
|
71
|
+
expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
|
|
72
|
+
expect(subject['my_key']).to eq('my_value')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it '#delete' do
|
|
76
|
+
expect_any_instance_of(logged_in_adapter).not_to receive(:delete)
|
|
77
|
+
expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
|
|
78
|
+
expect(subject.delete('my_key')).to eq('my_value')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it '#keys' do
|
|
82
|
+
expect_any_instance_of(logged_in_adapter).not_to receive(:keys)
|
|
83
|
+
expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
|
|
84
|
+
expect(subject.keys).to eq(['my_value', 'my_value2'])
|
|
85
|
+
end
|
|
44
86
|
end
|
|
45
87
|
end
|
|
46
88
|
|
|
47
|
-
context
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
context 'when fallback_to_logged_out_adapter is true' do
|
|
90
|
+
context 'when logged in' do
|
|
91
|
+
subject do
|
|
92
|
+
described_class.with_config(
|
|
93
|
+
logged_in: lambda { |context| true },
|
|
94
|
+
logged_in_adapter: logged_in_adapter,
|
|
95
|
+
logged_out_adapter: logged_out_adapter,
|
|
96
|
+
fallback_to_logged_out_adapter: true
|
|
53
97
|
).new(context)
|
|
54
|
-
|
|
98
|
+
end
|
|
55
99
|
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
it '#[]=' do
|
|
101
|
+
expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
|
|
102
|
+
expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
|
|
103
|
+
expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
|
|
104
|
+
subject['my_key'] = 'my_value'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it '#[]' do
|
|
108
|
+
expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
|
|
109
|
+
expect_any_instance_of(logged_out_adapter).not_to receive(:[])
|
|
110
|
+
expect(subject['my_key']).to eq('my_value')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it '#delete' do
|
|
114
|
+
expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
|
|
115
|
+
expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
|
|
116
|
+
expect(subject.delete('my_key')).to eq('my_value')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it '#keys' do
|
|
120
|
+
expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
|
|
121
|
+
expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
|
|
122
|
+
expect(subject.keys).to eq(['my_value', 'my_value2'])
|
|
123
|
+
end
|
|
124
|
+
end
|
|
58
125
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
126
|
+
context 'when logged out' do
|
|
127
|
+
subject do
|
|
128
|
+
described_class.with_config(
|
|
129
|
+
logged_in: lambda { |context| false },
|
|
130
|
+
logged_in_adapter: logged_in_adapter,
|
|
131
|
+
logged_out_adapter: logged_out_adapter,
|
|
132
|
+
fallback_to_logged_out_adapter: true
|
|
65
133
|
).new(context)
|
|
66
|
-
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it '#[]=' do
|
|
137
|
+
expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
|
|
138
|
+
expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
|
|
139
|
+
expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
|
|
140
|
+
subject['my_key'] = 'my_value'
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it '#[]' do
|
|
144
|
+
expect_any_instance_of(logged_in_adapter).not_to receive(:[])
|
|
145
|
+
expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
|
|
146
|
+
expect(subject['my_key']).to eq('my_value')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it '#delete' do
|
|
150
|
+
expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
|
|
151
|
+
expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
|
|
152
|
+
expect(subject.delete('my_key')).to eq('my_value')
|
|
153
|
+
end
|
|
67
154
|
|
|
68
|
-
|
|
155
|
+
it '#keys' do
|
|
156
|
+
expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
|
|
157
|
+
expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
|
|
158
|
+
expect(subject.keys).to eq(['my_value', 'my_value2'])
|
|
159
|
+
end
|
|
160
|
+
end
|
|
69
161
|
end
|
|
70
162
|
|
|
71
|
-
describe
|
|
72
|
-
before{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
it "when no logged in adapter" do
|
|
163
|
+
describe 'when errors in config' do
|
|
164
|
+
before { described_class.config.clear }
|
|
165
|
+
let(:some_proc) { ->{} }
|
|
166
|
+
|
|
167
|
+
it 'when no logged in adapter' do
|
|
77
168
|
expect{
|
|
78
169
|
described_class.with_config(
|
|
79
170
|
logged_in: some_proc,
|
|
80
|
-
logged_out_adapter:
|
|
81
|
-
|
|
171
|
+
logged_out_adapter: logged_out_adapter
|
|
172
|
+
).new(context)
|
|
82
173
|
}.to raise_error(StandardError, /:logged_in_adapter/)
|
|
83
174
|
end
|
|
84
|
-
|
|
175
|
+
|
|
176
|
+
it 'when no logged out adapter' do
|
|
85
177
|
expect{
|
|
86
178
|
described_class.with_config(
|
|
87
179
|
logged_in: some_proc,
|
|
88
|
-
logged_in_adapter:
|
|
89
|
-
|
|
180
|
+
logged_in_adapter: logged_in_adapter
|
|
181
|
+
).new(context)
|
|
90
182
|
}.to raise_error(StandardError, /:logged_out_adapter/)
|
|
91
183
|
end
|
|
92
|
-
|
|
184
|
+
|
|
185
|
+
it 'when no logged in detector' do
|
|
93
186
|
expect{
|
|
94
187
|
described_class.with_config(
|
|
95
|
-
logged_in_adapter:
|
|
96
|
-
logged_out_adapter:
|
|
97
|
-
|
|
188
|
+
logged_in_adapter: logged_in_adapter,
|
|
189
|
+
logged_out_adapter: logged_out_adapter
|
|
190
|
+
).new(context)
|
|
98
191
|
}.to raise_error(StandardError, /:logged_in$/)
|
|
99
192
|
end
|
|
100
193
|
end
|
|
101
|
-
|
|
102
194
|
end
|
data/spec/trial_spec.rb
CHANGED
|
@@ -176,6 +176,14 @@ describe Split::Trial do
|
|
|
176
176
|
|
|
177
177
|
expect_alternative(trial, 'basket')
|
|
178
178
|
end
|
|
179
|
+
|
|
180
|
+
context "when alternative is not found" do
|
|
181
|
+
it "falls back on next_alternative" do
|
|
182
|
+
user[experiment.key] = 'notfound'
|
|
183
|
+
expect(experiment).to receive(:next_alternative).and_call_original
|
|
184
|
+
expect_alternative(trial, alternatives)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
179
187
|
end
|
|
180
188
|
|
|
181
189
|
context "when user is a new participant" do
|
|
@@ -275,5 +283,17 @@ describe Split::Trial do
|
|
|
275
283
|
trial.choose!
|
|
276
284
|
end
|
|
277
285
|
end
|
|
286
|
+
|
|
287
|
+
context 'when experiment has winner' do
|
|
288
|
+
let(:trial) do
|
|
289
|
+
experiment.winner = 'cart'
|
|
290
|
+
Split::Trial.new(:user => user, :experiment => experiment)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'does not store' do
|
|
294
|
+
expect(user).to_not receive("[]=")
|
|
295
|
+
trial.choose!
|
|
296
|
+
end
|
|
297
|
+
end
|
|
278
298
|
end
|
|
279
299
|
end
|
data/spec/user_spec.rb
CHANGED
|
@@ -59,6 +59,17 @@ describe Split::User do
|
|
|
59
59
|
expect(@subject.keys).to include("link_color:finished")
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
context 'when already cleaned up' do
|
|
64
|
+
before do
|
|
65
|
+
@subject.cleanup_old_experiments!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'does not clean up again' do
|
|
69
|
+
expect(@subject).to_not receive(:keys_without_finished)
|
|
70
|
+
@subject.cleanup_old_experiments!
|
|
71
|
+
end
|
|
72
|
+
end
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
context "instantiated with custom adapter" do
|
data/split.gemspec
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
$:.push File.expand_path("../lib", __FILE__)
|
|
3
4
|
require "split/version"
|
|
4
5
|
|
|
@@ -24,8 +25,6 @@ Gem::Specification.new do |s|
|
|
|
24
25
|
s.required_ruby_version = '>= 1.9.3'
|
|
25
26
|
s.required_rubygems_version = '>= 2.0.0'
|
|
26
27
|
|
|
27
|
-
s.rubyforge_project = "split"
|
|
28
|
-
|
|
29
28
|
s.files = `git ls-files`.split("\n")
|
|
30
29
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
31
30
|
s.require_paths = ["lib"]
|
|
@@ -34,11 +33,12 @@ Gem::Specification.new do |s|
|
|
|
34
33
|
s.add_dependency 'sinatra', '>= 1.2.6'
|
|
35
34
|
s.add_dependency 'simple-random', '>= 0.9.3'
|
|
36
35
|
|
|
37
|
-
s.add_development_dependency 'bundler', '
|
|
36
|
+
s.add_development_dependency 'bundler', '>= 1.17'
|
|
38
37
|
s.add_development_dependency 'simplecov', '~> 0.15'
|
|
39
38
|
s.add_development_dependency 'rack-test', '~> 0.6'
|
|
40
39
|
s.add_development_dependency 'rake', '~> 12'
|
|
41
40
|
s.add_development_dependency 'rspec', '~> 3.7'
|
|
42
41
|
s.add_development_dependency 'pry', '~> 0.10'
|
|
43
42
|
s.add_development_dependency 'fakeredis', '~> 0.7'
|
|
43
|
+
s.add_development_dependency 'rails', '>= 4.2'
|
|
44
44
|
end
|