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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 969beb7809f021e2f73e70128dd7565081824240
4
- data.tar.gz: 1abbcd8a1c3b5d0776d3649049d8b34cada22cf9
3
+ metadata.gz: 8efebedf53fee289950045fc2396359440dc0e15
4
+ data.tar.gz: 94bba89fe604abab1cd2f60b45b66bff0424ee75
5
5
  SHA512:
6
- metadata.gz: ae0034fd9af434c7fec7be4dd0aabc427dca17c7990e150941251209323cbad98522d3057dd26abc6e1fffcb739e881d5cacc54c636b798cda322c1cdc0b056a
7
- data.tar.gz: f1e8ef2ca4f3ddc3995b72b33b3fa3dd2259c4520c8e691362a33e1ccfab9ce42aff6bc336b7723c1505db6a86d3ce4bcc90df07376201fbea208486ab3ef4f0
6
+ metadata.gz: 538a77d48203d92c91a42072d9f64e60f9ab3b1d8ee857d380bfcd61328c73f1d9d96682db1a5dbb1c43c8ae4d52c57b38d156863408b77673b879c17ac4fa1a
7
+ data.tar.gz: c9535d06da2da5bef7a547b9565515f227dc8789d5d690d0b68867a5b5b6835a6dcf7645ee2dfa7a37d072d47cb22b932a0d014bc51c83dc4cff3734de03a0f0
@@ -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 disqualify_empty_identifier?
127
- @disqualify_empty_identifier
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 disqualify(subject)
131
- identifier = retrieve_subject_identifier(subject)
132
- store_assignment(subject_assignment(identifier, nil, nil))
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 remove_subject(subject)
142
- remove_subject_identifier(retrieve_subject_identifier(subject))
155
+ def remove_subject_assignment(subject)
156
+ remove_subject_assignment_by_identifier(retrieve_subject_identifier(subject))
143
157
  end
144
158
 
145
- def remove_subject_identifier(subject_identifier)
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[assignment.experiment.handle] ||= {}
24
- @assignments[assignment.experiment.handle].delete(subject_identifier)
23
+ @assignments[experiment.handle] ||= {}
24
+ @assignments[experiment.handle].delete(subject_identifier)
25
25
  end
26
26
 
27
27
  def clear_experiment(experiment)
@@ -1,59 +1,88 @@
1
- def require_env(key)
2
- value = ENV[key.downcase].presence || ENV[key.upcase].presence
3
- raise "Provide #{key} as environment variable" if value.blank?
4
- value
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 :experiments do
33
+ namespace :verdict do
8
34
 
9
35
  desc "List all defined experiments"
10
- task :list => 'environment' do
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 = Verdict[require_env('experiment')] or raise "Experiment not found"
22
- subject_identifier = require_env('subject')
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 #{ENV['subject']} is not assigned to experiment #{experiment.handle} yet."
52
+ Verdict::Rake.stdout.puts "Subject `#{subject_identifier}` is not assigned to experiment `#{experiment.handle}` yet."
26
53
  elsif assignment.qualified?
27
- puts "Subject #{ENV['subject']} is assigned to group `#{assignment.group.handle}`"
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 #{ENV['subject']} is unqualified for experiment #{experiment.handle}"
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 = Verdict[require_env('experiment')] or raise "Experiment not found"
36
- group = experiment.group(require_env('group')) or raise "Group not found"
37
- assignment = experiment.subject_assignment(require_env('subject'), group, false)
38
- experiment.store_assignment(assignment)
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 "Disqualify a subject from an experiment"
42
- task :disqualify => 'environment' do
43
- experiment = Verdict[require_env('experiment')] or raise "Experiment not found"
44
- assignment = experiment.subject_assignment(require_env('subject'), nil, false)
45
- experiment.store_assignment(assignment)
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 = Verdict[require_env('experiment')] or raise "Experiment not found"
51
- experiment.remove_subject_identifier(require_env('subject'))
52
- end
81
+ experiment = Verdict::Rake.experiment
82
+ subject_identifier = Verdict::Rake.subject_identifier
53
83
 
54
- desc "Runs the cleanup tasks for an experiment"
55
- task :wrapup => 'environment' do
56
- experiment = Verdict[require_env('experiment')] or raise "Experiment not found"
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
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/verdict.rb CHANGED
@@ -40,7 +40,7 @@ module Verdict
40
40
  end
41
41
 
42
42
  require "verdict/version"
43
- require "verdict/railtie" if defined?(Rails)
43
+ require "verdict/railtie" if defined?(Rails::Railtie)
44
44
 
45
45
  require "verdict/metadata"
46
46
  require "verdict/experiment"
@@ -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 test_remove_assignment
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.remove_subject('subject_3')
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.2.1
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-02-12 00:00:00.000000000 Z
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.3
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