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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZDNjZTFjMmI3OTZlMDQyOTlkYTdjYzU5ZmNhZjMxYjcxODVhYmZmMQ==
5
- data.tar.gz: !binary |-
6
- MGU5YWNkZDY2YmIzNjhmZDAwNjFkODIzZGI0MGRmZjMwMWEwYzFlZg==
2
+ SHA1:
3
+ metadata.gz: bc058be7df27d778fc5c3d4333c1570c64edccc3
4
+ data.tar.gz: 7095d5c3a2e6408f29018ea2490f978debe05ed9
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MDI2ZDI3OTVhYjljZjU1Y2NiNDAyMWMwYTIyMTU1NTgwZGNhNTEzNWI0Nzdl
10
- ZWYwNGM2MDA1ODE3ZWE5ZjFkOGZlZmI5MWRkY2IxOTI2ZDQ4MzZjNTA4N2Ex
11
- MmYxZmNhMmYzOGM3Y2FkNTFiMDhmMTQ1ZjUwMzBjZjE2ZTViMGU=
12
- data.tar.gz: !binary |-
13
- YTYxYTM4NGMxNjUwOWUyM2ZkYjI2NTZhOTFmOGRlYjMzMWQ1ZjU4YmIzOTUy
14
- ZDQ2OTA0ZjNmZmY1OWJjMWVlOTI3NjliNzFmNmRkZjBkYzQ3NjNhNDIyODE1
15
- NjQxYmM0YjI3MGE1MTc0N2E4NWQ2MjczMDRhYmM0N2I1YTAxZDk=
6
+ metadata.gz: 0a74b192fd5d4a03a6524e9c8f88d761b8eb2e83e6eb33957f46d429d526d9506eaf9787ae6d3665a80424d077f32d1dbb72498b1c7a81bb6a5380d581662155
7
+ data.tar.gz: c73434af18dcaf65c05689daf3686ddc42a9bd6334018be0d7369fd577742c92ad6630ca4b82d539ba7ddb3aaa6a379a0a0204aa35cb9375dac3e09bd14b7229
@@ -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
@@ -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::Segmenter::StaticPercentage, &block)
42
- return @segmenter.groups unless block_given?
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 = nil)
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
- subject_qualifies?(subject, context) ?
203
- subject_assignment(subject_identifier, @segmenter.assign(subject_identifier, subject, context), nil) :
204
- subject_assignment(subject_identifier, nil, nil)
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
- subject_assignment(subject_identifier, @segmenter.assign(subject_identifier, subject, context), nil)
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
@@ -1,78 +1,87 @@
1
1
  require 'digest/md5'
2
2
 
3
- module Verdict::Segmenter
4
-
5
- class Base
6
-
7
- attr_reader :experiment, :groups
8
-
9
- def initialize(experiment)
10
- @experiment = experiment
11
- @groups = {}
12
- end
13
-
14
- def verify!
15
- end
16
-
17
- def group(identifier, subject, context)
18
- raise NotImplementedError
19
- end
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
- class StaticPercentage < Base
23
-
24
- class Group < Verdict::Group
25
-
26
- attr_reader :percentile_range
27
-
28
- def initialize(experiment, handle, percentile_range)
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
- def initialize(experiment)
47
- super
48
- @total_percentage_segmented = 0
49
- end
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
- def verify!
52
- raise Verdict::SegmentationError, "Should segment exactly 100% of the cases, but segments add up to #{@total_percentage_segmented}%." if @total_percentage_segmented != 100
53
- end
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
- def group(handle, size, &block)
56
- percentage = size.kind_of?(Hash) && size[:percentage] ? size[:percentage] : size
57
- n = case percentage
58
- when :rest; 100 - @total_percentage_segmented
59
- when :half; 50
60
- when Integer; percentage
61
- else Integer(percentage)
62
- end
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
- def assign(identifier, subject, context)
72
- percentile = Digest::MD5.hexdigest("#{@experiment.handle}#{identifier}").to_i(16) % 100
73
- _, group = groups.find { |_, group| group.percentile_range.include?(percentile) }
74
- raise Verdict::SegmentationError, "Could not get segment for subject #{identifier.inspect}!" unless group
75
- group
76
- end
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
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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 = stub(id: 'test_subject'), :my_goal)
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 StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
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.1.1
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-17 00:00:00.000000000 Z
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/static_percentage_segmenter_test.rb
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.1.4
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/static_percentage_segmenter_test.rb
161
+ - test/rollout_segmenter_test.rb
162
+ - test/static_segmenter_test.rb
156
163
  - test/test_helper.rb
157
- has_rdoc: