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.
@@ -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
@@ -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
@@ -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: