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