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,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require 'spec_helper'
3
- require 'time'
2
+
3
+ require "spec_helper"
4
+ require "time"
4
5
 
5
6
  describe Split::Experiment do
6
- def new_experiment(goals=[])
7
- Split::Experiment.new('link_color', :alternatives => ['blue', 'red', 'green'], :goals => goals)
7
+ def new_experiment(goals = [])
8
+ Split::Experiment.new("link_color", alternatives: ["blue", "red", "green"], goals: goals)
8
9
  end
9
10
 
10
11
  def alternative(color)
11
- Split::Alternative.new(color, 'link_color')
12
+ Split::Alternative.new(color, "link_color")
12
13
  end
13
14
 
14
15
  let(:experiment) { new_experiment }
@@ -17,10 +18,10 @@ describe Split::Experiment do
17
18
  let(:green) { alternative("green") }
18
19
 
19
20
  context "with an experiment" do
20
- let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"]) }
21
+ let(:experiment) { Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"]) }
21
22
 
22
23
  it "should have a name" do
23
- expect(experiment.name).to eq('basket_text')
24
+ expect(experiment.name).to eq("basket_text")
24
25
  end
25
26
 
26
27
  it "should have alternatives" do
@@ -28,7 +29,7 @@ describe Split::Experiment do
28
29
  end
29
30
 
30
31
  it "should have alternatives with correct names" do
31
- expect(experiment.alternatives.collect{|a| a.name}).to eq(['Basket', 'Cart'])
32
+ expect(experiment.alternatives.collect { |a| a.name }).to eq(["Basket", "Cart"])
32
33
  end
33
34
 
34
35
  it "should be resettable by default" do
@@ -37,7 +38,7 @@ describe Split::Experiment do
37
38
 
38
39
  it "should save to redis" do
39
40
  experiment.save
40
- expect(Split.redis.exists('basket_text')).to be true
41
+ expect(Split.redis.exists?("basket_text")).to be true
41
42
  end
42
43
 
43
44
  it "should save the start time to redis" do
@@ -45,14 +46,14 @@ describe Split::Experiment do
45
46
  expect(Time).to receive(:now).and_return(experiment_start_time)
46
47
  experiment.save
47
48
 
48
- expect(Split::ExperimentCatalog.find('basket_text').start_time).to eq(experiment_start_time)
49
+ expect(Split::ExperimentCatalog.find("basket_text").start_time).to eq(experiment_start_time)
49
50
  end
50
51
 
51
52
  it "should not save the start time to redis when start_manually is enabled" do
52
53
  expect(Split.configuration).to receive(:start_manually).and_return(true)
53
54
  experiment.save
54
55
 
55
- expect(Split::ExperimentCatalog.find('basket_text').start_time).to be_nil
56
+ expect(Split::ExperimentCatalog.find("basket_text").start_time).to be_nil
56
57
  end
57
58
 
58
59
  it "should save the selected algorithm to redis" do
@@ -60,16 +61,16 @@ describe Split::Experiment do
60
61
  experiment.algorithm = experiment_algorithm
61
62
  experiment.save
62
63
 
63
- expect(Split::ExperimentCatalog.find('basket_text').algorithm).to eq(experiment_algorithm)
64
+ expect(Split::ExperimentCatalog.find("basket_text").algorithm).to eq(experiment_algorithm)
64
65
  end
65
66
 
66
67
  it "should handle having a start time stored as a string" do
67
68
  experiment_start_time = Time.parse("Sat Mar 03 14:01:03")
68
69
  expect(Time).to receive(:now).twice.and_return(experiment_start_time)
69
70
  experiment.save
70
- Split.redis.hset(:experiment_start_times, experiment.name, experiment_start_time)
71
+ Split.redis.hset(:experiment_start_times, experiment.name, experiment_start_time.to_s)
71
72
 
72
- expect(Split::ExperimentCatalog.find('basket_text').start_time).to eq(experiment_start_time)
73
+ expect(Split::ExperimentCatalog.find("basket_text").start_time).to eq(experiment_start_time)
73
74
  end
74
75
 
75
76
  it "should handle not having a start time" do
@@ -79,17 +80,17 @@ describe Split::Experiment do
79
80
 
80
81
  Split.redis.hdel(:experiment_start_times, experiment.name)
81
82
 
82
- expect(Split::ExperimentCatalog.find('basket_text').start_time).to be_nil
83
+ expect(Split::ExperimentCatalog.find("basket_text").start_time).to be_nil
83
84
  end
84
85
 
85
86
  it "should not create duplicates when saving multiple times" do
86
87
  experiment.save
87
88
  experiment.save
88
- expect(Split.redis.exists('basket_text')).to be true
89
- expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['Basket', "Cart"])
89
+ expect(Split.redis.exists?("basket_text")).to be true
90
+ expect(Split.redis.lrange("basket_text", 0, -1)).to eq(['{"Basket":1}', '{"Cart":1}'])
90
91
  end
91
92
 
92
- describe 'new record?' do
93
+ describe "new record?" do
93
94
  it "should know if it hasn't been saved yet" do
94
95
  expect(experiment.new_record?).to be_truthy
95
96
  end
@@ -100,55 +101,82 @@ describe Split::Experiment do
100
101
  end
101
102
  end
102
103
 
103
- describe 'control' do
104
- it 'should be the first alternative' do
104
+ describe "control" do
105
+ it "should be the first alternative" do
105
106
  experiment.save
106
- expect(experiment.control.name).to eq('Basket')
107
+ expect(experiment.control.name).to eq("Basket")
107
108
  end
108
109
  end
109
110
  end
110
111
 
111
- describe 'initialization' do
112
+ describe "initialization" do
112
113
  it "should set the algorithm when passed as an option to the initializer" do
113
- experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
114
- expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
114
+ experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], algorithm: Split::Algorithms::Whiplash)
115
+ expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
115
116
  end
116
117
 
117
118
  it "should be possible to make an experiment not resettable" do
118
- experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
119
+ experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], resettable: false)
119
120
  expect(experiment.resettable).to be_falsey
120
121
  end
121
- end
122
122
 
123
- describe 'persistent configuration' do
123
+ context "from configuration" do
124
+ let(:experiment_name) { :my_experiment }
125
+ let(:experiments) do
126
+ {
127
+ experiment_name => {
128
+ alternatives: ["Control Opt", "Alt one"]
129
+ }
130
+ }
131
+ end
132
+
133
+ before { Split.configuration.experiments = experiments }
134
+
135
+ it "assigns default values to the experiment" do
136
+ expect(Split::Experiment.new(experiment_name).resettable).to eq(true)
137
+ end
138
+ end
139
+ end
124
140
 
141
+ describe "persistent configuration" do
125
142
  it "should persist resettable in redis" do
126
- experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
143
+ experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], resettable: false)
127
144
  experiment.save
128
145
 
129
- e = Split::ExperimentCatalog.find('basket_text')
146
+ e = Split::ExperimentCatalog.find("basket_text")
130
147
  expect(e).to eq(experiment)
131
148
  expect(e.resettable).to be_falsey
132
-
133
149
  end
134
150
 
135
- describe '#metadata' do
136
- let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash, :metadata => meta) }
137
- context 'simple hash' do
138
- let(:meta) { { 'basket' => 'a', 'cart' => 'b' } }
151
+ describe "#metadata" do
152
+ let(:experiment) { Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], algorithm: Split::Algorithms::Whiplash, metadata: meta) }
153
+ let(:meta) { { a: "b" } }
154
+
155
+ before do
156
+ experiment.save
157
+ end
158
+
159
+ it "should delete the key when metadata is removed" do
160
+ experiment.metadata = nil
161
+ experiment.save
162
+
163
+ expect(Split.redis.exists?(experiment.metadata_key)).to be_falsey
164
+ end
165
+
166
+ context "simple hash" do
167
+ let(:meta) { { "basket" => "a", "cart" => "b" } }
168
+
139
169
  it "should persist metadata in redis" do
140
- experiment.save
141
- e = Split::ExperimentCatalog.find('basket_text')
170
+ e = Split::ExperimentCatalog.find("basket_text")
142
171
  expect(e).to eq(experiment)
143
172
  expect(e.metadata).to eq(meta)
144
173
  end
145
174
  end
146
175
 
147
- context 'nested hash' do
148
- let(:meta) { { 'basket' => { 'one' => 'two' }, 'cart' => 'b' } }
176
+ context "nested hash" do
177
+ let(:meta) { { "basket" => { "one" => "two" }, "cart" => "b" } }
149
178
  it "should persist metadata in redis" do
150
- experiment.save
151
- e = Split::ExperimentCatalog.find('basket_text')
179
+ e = Split::ExperimentCatalog.find("basket_text")
152
180
  expect(e).to eq(experiment)
