split 3.4.1 → 4.0.4

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +7 -0
  4. data/.github/workflows/ci.yml +76 -0
  5. data/.rubocop.yml +177 -4
  6. data/CHANGELOG.md +87 -0
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +2 -1
  9. data/README.md +37 -9
  10. data/Rakefile +5 -5
  11. data/gemfiles/5.2.gemfile +1 -3
  12. data/gemfiles/6.0.gemfile +1 -3
  13. data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
  14. data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
  15. data/lib/split/algorithms/block_randomization.rb +6 -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 +22 -22
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +5 -4
  22. data/lib/split/configuration.rb +89 -94
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +54 -54
  25. data/lib/split/dashboard/paginator.rb +1 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +10 -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 +19 -4
  31. data/lib/split/dashboard.rb +42 -21
  32. data/lib/split/encapsulated_helper.rb +15 -8
  33. data/lib/split/engine.rb +1 -0
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +151 -124
  36. data/lib/split/experiment_catalog.rb +7 -8
  37. data/lib/split/extensions/string.rb +2 -1
  38. data/lib/split/goals_collection.rb +9 -10
  39. data/lib/split/helper.rb +50 -23
  40. data/lib/split/metric.rb +6 -6
  41. data/lib/split/persistence/cookie_adapter.rb +46 -44
  42. data/lib/split/persistence/dual_adapter.rb +7 -8
  43. data/lib/split/persistence/redis_adapter.rb +8 -4
  44. data/lib/split/persistence/session_adapter.rb +1 -2
  45. data/lib/split/persistence.rb +8 -6
  46. data/lib/split/redis_interface.rb +15 -29
  47. data/lib/split/trial.rb +43 -34
  48. data/lib/split/user.rb +25 -14
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +2 -3
  51. data/lib/split.rb +34 -27
  52. data/spec/algorithms/block_randomization_spec.rb +6 -5
  53. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  54. data/spec/algorithms/whiplash_spec.rb +4 -5
  55. data/spec/alternative_spec.rb +35 -36
  56. data/spec/cache_spec.rb +84 -0
  57. data/spec/combined_experiments_helper_spec.rb +18 -17
  58. data/spec/configuration_spec.rb +41 -45
  59. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  60. data/spec/dashboard/paginator_spec.rb +10 -9
  61. data/spec/dashboard_helpers_spec.rb +19 -18
  62. data/spec/dashboard_spec.rb +122 -38
  63. data/spec/encapsulated_helper_spec.rb +46 -22
  64. data/spec/experiment_catalog_spec.rb +14 -13
  65. data/spec/experiment_spec.rb +198 -118
  66. data/spec/goals_collection_spec.rb +18 -16
  67. data/spec/helper_spec.rb +454 -385
  68. data/spec/metric_spec.rb +14 -14
  69. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  70. data/spec/persistence/dual_adapter_spec.rb +71 -71
  71. data/spec/persistence/redis_adapter_spec.rb +35 -27
  72. data/spec/persistence/session_adapter_spec.rb +2 -3
  73. data/spec/persistence_spec.rb +1 -2
  74. data/spec/redis_interface_spec.rb +25 -82
  75. data/spec/spec_helper.rb +35 -24
  76. data/spec/split_spec.rb +11 -11
  77. data/spec/support/cookies_mock.rb +1 -2
  78. data/spec/trial_spec.rb +102 -75
  79. data/spec/user_spec.rb +60 -29
  80. data/split.gemspec +22 -21
  81. metadata +43 -40
  82. data/.rubocop_todo.yml +0 -679
  83. data/.travis.yml +0 -60
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
@@ -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":1}', '{"Cart":1}'])
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,79 +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
216
250
  end
217
251
 
218
- describe 'winner=' do
252
+ describe "winner=" do
219
253
  it "should allow you to specify a winner" do
220
254
  experiment.save
221
- experiment.winner = 'red'
222
- expect(experiment.winner.name).to eq('red')
255
+ experiment.winner = "red"
256
+ expect(experiment.winner.name).to eq("red")
223
257
  end
224
258
 
225
- context 'when has_winner state is memoized' do
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
226
265
  before { expect(experiment).to_not have_winner }
227
266
 
228
- it 'should keep has_winner state consistent' do
229
- experiment.winner = 'red'
267
+ it "should keep has_winner state consistent" do
268
+ experiment.winner = "red"
230
269
  expect(experiment).to have_winner
231
270
  end
232
271
  end
233
272
  end
234
273
 
235
- describe 'reset_winner' do
236
- before { experiment.winner = 'green' }
274
+ describe "reset_winner" do
275
+ before { experiment.winner = "green" }
237
276
 
238
- it 'should reset the winner' do
277
+ it "should reset the winner" do
239
278
  experiment.reset_winner
240
279
  expect(experiment.winner).to be_nil
241
280
  end
242
281
 
243
- context 'when has_winner state is memoized' do
282
+ context "when has_winner state is memoized" do
244
283
  before { expect(experiment).to have_winner }
245
284
 
246
- it 'should keep has_winner state consistent' do
285
+ it "should keep has_winner state consistent" do
247
286
  experiment.reset_winner
248
287
  expect(experiment).to_not have_winner
249
288
  end
250
289
  end
251
290
  end
252
291
 
253
- describe 'has_winner?' do
254
- context 'with winner' do
255
- before { experiment.winner = 'red' }
292
+ describe "has_winner?" do
293
+ context "with winner" do
294
+ before { experiment.winner = "red" }
256
295
 
257
- it 'returns true' do
296
+ it "returns true" do
258
297
  expect(experiment).to have_winner
259
298
  end
260
299
  end
261
300
 
262
- context 'without winner' do
263
- it 'returns false' do
301
+ context "without winner" do
302
+ it "returns false" do
264
303
  expect(experiment).to_not have_winner
265
304
  end
266
305
  end
267
306
 
268
- it 'memoizes has_winner state' do
307
+ it "memoizes has_winner state" do
269
308
  expect(experiment).to receive(:winner).once
270
309
  expect(experiment).to_not have_winner
271
310
  expect(experiment).to_not have_winner
272
311
  end
273
312
  end
274
313
 
275
- describe 'reset' do
314
+ describe "reset" do
276
315
  let(:reset_manually) { false }
277
316
 
278
317
  before do
@@ -282,10 +321,10 @@ describe Split::Experiment do
282
321
  green.increment_participation
283
322
  end
284
323
 
285
- it 'should reset all alternatives' do
286
- experiment.winner = 'green'
324
+ it "should reset all alternatives" do
325
+ experiment.winner = "green"
287
326
 
288
- expect(experiment.next_alternative.name).to eq('green')
327
+ expect(experiment.next_alternative.name).to eq("green")
289
328
  green.increment_participation
290
329
 
291
330
  experiment.reset
@@ -294,10 +333,10 @@ describe Split::Experiment do
294
333
  expect(green.completed_count).to eq(0)
295
334
  end
296
335
 
297
- it 'should reset the winner' do
298
- experiment.winner = 'green'
336
+ it "should reset the winner" do
337
+ experiment.winner = "green"
299
338
 
300
- expect(experiment.next_alternative.name).to eq('green')
339
+ expect(experiment.next_alternative.name).to eq("green")
301
340
  green.increment_participation
302
341
 
303
342
  experiment.reset
@@ -322,64 +361,80 @@ describe Split::Experiment do
322
361
  end
323
362
  end
324
363
 
325
- describe 'algorithm' do
326
- 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") }
327
366
 
328
- it 'should use the default algorithm if none is specified' do
367
+ it "should use the default algorithm if none is specified" do
329
368
  expect(experiment.algorithm).to eq(Split.configuration.algorithm)
330
369
  end
331
370
 
332
- 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
333
372
  experiment.algorithm = Split::Algorithms::Whiplash
334
373
  expect(experiment.algorithm).to eq(Split::Algorithms::Whiplash)
335
374
  end
336
375
  end
337
376
 
338
- describe '#next_alternative' do
339
- context 'with multiple alternatives' do
340
- 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") }
341
380
 
342
- context 'with winner' do
381
+ context "with winner" do
343
382
  it "should always return the winner" do
344
- green = Split::Alternative.new('green', 'link_color')
345
- experiment.winner = 'green'
383
+ green = Split::Alternative.new("green", "link_color")
384
+ experiment.winner = "green"
346
385
 
347
- expect(experiment.next_alternative.name).to eq('green')
386
+ expect(experiment.next_alternative.name).to eq("green")
348
387
  green.increment_participation
349
388
 
350
- expect(experiment.next_alternative.name).to eq('green')
389
+ expect(experiment.next_alternative.name).to eq("green")
351
390
  end
352
391
  end
353
392
 
354
- context 'without winner' do
393
+ context "without winner" do
355
394
  it "should use the specified algorithm" do
356
395
  experiment.algorithm = Split::Algorithms::Whiplash
357
- expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color'))
358
- 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")
359
398
  end
360
399
  end
361
400
  end
362
401
 
363
- context 'with single alternative' do
364
- 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") }
365
404
 
366
405
  it "should always return the only alternative" do
367
- expect(experiment.next_alternative.name).to eq('blue')
368
- 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")
369
408
  end
370
409
  end
371
410
  end
372
411
 
373
- 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
374
429
  def same_but_different_alternative
375
- Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'yellow', 'orange')
430
+ Split::ExperimentCatalog.find_or_create("link_color", "blue", "yellow", "orange")
376
431
  end
377
432
 
378
433
  it "should reset an experiment if it is loaded with different alternatives" do
379
434
  experiment.save
380
435
  blue.participant_count = 5
381
436
  same_experiment = same_but_different_alternative
382
- expect(same_experiment.alternatives.map(&:name)).to eq(['blue', 'yellow', 'orange'])
437
+ expect(same_experiment.alternatives.map(&:name)).to eq(["blue", "yellow", "orange"])
383
438
  expect(blue.participant_count).to eq(0)
