split 2.0.0 → 3.0.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +30 -0
  3. data/.csslintrc +2 -0
  4. data/.eslintignore +1 -0
  5. data/.eslintrc +213 -0
  6. data/.rubocop.yml +1156 -0
  7. data/.travis.yml +5 -2
  8. data/Appraisals +5 -4
  9. data/CHANGELOG.md +46 -0
  10. data/Gemfile +1 -0
  11. data/README.md +123 -20
  12. data/gemfiles/4.2.gemfile +1 -0
  13. data/gemfiles/5.0.gemfile +10 -0
  14. data/lib/split/algorithms/block_randomization.rb +22 -0
  15. data/lib/split/alternative.rb +29 -8
  16. data/lib/split/configuration.rb +13 -2
  17. data/lib/split/dashboard/helpers.rb +5 -1
  18. data/lib/split/dashboard/public/dashboard-filtering.js +3 -3
  19. data/lib/split/dashboard/views/_experiment.erb +35 -1
  20. data/lib/split/dashboard.rb +5 -0
  21. data/lib/split/encapsulated_helper.rb +4 -15
  22. data/lib/split/experiment.rb +63 -54
  23. data/lib/split/goals_collection.rb +1 -1
  24. data/lib/split/helper.rb +20 -0
  25. data/lib/split/persistence/dual_adapter.rb +3 -0
  26. data/lib/split/persistence.rb +5 -3
  27. data/lib/split/redis_interface.rb +51 -0
  28. data/lib/split/user.rb +3 -1
  29. data/lib/split/version.rb +1 -1
  30. data/lib/split/zscore.rb +1 -1
  31. data/lib/split.rb +34 -41
  32. data/spec/algorithms/block_randomization_spec.rb +32 -0
  33. data/spec/alternative_spec.rb +31 -0
  34. data/spec/configuration_spec.rb +19 -5
  35. data/spec/dashboard_helpers_spec.rb +14 -0
  36. data/spec/dashboard_spec.rb +15 -0
  37. data/spec/encapsulated_helper_spec.rb +35 -4
  38. data/spec/experiment_spec.rb +38 -7
  39. data/spec/helper_spec.rb +59 -16
  40. data/spec/persistence/dual_adapter_spec.rb +102 -0
  41. data/spec/redis_interface_spec.rb +111 -0
  42. data/spec/spec_helper.rb +11 -10
  43. data/spec/split_spec.rb +43 -0
  44. data/split.gemspec +2 -3
  45. metadata +23 -26
  46. data/gemfiles/4.1.gemfile +0 -8
  47. data/lib/split/algorithms.rb +0 -4
  48. data/lib/split/extensions/array.rb +0 -5
  49. data/lib/split/extensions.rb +0 -4
@@ -80,29 +80,15 @@ module Split
80
80
  validate!
81
81
 
82
82
  if new_record?
83
- Split.redis.sadd(:experiments, name)
84
83
  start unless Split.configuration.start_manually
85
- @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
86
- goals_collection.save
87
- save_metadata
88
- else
89
- existing_alternatives = load_alternatives_from_redis
90
- existing_goals = Split::GoalsCollection.new(@name).load_from_redis
91
- existing_metadata = load_metadata_from_redis
92
- unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals && existing_metadata == @metadata
93
- reset
94
- @alternatives.each(&:delete)
95
- goals_collection.delete
96
- delete_metadata
97
- Split.redis.del(@name)
98
- @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
99
- goals_collection.save
100
- save_metadata
101
- end
84
+ elsif experiment_configuration_has_changed?
85
+ reset unless Split.configuration.reset_manually
102
86
  end
103
87
 
104
- Split.redis.hset(experiment_config_key, :resettable, resettable)
105
- Split.redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
88
+ persist_experiment_configuration if new_record? || experiment_configuration_has_changed?
89
+
90
+ redis.hset(experiment_config_key, :resettable, resettable)
91
+ redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
106
92
  self