153
181
  expect(e.metadata).to eq(meta)
154
182
  end
@@ -156,32 +184,32 @@ describe Split::Experiment do
156
184
  end
157
185
 
158
186
  it "should persist algorithm in redis" do
159
- experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
187
+ experiment = Split::Experiment.new("basket_text", alternatives: ["Basket", "Cart"], algorithm: Split::Algorithms::Whiplash)
160
188
  experiment.save
161
189
 
162
- e = Split::ExperimentCatalog.find('basket_text')
190
+ e = Split::ExperimentCatalog.find("basket_text")
163
191
  expect(e).to eq(experiment)
164
192
  expect(e.algorithm).to eq(Split::Algorithms::Whiplash)
165
193
  end
166
194
 
167
195
  it "should persist a new experiment in redis, that does not exist in the configuration file" do
168
- experiment = Split::Experiment.new('foobar', :alternatives => ['tra', 'la'], :algorithm => Split::Algorithms::Whiplash)
196
+ experiment = Split::Experiment.new("foobar", alternatives: ["tra", "la"], algorithm: Split::Algorithms::Whiplash)
169
197
  experiment.save
170
198
 
171
- e = Split::ExperimentCatalog.find('foobar')
199
+ e = Split::ExperimentCatalog.find("foobar")
172
200
  expect(e).to eq(experiment)
173
- expect(e.alternatives.collect{|a| a.name}).to eq(['tra', 'la'])
201
+ expect(e.alternatives.collect { |a| a.name }).to eq(["tra", "la"])
174
202
  end
175
203
  end
176
204
 
177
- describe 'deleting' do
178
- it 'should delete itself' do
179
- experiment = Split::Experiment.new('basket_text', :alternatives => [ 'Basket', "Cart"])
205
+ describe "deleting" do
206
+ it "should delete itself" do
207
+ experiment = Split::Experiment.new("basket_text", alternatives: [ "Basket", "Cart"])
180
208
  experiment.save
181
209
 
182
210
  experiment.delete
183
- expect(Split.redis.exists('link_color')).to be false
184
- expect(Split::ExperimentCatalog.find('link_color')).to be_nil
211
+ expect(Split.redis.exists?("link_color")).to be false
212
+ expect(Split::ExperimentCatalog.find("link_color")).to be_nil
185
213
  end
186
214
 
187
215
  it "should increment the version" do
@@ -200,44 +228,90 @@ describe Split::Experiment do
200
228
  experiment.delete
201
229
  end
202
230
 
203
- it 'should reset the start time if the experiment should be manually started' do
231
+ it "should reset the start time if the experiment should be manually started" do
204
232
  Split.configuration.start_manually = true
205
233
  experiment.start
206
234
  experiment.delete
207
235
  expect(experiment.start_time).to be_nil
208
236
  end
209
- end
210
237
 
238
+ it "should default cohorting back to false" do
239
+ experiment.disable_cohorting
240
+ expect(experiment.cohorting_disabled?).to eq(true)
241
+ experiment.delete
242
+ expect(experiment.cohorting_disabled?).to eq(false)
243
+ end
244
+ end
211
245
 
212
- describe 'winner' do
246
+ describe "winner" do
213
247
  it "should have no winner initially" do
214
248
  expect(experiment.winner).to be_nil
215
249
  end
250
+ end
216
251
 
252
+ describe "winner=" do
217
253
  it "should allow you to specify a winner" do
218
254
  experiment.save
219
- experiment.winner = 'red'
220
- expect(experiment.winner.name).to eq('red')
255
+ experiment.winner = "red"
256
+ expect(experiment.winner.name).to eq("red")
257
+ end
258
+
259
+ it "should call the on_experiment_winner_choose hook" do
260
+ expect(Split.configuration.on_experiment_winner_choose).to receive(:call)
261
+ experiment.winner = "green"
262
+ end
263
+
264
+ context "when has_winner state is memoized" do
265
+ before { expect(experiment).to_not have_winner }
266
+
267
+ it "should keep has_winner state consistent" do
268
+ experiment.winner = "red"
269
+ expect(experiment).to have_winner
270
+ end
221
271
  end
222
272
  end
223
273
 
224
- describe 'has_winner?' do
225
- context 'with winner' do
226
- before { experiment.winner = 'red' }
274
+ describe "reset_winner" do
275
+ before { experiment.winner = "green" }
227
276
 
