split 3.2.0 → 4.0.5

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.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.github/dependabot.yml +7 -0
  6. data/.github/workflows/ci.yml +63 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +67 -1043
  9. data/CHANGELOG.md +174 -0
  10. data/CODE_OF_CONDUCT.md +3 -3
  11. data/CONTRIBUTING.md +1 -1
  12. data/Gemfile +6 -1
  13. data/README.md +79 -33
  14. data/Rakefile +6 -5
  15. data/lib/split/algorithms/block_randomization.rb +7 -6
  16. data/lib/split/algorithms/weighted_sample.rb +2 -1
  17. data/lib/split/algorithms/whiplash.rb +17 -18
  18. data/lib/split/algorithms.rb +14 -0
  19. data/lib/split/alternative.rb +25 -25
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +6 -5
  22. data/lib/split/configuration.rb +94 -91
  23. data/lib/split/dashboard/helpers.rb +9 -9
  24. data/lib/split/dashboard/pagination_helpers.rb +86 -0
  25. data/lib/split/dashboard/paginator.rb +17 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +19 -2
  28. data/lib/split/dashboard/views/_controls.erb +13 -0
  29. data/lib/split/dashboard/views/_experiment.erb +2 -1
  30. data/lib/split/dashboard/views/index.erb +24 -5
  31. data/lib/split/dashboard/views/layout.erb +1 -1
  32. data/lib/split/dashboard.rb +47 -20
  33. data/lib/split/encapsulated_helper.rb +15 -8
  34. data/lib/split/engine.rb +7 -4
  35. data/lib/split/exceptions.rb +1 -0
  36. data/lib/split/experiment.rb +160 -122
  37. data/lib/split/experiment_catalog.rb +7 -8
  38. data/lib/split/extensions/string.rb +2 -1
  39. data/lib/split/goals_collection.rb +10 -10
  40. data/lib/split/helper.rb +56 -24
  41. data/lib/split/metric.rb +6 -6
  42. data/lib/split/persistence/cookie_adapter.rb +52 -15
  43. data/lib/split/persistence/dual_adapter.rb +53 -12
  44. data/lib/split/persistence/redis_adapter.rb +8 -4
  45. data/lib/split/persistence/session_adapter.rb +1 -2
  46. data/lib/split/persistence.rb +8 -6
  47. data/lib/split/redis_interface.rb +16 -31
  48. data/lib/split/trial.rb +48 -41
  49. data/lib/split/user.rb +30 -15
  50. data/lib/split/version.rb +2 -4
  51. data/lib/split/zscore.rb +2 -3
  52. data/lib/split.rb +39 -25
  53. data/spec/algorithms/block_randomization_spec.rb +6 -5
  54. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  55. data/spec/algorithms/whiplash_spec.rb +4 -5
  56. data/spec/alternative_spec.rb +35 -36
  57. data/spec/cache_spec.rb +84 -0
  58. data/spec/combined_experiments_helper_spec.rb +18 -17
  59. data/spec/configuration_spec.rb +41 -45
  60. data/spec/dashboard/pagination_helpers_spec.rb +202 -0
  61. data/spec/dashboard/paginator_spec.rb +38 -0
  62. data/spec/dashboard_helpers_spec.rb +19 -18
  63. data/spec/dashboard_spec.rb +153 -48
  64. data/spec/encapsulated_helper_spec.rb +47 -23
  65. data/spec/experiment_catalog_spec.rb +14 -13
  66. data/spec/experiment_spec.rb +224 -111
  67. data/spec/goals_collection_spec.rb +18 -16
  68. data/spec/helper_spec.rb +539 -419
  69. data/spec/metric_spec.rb +14 -14
  70. data/spec/persistence/cookie_adapter_spec.rb +105 -27
  71. data/spec/persistence/dual_adapter_spec.rb +158 -66
  72. data/spec/persistence/redis_adapter_spec.rb +35 -27
  73. data/spec/persistence/session_adapter_spec.rb +2 -3
  74. data/spec/persistence_spec.rb +1 -2
  75. data/spec/redis_interface_spec.rb +25 -82
  76. data/spec/spec_helper.rb +38 -24
  77. data/spec/split_spec.rb +18 -18
  78. data/spec/support/cookies_mock.rb +1 -2
  79. data/spec/trial_spec.rb +117 -70
  80. data/spec/user_spec.rb +69 -27
  81. data/split.gemspec +26 -22
  82. metadata +85 -37
  83. data/.travis.yml +0 -41
  84. data/Appraisals +0 -13
  85. data/gemfiles/4.2.gemfile +0 -9
  86. data/gemfiles/5.0.gemfile +0 -10
  87. data/gemfiles/5.1.gemfile +0 -10
