verdict 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|