107
93
  end
108
94
 
@@ -115,7 +101,7 @@ module Split
115
101
  end
116
102
 
117
103
  def new_record?
118
- !Split.redis.exists(name)
104
+ !redis.exists(name)
119
105
  end
120
106
 
121
107
  def ==(obj)
@@ -149,8 +135,9 @@ module Split
149
135
  end
150
136
 
151
137
  def winner
152
- if w = Split.redis.hget(:experiment_winner, name)
153
- Split::Alternative.new(w, name)
138
+ experiment_winner = redis.hget(:experiment_winner, name)
139
+ if experiment_winner
140
+ Split::Alternative.new(experiment_winner, name)
154
141
  else
155
142
  nil
156
143
  end
@@ -161,7 +148,7 @@ module Split
161
148
  end
162
149
 
163
150
  def winner=(winner_name)
164
- Split.redis.hset(:experiment_winner, name, winner_name.to_s)
151
+ redis.hset(:experiment_winner, name, winner_name.to_s)
165
152
  end
166
153
 
167
154
  def participant_count
@@ -173,21 +160,21 @@ module Split
173
160
  end
174
161
 
175
162
  def reset_winner
176
- Split.redis.hdel(:experiment_winner, name)
163
+ redis.hdel(:experiment_winner, name)
177
164
  end
178
165
 
179
166
  def start
180
- Split.redis.hset(:experiment_start_times, @name, Time.now.to_i)
167
+ redis.hset(:experiment_start_times, @name, Time.now.to_i)
181
168
  end
182
169
 
183
170
  def start_time
184
- t = Split.redis.hget(:experiment_start_times, @name)
171
+ t = redis.hget(:experiment_start_times, @name)
185
172
  if t
186
173
  # Check if stored time is an integer
187
174
  if t =~ /^[-+]?[0-9]+$/
188
- t = Time.at(t.to_i)
175
+ Time.at(t.to_i)
189
176
  else
190
- t = Time.parse(t)
177
+ Time.parse(t)
191
178
  end
192
179
  end
193
180
  end
@@ -205,11 +192,11 @@ module Split
205
192
  end
206
193
 
207
194
  def version
208
- @version ||= (Split.redis.get("#{name.to_s}:version").to_i || 0)
195
+ @version ||= (redis.get("#{name}:version").to_i || 0)
209
196
  end
210
197
 
211
198
  def increment_version
212
- @version = Split.redis.incr("#{name}:version")
199
+ @version = redis.incr("#{name}:version")
213
200
  end
214
201
 
215
202
  def key
@@ -247,24 +234,21 @@ module Split
247
234
  def delete
248
235
  Split.configuration.on_before_experiment_delete.call(self)
249
236
  if Split.configuration.start_manually
250
- Split.redis.hdel(:experiment_start_times, @name)
237
+ redis.hdel(:experiment_start_times, @name)
251
238
  end
252
- alternatives.each(&:delete)
253
239
  reset_winner
254
- Split.redis.srem(:experiments, name)
255
- Split.redis.del(name)
256
- goals_collection.delete
257
- delete_metadata
240
+ redis.srem(:experiments, name)
241
+ remove_experiment_configuration
258
242
  Split.configuration.on_experiment_delete.call(self)
259
243
  increment_version
260
244
  end
261
245
 
262
246
  def delete_metadata
263
- Split.redis.del(metadata_key)
247
+ redis.del(metadata_key)
264
248
  end
265
249
 
266
250
  def load_from_redis
267
- exp_config = Split.redis.hgetall(experiment_config_key)
251
+ exp_config = redis.hgetall(experiment_config_key)
268
252
 
