verdict 0.1.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 +15 -0
- data/.gitignore +17 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.md +89 -0
- data/Rakefile +11 -0
- data/lib/verdict.rb +54 -0
- data/lib/verdict/assignment.rb +61 -0
- data/lib/verdict/conversion.rb +32 -0
- data/lib/verdict/event_logger.rb +20 -0
- data/lib/verdict/experiment.rb +220 -0
- data/lib/verdict/group.rb +36 -0
- data/lib/verdict/metadata.rb +30 -0
- data/lib/verdict/railtie.rb +12 -0
- data/lib/verdict/segmenter.rb +78 -0
- data/lib/verdict/storage.rb +138 -0
- data/lib/verdict/tasks.rake +59 -0
- data/lib/verdict/version.rb +3 -0
- data/test/assignment_test.rb +76 -0
- data/test/conversion_test.rb +37 -0
- data/test/event_logger_test.rb +43 -0
- data/test/experiment_test.rb +280 -0
- data/test/experiments_repository_test.rb +32 -0
- data/test/group_test.rb +42 -0
- data/test/memory_subject_storage_test.rb +45 -0
- data/test/metadata_test.rb +44 -0
- data/test/redis_subject_storage_test.rb +89 -0
- data/test/static_percentage_segmenter_test.rb +94 -0
- data/test/test_helper.rb +10 -0
- data/verdict.gemspec +25 -0
- metadata +157 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RedisSubjectStorageTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
|
7
|
+
@storage = storage = Verdict::Storage::RedisStorage.new(@redis)
|
8
|
+
@experiment = Verdict::Experiment.new(:redis_storage) do
|
9
|
+
qualify { |s| s == 'subject_1' }
|
10
|
+
groups { group :all, 100 }
|
11
|
+
storage storage, store_unqualified: true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
@storage.clear_experiment(@experiment)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_generate_experiment_key_should_generate_namespaced_key
|
20
|
+
assert_equal 'experiments/redis_storage', @storage.send(:generate_experiment_key, @experiment)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_store_and_retrieve_qualified_assignment
|
24
|
+
experiment_key = @storage.send(:generate_experiment_key, @experiment)
|
25
|
+
assert !@redis.hexists(experiment_key, 'subject_1')
|
26
|
+
|
27
|
+
new_assignment = @experiment.assign('subject_1')
|
28
|
+
assert new_assignment.qualified?
|
29
|
+
assert !new_assignment.returning?
|
30
|
+
|
31
|
+
assert @redis.hexists(experiment_key, 'subject_1')
|
32
|
+
|
33
|
+
returning_assignment = @experiment.assign('subject_1')
|
34
|
+
assert returning_assignment.returning?
|
35
|
+
assert_equal new_assignment.experiment, returning_assignment.experiment
|
36
|
+
assert_equal new_assignment.group, returning_assignment.group
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_store_and_retrieve_unqualified_assignment
|
40
|
+
experiment_key = @storage.send(:generate_experiment_key, @experiment)
|
41
|
+
assert !@redis.hexists(experiment_key, 'subject_2')
|
42
|
+
|
43
|
+
new_assignment = @experiment.assign('subject_2')
|
44
|
+
|
45
|
+
assert !new_assignment.returning?
|
46
|
+
assert !new_assignment.qualified?
|
47
|
+
assert @redis.hexists(experiment_key, 'subject_2')
|
48
|
+
|
49
|
+
returning_assignment = @experiment.assign('subject_2')
|
50
|
+
assert returning_assignment.returning?
|
51
|
+
assert_equal new_assignment.experiment, returning_assignment.experiment
|
52
|
+
assert_equal new_assignment.group, returning_assignment.group
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
|
56
|
+
@redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
|
57
|
+
assert !@experiment.assign('subject_1').qualified?
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
|
61
|
+
@redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
|
62
|
+
assert !@experiment.assign('subject_1').qualified?
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_remove_assignment
|
66
|
+
experiment_key = @storage.send(:generate_experiment_key, @experiment)
|
67
|
+
@experiment.assign('subject_3')
|
68
|
+
assert @redis.hexists(experiment_key, 'subject_3')
|
69
|
+
@experiment.remove_subject('subject_3')
|
70
|
+
assert !@redis.hexists(experiment_key, 'subject_3')
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_clear_experiment
|
74
|
+
experiment_key = @storage.send(:generate_experiment_key, @experiment)
|
75
|
+
new_assignment = @experiment.assign('subject_3')
|
76
|
+
assert @redis.exists(experiment_key)
|
77
|
+
@experiment.wrapup
|
78
|
+
assert !@redis.exists(experiment_key)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_started_at
|
82
|
+
key = @storage.send(:generate_experiment_start_timestamp_key, @experiment)
|
83
|
+
|
84
|
+
assert !@redis.exists(key)
|
85
|
+
a = @experiment.send(:ensure_experiment_has_started)
|
86
|
+
assert @redis.exists(key)
|
87
|
+
assert_equal a, @experiment.started_at
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class StaticPercentageSegmenterTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
MockExperiment = Struct.new(:handle)
|
6
|
+
|
7
|
+
def setup
|
8
|
+
Verdict.repository.clear
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_add_up_to_100_percent
|
12
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
13
|
+
s.group :segment1, 1
|
14
|
+
s.group :segment2, 54
|
15
|
+
s.group :segment3, 27
|
16
|
+
s.group :segment4, 18
|
17
|
+
s.verify!
|
18
|
+
|
19
|
+
assert_equal ['segment1', 'segment2', 'segment3', 'segment4'], s.groups.keys
|
20
|
+
assert_equal 0 ... 1, s.groups['segment1'].percentile_range
|
21
|
+
assert_equal 1 ... 55, s.groups['segment2'].percentile_range
|
22
|
+
assert_equal 55 ... 82, s.groups['segment3'].percentile_range
|
23
|
+
assert_equal 82 ... 100, s.groups['segment4'].percentile_range
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_definition_ofhalf_and_rest
|
27
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
28
|
+
s.group :first_half, :half
|
29
|
+
s.group :second_half, :rest
|
30
|
+
s.verify!
|
31
|
+
|
32
|
+
assert_equal ['first_half', 'second_half'], s.groups.keys
|
33
|
+
assert_equal 0 ... 50, s.groups['first_half'].percentile_range
|
34
|
+
assert_equal 50 ... 100, s.groups['second_half'].percentile_range
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_raises_if_less_than_100_percent
|
38
|
+
assert_raises(Verdict::SegmentationError) do
|
39
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
40
|
+
s.group :too_little, 99
|
41
|
+
s.verify!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_raises_if_greather_than_100_percent
|
46
|
+
assert_raises(Verdict::SegmentationError) do
|
47
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
48
|
+
s.group :too_much, 101
|
49
|
+
s.verify!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_consistent_assignment_for_subjects
|
54
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
55
|
+
s.group :first_half, :half
|
56
|
+
s.group :second_half, :rest
|
57
|
+
s.verify!
|
58
|
+
|
59
|
+
3.times do
|
60
|
+
assert s.groups['first_half'] === s.assign(1, nil, nil)
|
61
|
+
assert s.groups['second_half'] === s.assign(2, nil, nil)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_fair_segmenting
|
66
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
67
|
+
s.group :first_third, 33
|
68
|
+
s.group :second_third, 33
|
69
|
+
s.group :final_third, :rest
|
70
|
+
s.verify!
|
71
|
+
|
72
|
+
assignments = { :first_third => 0, :second_third => 0, :final_third => 0 }
|
73
|
+
200.times do |n|
|
74
|
+
assignment = s.assign(n, nil, nil)
|
75
|
+
assignments[assignment.to_sym] += 1
|
76
|
+
end
|
77
|
+
|
78
|
+
assert_equal 200, assignments.values.reduce(0, :+)
|
79
|
+
assert (60..72).include?(assignments[:first_third]), 'The groups should be roughly the same size.'
|
80
|
+
assert (60..72).include?(assignments[:second_third]), 'The groups should be roughly the same size.'
|
81
|
+
assert (60..72).include?(assignments[:final_third]), 'The groups should be roughly the same size.'
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_group_json_export
|
85
|
+
s = Verdict::Segmenter::StaticPercentage.new(MockExperiment.new('test'))
|
86
|
+
s.group :first_third, 33
|
87
|
+
s.group :rest, :rest
|
88
|
+
s.verify!
|
89
|
+
|
90
|
+
json = JSON.parse(s.groups['rest'].to_json)
|
91
|
+
assert_equal 'rest', json['handle']
|
92
|
+
assert_equal 67, json['percentage']
|
93
|
+
end
|
94
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "minitest/autorun"
|
3
|
+
require "minitest/pride"
|
4
|
+
require "mocha/setup"
|
5
|
+
require "timecop"
|
6
|
+
require "verdict"
|
7
|
+
require "redis"
|
8
|
+
|
9
|
+
REDIS_HOST = ENV['REDIS_HOST'].nil? ? '127.0.0.1' : ENV['REDIS_HOST']
|
10
|
+
REDIS_PORT = (ENV['REDIS_PORT'].nil? ? '6379' : ENV['REDIS_PORT']).to_i
|
data/verdict.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'verdict/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "verdict"
|
8
|
+
gem.version = Verdict::VERSION
|
9
|
+
gem.authors = ["Shopify"]
|
10
|
+
gem.email = ["kevin.mcphillips@shopify.com", "willem@shopify.com"]
|
11
|
+
gem.description = %q{Shopify Experiments classes}
|
12
|
+
gem.summary = %q{A library to centrally define experiments for your application, and collect assignment information.}
|
13
|
+
gem.homepage = "http://github.com/Shopify/verdict"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency "minitest", '~> 4.2'
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
gem.add_development_dependency "mocha"
|
23
|
+
gem.add_development_dependency "timecop"
|
24
|
+
gem.add_development_dependency "redis"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: verdict
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shopify
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: redis
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Shopify Experiments classes
|
84
|
+
email:
|
85
|
+
- kevin.mcphillips@shopify.com
|
86
|
+
- willem@shopify.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- .gitignore
|
92
|
+
- .travis.yml
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/verdict.rb
|
98
|
+
- lib/verdict/assignment.rb
|
99
|
+
- lib/verdict/conversion.rb
|
100
|
+
- lib/verdict/event_logger.rb
|
101
|
+
- lib/verdict/experiment.rb
|
102
|
+
- lib/verdict/group.rb
|
103
|
+
- lib/verdict/metadata.rb
|
104
|
+
- lib/verdict/railtie.rb
|
105
|
+
- lib/verdict/segmenter.rb
|
106
|
+
- lib/verdict/storage.rb
|
107
|
+
- lib/verdict/tasks.rake
|
108
|
+
- lib/verdict/version.rb
|
109
|
+
- test/assignment_test.rb
|
110
|
+
- test/conversion_test.rb
|
111
|
+
- test/event_logger_test.rb
|
112
|
+
- test/experiment_test.rb
|
113
|
+
- test/experiments_repository_test.rb
|
114
|
+
- test/group_test.rb
|
115
|
+
- test/memory_subject_storage_test.rb
|
116
|
+
- test/metadata_test.rb
|
117
|
+
- test/redis_subject_storage_test.rb
|
118
|
+
- test/static_percentage_segmenter_test.rb
|
119
|
+
- test/test_helper.rb
|
120
|
+
- verdict.gemspec
|
121
|
+
homepage: http://github.com/Shopify/verdict
|
122
|
+
licenses: []
|
123
|
+
metadata: {}
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ! '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 2.1.4
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: A library to centrally define experiments for your application, and collect
|
144
|
+
assignment information.
|
145
|
+
test_files:
|
146
|
+
- test/assignment_test.rb
|
147
|
+
- test/conversion_test.rb
|
148
|
+
- test/event_logger_test.rb
|
149
|
+
- test/experiment_test.rb
|
150
|
+
- test/experiments_repository_test.rb
|
151
|
+
- test/group_test.rb
|
152
|
+
- test/memory_subject_storage_test.rb
|
153
|
+
- test/metadata_test.rb
|
154
|
+
- test/redis_subject_storage_test.rb
|
155
|
+
- test/static_percentage_segmenter_test.rb
|
156
|
+
- test/test_helper.rb
|
157
|
+
has_rdoc:
|