verdict 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/lib/verdict/assignment.rb +10 -1
- data/lib/verdict/experiment.rb +25 -13
- data/lib/verdict/fixed_percentage_segmenter.rb +52 -0
- data/lib/verdict/rollout_segmenter.rb +53 -0
- data/lib/verdict/segmenter.rb +76 -67
- data/lib/verdict/static_segmenter.rb +24 -0
- data/lib/verdict/version.rb +1 -1
- data/test/experiment_test.rb +21 -3
- data/test/{static_percentage_segmenter_test.rb → fixed_percentage_segmenter_test.rb} +8 -14
- data/test/rollout_segmenter_test.rb +29 -0
- data/test/static_segmenter_test.rb +25 -0
- metadata +22 -16
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
MGU5YWNkZDY2YmIzNjhmZDAwNjFkODIzZGI0MGRmZjMwMWEwYzFlZg==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bc058be7df27d778fc5c3d4333c1570c64edccc3
|
4
|
+
data.tar.gz: 7095d5c3a2e6408f29018ea2490f978debe05ed9
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
ZWYwNGM2MDA1ODE3ZWE5ZjFkOGZlZmI5MWRkY2IxOTI2ZDQ4MzZjNTA4N2Ex
|
11
|
-
MmYxZmNhMmYzOGM3Y2FkNTFiMDhmMTQ1ZjUwMzBjZjE2ZTViMGU=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
YTYxYTM4NGMxNjUwOWUyM2ZkYjI2NTZhOTFmOGRlYjMzMWQ1ZjU4YmIzOTUy
|
14
|
-
ZDQ2OTA0ZjNmZmY1OWJjMWVlOTI3NjliNzFmNmRkZjBkYzQ3NjNhNDIyODE1
|
15
|
-
NjQxYmM0YjI3MGE1MTc0N2E4NWQ2MjczMDRhYmM0N2I1YTAxZDk=
|
6
|
+
metadata.gz: 0a74b192fd5d4a03a6524e9c8f88d761b8eb2e83e6eb33957f46d429d526d9506eaf9787ae6d3665a80424d077f32d1dbb72498b1c7a81bb6a5380d581662155
|
7
|
+
data.tar.gz: c73434af18dcaf65c05689daf3686ddc42a9bd6334018be0d7369fd577742c92ad6630ca4b82d539ba7ddb3aaa6a379a0a0204aa35cb9375dac3e09bd14b7229
|
data/lib/verdict/assignment.rb
CHANGED
@@ -2,12 +2,13 @@ class Verdict::Assignment
|
|
2
2
|
|
3
3
|
attr_reader :experiment, :subject_identifier, :group, :created_at
|
4
4
|
|
5
|
-
def initialize(experiment, subject_identifier, group, originally_created_at)
|
5
|
+
def initialize(experiment, subject_identifier, group, originally_created_at, temporary = false)
|
6
6
|
@experiment = experiment
|
7
7
|
@subject_identifier = subject_identifier
|
8
8
|
@group = group
|
9
9
|
@returning = !originally_created_at.nil?
|
10
10
|
@created_at = originally_created_at || Time.now.utc
|
11
|
+
@temporary = temporary
|
11
12
|
end
|
12
13
|
|
13
14
|
def subject
|
@@ -18,6 +19,14 @@ class Verdict::Assignment
|
|
18
19
|
!group.nil?
|
19
20
|
end
|
20
21
|
|
22
|
+
def permanent?
|
23
|
+
!@temporary
|
24
|
+
end
|
25
|
+
|
26
|
+
def temporary?
|
27
|
+
@temporary
|
28
|
+
end
|
29
|
+
|
21
30
|
def returning
|
22
31
|
self.class.new(@experiment, @subject_identifier, @group, @created_at)
|
23
32
|
end
|
data/lib/verdict/experiment.rb
CHANGED
@@ -38,14 +38,20 @@ class Verdict::Experiment
|
|
38
38
|
segmenter.groups[handle.to_s]
|
39
39
|
end
|
40
40
|
|
41
|
-
def groups(segmenter_class = Verdict::
|
42
|
-
return
|
41
|
+
def groups(segmenter_class = Verdict::FixedPercentageSegmenter, &block)
|
42
|
+
return segmenter.groups unless block_given?
|
43
43
|
@segmenter ||= segmenter_class.new(self)
|
44
44
|
@segmenter.instance_eval(&block)
|
45
45
|
@segmenter.verify!
|
46
46
|
return self
|
47
47
|
end
|
48
48
|
|
49
|
+
def rollout_percentage(percentage, rollout_group_name = :enabled)
|
50
|
+
groups(Verdict::RolloutSegmenter) do
|
51
|
+
group rollout_group_name, percentage
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
49
55
|
def qualify(&block)
|
50
56
|
@qualifier = block
|
51
57
|
end
|
@@ -72,8 +78,8 @@ class Verdict::Experiment
|
|
72
78
|
segmenter.groups.keys
|
73
79
|
end
|
74
80
|
|
75
|
-
def subject_assignment(subject_identifier, group, originally_created_at =
|
76
|
-
Verdict::Assignment.new(self, subject_identifier, group, originally_created_at)
|
81
|
+
def subject_assignment(subject_identifier, group, originally_created_at, temporary = false)
|
82
|
+
Verdict::Assignment.new(self, subject_identifier, group, originally_created_at, temporary)
|
77
83
|
end
|
78
84
|
|
79
85
|
def subject_conversion(subject_identifier, goal, created_at = Time.now.utc)
|
@@ -84,6 +90,7 @@ class Verdict::Experiment
|
|
84
90
|
identifier = retrieve_subject_identifier(subject)
|
85
91
|
conversion = subject_conversion(identifier, goal)
|
86
92
|
event_logger.log_conversion(conversion)
|
93
|
+
segmenter.conversion_feedback(identifier, subject, conversion)
|
87
94
|
conversion
|
88
95
|
rescue Verdict::EmptySubjectIdentifier
|
89
96
|
raise unless disqualify_empty_identifier?
|
@@ -194,25 +201,30 @@ class Verdict::Experiment
|
|
194
201
|
end
|
195
202
|
|
196
203
|
def should_store_assignment?(assignment)
|
197
|
-
!assignment.returning? && (store_unqualified? || assignment.qualified?)
|
204
|
+
assignment.permanent? && !assignment.returning? && (store_unqualified? || assignment.qualified?)
|
198
205
|
end
|
199
206
|
|
200
207
|
def assignment_with_unqualified_persistence(subject_identifier, subject, context)
|
201
|
-
fetch_assignment(subject_identifier)
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
208
|
+
previous_assignment = fetch_assignment(subject_identifier)
|
209
|
+
return previous_assignment unless previous_assignment.nil?
|
210
|
+
if subject_qualifies?(subject, context)
|
211
|
+
group = segmenter.assign(subject_identifier, subject, context)
|
212
|
+
subject_assignment(subject_identifier, group, nil, group.nil?)
|
213
|
+
else
|
214
|
+
subject_assignment(subject_identifier, nil, nil)
|
215
|
+
end
|
206
216
|
end
|
207
217
|
|
208
218
|
def assignment_without_unqualified_persistence(subject_identifier, subject, context)
|
209
219
|
if subject_qualifies?(subject, context)
|
210
|
-
fetch_assignment(subject_identifier)
|
211
|
-
|
220
|
+
previous_assignment = fetch_assignment(subject_identifier)
|
221
|
+
return previous_assignment unless previous_assignment.nil?
|
222
|
+
group = segmenter.assign(subject_identifier, subject, context)
|
223
|
+
subject_assignment(subject_identifier, group, nil, group.nil?)
|
212
224
|
else
|
213
225
|
subject_assignment(subject_identifier, nil, nil)
|
214
226
|
end
|
215
|
-
end
|
227
|
+
end
|
216
228
|
|
217
229
|
def subject_identifier(subject)
|
218
230
|
subject.respond_to?(:id) ? subject.id : subject.to_s
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Verdict::FixedPercentageSegmenter < Verdict::Segmenter
|
2
|
+
|
3
|
+
class Group < Verdict::Group
|
4
|
+
|
5
|
+
attr_reader :percentile_range
|
6
|
+
|
7
|
+
def initialize(experiment, handle, percentile_range)
|
8
|
+
super(experiment, handle)
|
9
|
+
@percentile_range = percentile_range
|
10
|
+
end
|
11
|
+
|
12
|
+
def percentage_size
|
13
|
+
percentile_range.end - percentile_range.begin
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#{handle} (#{percentage_size}%)"
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json(options = {})
|
21
|
+
super(options).merge(percentage: percentage_size)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(experiment)
|
26
|
+
super
|
27
|
+
@total_percentage_segmented = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def verify!
|
31
|
+
raise Verdict::SegmentationError, "Should segment exactly 100% of the cases, but segments add up to #{@total_percentage_segmented}%." if @total_percentage_segmented != 100
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_group(handle, size)
|
35
|
+
percentage = size.kind_of?(Hash) && size[:percentage] ? size[:percentage] : size
|
36
|
+
n = case percentage
|
37
|
+
when :rest; 100 - @total_percentage_segmented
|
38
|
+
when :half; 50
|
39
|
+
when Integer; percentage
|
40
|
+
else Integer(percentage)
|
41
|
+
end
|
42
|
+
|
43
|
+
group = Group.new(experiment, handle, @total_percentage_segmented ... (@total_percentage_segmented + n))
|
44
|
+
@total_percentage_segmented += n
|
45
|
+
return group
|
46
|
+
end
|
47
|
+
|
48
|
+
def assign(identifier, subject, context)
|
49
|
+
percentile = Digest::MD5.hexdigest("#{@experiment.handle}#{identifier}").to_i(16) % 100
|
50
|
+
groups.values.find { |group| group.percentile_range.include?(percentile) }
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Verdict::RolloutSegmenter < Verdict::Segmenter
|
2
|
+
|
3
|
+
class Group < Verdict::Group
|
4
|
+
|
5
|
+
attr_reader :percentile_range
|
6
|
+
|
7
|
+
def initialize(experiment, handle, percentile_range)
|
8
|
+
super(experiment, handle)
|
9
|
+
@percentile_range = percentile_range
|
10
|
+
end
|
11
|
+
|
12
|
+
def percentage_size
|
13
|
+
percentile_range.end - percentile_range.begin
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#{handle} (#{percentage_size}%)"
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json(options = {})
|
21
|
+
super(options).merge(percentage: percentage_size)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(experiment)
|
26
|
+
super
|
27
|
+
@total_percentage_segmented = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def verify!
|
31
|
+
raise Verdict::SegmentationError, "Should segment less than 100% of the cases, but segments add up to #{@total_percentage_segmented}%." if @total_percentage_segmented >= 100
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_group(handle, size)
|
35
|
+
percentage = size.kind_of?(Hash) && size[:percentage] ? size[:percentage] : size
|
36
|
+
n = case percentage
|
37
|
+
when :rest; 100 - @total_percentage_segmented
|
38
|
+
when :half; 50
|
39
|
+
when Integer; percentage
|
40
|
+
else Integer(percentage)
|
41
|
+
end
|
42
|
+
|
43
|
+
group = Group.new(experiment, handle, @total_percentage_segmented ... (@total_percentage_segmented + n))
|
44
|
+
@total_percentage_segmented += n
|
45
|
+
return group
|
46
|
+
end
|
47
|
+
|
48
|
+
def assign(identifier, subject, context)
|
49
|
+
percentile = Digest::MD5.hexdigest("#{@experiment.handle}#{identifier}").to_i(16) % 100
|
50
|
+
groups.values.find { |group| group.percentile_range.include?(percentile) }
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/verdict/segmenter.rb
CHANGED
@@ -1,78 +1,87 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
3
|
+
# Base class of all segmenters.
|
4
|
+
#
|
5
|
+
# The segmenter is responsible for assigning subjects to groups. You can
|
6
|
+
# implement any assignment strategy you like by subclassing this class and
|
7
|
+
# using it in your experiment.
|
8
|
+
#
|
9
|
+
# - You should implement the register_group method for the experiment definition DSL
|
10
|
+
# to make the system aware of the groups that the segmenter could return.
|
11
|
+
# - The verify! method is called after all the groups have been defined, so it can
|
12
|
+
# detect internal inconsistencies in the group definitions.
|
13
|
+
# - The assign method is where your assignment magic lives.
|
14
|
+
class Verdict::Segmenter
|
15
|
+
|
16
|
+
# The experiment to which this segmenter is associated
|
17
|
+
attr_reader :experiment
|
18
|
+
|
19
|
+
# A hash of the groups that are defined in this experiment, indexed by their
|
20
|
+
# handle. The assign method should return one of the groups in this hash
|
21
|
+
attr_reader :groups
|
22
|
+
|
23
|
+
def initialize(experiment)
|
24
|
+
@experiment = experiment
|
25
|
+
@groups = {}
|
20
26
|
end
|
21
27
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
super(experiment, handle)
|
30
|
-
@percentile_range = percentile_range
|
31
|
-
end
|
32
|
-
|
33
|
-
def percentage_size
|
34
|
-
percentile_range.end - percentile_range.begin
|
35
|
-
end
|
36
|
-
|
37
|
-
def to_s
|
38
|
-
"#{handle} (#{percentage_size}%)"
|
39
|
-
end
|
40
|
-
|
41
|
-
def as_json(options = {})
|
42
|
-
super(options).merge(percentage: percentage_size)
|
43
|
-
end
|
44
|
-
end
|
28
|
+
# DSL method to register a group. It calls the register_group method of the
|
29
|
+
# segmenter implementation
|
30
|
+
def group(handle, *args, &block)
|
31
|
+
group = register_group(handle, *args)
|
32
|
+
@groups[group.handle] = group
|
33
|
+
group.instance_eval(&block) if block_given?
|
34
|
+
end
|
45
35
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
36
|
+
# The group method is called from the experiment definition DSL.
|
37
|
+
# It should register a new group to the segmenter, with the given handle.
|
38
|
+
#
|
39
|
+
# - The handle parameter is a symbol that uniquely identifies the group within
|
40
|
+
# this experiment.
|
41
|
+
# - The return value of this method should be a Verdict::Group instance.
|
42
|
+
def register_group(handle, *args)
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
50
45
|
|
51
|
-
|
52
|
-
|
53
|
-
|
46
|
+
# The verify! method is called after all the groups have been defined in the
|
47
|
+
# experiment definition DSL. You can run any consistency checks in this method,
|
48
|
+
# and if anything is off, you can raise a Verdict::SegmentationError to
|
49
|
+
# signify the problem.
|
50
|
+
def verify!
|
51
|
+
# noop by default
|
52
|
+
end
|
54
53
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
54
|
+
# The assign method is called to assign a subject to one of the groups that have been defined
|
55
|
+
# in the segmenter implementation.
|
56
|
+
#
|
57
|
+
# - The identifier parameter is a string that uniquely identifies the subject.
|
58
|
+
# - The subject paramater is the subject instance that was passed to the framework,
|
59
|
+
# when the application code calls Experiment#assign or Experiment#switch.
|
60
|
+
# - The context parameter is an object that was passed to the framework, you can use this
|
61
|
+
# object any way you like in your segmenting logic.
|
62
|
+
#
|
63
|
+
# This method should return the Verdict::Group instance to which the subject should be assigned.
|
64
|
+
# This instance should be one of the group instance that was registered in the definition DSL.
|
65
|
+
def assign(identifier, subject, context)
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
63
68
|
|
64
|
-
group = Group.new(experiment, handle, @total_percentage_segmented ... (@total_percentage_segmented + n))
|
65
|
-
@groups[group.handle] = group
|
66
|
-
@total_percentage_segmented += n
|
67
|
-
group.instance_eval(&block) if block_given?
|
68
|
-
return group
|
69
|
-
end
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
70
|
+
# This method is called whenever a subjects converts to a goal, i.e., when Experiment#convert
|
71
|
+
# is called. You can use this to implement a feedback loop in your segmenter.
|
72
|
+
#
|
73
|
+
# - The identifier parameter is a string that uniquely identifies the subject.
|
74
|
+
# - The subject paramater is the subject instance that was passed to the framework,
|
75
|
+
# when the application code calls Experiment#assign or Experiment#switch.
|
76
|
+
# - The conversion parameter is a Verdict::Conversion instance that describes what
|
77
|
+
# goal the subject converted to.
|
78
|
+
#
|
79
|
+
# The return value of this method is not used.
|
80
|
+
def conversion_feedback(identifier, subject, conversion)
|
81
|
+
# noop by default
|
77
82
|
end
|
78
83
|
end
|
84
|
+
|
85
|
+
require 'verdict/static_segmenter'
|
86
|
+
require 'verdict/fixed_percentage_segmenter'
|
87
|
+
require 'verdict/rollout_segmenter'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Verdict::StaticSegmenter < Verdict::Segmenter
|
2
|
+
|
3
|
+
class Group < Verdict::Group
|
4
|
+
|
5
|
+
attr_reader :subject_identifiers
|
6
|
+
|
7
|
+
def initialize(experiment, handle, subject_identifiers)
|
8
|
+
super(experiment, handle)
|
9
|
+
@subject_identifiers = subject_identifiers
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_json(options = {})
|
13
|
+
super(options).merge(subject_identifiers: subject_identifiers)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def register_group(handle, subject_identifiers)
|
18
|
+
Group.new(experiment, handle, subject_identifiers)
|
19
|
+
end
|
20
|
+
|
21
|
+
def assign(identifier, subject, context)
|
22
|
+
groups.values.find { |group| group.subject_identifiers.include?(identifier) }
|
23
|
+
end
|
24
|
+
end
|
data/lib/verdict/version.rb
CHANGED
data/test/experiment_test.rb
CHANGED
@@ -153,6 +153,20 @@ class ExperimentTest < MiniTest::Unit::TestCase
|
|
153
153
|
e.assign(mock('subject'))
|
154
154
|
end
|
155
155
|
|
156
|
+
def test_dont_store_when_segmenter_returns_nil
|
157
|
+
mock_store = Verdict::Storage::MockStorage.new
|
158
|
+
e = Verdict::Experiment.new('test') do
|
159
|
+
groups { group :all, 100 }
|
160
|
+
storage mock_store, store_unqualified: true
|
161
|
+
end
|
162
|
+
|
163
|
+
e.segmenter.stubs(:assign).returns(nil)
|
164
|
+
mock_store.expects(:store_assignment).never
|
165
|
+
|
166
|
+
assignment = e.assign(mock('subject'))
|
167
|
+
assert !assignment.qualified?
|
168
|
+
end
|
169
|
+
|
156
170
|
def test_disqualify
|
157
171
|
e = Verdict::Experiment.new('test') do
|
158
172
|
groups { group :all, 100 }
|
@@ -177,12 +191,16 @@ class ExperimentTest < MiniTest::Unit::TestCase
|
|
177
191
|
end
|
178
192
|
|
179
193
|
def test_conversion_event_logging
|
180
|
-
e = Verdict::Experiment.new('test')
|
194
|
+
e = Verdict::Experiment.new('test')do
|
195
|
+
groups { group :all, 100 }
|
196
|
+
end
|
181
197
|
|
198
|
+
subject = stub(id: 'test_subject')
|
182
199
|
e.stubs(:event_logger).returns(logger = mock('logger'))
|
183
200
|
logger.expects(:log_conversion).with(kind_of(Verdict::Conversion))
|
201
|
+
e.segmenter.expects(:conversion_feedback).with('test_subject', subject, kind_of(Verdict::Conversion))
|
184
202
|
|
185
|
-
conversion = e.convert(subject
|
203
|
+
conversion = e.convert(subject, :my_goal)
|
186
204
|
assert_equal 'test_subject', conversion.subject_identifier
|
187
205
|
assert_equal :my_goal, conversion.goal
|
188
206
|
end
|
@@ -229,7 +247,7 @@ class ExperimentTest < MiniTest::Unit::TestCase
|
|
229
247
|
storage storage_mock
|
230
248
|
end
|
231
249
|
|
232
|
-
storage_mock.expects(:retrieve_assignment).returns(e.subject_assignment(mock('subject_identifier'), e.group(:all)))
|
250
|
+
storage_mock.expects(:retrieve_assignment).returns(e.subject_assignment(mock('subject_identifier'), e.group(:all), nil))
|
233
251
|
storage_mock.expects(:store_assignment).raises(Verdict::StorageError, 'storage write issues')
|
234
252
|
rescued_assignment = e.assign(stub(id: 456))
|
235
253
|
assert !rescued_assignment.qualified?
|
@@ -1,15 +1,9 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class
|
4
|
-
|
5
|
-
MockExperiment = Struct.new(:handle)
|
6
|
-
|
7
|
-
def setup
|
8
|
-
Verdict.repository.clear
|
9
|
-
end
|
3
|
+
class FixedPercentageSegmenterTest < MiniTest::Unit::TestCase
|
10
4
|
|
11
5
|
def test_add_up_to_100_percent
|
12
|
-
s = Verdict::
|
6
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
13
7
|
s.group :segment1, 1
|
14
8
|
s.group :segment2, 54
|
15
9
|
s.group :segment3, 27
|
@@ -24,7 +18,7 @@ class StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
|
|
24
18
|
end
|
25
19
|
|
26
20
|
def test_definition_ofhalf_and_rest
|
27
|
-
s = Verdict::
|
21
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
28
22
|
s.group :first_half, :half
|
29
23
|
s.group :second_half, :rest
|
30
24
|
s.verify!
|
@@ -36,7 +30,7 @@ class StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
|
|
36
30
|
|
37
31
|
def test_raises_if_less_than_100_percent
|
38
32
|
assert_raises(Verdict::SegmentationError) do
|
39
|
-
s = Verdict::
|
33
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
40
34
|
s.group :too_little, 99
|
41
35
|
s.verify!
|
42
36
|
end
|
@@ -44,14 +38,14 @@ class StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
|
|
44
38
|
|
45
39
|
def test_raises_if_greather_than_100_percent
|
46
40
|
assert_raises(Verdict::SegmentationError) do
|
47
|
-
s = Verdict::
|
41
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
48
42
|
s.group :too_much, 101
|
49
43
|
s.verify!
|
50
44
|
end
|
51
45
|
end
|
52
46
|
|
53
47
|
def test_consistent_assignment_for_subjects
|
54
|
-
s = Verdict::
|
48
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
55
49
|
s.group :first_half, :half
|
56
50
|
s.group :second_half, :rest
|
57
51
|
s.verify!
|
@@ -63,7 +57,7 @@ class StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
|
|
63
57
|
end
|
64
58
|
|
65
59
|
def test_fair_segmenting
|
66
|
-
s = Verdict::
|
60
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
67
61
|
s.group :first_third, 33
|
68
62
|
s.group :second_third, 33
|
69
63
|
s.group :final_third, :rest
|
@@ -82,7 +76,7 @@ class StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
|
|
82
76
|
end
|
83
77
|
|
84
78
|
def test_group_json_export
|
85
|
-
s = Verdict::
|
79
|
+
s = Verdict::FixedPercentageSegmenter.new(Verdict::Experiment.new('test'))
|
86
80
|
s.group :first_third, 33
|
87
81
|
s.group :rest, :rest
|
88
82
|
s.verify!
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RolloutPercentageSegmenterTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@experiment = Verdict::Experiment.new('test') do
|
7
|
+
rollout_percentage 50
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_assignment
|
12
|
+
included_subject = stub(id: 1)
|
13
|
+
excluded_subject = stub(id: 2)
|
14
|
+
|
15
|
+
included_assignment = @experiment.assign(included_subject)
|
16
|
+
assert included_assignment.qualified?
|
17
|
+
assert included_assignment.permanent?
|
18
|
+
|
19
|
+
excluded_assignment = @experiment.assign(excluded_subject)
|
20
|
+
assert !excluded_assignment.qualified?
|
21
|
+
assert excluded_assignment.temporary?
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_group_json_representation
|
25
|
+
json = JSON.parse(@experiment.segmenter.groups['enabled'].to_json)
|
26
|
+
assert_equal 'enabled', json['handle']
|
27
|
+
assert_equal 50, json['percentage']
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class StaticSegmenterTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@segmenter = Verdict::StaticSegmenter.new(Verdict::Experiment.new('test'))
|
7
|
+
@segmenter.group :beta, ['id1', 'id2']
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_group_definition
|
11
|
+
assert_equal ['beta'], @segmenter.groups.keys
|
12
|
+
assert_equal ['id1', 'id2'], @segmenter.groups['beta'].subject_identifiers
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_group_json_representation
|
16
|
+
json = JSON.parse(@segmenter.groups['beta'].to_json)
|
17
|
+
assert_equal 'beta', json['handle']
|
18
|
+
assert_equal ['id1', 'id2'], json['subject_identifiers']
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_assigment
|
22
|
+
assert_equal @segmenter.groups['beta'], @segmenter.assign('id2', stub(id: 'id2'), nil)
|
23
|
+
assert_equal nil, @segmenter.assign('id3', stub(id: 'id3'), nil)
|
24
|
+
end
|
25
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: verdict
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -28,56 +28,56 @@ dependencies:
|
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - '>='
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: mocha
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - '>='
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: timecop
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - '>='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: redis
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - '>='
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - '>='
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
description: Shopify Experiments classes
|
@@ -99,10 +99,13 @@ files:
|
|
99
99
|
- lib/verdict/conversion.rb
|
100
100
|
- lib/verdict/event_logger.rb
|
101
101
|
- lib/verdict/experiment.rb
|
102
|
+
- lib/verdict/fixed_percentage_segmenter.rb
|
102
103
|
- lib/verdict/group.rb
|
103
104
|
- lib/verdict/metadata.rb
|
104
105
|
- lib/verdict/railtie.rb
|
106
|
+
- lib/verdict/rollout_segmenter.rb
|
105
107
|
- lib/verdict/segmenter.rb
|
108
|
+
- lib/verdict/static_segmenter.rb
|
106
109
|
- lib/verdict/storage.rb
|
107
110
|
- lib/verdict/tasks.rake
|
108
111
|
- lib/verdict/version.rb
|
@@ -111,11 +114,13 @@ files:
|
|
111
114
|
- test/event_logger_test.rb
|
112
115
|
- test/experiment_test.rb
|
113
116
|
- test/experiments_repository_test.rb
|
117
|
+
- test/fixed_percentage_segmenter_test.rb
|
114
118
|
- test/group_test.rb
|
115
119
|
- test/memory_subject_storage_test.rb
|
116
120
|
- test/metadata_test.rb
|
117
121
|
- test/redis_subject_storage_test.rb
|
118
|
-
- test/
|
122
|
+
- test/rollout_segmenter_test.rb
|
123
|
+
- test/static_segmenter_test.rb
|
119
124
|
- test/test_helper.rb
|
120
125
|
- verdict.gemspec
|
121
126
|
homepage: http://github.com/Shopify/verdict
|
@@ -127,17 +132,17 @@ require_paths:
|
|
127
132
|
- lib
|
128
133
|
required_ruby_version: !ruby/object:Gem::Requirement
|
129
134
|
requirements:
|
130
|
-
- -
|
135
|
+
- - '>='
|
131
136
|
- !ruby/object:Gem::Version
|
132
137
|
version: '0'
|
133
138
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
139
|
requirements:
|
135
|
-
- -
|
140
|
+
- - '>='
|
136
141
|
- !ruby/object:Gem::Version
|
137
142
|
version: '0'
|
138
143
|
requirements: []
|
139
144
|
rubyforge_project:
|
140
|
-
rubygems_version: 2.
|
145
|
+
rubygems_version: 2.0.14
|
141
146
|
signing_key:
|
142
147
|
specification_version: 4
|
143
148
|
summary: A library to centrally define experiments for your application, and collect
|
@@ -148,10 +153,11 @@ test_files:
|
|
148
153
|
- test/event_logger_test.rb
|
149
154
|
- test/experiment_test.rb
|
150
155
|
- test/experiments_repository_test.rb
|
156
|
+
- test/fixed_percentage_segmenter_test.rb
|
151
157
|
- test/group_test.rb
|
152
158
|
- test/memory_subject_storage_test.rb
|
153
159
|
- test/metadata_test.rb
|
154
160
|
- test/redis_subject_storage_test.rb
|
155
|
-
- test/
|
161
|
+
- test/rollout_segmenter_test.rb
|
162
|
+
- test/static_segmenter_test.rb
|
156
163
|
- test/test_helper.rb
|
157
|
-
has_rdoc:
|