269
253
  options = {
270
254
  resettable: exp_config['resettable'],
@@ -297,8 +281,6 @@ module Split
297
281
  end
298
282
 
299
283
  def estimate_winning_alternative(goal = nil)
300
- # TODO - refactor out functionality to work with and without goals
301
-
302
284
  # initialize a hash of beta distributions based on the alternatives' conversion rates
303
285
  beta_params = calc_beta_params(goal)
304
286
 
@@ -395,11 +377,11 @@ module Split
395
377
  end
396
378
 
397
379
  def calc_time=(time)
398
- Split.redis.hset(experiment_config_key, :calc_time, time)
380
+ redis.hset(experiment_config_key, :calc_time, time)
399
381
  end
400
382
 
401
383
  def calc_time
402
- Split.redis.hget(experiment_config_key, :calc_time).to_i
384
+ redis.hget(experiment_config_key, :calc_time).to_i
403
385
  end
404
386
 
405
387
  def jstring(goal = nil)
@@ -418,11 +400,11 @@ module Split
418
400
  end
419
401
 
420
402
  def load_metadata_from_configuration
421
- metadata = Split.configuration.experiment_for(@name)[:metadata]
403
+ Split.configuration.experiment_for(@name)[:metadata]
422
404
  end
423
405
 
424
406
  def load_metadata_from_redis
425
- meta = Split.redis.get(metadata_key)
407
+ meta = redis.get(metadata_key)
426
408
  JSON.parse(meta) unless meta.nil?
427
409
  end
428
410
 
@@ -437,22 +419,49 @@ module Split
437
419
  end
438
420
 
439
421
  def load_alternatives_from_redis
440
- case Split.redis.type(@name)
422
+ case redis.type(@name)
441
423
  when 'set' # convert legacy sets to lists
442
- alts = Split.redis.smembers(@name)
443
- Split.redis.del(@name)
444
- alts.reverse.each {|a| Split.redis.lpush(@name, a) }
445
- Split.redis.lrange(@name, 0, -1)
424
+ alts = redis.smembers(@name)
425
+ redis.del(@name)
426
+ alts.reverse.each {|a| redis.lpush(@name, a) }
427
+ redis.lrange(@name, 0, -1)
446
428
  else
447
- Split.redis.lrange(@name, 0, -1)
429
+ redis.lrange(@name, 0, -1)
448
430
  end
449
431
  end
450
432
 
451
- def save_metadata
452
- Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
433
+ private
434
+
435
+ def redis
436
+ Split.redis
453
437
  end
454
438
 
455
- private
439
+ def redis_interface
440
+ RedisInterface.new
441
+ end
442
+
443
+ def persist_experiment_configuration
444
+ redis_interface.add_to_set(:experiments, name)
445
+ redis_interface.persist_list(name, @alternatives.map(&:name))
446
+ goals_collection.save
447
+ redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
448
+ end
449
+
450
+ def remove_experiment_configuration
451
+ @alternatives.each(&:delete)
452
+ goals_collection.delete
453
+ delete_metadata
454
+ redis.del(@name)
455
+ end
456
+
457
+ def experiment_configuration_has_changed?
458
+ existing_alternatives = load_alternatives_from_redis
459
+ existing_goals = Split::GoalsCollection.new(@name).load_from_redis
460
+ existing_metadata = load_metadata_from_redis
461
+ existing_alternatives != @alternatives.map(&:name) ||
462
+ existing_goals != @goals ||
463
+ existing_metadata != @metadata
464
+ end
456
465
 
457
466
  def goals_collection
458
467
  Split::GoalsCollection.new(@name, @goals)
@@ -22,7 +22,7 @@ module Split
22
22
 
23
23
  def save
24
24
  return false if @goals.nil?
25
- @goals.reverse.each { |goal| Split.redis.lpush(goals_key, goal) }
25
+ RedisInterface.new.persist_list(goals_key, @goals)
26
26
  end
27
27
 
28
28
  def validate!
data/lib/split/helper.rb CHANGED
@@ -76,6 +76,26 @@ module Split
76
76
  Split.configuration.db_failover_on_db_error.call(e)
77
77
  end
78
78
 
79
+ def ab_record_extra_info(metric_descriptor, key, value = 1)
80
+ return if exclude_visitor? || Split.configuration.disabled?
81
+ metric_descriptor, goals = normalize_metric(metric_descriptor)
82
+ experiments = Metric.possible_experiments(metric_descriptor)
83
+
84
+ if experiments.any?
85
+ experiments.each do |experiment|
86
+ alternative_name = ab_user[experiment.key]
87
+
88
+ if alternative_name
89
+ alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
90
+ alternative.record_extra_info(key, value) if alternative
91
+ end
92
+ end
93
+ end
94
+ rescue => e
95
+ raise unless Split.configuration.db_failover
96
+ Split.configuration.db_failover_on_db_error.call(e)
97
+ end
98
+
79
99
  def override_present?(experiment_name)
80
100
  override_alternative(experiment_name)
81
101
  end
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
2
5
  module Split
3
6
  module Persistence
4
7
  class DualAdapter
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
- %w[session_adapter cookie_adapter redis_adapter dual_adapter].each do |f|
3
- require "split/persistence/#{f}"
4
- end
5
2
 
6
3
  module Split
7
4
  module Persistence
5
+ require 'split/persistence/cookie_adapter'
6
+ require 'split/persistence/dual_adapter'
7
+ require 'split/persistence/redis_adapter'
8
+ require 'split/persistence/session_adapter'
9
+
8
10
  ADAPTERS = {
9
11
  :cookie => Split::Persistence::CookieAdapter,
10
12
  :session => Split::Persistence::SessionAdapter
@@ -0,0 +1,51 @@
1
+ module Split
2
+ # Simplifies the interface to Redis.
3
+ class RedisInterface
4
+ def initialize
5
+ self.redis = Split.redis
6
+ end
7
+
8
+ def persist_list(list_name, list_values)
9
+ max_index = list_length(list_name) - 1
10
+ list_values.each_with_index do |value, index|
11
+ if index > max_index
12
+ add_to_list(list_name, value)
13
+ else
14
+ set_list_index(list_name, index, value)
15
+ end
16
+ end
17
+ make_list_length(list_name, list_values.length)
18
+ list_values
19
+ end
20
+
21
+ def add_to_list(list_name, value)
22
+ redis.rpush(list_name, value)
23
+ end
24
+
25
+ def set_list_index(list_name, index, value)
26
+ redis.lset(list_name, index, value)
27
+ end
28
+
29
+ def list_length(list_name)
30
+ redis.llen(list_name)
31
+ end
32
+
33
+ def remove_last_item_from_list(list_name)
34
+ redis.rpop(list_name)
35
+ end
36
+
37
+ def make_list_length(list_name, new_length)
38
+ while list_length(list_name) > new_length
39
+ remove_last_item_from_list(list_name)
40
+ end
41
+ end
42
+
43
+ def add_to_set(set_name, value)
44
+ redis.sadd(set_name, value) unless redis.sismember(set_name, value)
45
+ end
46
+
47
+ private
48
+
49
+ attr_accessor :redis
50
+ end
51
+ end
data/lib/split/user.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Split
2
4
  class User
3
5
  extend Forwardable
@@ -21,7 +23,7 @@ module Split
21
23
  def max_experiments_reached?(experiment_key)
22
24
  if Split.configuration.allow_multiple_experiments == 'control'
23
25
  experiments = active_experiments
24
- count_control = experiments.values.count {|v| v == 'control'}
26
+ count_control = experiments.count {|k,v| k == experiment_key || v == 'control'}
25
27
  experiments.size > count_control
26
28
  else
27
29
  !Split.configuration.allow_multiple_experiments &&
data/lib/split/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Split
3
- MAJOR = 2
3
+ MAJOR = 3
4
4
  MINOR = 0
5
5
  PATCH = 0
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
data/lib/split/zscore.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Split
3
- module Zscore
3
+ class Zscore
4
4
 
5
5
  include Math
6
6
 
data/lib/split.rb CHANGED
@@ -1,54 +1,47 @@
1
1
  # frozen_string_literal: true
2
- %w[algorithms
3
- alternative
4
- configuration
5
- exceptions
6
- experiment
7
- experiment_catalog
8
- extensions
9
- goals_collection
10
- helper
11
- metric
12
- persistence
13
- encapsulated_helper
14
- trial
15
- user
16
- version
17
- zscore].each do |f|
18
- require "split/#{f}"
19
- end
2
+ require 'redis'
20
3
 
4
+ require 'split/algorithms/block_randomization'
5
+ require 'split/algorithms/weighted_sample'
6
+ require 'split/algorithms/whiplash'
7
+ require 'split/alternative'
8
+ require 'split/configuration'
9
+ require 'split/encapsulated_helper'
10
+ require 'split/exceptions'
11
+ require 'split/experiment'
12
+ require 'split/experiment_catalog'
13
+ require 'split/extensions/string'
14
+ require 'split/goals_collection'
15
+ require 'split/helper'
16
+ require 'split/metric'
17
+ require 'split/persistence'
18
+ require 'split/redis_interface'
19
+ require 'split/trial'
20
+ require 'split/user'
21
+ require 'split/version'
22
+ require 'split/zscore'
21
23
  require 'split/engine' if defined?(Rails)
22
- require 'redis/namespace'
23
24
 
24
25
  module Split
25
26
  extend self
26
27
  attr_accessor :configuration
27
28
 
28
29
  # Accepts:
29
- # 1. A 'hostname:port' string
30
- # 2. A 'hostname:port:db' string (to select the Redis db)
31
- # 3. A 'hostname:port/namespace' string (to set the Redis namespace)
32
- # 4. A redis URL string 'redis://host:port'
33
- # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
34
- # or `Redis::Namespace`.
30
+ # 1. A redis URL (valid for `Redis.new(url: url)`)
31
+ # 2. an options hash compatible with `Redis.new`
32
+ # 3. or a valid Redis instance (one that responds to `#smembers`). Likely,
33
+ # this will be an instance of either `Redis`, `Redis::Client`,
34
+ # `Redis::DistRedis`, or `Redis::Namespace`.
35
35
  def redis=(server)
36
- if server.respond_to? :split
37
- if server["redis://"]
38
- redis = Redis.connect(:url => server, :thread_safe => true)
39
- else
40
- server, namespace = server.split('/', 2)
41
- host, port, db = server.split(':')
42
- redis = Redis.new(:host => host, :port => port,
43
- :thread_safe => true, :db => db)
44
- end
45
- namespace ||= :split
46
-
47
- @redis = Redis::Namespace.new(namespace, :redis => redis)
48
- elsif server.respond_to? :namespace=
49
- @redis = server
36
+ @redis = if server.is_a?(String)
37
+ Redis.new(:url => server, :thread_safe => true)
38
+ elsif server.is_a?(Hash)
39
+ Redis.new(server.merge(:thread_safe => true))
40
+ elsif server.respond_to?(:smembers)
41
+ server
50
42
  else
51
- @redis = Redis::Namespace.new(:split, :redis => server)
43
+ raise ArgumentError,
44
+ "You must supply a url, options hash or valid Redis connection instance"
52
45
  end
53
46
  end
54
47
 
@@ -56,7 +49,7 @@ module Split
56
49
  # create a new one.
57
50
  def redis
58
51
  return @redis if @redis
59
- self.redis = self.configuration.redis_url
52
+ self.redis = self.configuration.redis
60
53
  self.redis
61
54
  end
62
55
 
@@ -0,0 +1,32 @@
1
+ require "spec_helper"
2
+
3
+ describe Split::Algorithms::BlockRandomization do
4
+
5
+ let(:experiment) { Split::Experiment.new 'experiment' }
6
+ let(:alternative_A) { Split::Alternative.new 'A', 'experiment' }
7
+ let(:alternative_B) { Split::Alternative.new 'B', 'experiment' }
8
+ let(:alternative_C) { Split::Alternative.new 'C', 'experiment' }
9
+
10
+ before :each do
11
+ allow(experiment).to receive(:alternatives) { [alternative_A, alternative_B, alternative_C] }
12
+ end
13
+
14
+ it "should return an alternative" do
15
+ expect(Split::Algorithms::BlockRandomization.choose_alternative(experiment).class).to eq(Split::Alternative)
16
+ end
17
+
18
+ it "should always return the minimum participation option" do
19
+ allow(alternative_A).to receive(:participant_count) { 1 }
20
+ allow(alternative_B).to receive(:participant_count) { 1 }
21
+ allow(alternative_C).to receive(:participant_count) { 0 }
22
+ expect(Split::Algorithms::BlockRandomization.choose_alternative(experiment)).to eq(alternative_C)
23
+ end
24
+
25
+ it "should return one of the minimum participation options when multiple" do
26
+ allow(alternative_A).to receive(:participant_count) { 0 }
27
+ allow(alternative_B).to receive(:participant_count) { 0 }
28
+ allow(alternative_C).to receive(:participant_count) { 0 }
29
+ alternative = Split::Algorithms::BlockRandomization.choose_alternative(experiment)
30
+ expect([alternative_A, alternative_B, alternative_C].include?(alternative)).to be(true)
31
+ end
32
+ end
@@ -274,4 +274,35 @@ describe Split::Alternative do
274
274
  expect(control.z_score(goal2)).to eq('N/A')
275
275
  end
276
276
  end
277
+
278
+ describe "extra_info" do
279
+ it "reads saved value of recorded_info in redis" do
280
+ saved_recorded_info = {"key_1" => 1, "key_2" => "2"}
281
+ Split.redis.hset "#{alternative.experiment_name}:#{alternative.name}", 'recorded_info', saved_recorded_info.to_json
282
+ extra_info = alternative.extra_info
283
+
284
+ expect(extra_info).to eql(saved_recorded_info)
285
+ end
286
+ end
287
+
288
+ describe "record_extra_info" do
289
+ it "saves key" do
290
+ alternative.record_extra_info("signup", 1)
291
+ expect(alternative.extra_info["signup"]).to eql(1)
292
+ end
293
+
294
+ it "adds value to saved key's value second argument is number" do
295
+ alternative.record_extra_info("signup", 1)
296
+ alternative.record_extra_info("signup", 2)
297
+ expect(alternative.extra_info["signup"]).to eql(3)
298
+ end
299
+
300
+ it "sets saved's key value to the second argument if it's a string" do
301
+ alternative.record_extra_info("signup", "Value 1")
302
+ expect(alternative.extra_info["signup"]).to eql("Value 1")
303
+
304
+ alternative.record_extra_info("signup", "Value 2")
305
+ expect(alternative.extra_info["signup"]).to eql("Value 2")
306
+ end
307
+ end
277
308
  end
@@ -212,20 +212,34 @@ describe Split::Configuration do
212
212
  expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
213
213
  end
214
214
 
215
- context "configuration URL" do
215
+ context 'redis_url configuration [DEPRECATED]' do
216
+ it 'should warn on set and assign to #redis' do
217
+ expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
218
+ @config.redis_url = 'example_url'
219
+ expect(@config.redis).to eq('example_url')
220
+ end
221
+
222
+ it 'should warn on get and return #redis' do
223
+ expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
224
+ @config.redis = 'example_url'
225
+ expect(@config.redis_url).to eq('example_url')
226
+ end
227
+ end
228
+
229
+ context "redis configuration" do
216
230
  it "should default to local redis server" do
217
- expect(@config.redis_url).to eq("localhost:6379")
231
+ expect(@config.redis).to eq("redis://localhost:6379")
218
232
  end
219
233
 
220
234
  it "should allow for redis url to be configured" do
221
- @config.redis_url = "custom_redis_url"
222
- expect(@config.redis_url).to eq("custom_redis_url")
235
+ @config.redis = "custom_redis_url"
236
+ expect(@config.redis).to eq("custom_redis_url")
223
237
  end
224
238
 
225
239
  context "provided REDIS_URL environment variable" do
226
240
  it "should use the ENV variable" do
227
241
  ENV['REDIS_URL'] = "env_redis_url"
228
- expect(Split::Configuration.new.redis_url).to eq("env_redis_url")
242
+ expect(Split::Configuration.new.redis).to eq("env_redis_url")
229
243
  end
230
244
  end
231
245
  end
@@ -24,5 +24,19 @@ describe Split::DashboardHelpers do
24
24
  expect(confidence_level(2.58)).to eq('99% confidence')
25
25
  expect(confidence_level(3.00)).to eq('99% confidence')
26
26
  end
27
+
28
+ describe '#round' do
29
+ it 'can round number strings' do
30
+ expect(round('3.1415')).to eq BigDecimal.new('3.14')
31
+ end
32
+
33
+ it 'can round number strings for precsion' do
34
+ expect(round('3.1415', 1)).to eq BigDecimal.new('3.1')
35
+ end
36
+
37
+ it 'can handle invalid number strings' do
38
+ expect(round('N/A')).to be_zero
39
+ end
40
+ end
27
41
  end
28
42
  end
@@ -73,6 +73,21 @@ describe Split::Dashboard do
73
73
  end
74
74
  end
75
75
 
76
+ describe "force alternative" do
77
+ let!(:user) do
78
+ Split::User.new(@app, { experiment.name => 'a' })
79
+ end
80
+
81
+ before do
82
+ allow(Split::User).to receive(:new).and_return(user)
83
+ end
84
+
85
+ it "should set current user's alternative" do
86
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "b"
87
+ expect(user[experiment.name]).to eq("b")
88
+ end
89
+ end
90
+
76
91
  describe "index page" do
77
92
  context "with winner" do
78
93
  before { experiment.winner = 'red' }
@@ -4,18 +4,49 @@ require 'spec_helper'
4
4
  describe Split::EncapsulatedHelper do
5
5
  include Split::EncapsulatedHelper
6
6
 
7
- before do
8
- allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
9
- .and_return(mock_user)
10
- end
11
7
 
12
8
  def params
13
9
  raise NoMethodError, 'This method is not really defined'
14
10
  end
15
11
 
16
12
  describe "ab_test" do
13
+ before do
14
+ allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
15
+ .and_return(mock_user)
16
+ end
17
+
17
18
  it "should not raise an error when params raises an error" do
19
+ expect{ params }.to raise_error(NoMethodError)
18
20
  expect(lambda { ab_test('link_color', 'blue', 'red') }).not_to raise_error
19
21
  end
22
+
23
+ it "calls the block with selected alternative" do
24
+ expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', nil)
25
+ end
26
+
27
+ context "inside a view" do
28
+
29
+ it "works inside ERB" do
30
+ require 'erb'
31
+ template = ERB.new(<<-ERB.split(/\s+/s).map(&:strip).join(' '), nil, "%")
32
+ foo <% ab_test(:foo, '1', '2') do |alt, meta| %>
33
+ static <%= alt %>
34
+ <% end %>
35
+ ERB
36
+ expect(template.result(binding)).to match /foo static \d/
37
+ end
38
+
39
+ end
40
+ end
41
+
42
+ describe "context" do
43
+ it 'is passed in shim' do
44
+ ctx = Class.new{
45
+ include Split::EncapsulatedHelper
46
+ public :session
47
+ }.new
48
+ expect(ctx).to receive(:session){{}}
49
+ expect{ ctx.ab_test('link_color', 'blue', 'red') }.not_to raise_error
50
+ end
20
51
  end
21
52
  end