228
- it 'returns true' do
277
+ it "should reset the winner" do
278
+ experiment.reset_winner
279
+ expect(experiment.winner).to be_nil
280
+ end
281
+
282
+ context "when has_winner state is memoized" do
283
+ before { expect(experiment).to have_winner }
284
+
285
+ it "should keep has_winner state consistent" do
286
+ experiment.reset_winner
287
+ expect(experiment).to_not have_winner
288
+ end
289
+ end
290
+ end
291
+
292
+ describe "has_winner?" do
293
+ context "with winner" do
294
+ before { experiment.winner = "red" }
295
+
296
+ it "returns true" do
229
297
  expect(experiment).to have_winner
230
298
  end
231
299
  end
232
300
 
233
- context 'without winner' do
234
- it 'returns false' do
301
+ context "without winner" do
302
+ it "returns false" do
235
303
  expect(experiment).to_not have_winner
236
304
  end
237
305
  end
306
+
307
+ it "memoizes has_winner state" do
308
+ expect(experiment).to receive(:winner).once
309
+ expect(experiment).to_not have_winner
310
+ expect(experiment).to_not have_winner
311
+ end
238
312
  end
239
313
 
240
- describe 'reset' do
314
+ describe "reset" do
241
315
  let(:reset_manually) { false }
242
316
 
243
317
  before do
@@ -247,10 +321,10 @@ describe Split::Experiment do
247
321
  green.increment_participation
248
322
  end
249
323
 
250
- it 'should reset all alternatives' do
251
- experiment.winner = 'green'
324
+ it "should reset all alternatives" do
325
+ experiment.winner = "green"
252
326
 
253
- expect(experiment.next_alternative.name).to eq('green')
327
+ expect(experiment.next_alternative.name).to eq("green")
254
328
  green.increment_participation
255
329
 
256
330
  experiment.reset
@@ -259,10 +333,10 @@ describe Split::Experiment do
259
333
  expect(green.completed_count).to eq(0)
260
334
  end
261
335
 
262
- it 'should reset the winner' do
263
- experiment.winner = 'green'
336
+ it "should reset the winner" do
337
+ experiment.winner = "green"
264
338
 
265
- expect(experiment.next_alternative.name).to eq('green')
339
+ expect(experiment.next_alternative.name).to eq("green")
266
340
  green.increment_participation
267
341
 
268
342
  experiment.reset
@@ -287,64 +361,80 @@ describe Split::Experiment do
287
361
  end
288
362
  end
289
363
 
290
- describe 'algorithm' do
291
- let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'red', 'green') }
364
+ describe "algorithm" do
365
+ let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue", "red", "green") }
292
366
 
293
- it 'should use the default algorithm if none is specified' do
367
+ it "should use the default algorithm if none is specified" do
294
368
  expect(experiment.algorithm).to eq(Split.configuration.algorithm)
295
369
  end
296
370
 
297
- it 'should use the user specified algorithm for this experiment if specified' do
371
+ it "should use the user specified algorithm for this experiment if specified" do
298
372
  experiment.algorithm = Split::Algorithms::Whiplash
299
373
  expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
300
374
  end
301
375
  end
302
376
 
303
- describe '#next_alternative' do
304
- context 'with multiple alternatives' do
305
- let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'red', 'green') }
377
+ describe "#next_alternative" do
378
+ context "with multiple alternatives" do
379
+ let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue", "red", "green") }
306
380
 
307
- context 'with winner' do
381
+ context "with winner" do
308
382
  it "should always return the winner" do
309
- green = Split::Alternative.new('green', 'link_color')
310
- experiment.winner = 'green'
383
+ green = Split::Alternative.new("green", "link_color")
384
+ experiment.winner = "green"
311
385
 
312
- expect(experiment.next_alternative.name).to eq('green')
386
+ expect(experiment.next_alternative.name).to eq("green")
313
387
  green.increment_participation
314
388
 
315
- expect(experiment.next_alternative.name).to eq('green')
389
+ expect(experiment.next_alternative.name).to eq("green")
316
390
  end
317
391
  end
318
392
 
319
- context 'without winner' do
393
+ context "without winner" do
320
394
  it "should use the specified algorithm" do
321
395
  experiment.algorithm = Split::Algorithms::Whiplash