384
439
  end
385
440
 
@@ -392,7 +447,22 @@ describe Split::Experiment do
392
447
  expect(same_experiment_again.version).to eq(1)
393
448
  end
394
449
 
395
- 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
396
466
  let(:reset_manually) { false }
397
467
 
398
468
  before do
@@ -405,15 +475,15 @@ describe Split::Experiment do
405
475
  experiment.save
406
476
  end
407
477
 
408
- it 'resets all alternatives' do
478
+ it "resets all alternatives" do
409
479
  expect(green.participant_count).to eq(0)
410
480
  expect(green.completed_count).to eq(0)
411
481
  end
412
482
 
413
- context 'when reset_manually is set' do
483
+ context "when reset_manually is set" do
414
484
  let(:reset_manually) { true }
415
485
 
416
- it 'does not reset alternatives' do
486
+ it "does not reset alternatives" do
417
487
  expect(green.participant_count).to eq(2)
418
488
  expect(green.completed_count).to eq(0)
419
489
  end
@@ -421,16 +491,16 @@ describe Split::Experiment do
421
491
  end
422
492
  end
423
493
 
424
- describe 'alternatives passed as non-strings' do
494
+ describe "alternatives passed as non-strings" do
425
495
  it "should throw an exception if an alternative is passed that is not a string" do
426
- expect(lambda { Split::ExperimentCatalog.find_or_create('link_color', :blue, :red) }).to raise_error(ArgumentError)
427
- 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)
428
498
  end
429
499
  end
430
500
 
431
- describe 'specifying weights' do
501
+ describe "specifying weights" do
432
502
  let(:experiment_with_weight) {
433
- Split::ExperimentCatalog.find_or_create('link_color', {'blue' => 1}, {'red' => 2 })
503
+ Split::ExperimentCatalog.find_or_create("link_color", { "blue" => 1 }, { "red" => 2 })
434
504
  }
435
505
 
436
506
  it "should work for a new experiment" do
@@ -449,7 +519,7 @@ describe Split::Experiment do
449
519
  }
450
520
 
451
521
  context "saving experiment" do
452
- let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green') }
522
+ let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({ "link_color" => ["purchase", "refund"] }, "blue", "red", "green") }
453
523
 
454
524
  before { experiment.save }
455
525
 
@@ -461,7 +531,6 @@ describe Split::Experiment do
461
531
  same_but_different_goals
462
532
  expect(Split::ExperimentCatalog.find("link_color").goals).to eq(["purchase", "refund"])
463
533
  end
464
-
465
534
  end
466
535
 
467
536
  it "should have goals" do
@@ -470,9 +539,9 @@ describe Split::Experiment do
470
539
 
471
540
  context "find or create experiment" do
472
541
  it "should have correct goals" do
473
- 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")
474
543
  expect(experiment.goals).to eq(["purchase", "refund"])
475
- experiment = Split::ExperimentCatalog.find_or_create('link_color3', 'blue', 'red', 'green')
544
+ experiment = Split::ExperimentCatalog.find_or_create("link_color3", "blue", "red", "green")
476
545
  expect(experiment.goals).to eq([])
477
546
  end
478
547
  end
@@ -480,19 +549,19 @@ describe Split::Experiment do
480
549
 
481
550
  describe "beta probability calculation" do
482
551
  it "should return a hash with the probability of each alternative being the best" do
483
- experiment = Split::ExperimentCatalog.find_or_create('mathematicians', 'bernoulli', 'poisson', 'lagrange')
552
+ experiment = Split::ExperimentCatalog.find_or_create("mathematicians", "bernoulli", "poisson", "lagrange")
484
553
  experiment.calc_winning_alternatives
485
554
  expect(experiment.alternative_probabilities).not_to be_nil
486
555
  end
487
556
 
488
557
  it "should return between 46% and 54% probability for an experiment with 2 alternatives and no data" do
489
- experiment = Split::ExperimentCatalog.find_or_create('scientists', 'einstein', 'bohr')
558
+ experiment = Split::ExperimentCatalog.find_or_create("scientists", "einstein", "bohr")
490
559
  experiment.calc_winning_alternatives
491
560
  expect(experiment.alternatives[0].p_winner).to be_within(0.04).of(0.50)
492
561
  end
493
562
 
494
- it "should calculate the probability of being the winning alternative separately for each goal", :skip => true do
495
- 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")
496
565
  goal1 = experiment.goals[0]
497
566
  goal2 = experiment.goals[1]
498
567
  experiment.alternatives.each do |alternative|
@@ -507,8 +576,19 @@ describe Split::Experiment do
507
576
  expect(p_goal1).not_to be_within(0.04).of(p_goal2)
508
577
  end
509
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
+
510
590
  it "should return nil and not re-calculate probabilities if they have already been calculated today" do
511
- 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")
512
592
  expect(experiment.calc_winning_alternatives).not_to be nil
513
593
  expect(experiment.calc_winning_alternatives).to be nil
514
594
  end