split 3.3.2 → 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.
- checksums.yaml +4 -4
- data/.eslintrc +1 -1
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +63 -0
- data/.rspec +1 -0
- data/.rubocop.yml +67 -1043
- data/CHANGELOG.md +121 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -1
- data/README.md +51 -21
- data/Rakefile +6 -5
- data/lib/split/algorithms/block_randomization.rb +7 -6
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +17 -18
- data/lib/split/algorithms.rb +14 -0
- data/lib/split/alternative.rb +25 -25
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +5 -4
- data/lib/split/configuration.rb +94 -96
- data/lib/split/dashboard/helpers.rb +7 -7
- data/lib/split/dashboard/pagination_helpers.rb +56 -57
- data/lib/split/dashboard/paginator.rb +1 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +10 -2
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/_experiment.erb +2 -1
- data/lib/split/dashboard/views/index.erb +19 -4
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +46 -21
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +7 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +160 -122
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +10 -10
- data/lib/split/helper.rb +52 -24
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +47 -44
- data/lib/split/persistence/dual_adapter.rb +53 -12
- data/lib/split/persistence/redis_adapter.rb +8 -4
- data/lib/split/persistence/session_adapter.rb +1 -2
- data/lib/split/persistence.rb +8 -6
- data/lib/split/redis_interface.rb +16 -29
- data/lib/split/trial.rb +44 -35
- data/lib/split/user.rb +30 -15
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +35 -28
- data/spec/algorithms/block_randomization_spec.rb +6 -5
- data/spec/algorithms/weighted_sample_spec.rb +6 -5
- data/spec/algorithms/whiplash_spec.rb +4 -5
- data/spec/alternative_spec.rb +35 -36
- data/spec/cache_spec.rb +84 -0
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +41 -45
- data/spec/dashboard/pagination_helpers_spec.rb +71 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +153 -48
- data/spec/encapsulated_helper_spec.rb +47 -23
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +224 -111
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +531 -424
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +26 -11
- data/spec/persistence/dual_adapter_spec.rb +158 -66
- data/spec/persistence/redis_adapter_spec.rb +35 -27
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +25 -82
- data/spec/spec_helper.rb +38 -24
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +102 -75
- data/spec/user_spec.rb +69 -27
- data/split.gemspec +26 -23
- metadata +68 -42
- data/.travis.yml +0 -66
- data/Appraisals +0 -19
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -9
- data/gemfiles/5.1.gemfile +0 -9
- data/gemfiles/5.2.gemfile +0 -9
- data/gemfiles/6.0.gemfile +0 -9
|
@@ -1,111 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
2
4
|
|
|
3
5
|
describe Split::RedisInterface do
|
|
4
|
-
let(:list_name) {
|
|
5
|
-
let(: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
|
|
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
|
|
16
|
-
expect(Split.redis.lindex(list_name, 1)).to eq
|
|
17
|
-
expect(Split.redis.lindex(list_name, 2)).to eq
|
|
18
|
-
expect(Split.redis.lindex(list_name, 3)).to eq
|
|
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
|
|
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, [
|
|
26
|
-
expect(Split.redis.lindex(list_name, 0)).to eq
|
|
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
|
|
33
|
-
subject(:
|
|
34
|
-
interface.
|
|
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
|
-
|
|
96
|
-
expect(Split.redis.
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
5
|
-
require 'bundler/setup'
|
|
3
|
+
ENV["RACK_ENV"] = "test"
|
|
6
4
|
|
|
7
|
-
require
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
require 'split'
|
|
11
|
-
require 'ostruct'
|
|
12
|
-
require 'yaml'
|
|
5
|
+
require "rubygems"
|
|
6
|
+
require "bundler/setup"
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
require "simplecov"
|
|
9
|
+
SimpleCov.start
|
|
15
10
|
|
|
16
|
-
require "
|
|
11
|
+
require "split"
|
|
12
|
+
require "yaml"
|
|
13
|
+
require "pry"
|
|
17
14
|
|
|
18
|
-
|
|
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 =
|
|
26
|
-
Split.redis.
|
|
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 =
|
|
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
|
|
46
|
-
@request ||=
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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,18 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'spec_helper'
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
require "spec_helper"
|
|
5
4
|
|
|
5
|
+
RSpec.describe Split do
|
|
6
6
|
around(:each) do |ex|
|
|
7
|
-
old_env, old_redis = [ENV.delete(
|
|
7
|
+
old_env, old_redis = [ENV.delete("REDIS_URL"), Split.redis]
|
|
8
8
|
ex.run
|
|
9
|
-
ENV[
|
|
9
|
+
ENV["REDIS_URL"] = old_env
|
|
10
10
|
Split.redis = old_redis
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
describe
|
|
14
|
-
it
|
|
15
|
-
Split.redis =
|
|
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
18
|
client = Split.redis.connection
|
|
@@ -20,8 +20,8 @@ RSpec.describe Split do
|
|
|
20
20
|
expect(client[:port]).to eq(6379)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
it
|
|
24
|
-
Split.redis = {host:
|
|
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
27
|
client = Split.redis.connection
|
|
@@ -30,13 +30,13 @@ RSpec.describe Split do
|
|
|
30
30
|
expect(client[:db]).to eq(12)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
it
|
|
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
|
|
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
|
data/spec/trial_spec.rb
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require
|
|
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) { [
|
|
8
|
+
let(:alternatives) { ["basket", "cart"] }
|
|
8
9
|
let(:experiment) do
|
|
9
|
-
Split::Experiment.new(
|
|
10
|
+
Split::Experiment.new("basket_text", alternatives: alternatives).save
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
it "should be initializeable" do
|
|
13
|
-
experiment = double(
|
|
14
|
-
alternative = double(
|
|
15
|
-
trial = Split::Trial.new(:
|
|
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(
|
|
23
|
-
trial = Split::Trial.new(:
|
|
24
|
-
:
|
|
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(
|
|
31
|
+
experiment = Split::Experiment.new("basket_text", alternatives: ["basket", "cart"])
|
|
31
32
|
experiment.save
|
|
32
33
|
|
|
33
|
-
trial = Split::Trial.new(:
|
|
34
|
-
expect(trial.alternative.name).to eq(
|
|
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(
|
|
42
|
+
Split::Experiment.new("basket_text", alternatives: alternatives, metadata: metadata).save
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
it
|
|
45
|
-
trial = Split::Trial.new(:
|
|
46
|
-
:
|
|
47
|
-
expect(trial.metadata).to eq(metadata[
|
|
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
|
|
51
|
-
trial = Split::Trial.new(:
|
|
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:
|
|
60
|
+
let(:context) { double(on_trial_callback: "test callback") }
|
|
60
61
|
let(:trial) do
|
|
61
|
-
Split::Trial.new(:
|
|
62
|
+
Split::Trial.new(user: user, experiment: experiment)
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
shared_examples_for
|
|
65
|
-
it
|
|
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
|
|
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
|
|
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) {
|
|
92
|
+
let(:override) { "cart" }
|
|
92
93
|
let(:trial) do
|
|
93
|
-
Split::Trial.new(:
|
|
94
|
+
Split::Trial.new(user: user, experiment: experiment, override: override)
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
it_behaves_like
|
|
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
|
|
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(:
|
|
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,
|
|
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,
|
|
136
|
+
expect_alternative(trial, "basket")
|
|
136
137
|
|
|
137
138
|
Split.configuration.enabled = true
|
|
138
139
|
Split.configuration.on_trial = nil
|
|
@@ -141,45 +142,45 @@ describe Split::Trial do
|
|
|
141
142
|
|
|
142
143
|
context "when experiment has winner" do
|
|
143
144
|
let(:trial) do
|
|
144
|
-
Split::Trial.new(:
|
|
145
|
+
Split::Trial.new(user: user, experiment: experiment)
|
|
145
146
|
end
|
|
146
147
|
|
|
147
|
-
it_behaves_like
|
|
148
|
+
it_behaves_like "a trial with callbacks"
|
|
148
149
|
|
|
149
150
|
it "picks the winner" do
|
|
150
|
-
experiment.winner =
|
|
151
|
+
experiment.winner = "cart"
|
|
151
152
|
expect(experiment).to_not receive(:next_alternative)
|
|
152
153
|
|
|
153
|
-
expect_alternative(trial,
|
|
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(:
|
|
160
|
+
Split::Trial.new(user: user, experiment: experiment, exclude: true)
|
|
160
161
|
end
|
|
161
162
|
|
|
162
|
-
it_behaves_like
|
|
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,
|
|
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
|
|
172
|
+
it_behaves_like "a trial with callbacks"
|
|
172
173
|
|
|
173
174
|
it "picks the same alternative" do
|
|
174
|
-
user[experiment.key] =
|
|
175
|
+
user[experiment.key] = "basket"
|
|
175
176
|
expect(experiment).to_not receive(:next_alternative)
|
|
176
177
|
|
|
177
|
-
expect_alternative(trial,
|
|
178
|
+
expect_alternative(trial, "basket")
|
|
178
179
|
end
|
|
179
180
|
|
|
180
181
|
context "when alternative is not found" do
|
|
181
182
|
it "falls back on next_alternative" do
|
|
182
|
-
user[experiment.key] =
|
|
183
|
+
user[experiment.key] = "notfound"
|
|
183
184
|
expect(experiment).to receive(:next_alternative).and_call_original
|
|
184
185
|
expect_alternative(trial, alternatives)
|
|
185
186
|
end
|
|
@@ -198,42 +199,68 @@ describe Split::Trial do
|
|
|
198
199
|
expect(trial.alternative.name).to_not be_empty
|
|
199
200
|
Split.configuration.on_trial_choose = nil
|
|
200
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
|
|
201
229
|
end
|
|
202
230
|
end
|
|
203
231
|
|
|
204
232
|
describe "#complete!" do
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
it
|
|
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
|
|
208
236
|
trial.choose!
|
|
209
237
|
old_completed_count = trial.alternative.completed_count
|
|
210
238
|
trial.complete!
|
|
211
|
-
expect(trial.alternative.completed_count).to
|
|
239
|
+
expect(trial.alternative.completed_count).to eq(old_completed_count + 1)
|
|
212
240
|
end
|
|
213
241
|
end
|
|
214
242
|
|
|
215
|
-
context
|
|
216
|
-
let(:goals) { [
|
|
217
|
-
let(:trial) { Split::Trial.new(:
|
|
218
|
-
shared_examples_for "goal completion" do
|
|
219
|
-
it 'should not complete the trial' do
|
|
220
|
-
trial.choose!
|
|
221
|
-
old_completed_count = trial.alternative.completed_count
|
|
222
|
-
trial.complete!(goal)
|
|
223
|
-
expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
|
|
224
|
-
end
|
|
225
|
-
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) }
|
|
226
246
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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) }
|
|
230
252
|
end
|
|
253
|
+
end
|
|
231
254
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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)
|
|
235
263
|
end
|
|
236
|
-
|
|
237
264
|
end
|
|
238
265
|
end
|
|
239
266
|
|
|
@@ -242,7 +269,7 @@ describe Split::Trial do
|
|
|
242
269
|
|
|
243
270
|
context "when override is present" do
|
|
244
271
|
it "stores when store_override is true" do
|
|
245
|
-
trial = Split::Trial.new(:
|
|
272
|
+
trial = Split::Trial.new(user: user, experiment: experiment, override: "basket")
|
|
246
273
|
|
|
247
274
|
Split.configuration.store_override = true
|
|
248
275
|
expect(user).to receive("[]=")
|
|
@@ -251,7 +278,7 @@ describe Split::Trial do
|
|
|
251
278
|
end
|
|
252
279
|
|
|
253
280
|
it "does not store when store_override is false" do
|
|
254
|
-
trial = Split::Trial.new(:
|
|
281
|
+
trial = Split::Trial.new(user: user, experiment: experiment, override: "basket")
|
|
255
282
|
|
|
256
283
|
expect(user).to_not receive("[]=")
|
|
257
284
|
trial.choose!
|
|
@@ -260,7 +287,7 @@ describe Split::Trial do
|
|
|
260
287
|
|
|
261
288
|
context "when disabled is present" do
|
|
262
289
|
it "stores when store_override is true" do
|
|
263
|
-
trial = Split::Trial.new(:
|
|
290
|
+
trial = Split::Trial.new(user: user, experiment: experiment, disabled: true)
|
|
264
291
|
|
|
265
292
|
Split.configuration.store_override = true
|
|
266
293
|
expect(user).to receive("[]=")
|
|
@@ -268,7 +295,7 @@ describe Split::Trial do
|
|
|
268
295
|
end
|
|
269
296
|
|
|
270
297
|
it "does not store when store_override is false" do
|
|
271
|
-
trial = Split::Trial.new(:
|
|
298
|
+
trial = Split::Trial.new(user: user, experiment: experiment, disabled: true)
|
|
272
299
|
|
|
273
300
|
expect(user).to_not receive("[]=")
|
|
274
301
|
trial.choose!
|
|
@@ -277,20 +304,20 @@ describe Split::Trial do
|
|
|
277
304
|
|
|
278
305
|
context "when exclude is present" do
|
|
279
306
|
it "does not store" do
|
|
280
|
-
trial = Split::Trial.new(:
|
|
307
|
+
trial = Split::Trial.new(user: user, experiment: experiment, exclude: true)
|
|
281
308
|
|
|
282
309
|
expect(user).to_not receive("[]=")
|
|
283
310
|
trial.choose!
|
|
284
311
|
end
|
|
285
312
|
end
|
|
286
313
|
|
|
287
|
-
context
|
|
314
|
+
context "when experiment has winner" do
|
|
288
315
|
let(:trial) do
|
|
289
|
-
experiment.winner =
|
|
290
|
-
Split::Trial.new(:
|
|
316
|
+
experiment.winner = "cart"
|
|
317
|
+
Split::Trial.new(user: user, experiment: experiment)
|
|
291
318
|
end
|
|
292
319
|
|
|
293
|
-
it
|
|
320
|
+
it "does not store" do
|
|
294
321
|
expect(user).to_not receive("[]=")
|
|
295
322
|
trial.choose!
|
|
296
323
|
end
|