322
- expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color'))
323
- expect(experiment.next_alternative.name).to eq('green')
396
+ expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new("green", "link_color"))
397
+ expect(experiment.next_alternative.name).to eq("green")
324
398
  end
325
399
  end
326
400
  end
327
401
 
328
- context 'with single alternative' do
329
- let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue') }
402
+ context "with single alternative" do
403
+ let(:experiment) { Split::ExperimentCatalog.find_or_create("link_color", "blue") }
330
404
 
331
405
  it "should always return the only alternative" do
332
- expect(experiment.next_alternative.name).to eq('blue')
333
- expect(experiment.next_alternative.name).to eq('blue')
406
+ expect(experiment.next_alternative.name).to eq("blue")
407
+ expect(experiment.next_alternative.name).to eq("blue")
334
408
  end
335
409
  end
336
410
  end
337
411
 
338
- describe 'changing an existing experiment' do
412
+ describe "#cohorting_disabled?" do
413
+ it "returns false when nothing has been configured" do
414
+ expect(experiment.cohorting_disabled?).to eq false
415
+ end
416
+
417
+ it "returns true when enable_cohorting is performed" do
418
+ experiment.enable_cohorting
419
+ expect(experiment.cohorting_disabled?).to eq false
420
+ end
421
+
422
+ it "returns false when nothing has been configured" do
423
+ experiment.disable_cohorting
424
+ expect(experiment.cohorting_disabled?).to eq true
425
+ end
426
+ end
427
+
428
+ describe "changing an existing experiment" do
339
429
  def same_but_different_alternative
340
- Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'yellow', 'orange')
430
+ Split::ExperimentCatalog.find_or_create("link_color", "blue", "yellow", "orange")
341
431
  end
342
432
 
343
433
  it "should reset an experiment if it is loaded with different alternatives" do
344
434
  experiment.save
345
435
  blue.participant_count = 5
346
436
  same_experiment = same_but_different_alternative
347
- expect(same_experiment.alternatives.map(&:name)).to eq(['blue', 'yellow', 'orange'])
437
+ expect(same_experiment.alternatives.map(&:name)).to eq(["blue", "yellow", "orange"])
348
438
  expect(blue.participant_count).to eq(0)
349
439
  end
350
440
 
@@ -357,7 +447,22 @@ describe Split::Experiment do
357
447
  expect(same_experiment_again.version).to eq(1)
358
448
  end
359
449
 
360
- context 'when experiment configuration is changed' do
450
+ context "when metadata is changed" do
451
+ it "should increase version" do
452
+ experiment.save
453
+ experiment.metadata = { "foo" => "bar" }
454
+
455
+ expect { experiment.save }.to change { experiment.version }.by(1)
456
+ end
457
+
458
+ it "does not increase version" do
459
+ experiment.metadata = nil
460
+ experiment.save
461
+ expect { experiment.save }.to change { experiment.version }.by(0)
462
+ end
463
+ end
464
+
465
+ context "when experiment configuration is changed" do
361
466
  let(:reset_manually) { false }
362
467
 
363
468
  before do
@@ -370,15 +475,15 @@ describe Split::Experiment do
370
475
  experiment.save
371
476
  end
372
477
 
373
- it 'resets all alternatives' do
478
+ it "resets all alternatives" do
374
479
  expect(green.participant_count).to eq(0)
375
480
  expect(green.completed_count).to eq(0)
376
481
  end
377
482
 
378
- context 'when reset_manually is set' do
483
+ context "when reset_manually is set" do
379
484
  let(:reset_manually) { true }
380
485
 
381
- it 'does not reset alternatives' do
486
+ it "does not reset alternatives" do
382
487
  expect(green.participant_count).to eq(2)
383
488
  expect(green.completed_count).to eq(0)
384
489
  end
@@ -386,16 +491,16 @@ describe Split::Experiment do
386
491
  end
387
492
  end
388
493
 
389
- describe 'alternatives passed as non-strings' do
494
+ describe "alternatives passed as non-strings" do
390
495
  it "should throw an exception if an alternative is passed that is not a string" do
391
- expect(lambda { Split::ExperimentCatalog.find_or_create('link_color', :blue, :red) }).to raise_error(ArgumentError)
392
- expect(lambda { Split::ExperimentCatalog.find_or_create('link_enabled', true, false) }).to raise_error(ArgumentError)
496
+ expect { Split::ExperimentCatalog.find_or_create("link_color", :blue, :red) }.to raise_error(ArgumentError)
497
+ expect { Split::ExperimentCatalog.find_or_create("link_enabled", true, false) }.to raise_error(ArgumentError)
393
498
  end
