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 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