verdict 0.1.1 → 0.2.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.
- 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:
|