394
499
  end
395
500
 
396
- describe 'specifying weights' do
501
+ describe "specifying weights" do
397
502
  let(:experiment_with_weight) {
398
- Split::ExperimentCatalog.find_or_create('link_color', {'blue' => 1}, {'red' => 2 })
503
+ Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 2 })
399
504
  }
400
505
 
401
506
  it "should work for a new experiment" do
@@ -414,9 +519,7 @@ describe Split::Experiment do
414
519
  }
415
520
 
416
521
  context "saving experiment" do
417
- def same_but_different_goals
418
- Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green')
419
- end
522
+ let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({ "link_color" => ["purchase", "refund"] }, "blue", "red", "green") }
420
523
 
421
524
  before { experiment.save }
422
525
 
@@ -425,10 +528,9 @@ describe Split::Experiment do
425
528
  end
426
529
 
427
530
  it "should reset an experiment if it is loaded with different goals" do
428
- same_experiment = same_but_different_goals
531
+ same_but_different_goals
429
532
  expect(Split::ExperimentCatalog.find("link_color").goals).to eq(["purchase", "refund"])
430
533
  end
431
-
432
534
  end
433
535
 
434
536
  it "should have goals" do
@@ -437,9 +539,9 @@ describe Split::Experiment do
437
539
 
438
540
  context "find or create experiment" do
439
541
  it "should have correct goals" do
440
- experiment = Split::ExperimentCatalog.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
542
+ experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
441
543
  expect(experiment.goals).to eq(["purchase", "refund"])
442
- experiment = Split::ExperimentCatalog.find_or_create('link_color3', 'blue', 'red', 'green')
544
+ experiment = Split::ExperimentCatalog.find_or_create("link_color3", "blue", "red", "green")
443
545
  expect(experiment.goals).to eq([])
444
546
  end
445
547
  end
@@ -447,19 +549,19 @@ describe Split::Experiment do
447
549
 
448
550
  describe "beta probability calculation" do
449
551
  it "should return a hash with the probability of each alternative being the best" do
450
- experiment = Split::ExperimentCatalog.find_or_create('mathematicians', 'bernoulli', 'poisson', 'lagrange')
552
+ experiment = Split::ExperimentCatalog.find_or_create("mathematicians", "bernoulli", "poisson", "lagrange")
451
553
  experiment.calc_winning_alternatives
452
554
  expect(experiment.alternative_probabilities).not_to be_nil
453
555
  end
454
556
 
455
557
  it "should return between 46% and 54% probability for an experiment with 2 alternatives and no data" do
456
- experiment = Split::ExperimentCatalog.find_or_create('scientists', 'einstein', 'bohr')
558
+ experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
457
559
  experiment.calc_winning_alternatives
458
560
  expect(experiment.alternatives[0].p_winner).to be_within(0.04).of(0.50)
459
561
  end
460
562
 
461
- it "should calculate the probability of being the winning alternative separately for each goal", :skip => true do
462
- experiment = Split::ExperimentCatalog.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
563
+ it "should calculate the probability of being the winning alternative separately for each goal", skip: true do
564
+ experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
463
565
  goal1 = experiment.goals[0]
464
566
  goal2 = experiment.goals[1]
465
567
  experiment.alternatives.each do |alternative|
@@ -474,8 +576,19 @@ describe Split::Experiment do
474
576
  expect(p_goal1).not_to be_within(0.04).of(p_goal2)
475
577
  end
476
578
 
579
+ it "should not calculate when data is not valid for beta distribution" do
580
+ experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
581
+
582
+ experiment.alternatives.each do |alternative|
583
+ alternative.participant_count = 9
584
+ alternative.set_completed_count(10)
585
+ end
586
+
587
+ expect { experiment.calc_winning_alternatives }.to_not raise_error
588
+ end
589
+
477
590
  it "should return nil and not re-calculate probabilities if they have already been calculated today" do
478
- experiment = Split::ExperimentCatalog.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
591
+ experiment = Split::ExperimentCatalog.find_or_create({ "link_color3" => ["purchase", "refund"] }, "blue", "red", "green")
479
592
  expect(experiment.calc_winning_alternatives).not_to be nil
480
593
  expect(experiment.calc_winning_alternatives).to be nil
481
594
  end