@@ -1,111 +1,54 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
2
4
 
3
5
  describe Split::RedisInterface do
4
- let(:list_name) { 'list_name' }
5
- let(:set_name) { 'set_name' }
6
+ let(:list_name) { "list_name" }
7
+ let(:set_name) { "set_name" }
6
8
  let(:interface) { described_class.new }
7
9
 
8
- describe '#persist_list' do
10
+ describe "#persist_list" do
9
11
  subject(:persist_list) do
10
12
  interface.persist_list(list_name, %w(a b c d))
11
13
  end
12
14
 
13
15
  specify do
14
16
  expect(persist_list).to eq %w(a b c d)
15
- expect(Split.redis.lindex(list_name, 0)).to eq 'a'
16
- expect(Split.redis.lindex(list_name, 1)).to eq 'b'
17
- expect(Split.redis.lindex(list_name, 2)).to eq 'c'
18
- expect(Split.redis.lindex(list_name, 3)).to eq 'd'
17
+ expect(Split.redis.lindex(list_name, 0)).to eq "a"
18
+ expect(Split.redis.lindex(list_name, 1)).to eq "b"
19
+ expect(Split.redis.lindex(list_name, 2)).to eq "c"
20
+ expect(Split.redis.lindex(list_name, 3)).to eq "d"
19
21
  expect(Split.redis.llen(list_name)).to eq 4
20
22
  end
21
23
 
22
- context 'list is overwritten but not deleted' do
24
+ context "list is overwritten but not deleted" do
23
25
  specify do
24
26
  expect(persist_list).to eq %w(a b c d)
25
- interface.persist_list(list_name, ['z'])
26
- expect(Split.redis.lindex(list_name, 0)).to eq 'z'
27
+ interface.persist_list(list_name, ["z"])
28
+ expect(Split.redis.lindex(list_name, 0)).to eq "z"
27
29
  expect(Split.redis.llen(list_name)).to eq 1
28
30
  end
29
31
  end
30
32
  end
31
33
 
32
- describe '#add_to_list' do
33
- subject(:add_to_list) do
34
- interface.add_to_list(list_name, 'y')
35
- interface.add_to_list(list_name, 'z')
36
- end
37
-
38
- specify do
39
- add_to_list
40
- expect(Split.redis.lindex(list_name, 0)).to eq 'y'
41
- expect(Split.redis.lindex(list_name, 1)).to eq 'z'
42
- expect(Split.redis.llen(list_name)).to eq 2
43
- end
44
- end
45
-
46
- describe '#set_list_index' do
47
- subject(:set_list_index) do
48
- interface.add_to_list(list_name, 'y')
49
- interface.add_to_list(list_name, 'z')
50
- interface.set_list_index(list_name, 0, 'a')
51
- end
52
-
53
- specify do
54
- set_list_index
55
- expect(Split.redis.lindex(list_name, 0)).to eq 'a'
56
- expect(Split.redis.lindex(list_name, 1)).to eq 'z'
57
- expect(Split.redis.llen(list_name)).to eq 2
58
- end
59
- end
60
-
61
- describe '#list_length' do
62
- subject(:list_length) do
63
- interface.add_to_list(list_name, 'y')
64
- interface.add_to_list(list_name, 'z')
65
- interface.list_length(list_name)
66
- end
67
-
68
- specify do
69
- expect(list_length).to eq 2
70
- end
71
- end
72
-
73
- describe '#remove_last_item_from_list' do
74
- subject(:remove_last_item_from_list) do
75
- interface.add_to_list(list_name, 'y')
76
- interface.add_to_list(list_name, 'z')
77
- interface.remove_last_item_from_list(list_name)
78
- end
79
-
80
- specify do
81
- remove_last_item_from_list
82
- expect(Split.redis.lindex(list_name, 0)).to eq 'y'
83
- expect(Split.redis.llen(list_name)).to eq 1
84
- end
85
- end
86
-
87
- describe '#make_list_length' do
88
- subject(:make_list_length) do
89
- interface.add_to_list(list_name, 'y')
90
- interface.add_to_list(list_name, 'z')
91
- interface.make_list_length(list_name, 1)
34
+ describe "#add_to_set" do
35
+ subject(:add_to_set) do
36
+ interface.add_to_set(set_name, "something")
92
37
  end
93
38
 
94
39
  specify do
95
- make_list_length
96
- expect(Split.redis.lindex(list_name, 0)).to eq 'y'
97
- expect(Split.redis.llen(list_name)).to eq 1
40
+ add_to_set
41
+ expect(Split.redis.sismember(set_name, "something")).to be true
98
42
  end
99
- end
100
43
 
101
- describe '#add_to_set' do
102
- subject(:add_to_set) do
103
- interface.add_to_set(set_name, 'something')
104
- end
44
+ context "when a Redis version is used that supports the 'sadd?' method" do
45
+ before { expect(Split.redis).to receive(:respond_to?).with(:sadd?).and_return(true) }
105
46
 
106
- specify do
107
- add_to_set
108
- expect(Split.redis.sismember(set_name, 'something')).to be true
47
+ it "will use this method instead of 'sadd'" do
48
+ expect(Split.redis).to receive(:sadd?).with(set_name, "something")
49
+ expect(Split.redis).not_to receive(:sadd).with(set_name, "something")
50
+ add_to_set
51
+ end
109
52
  end
110
53
  end
111
54
  end
data/spec/spec_helper.rb CHANGED
@@ -1,37 +1,38 @@
1
1
  # frozen_string_literal: true
2
- ENV['RACK_ENV'] = "test"
3
2
 
4
- require 'rubygems'
5
- require 'bundler/setup'
3
+ ENV["RACK_ENV"] = "test"
6
4
 
7
- require 'simplecov'
8
- SimpleCov.start
9
-
10
- require 'split'
11
- require 'ostruct'
12
- require 'yaml'
5
+ require "rubygems"
6
+ require "bundler/setup"
13
7
 
14
- Dir['./spec/support/*.rb'].each { |f| require f }
8
+ require "simplecov"
9
+ SimpleCov.start
15
10
 
16
- require "fakeredis"
11
+ require "split"
12
+ require "yaml"
13
+ require "pry"
17
14
 
18
- G_fakeredis = Redis.new
15
+ Dir["./spec/support/*.rb"].each { |f| require f }
19
16
 
20
17
  module GlobalSharedContext
21
18
  extend RSpec::SharedContext
22
- let(:mock_user){ Split::User.new(double(session: {})) }
19
+ let(:mock_user) { Split::User.new(double(session: {})) }
20
+
23
21
  before(:each) do
24
22
  Split.configuration = Split::Configuration.new
25
- Split.redis = G_fakeredis
26
- Split.redis.flushall
23
+ Split.redis = Redis.new
24
+ Split.redis.select(10)
25
+ Split.redis.flushdb
26
+ Split::Cache.clear
27
27
  @ab_user = mock_user
28
- params = nil
28
+ @params = nil
29
29
  end
