verdict 0.2.1 → 0.3.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 +4 -4
- data/lib/verdict/experiment.rb +28 -10
- data/lib/verdict/storage/memory_storage.rb +2 -2
- data/lib/verdict/tasks.rake +59 -30
- data/lib/verdict/version.rb +1 -1
- data/lib/verdict.rb +1 -1
- data/test/experiment_test.rb +32 -12
- data/test/rake_tasks_test.rb +89 -0
- data/test/storage/redis_subject_storage_test.rb +2 -2
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8efebedf53fee289950045fc2396359440dc0e15
|
4
|
+
data.tar.gz: 94bba89fe604abab1cd2f60b45b66bff0424ee75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 538a77d48203d92c91a42072d9f64e60f9ab3b1d8ee857d380bfcd61328c73f1d9d96682db1a5dbb1c43c8ae4d52c57b38d156863408b77673b879c17ac4fa1a
|
7
|
+
data.tar.gz: c9535d06da2da5bef7a547b9565515f227dc8789d5d690d0b68867a5b5b6835a6dcf7645ee2dfa7a37d072d47cb22b932a0d014bc51c83dc4cff3734de03a0f0
|
data/lib/verdict/experiment.rb
CHANGED
@@ -59,7 +59,7 @@ class Verdict::Experiment
|
|
59
59
|
|
60
60
|
def storage(subject_storage = nil, options = {})
|
61
61
|
return @subject_storage if subject_storage.nil?
|
62
|
-
|
62
|
+
|
63
63
|
@store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
|
64
64
|
@subject_storage = case subject_storage
|
65
65
|
when :memory; Verdict::Storage::MemoryStorage.new
|
@@ -86,7 +86,7 @@ class Verdict::Experiment
|
|
86
86
|
segmenter.groups.keys
|
87
87
|
end
|
88
88
|
|
89
|
-
def subject_assignment(subject_identifier, group, originally_created_at, temporary = false)
|
89
|
+
def subject_assignment(subject_identifier, group, originally_created_at = nil, temporary = false)
|
90
90
|
Verdict::Assignment.new(self, subject_identifier, group, originally_created_at, temporary)
|
91
91
|
end
|
92
92
|
|
@@ -123,26 +123,40 @@ class Verdict::Experiment
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
126
|
-
def
|
127
|
-
|
126
|
+
def assign_manually(subject, group)
|
127
|
+
identifier = retrieve_subject_identifier(subject)
|
128
|
+
assign_manually_by_identifier(identifier, group)
|
128
129
|
end
|
129
130
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
131
|
+
def assign_manually_by_identifier(subject_identifier, group)
|
132
|
+
assignment = subject_assignment(subject_identifier, group)
|
133
|
+
if !assignment.qualified? && !store_unqualified?
|
134
|
+
raise Verdict::Error, "Unqualified subject assignments are not stored for this experiment, so manual disqualification is impossible. Consider setting :store_unqualified to true for this experiment."
|
135
|
+
end
|
136
|
+
|
137
|
+
store_assignment(assignment)
|
138
|
+
assignment
|
139
|
+
end
|
140
|
+
|
141
|
+
def disqualify_manually(subject)
|
142
|
+
assign_manually(subject, nil)
|
133
143
|
end
|
134
144
|
|
145
|
+
def disqualify_manually_by_identifier(subject_identifier)
|
146
|
+
assign_manually_by_identifier(subject_identifier, nil)
|
147
|
+
end
|
148
|
+
|
135
149
|
def store_assignment(assignment)
|
136
150
|
@subject_storage.store_assignment(assignment) if should_store_assignment?(assignment)
|
137
151
|
event_logger.log_assignment(assignment)
|
138
152
|
assignment
|
139
153
|
end
|
140
154
|
|
141
|
-
def
|
142
|
-
|
155
|
+
def remove_subject_assignment(subject)
|
156
|
+
remove_subject_assignment_by_identifier(retrieve_subject_identifier(subject))
|
143
157
|
end
|
144
158
|
|
145
|
-
def
|
159
|
+
def remove_subject_assignment_by_identifier(subject_identifier)
|
146
160
|
@subject_storage.remove_assignment(self, subject_identifier)
|
147
161
|
end
|
148
162
|
|
@@ -202,6 +216,10 @@ class Verdict::Experiment
|
|
202
216
|
@subject_storage.retrieve_assignment(self, subject_identifier)
|
203
217
|
end
|
204
218
|
|
219
|
+
def disqualify_empty_identifier?
|
220
|
+
@disqualify_empty_identifier
|
221
|
+
end
|
222
|
+
|
205
223
|
protected
|
206
224
|
|
207
225
|
def default_options
|
@@ -20,8 +20,8 @@ module Verdict
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def remove_assignment(experiment, subject_identifier)
|
23
|
-
@assignments[
|
24
|
-
@assignments[
|
23
|
+
@assignments[experiment.handle] ||= {}
|
24
|
+
@assignments[experiment.handle].delete(subject_identifier)
|
25
25
|
end
|
26
26
|
|
27
27
|
def clear_experiment(experiment)
|
data/lib/verdict/tasks.rake
CHANGED
@@ -1,59 +1,88 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Verdict
|
2
|
+
module Rake
|
3
|
+
def self.require_env(key)
|
4
|
+
if ENV.has_key?(key.upcase) && !ENV[key.upcase].empty?
|
5
|
+
ENV[key.upcase]
|
6
|
+
elsif ENV.has_key?(key.downcase) && !ENV[key.downcase].empty?
|
7
|
+
ENV[key.downcase]
|
8
|
+
else
|
9
|
+
raise ArgumentError, "Provide #{key.upcase} as environment variable"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.stdout
|
14
|
+
$stdout
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.experiment
|
18
|
+
experiment_handle = Verdict::Rake.require_env('experiment')
|
19
|
+
Verdict[experiment_handle] or raise "Experiment `#{handle}` not found"
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.group
|
23
|
+
group_handle = Verdict::Rake.require_env('group')
|
24
|
+
experiment.group(group_handle) or raise "Group `#{group_handle}` not found."
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.subject_identifier
|
28
|
+
Verdict::Rake.require_env('subject')
|
29
|
+
end
|
30
|
+
end
|
5
31
|
end
|
6
32
|
|
7
|
-
namespace :
|
33
|
+
namespace :verdict do
|
8
34
|
|
9
35
|
desc "List all defined experiments"
|
10
|
-
task :
|
36
|
+
task :experiments => 'environment' do
|
11
37
|
length = Verdict.repository.keys.map(&:length).max
|
12
38
|
Verdict.repository.each do |_, experiment|
|
13
|
-
print "#{experiment.handle.ljust(length)} | "
|
14
|
-
print "Groups: #{experiment.groups.values.map(&:to_s).join(', ')}"
|
15
|
-
puts
|
39
|
+
Verdict::Rake.stdout.print "#{experiment.handle.ljust(length)} | "
|
40
|
+
Verdict::Rake.stdout.print "Groups: #{experiment.groups.values.map(&:to_s).join(', ')}"
|
41
|
+
Verdict::Rake.stdout.puts
|
16
42
|
end
|
17
43
|
end
|
18
44
|
|
19
45
|
desc "Looks up the assignment for a given experiment and subject"
|
20
46
|
task :lookup_assignment => 'environment' do
|
21
|
-
experiment
|
22
|
-
subject_identifier =
|
47
|
+
experiment = Verdict::Rake.experiment
|
48
|
+
subject_identifier = Verdict::Rake.subject_identifier
|
49
|
+
|
23
50
|
assignment = experiment.lookup_assignment_for_identifier(subject_identifier)
|
24
51
|
if assignment.nil?
|
25
|
-
puts "Subject
|
52
|
+
Verdict::Rake.stdout.puts "Subject `#{subject_identifier}` is not assigned to experiment `#{experiment.handle}` yet."
|
26
53
|
elsif assignment.qualified?
|
27
|
-
puts "Subject
|
54
|
+
Verdict::Rake.stdout.puts "Subject `#{subject_identifier}` is assigned to group `#{assignment.group.handle}` of experiment `#{experiment.handle}`."
|
28
55
|
else
|
29
|
-
puts "Subject
|
56
|
+
Verdict::Rake.stdout.puts "Subject `#{subject_identifier}` is unqualified for experiment `#{experiment.handle}`."
|
30
57
|
end
|
31
58
|
end
|
32
59
|
|
33
60
|
desc "Manually assign a subject to a given group in an experiment"
|
34
61
|
task :assign_manually => 'environment' do
|
35
|
-
experiment
|
36
|
-
group
|
37
|
-
|
38
|
-
|
62
|
+
experiment = Verdict::Rake.experiment
|
63
|
+
group = Verdict::Rake.group
|
64
|
+
subject_identifier = Verdict::Rake.subject_identifier
|
65
|
+
|
66
|
+
experiment.assign_manually_by_identifier(Verdict::Rake.require_env('subject'), group)
|
67
|
+
Verdict::Rake.stdout.puts "Subject `#{subject_identifier}` has been assigned to group `#{group.handle}` of experiment `#{experiment.handle}`."
|
39
68
|
end
|
40
69
|
|
41
|
-
desc "
|
42
|
-
task :
|
43
|
-
experiment
|
44
|
-
|
45
|
-
|
70
|
+
desc "Manually disqualify a subject from an experiment"
|
71
|
+
task :disqualify_manually => 'environment' do
|
72
|
+
experiment = Verdict::Rake.experiment
|
73
|
+
subject_identifier = Verdict::Rake.subject_identifier
|
74
|
+
|
75
|
+
experiment.disqualify_manually_by_identifier(subject_identifier)
|
76
|
+
Verdict::Rake.stdout.puts "Subject `#{subject_identifier}` has been disqualified from experiment `#{experiment.handle}`."
|
46
77
|
end
|
47
78
|
|
48
79
|
desc "Removes the assignment for a subject so it will be reassigned to the experiment."
|
49
80
|
task :remove_assignment => 'environment' do
|
50
|
-
experiment
|
51
|
-
|
52
|
-
end
|
81
|
+
experiment = Verdict::Rake.experiment
|
82
|
+
subject_identifier = Verdict::Rake.subject_identifier
|
53
83
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
experiment.wrapup
|
84
|
+
experiment.remove_subject_assignment_by_identifier(subject_identifier)
|
85
|
+
Verdict::Rake.stdout.puts "Removed assignment of subject with identifier `#{subject_identifier}`."
|
86
|
+
Verdict::Rake.stdout.puts "The subject will be reasigned when it encounters the experiment `#{experiment.handle}` again."
|
58
87
|
end
|
59
88
|
end
|
data/lib/verdict/version.rb
CHANGED
data/lib/verdict.rb
CHANGED
data/test/experiment_test.rb
CHANGED
@@ -138,6 +138,38 @@ class ExperimentTest < Minitest::Test
|
|
138
138
|
e.assign(mock('subject'))
|
139
139
|
end
|
140
140
|
|
141
|
+
def test_assign_manually_stores_assignment
|
142
|
+
mock_store = Verdict::Storage::MockStorage.new
|
143
|
+
e = Verdict::Experiment.new('test') do
|
144
|
+
storage mock_store, store_unqualified: true
|
145
|
+
groups { group :all, 100 }
|
146
|
+
end
|
147
|
+
|
148
|
+
group = e.group('all')
|
149
|
+
mock_store.expects(:store_assignment).once
|
150
|
+
e.assign_manually(mock('subject'), group)
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_disqualify_manually
|
154
|
+
e = Verdict::Experiment.new('test', store_unqualified: true) do
|
155
|
+
groups { group :all, 100 }
|
156
|
+
end
|
157
|
+
|
158
|
+
subject = stub(id: 'walrus')
|
159
|
+
original_assignment = e.assign(subject)
|
160
|
+
assert original_assignment.qualified?
|
161
|
+
new_assignment = e.disqualify_manually(subject)
|
162
|
+
assert !new_assignment.qualified?
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_disqualify_manually_fails_with_store_unqualified_disabled
|
166
|
+
e = Verdict::Experiment.new('test', store_unqualified: false) do
|
167
|
+
groups { group :all, 100 }
|
168
|
+
end
|
169
|
+
|
170
|
+
assert_raises(Verdict::Error) { e.disqualify_manually('subject') }
|
171
|
+
end
|
172
|
+
|
141
173
|
def test_returning_qualified_assignment_with_store_unqualified
|
142
174
|
mock_store, mock_qualifier = Verdict::Storage::MockStorage.new, mock('qualifier')
|
143
175
|
e = Verdict::Experiment.new('test') do
|
@@ -167,18 +199,6 @@ class ExperimentTest < Minitest::Test
|
|
167
199
|
assert !assignment.qualified?
|
168
200
|
end
|
169
201
|
|
170
|
-
def test_disqualify
|
171
|
-
e = Verdict::Experiment.new('test') do
|
172
|
-
groups { group :all, 100 }
|
173
|
-
end
|
174
|
-
|
175
|
-
subject = stub(id: 'walrus')
|
176
|
-
original_assignment = e.assign(subject)
|
177
|
-
assert original_assignment.qualified?
|
178
|
-
new_assignment = e.disqualify(subject)
|
179
|
-
assert !new_assignment.qualified?
|
180
|
-
end
|
181
|
-
|
182
202
|
def test_assignment_event_logging
|
183
203
|
e = Verdict::Experiment.new('test') do
|
184
204
|
groups { group :all, 100 }
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class RakeTasksTest < Minitest::Test
|
5
|
+
|
6
|
+
def setup
|
7
|
+
require 'rake' unless defined?(Rake)
|
8
|
+
Rake::Task.define_task(:environment)
|
9
|
+
Rake.application.rake_require('verdict/tasks')
|
10
|
+
|
11
|
+
@experiment = Verdict::Experiment.define(:rake, store_unqualified: true) do
|
12
|
+
groups do
|
13
|
+
group :a, 50
|
14
|
+
group :b, 50
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
ENV['EXPERIMENT'] = 'rake'
|
19
|
+
ENV['SUBJECT'] = '1'
|
20
|
+
ENV['GROUP'] = 'a'
|
21
|
+
|
22
|
+
Verdict::Rake.stubs(:stdout).returns(@stdout = StringIO.new)
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
Rake::Task["environment"].reenable
|
27
|
+
Rake::Task["verdict:lookup_assignment"].reenable
|
28
|
+
Rake::Task["verdict:experiments"].reenable
|
29
|
+
Rake::Task["verdict:assign_manually"].reenable
|
30
|
+
Rake::Task["verdict:disqualify_manually"].reenable
|
31
|
+
Rake::Task["verdict:remove_assignment"].reenable
|
32
|
+
|
33
|
+
Verdict.repository.clear
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_require_env
|
37
|
+
assert_equal 'a', Verdict::Rake.require_env('GROUP')
|
38
|
+
assert_equal 'a', Verdict::Rake.require_env('group')
|
39
|
+
|
40
|
+
ENV['group'] = 'b'
|
41
|
+
assert_equal 'a', Verdict::Rake.require_env('group') # uppercase has presedence
|
42
|
+
|
43
|
+
assert_raises(ArgumentError) { Verdict::Rake.require_env('non_existent_env') }
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_experiment_list
|
47
|
+
Rake.application.invoke_task("verdict:experiments")
|
48
|
+
assert_equal 'rake | Groups: a (50%), b (50%)', @stdout.string.chomp
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_lookup_assignment_fails_without_experiment_env_variable
|
52
|
+
ENV['EXPERIMENT'] = ''
|
53
|
+
assert_raises(ArgumentError) { Rake.application.invoke_task("verdict:lookup_assignment") }
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_lookup_assignment_fails_without_subject_env_variable
|
57
|
+
ENV.delete('SUBJECT')
|
58
|
+
assert_raises(ArgumentError) { Rake.application.invoke_task("verdict:lookup_assignment") }
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_assign_manually
|
62
|
+
Rake.application.invoke_task("verdict:assign_manually")
|
63
|
+
assert_equal 'Subject `1` has been assigned to group `a` of experiment `rake`.', @stdout.string.chomp
|
64
|
+
|
65
|
+
@stdout.string = ""
|
66
|
+
Rake.application.invoke_task("verdict:lookup_assignment")
|
67
|
+
assert_equal 'Subject `1` is assigned to group `a` of experiment `rake`.', @stdout.string.chomp
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_disqualify_manually
|
71
|
+
Rake.application.invoke_task("verdict:disqualify_manually")
|
72
|
+
assert_equal 'Subject `1` has been disqualified from experiment `rake`.', @stdout.string.chomp
|
73
|
+
|
74
|
+
@stdout.string = ""
|
75
|
+
Rake.application.invoke_task("verdict:lookup_assignment")
|
76
|
+
assert_equal 'Subject `1` is unqualified for experiment `rake`.', @stdout.string.chomp
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_remove_assignment
|
80
|
+
@experiment.assign('1')
|
81
|
+
Rake.application.invoke_task("verdict:remove_assignment")
|
82
|
+
assert_equal 'Removed assignment of subject with identifier `1`.', @stdout.string.lines.to_a[0].chomp
|
83
|
+
assert_equal 'The subject will be reasigned when it encounters the experiment `rake` again.', @stdout.string.lines.to_a[1].chomp
|
84
|
+
|
85
|
+
@stdout.string = ""
|
86
|
+
Rake.application.invoke_task("verdict:lookup_assignment")
|
87
|
+
assert_equal 'Subject `1` is not assigned to experiment `rake` yet.', @stdout.string.chomp
|
88
|
+
end
|
89
|
+
end
|
@@ -62,11 +62,11 @@ class RedisSubjectStorageTest < Minitest::Test
|
|
62
62
|
assert !@experiment.assign('subject_1').qualified?
|
63
63
|
end
|
64
64
|
|
65
|
-
def
|
65
|
+
def test_remove_subject_assignment
|
66
66
|
experiment_key = @storage.send(:generate_experiment_key, @experiment)
|
67
67
|
@experiment.assign('subject_3')
|
68
68
|
assert @redis.hexists(experiment_key, 'subject_3')
|
69
|
-
@experiment.
|
69
|
+
@experiment.remove_subject_assignment('subject_3')
|
70
70
|
assert !@redis.hexists(experiment_key, 'subject_3')
|
71
71
|
end
|
72
72
|
|
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.3.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-
|
11
|
+
date: 2014-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -120,6 +120,7 @@ files:
|
|
120
120
|
- test/experiments_repository_test.rb
|
121
121
|
- test/group_test.rb
|
122
122
|
- test/metadata_test.rb
|
123
|
+
- test/rake_tasks_test.rb
|
123
124
|
- test/segmenters/fixed_percentage_segmenter_test.rb
|
124
125
|
- test/segmenters/rollout_segmenter_test.rb
|
125
126
|
- test/segmenters/static_segmenter_test.rb
|
@@ -146,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
147
|
version: '0'
|
147
148
|
requirements: []
|
148
149
|
rubyforge_project:
|
149
|
-
rubygems_version: 2.0.
|
150
|
+
rubygems_version: 2.0.14
|
150
151
|
signing_key:
|
151
152
|
specification_version: 4
|
152
153
|
summary: A library to centrally define experiments for your application, and collect
|
@@ -159,6 +160,7 @@ test_files:
|
|
159
160
|
- test/experiments_repository_test.rb
|
160
161
|
- test/group_test.rb
|
161
162
|
- test/metadata_test.rb
|
163
|
+
- test/rake_tasks_test.rb
|
162
164
|
- test/segmenters/fixed_percentage_segmenter_test.rb
|
163
165
|
- test/segmenters/rollout_segmenter_test.rb
|
164
166
|
- test/segmenters/static_segmenter_test.rb
|