30
30
  end
31
31
 
32
32
  RSpec.configure do |config|
33
- config.order = 'random'
33
+ config.order = "random"
34
34
  config.include GlobalSharedContext
35
+ config.raise_errors_for_deprecations!
35
36
  end
36
37
 
37
38
  def session
@@ -42,11 +43,24 @@ def params
42
43
  @params ||= {}
43
44
  end
44
45
 
45
- def request(ua = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27')
46
- @request ||= begin
47
- r = OpenStruct.new
48
- r.user_agent = ua
49
- r.ip = '192.168.1.1'
50
- r
51
- end
46
+ def request
47
+ @request ||= build_request
48
+ end
49
+
50
+ DummyRequest = Struct.new(:user_agent, :ip, :params, :cookies, :headers)
51
+
52
+ def build_request(
53
+ user_agent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27",
54
+ ip: "192.168.1.1",
55
+ params: {},
56
+ cookies: {},
57
+ headers: {}
58
+ )
59
+ r = DummyRequest.new
60
+ r.user_agent = user_agent
61
+ r.ip = ip
62
+ r.params = params
63
+ r.cookies = cookies
64
+ r.headers = headers
65
+ r
52
66
  end
data/spec/split_spec.rb CHANGED
@@ -1,42 +1,42 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
3
2
 
4
- RSpec.describe Split do
3
+ require "spec_helper"
5
4
 
5
+ RSpec.describe Split do
6
6
  around(:each) do |ex|
7
- old_env, old_redis = [ENV.delete('REDIS_URL'), Split.redis]
7
+ old_env, old_redis = [ENV.delete("REDIS_URL"), Split.redis]
8
8
  ex.run
9
- ENV['REDIS_URL'] = old_env
9
+ ENV["REDIS_URL"] = old_env
10
10
  Split.redis = old_redis
11
11
  end
12
12
 
13
- describe '#redis=' do
14
- it 'accepts a url string' do
15
- Split.redis = 'redis://localhost:6379'
13
+ describe "#redis=" do
14
+ it "accepts a url string" do
15
+ Split.redis = "redis://localhost:6379"
16
16
  expect(Split.redis).to be_a(Redis)
17
17
 
18
- client = Split.redis.client
19
- expect(client.host).to eq("localhost")
20
- expect(client.port).to eq(6379)
18
+ client = Split.redis.connection
19
+ expect(client[:host]).to eq("localhost")
20
+ expect(client[:port]).to eq(6379)
21
21
  end
22
22
 
23
- it 'accepts an options hash' do
24
- Split.redis = {host: 'localhost', port: 6379, db: 12}
23
+ it "accepts an options hash" do
24
+ Split.redis = { host: "localhost", port: 6379, db: 12 }
25
25
  expect(Split.redis).to be_a(Redis)
26
26
 
27
- client = Split.redis.client
28
- expect(client.host).to eq("localhost")
29
- expect(client.port).to eq(6379)
30
- expect(client.db).to eq(12)
27
+ client = Split.redis.connection
28
+ expect(client[:host]).to eq("localhost")
29
+ expect(client[:port]).to eq(6379)
30
+ expect(client[:db]).to eq(12)
31
31
  end
32
32
 
33
- it 'accepts a valid Redis instance' do
33
+ it "accepts a valid Redis instance" do
34
34
  other_redis = Redis.new(url: "redis://localhost:6379")
35
35
  Split.redis = other_redis
36
36
  expect(Split.redis).to eq(other_redis)
37
37
  end
38
38
 
39
- it 'raises an ArgumentError when server cannot be determined' do
39
+ it "raises an ArgumentError when server cannot be determined" do
40
40
  expect { Split.redis = Object.new }.to raise_error(ArgumentError)
41
41
  end
42
42
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
- class CookiesMock
3
2
 
3
+ class CookiesMock
4
4
  def initialize
5
5
  @cookies = {}
6
6
  end
@@ -16,5 +16,4 @@ class CookiesMock
16
16
  def delete(key)
17
17
  @cookies.delete(key)
18
18
  end
19
-
20
19
  end
data/spec/trial_spec.rb CHANGED
@@ -1,54 +1,55 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
3
- require 'split/trial'
2
+
3
+ require "spec_helper"
4
+ require "split/trial"
4
5
 
5
6
  describe Split::Trial do
6
7
  let(:user) { mock_user }
7
- let(:alternatives) { ['basket', 'cart'] }
8
+ let(:alternatives) { ["basket", "cart"] }
8
9
  let(:experiment) do
9
- Split::Experiment.new('basket_text', :alternatives => alternatives).save
10
+ Split::Experiment.new("basket_text", alternatives: alternatives).save
10
11
  end
11
12
 
12
13
  it "should be initializeable" do
13
- experiment = double('experiment')
14
- alternative = double('alternative', :kind_of? => Split::Alternative)
15
- trial = Split::Trial.new(:experiment => experiment, :alternative => alternative)
14
+ experiment = double("experiment")
15
+ alternative = double("alternative", kind_of?: Split::Alternative)
16
+ trial = Split::Trial.new(experiment: experiment, alternative: alternative)
16
17
  expect(trial.experiment).to eq(experiment)
17
18
  expect(trial.alternative).to eq(alternative)
18
19
  end
19
20
 
20
21
  describe "alternative" do
21
22
  it "should use the alternative if specified" do
22
- alternative = double('alternative', :kind_of? => Split::Alternative)
23
- trial = Split::Trial.new(:experiment => double('experiment'),
24
- :alternative => alternative, :user => user)
23
+ alternative = double("alternative", kind_of?: Split::Alternative)
24
+ trial = Split::Trial.new(experiment: double("experiment"),
25
+ alternative: alternative, user: user)
25
26
  expect(trial).not_to receive(:choose)
26
27
  expect(trial.alternative).to eq(alternative)
27
28
  end
28
29
 
29
30
  it "should load the alternative when the alternative name is set" do
30
- experiment = Split::Experiment.new('basket_text', :alternatives => ['basket', 'cart'])
31
+ experiment = Split::Experiment.new("basket_text", alternatives: ["basket", "cart"])
31
32
  experiment.save
32
33
 
33
- trial = Split::Trial.new(:experiment => experiment, :alternative => 'basket')
34
- expect(trial.alternative.name).to eq('basket')
34
+ trial = Split::Trial.new(experiment: experiment, alternative: "basket")
35
+ expect(trial.alternative.name).to eq("basket")
35
36
  end
36
37
  end
37
38
 
38
39
  describe "metadata" do
39
40
  let(:metadata) { Hash[alternatives.map { |k| [k, "Metadata for #{k}"] }] }
40
41
  let(:experiment) do
41
- Split::Experiment.new('basket_text', :alternatives => alternatives, :metadata => metadata).save
42
+ Split::Experiment.new("basket_text", alternatives: alternatives, metadata: metadata).save
42
43
  end
43
44
 
44
- it 'has metadata on each trial' do
45
- trial = Split::Trial.new(:experiment => experiment, :user => user, :metadata => metadata['cart'],
46
- :override => 'cart')
47
- expect(trial.metadata).to eq(metadata['cart'])
45
+ it "has metadata on each trial" do
46
+ trial = Split::Trial.new(experiment: experiment, user: user, metadata: metadata["cart"],
47
+ override: "cart")
48
+ expect(trial.metadata).to eq(metadata["cart"])
48
49
  end
49
50
 
50
- it 'has metadata on each trial from the experiment' do
51
- trial = Split::Trial.new(:experiment => experiment, :user => user)
51
+ it "has metadata on each trial from the experiment" do
52
+ trial = Split::Trial.new(experiment: experiment, user: user)
52
53
  trial.choose!
53
54
  expect(trial.metadata).to eq(metadata[trial.alternative.name])
54
55
  expect(trial.metadata).to match(/#{trial.alternative.name}/)
@@ -56,24 +57,24 @@ describe Split::Trial do
56
57
  end
57
58
 
58
59
  describe "#choose!" do
59
- let(:context) { double(on_trial_callback: 'test callback') }
60
+ let(:context) { double(on_trial_callback: "test callback") }
60
61
  let(:trial) do
61
- Split::Trial.new(:user => user, :experiment => experiment)
62
+ Split::Trial.new(user: user, experiment: experiment)
62
63
  end
63
64
 
64
- shared_examples_for 'a trial with callbacks' do
65
- it 'does not run if on_trial callback is not respondable' do
65
+ shared_examples_for "a trial with callbacks" do
66
+ it "does not run if on_trial callback is not respondable" do
66
67
  Split.configuration.on_trial = :foo
67
68
  allow(context).to receive(:respond_to?).with(:foo, true).and_return false
68
69
  expect(context).to_not receive(:foo)
69
70
  trial.choose! context
70
71
  end
71
- it 'runs on_trial callback' do
72
+ it "runs on_trial callback" do
72
73
  Split.configuration.on_trial = :on_trial_callback
73
74
  expect(context).to receive(:on_trial_callback)
74
75
  trial.choose! context
75
76
  end
76
- it 'does not run nil on_trial callback' do
77
+ it "does not run nil on_trial callback" do
77
78
  Split.configuration.on_trial = nil
78
79
  expect(context).not_to receive(:on_trial_callback)
79
80
  trial.choose! context
@@ -88,12 +89,12 @@ describe Split::Trial do
88
89
  end
89
90
 
90
91
  context "when override is present" do
91
- let(:override) { 'cart' }
92
+ let(:override) { "cart" }
92
93
  let(:trial) do
93
- Split::Trial.new(:user => user, :experiment => experiment, :override => override)
94
+ Split::Trial.new(user: user, experiment: experiment, override: override)
94
95
  end
95
96
 
96
- it_behaves_like 'a trial with callbacks'
97
+ it_behaves_like "a trial with callbacks"
97
98
 
98
99
  it "picks the override" do
99
100
  expect(experiment).to_not receive(:next_alternative)
@@ -102,7 +103,7 @@ describe Split::Trial do
102
103
 
103
104
  context "when alternative doesn't exist" do
104
105
  let(:override) { nil }
105
- it 'falls back on next_alternative' do
106
+ it "falls back on next_alternative" do
106
107
  expect(experiment).to receive(:next_alternative).and_call_original
107
108
  expect_alternative(trial, alternatives)
108
109
  end
@@ -111,7 +112,7 @@ describe Split::Trial do
111
112
 
112
113
  context "when disabled option is true" do
113
114
  let(:trial) do
114
- Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
115
+ Split::Trial.new(user: user, experiment: experiment, disabled: true)
115
116
  end
116
117
 
117
118
  it "picks the control", :aggregate_failures do
@@ -120,7 +121,7 @@ describe Split::Trial do
120
121
 
121
122
  expect(context).not_to receive(:on_trial_callback)
122
123
 
123
- expect_alternative(trial, 'basket')
124
+ expect_alternative(trial, "basket")
124
125
  Split.configuration.on_trial = nil
125
126
  end
126
127
  end
@@ -132,7 +133,7 @@ describe Split::Trial do
132
133
 
133
134
  expect(experiment).to_not receive(:next_alternative)
134
135
  expect(context).not_to receive(:on_trial_callback)
135
- expect_alternative(trial, 'basket')
136
+ expect_alternative(trial, "basket")
136
137
 
137
138
  Split.configuration.enabled = true
138
139
  Split.configuration.on_trial = nil
@@ -141,40 +142,48 @@ describe Split::Trial do
141
142
 
142
143
  context "when experiment has winner" do
143
144
  let(:trial) do
144
- Split::Trial.new(:user => user, :experiment => experiment)
145
+ Split::Trial.new(user: user, experiment: experiment)
145
146
  end
146
147
 
147
- it_behaves_like 'a trial with callbacks'
148
+ it_behaves_like "a trial with callbacks"
148
149
 
149
150
  it "picks the winner" do
150
- experiment.winner = 'cart'
151
+ experiment.winner = "cart"
151
152
  expect(experiment).to_not receive(:next_alternative)
152
153
 
153
- expect_alternative(trial, 'cart')
154
+ expect_alternative(trial, "cart")
154
155
  end
155
156
  end
156
157
 
157
158
  context "when exclude is true" do
158
159
  let(:trial) do
159
- Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
160
+ Split::Trial.new(user: user, experiment: experiment, exclude: true)
160
161
  end
161
162
 
162
- it_behaves_like 'a trial with callbacks'
163
+ it_behaves_like "a trial with callbacks"
163
164
 
164
165
  it "picks the control" do
165
166
  expect(experiment).to_not receive(:next_alternative)
166
- expect_alternative(trial, 'basket')
167
+ expect_alternative(trial, "basket")
167
168
  end
168
169
  end
169
170
 
170
171
  context "when user is already participating" do
171
- it_behaves_like 'a trial with callbacks'
172
+ it_behaves_like "a trial with callbacks"
172
173
 
173
174
  it "picks the same alternative" do
174
- user[experiment.key] = 'basket'
175
+ user[experiment.key] = "basket"
175
176
  expect(experiment).to_not receive(:next_alternative)
176
177
 
177
- expect_alternative(trial, 'basket')
178
+ expect_alternative(trial, "basket")
179
+ end
180
+
181
+ context "when alternative is not found" do
182
+ it "falls back on next_alternative" do
183
+ user[experiment.key] = "notfound"
184
+ expect(experiment).to receive(:next_alternative).and_call_original
185
+ expect_alternative(trial, alternatives)
186
+ end
178
187
  end
179
188
  end
180
189
 
@@ -190,42 +199,68 @@ describe Split::Trial do
190
199
  expect(trial.alternative.name).to_not be_empty
191
200
  Split.configuration.on_trial_choose = nil
192
201
  end
202
+
203
+ it "assigns user to an alternative" do
204
+ trial.choose! context
205
+
206
+ expect(alternatives).to include(user[experiment.name])
207
+ end
208
+
209
+ context "when cohorting is disabled" do
210
+ before(:each) { allow(experiment).to receive(:cohorting_disabled?).and_return(true) }
211
+
212
+ it "picks the control and does not run on_trial callbacks" do
213
+ Split.configuration.on_trial = :on_trial_callback
214
+
215
+ expect(experiment).to_not receive(:next_alternative)
216
+ expect(context).not_to receive(:on_trial_callback)
217
+ expect_alternative(trial, "basket")
218
+
219
+ Split.configuration.enabled = true
220
+ Split.configuration.on_trial = nil
221
+ end
222
+
223
+ it "user is not assigned an alternative" do
224
+ trial.choose! context
225
+
226
+ expect(user[experiment]).to eq(nil)
227
+ end
228
+ end
193
229
  end
194
230
  end
195
231
 
196
232
  describe "#complete!" do
197
- let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
198
- context 'when there are no goals' do
199
- it 'should complete the trial' do
233
+ context "when there are no goals" do
234
+ let(:trial) { Split::Trial.new(user: user, experiment: experiment) }
235
+ it "should complete the trial" do
200
236
  trial.choose!
201
237
  old_completed_count = trial.alternative.completed_count
202
238
  trial.complete!
203
- expect(trial.alternative.completed_count).to be(old_completed_count+1)
239
+ expect(trial.alternative.completed_count).to eq(old_completed_count + 1)
204
240
  end
205
241
  end
206
242
 
207
- context 'when there are many goals' do
208
- let(:goals) { ['first', 'second'] }
209
- let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
210
- shared_examples_for "goal completion" do
211
- it 'should not complete the trial' do
212
- trial.choose!
213
- old_completed_count = trial.alternative.completed_count
214
- trial.complete!(goal)
215
- expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
216
- end
217
- end
243
+ context "when there are many goals" do
244
+ let(:goals) { [ "goal1", "goal2" ] }
245
+ let(:trial) { Split::Trial.new(user: user, experiment: experiment, goals: goals) }
218
246
 
219
- describe 'Array of Goals' do
220
- let(:goal) { [goals.first] }
221
- it_behaves_like 'goal completion'
247
+ it "increments the completed count corresponding to the goals" do
248
+ trial.choose!
249
+ old_completed_counts = goals.map { |goal| [goal, trial.alternative.completed_count(goal)] }.to_h
250
+ trial.complete!
251
+ goals.each { | goal | expect(trial.alternative.completed_count(goal)).to eq(old_completed_counts[goal] + 1) }
222
252
  end
253
+ end
223
254
 
224
- describe 'String of Goal' do
225
- let(:goal) { goals.first }
226
- it_behaves_like 'goal completion'
255
+ context "when there is 1 goal of type string" do
256
+ let(:goal) { "goal" }
257
+ let(:trial) { Split::Trial.new(user: user, experiment: experiment, goals: goal) }
258
+ it "increments the completed count corresponding to the goal" do
259
+ trial.choose!
260
+ old_completed_count = trial.alternative.completed_count(goal)
261
+ trial.complete!
262
+ expect(trial.alternative.completed_count(goal)).to eq(old_completed_count + 1)
227
263
  end
228
-
229
264
  end
230
265
  end
231
266
 
@@ -234,7 +269,7 @@ describe Split::Trial do
234
269
 
235
270
  context "when override is present" do
236
271
  it "stores when store_override is true" do
237
- trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'basket')
272
+ trial = Split::Trial.new(user: user, experiment: experiment, override: "basket")
238
273
 
239
274
  Split.configuration.store_override = true
240
275
  expect(user).to receive("[]=")
@@ -243,7 +278,7 @@ describe Split::Trial do
243
278
  end
244
279
 
245
280
  it "does not store when store_override is false" do
246
- trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'basket')
281
+ trial = Split::Trial.new(user: user, experiment: experiment, override: "basket")
247
282
 
248
283
  expect(user).to_not receive("[]=")
249
284
  trial.choose!
@@ -252,7 +287,7 @@ describe Split::Trial do
252
287
 
253
288
  context "when disabled is present" do
254
289
  it "stores when store_override is true" do
255
- trial = Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
290
+ trial = Split::Trial.new(user: user, experiment: experiment, disabled: true)
256
291
 
257
292
  Split.configuration.store_override = true
258
293
  expect(user).to receive("[]=")
@@ -260,7 +295,7 @@ describe Split::Trial do
260
295
  end
261
296
 
262
297
  it "does not store when store_override is false" do
263
- trial = Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
298
+ trial = Split::Trial.new(user: user, experiment: experiment, disabled: true)
264
299
 
265
300
  expect(user).to_not receive("[]=")
266
301
  trial.choose!
@@ -269,8 +304,20 @@ describe Split::Trial do
269
304
 
270
305
  context "when exclude is present" do
271
306
  it "does not store" do
272
- trial = Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
307
+ trial = Split::Trial.new(user: user, experiment: experiment, exclude: true)
308
+
309
+ expect(user).to_not receive("[]=")
310
+ trial.choose!
311
+ end
312
+ end
273
313
 
314
+ context "when experiment has winner" do
315
+ let(:trial) do
316
+ experiment.winner = "cart"
317
+ Split::Trial.new(user: user, experiment: experiment)
318
+ end
319
+
320
+ it "does not store" do
274
321
  expect(user).to_not receive("[]=")
275
322
  trial.choose!
276
323
  end