verdict 0.13.0 → 0.16.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
  SHA256:
3
- metadata.gz: ec6df6bdd38dc947ff1072e1dce844ab4d3ddd522701c8de578a35b4c9c51be8
4
- data.tar.gz: e6b62b2c579a363f37526079389bea4cb16eb28ed7573dec2d3fa8f8c74ee4ed
3
+ metadata.gz: c8ad702f655cca4734df9f1019f7df665cbe04914468bdcb07ae2a5e9fcc27ca
4
+ data.tar.gz: f31057b426bb631a36d99bc9a6f1588078b0009909ad25c2f5daa2180a407d66
5
5
  SHA512:
6
- metadata.gz: 1399fa37c4575888ffb83aea54469784db11ceef1ebc99f5c1fa5de75b936f460b1f34dc717c28f4f19a6e969c3a85a3faad022119113cd94f754ad61b4fdb85
7
- data.tar.gz: 7f1836b8d23ca79a4338f0a4f4ed244ebe7bdbd8b9f03529f2cb00e110c280b1033e112928da900d805112eb0036e0f74278ea021514016541accf01d8c3308c
6
+ metadata.gz: 1ac4f9f4cef8e978fc36ea7848cee6b4ab15cb895d57598388f4b42997f141189a51c511f4a4b877f254b8a0d32909a153aa00da8b7e42940dbaed6d7ec9e4ec
7
+ data.tar.gz: f03f49ca42347dc3bf54c0ab449df49df32e6b5fc175392e3259b4e238324de03406f64d38a17e8744f662ec9543319a41bebec0df12031f9f0ab03bb05253c2
@@ -3,9 +3,9 @@ cache: bundler
3
3
  before_install: gem update bundler
4
4
  script: bundle exec rake
5
5
  rvm:
6
- - 2.0
7
- - 2.1
8
- - 2.3.3
6
+ - 2.5
7
+ - 2.6
8
+ - 2.7
9
9
  - ruby-head
10
10
  - jruby
11
11
  matrix:
@@ -1,3 +1,18 @@
1
+ ## v0.16.0
2
+ * Allow configuring the `RedisStorage` with a [`ConnectionPool`](https://github.com/mperham/connection_pool) instead of a raw `Redis` connection.
3
+
4
+ ## v0.15.2
5
+ * Fix edge case where inheriting from `Verdict::Experiment` and overriding `subject_qualifies?` resulted in an error.
6
+
7
+ ## v0.15.1
8
+ * Make the `dynamic_qualifies` parameter in `Verdict::Experiment#subject_qualifies?` optional. This fixes a bug where users that were previously calling this method directly experienced issues after v0.15.0
9
+
10
+ ## v0.15.0
11
+ * Add optional `qualifiers` parameter to the `Verdict::Experiment#switch` method. This parameter accepts an array of procs and is used as additional qualifiers. The purpose of this parameter is to allow users to define qualification logic outside of the experiment definition.
12
+
13
+ ## v0.14.0
14
+ * Add optional experiment definition method `schedule_stop_new_assignment_timestamp` to support limiting experiment's assignment lifetime with another pre-determined time interval. It allows users to have an assignment cooldown period for stable analysis of the experiment results. Experiment's lifetime now becomes: start experiment -> stop new assignments -> end experiment.
15
+
1
16
  ## v0.13.0
2
17
 
3
18
  * Add optional experiment definition methods `schedule_start_timestamp` and `schedule_end_timestamp` to support limiting experiment's lifetime in a pre-determined time interval.
@@ -52,9 +52,17 @@ class Verdict::Experiment
52
52
  return self
53
53
  end
54
54
 
55
- # Optional: Together with the "end timestamp", limits the experiment run timeline within
56
- # the given time interval. When experiment is not scheduled, subject switch returns nil.
57
- # This is useful when the experimenter requires the experiment to run in a strict timeline.
55
+ # Optional: Together with the "end_timestamp" and "stop_new_assignment_timestamp", limits the experiment run timeline within
56
+ # the given time interval.
57
+ #
58
+ # Timestamps definitions:
59
+ # start_timestamp: Experiment's start time. No assignments are made i.e. switch will return nil before this timestamp.
60
+ # stop_new_assignment_timestamp: Experiment's new assignment stop time. No new assignments are made
61
+ # i.e. switch returns nil for new assignments but the existing assignments are preserved.
62
+ # end_timestamp: Experiment's end time. No assignments are made i.e. switch returns nil after this timestamp.
63
+ #
64
+ # Experiment run timeline:
65
+ # start_timestamp -> (new assignments occur) -> stop_new_assignment_timestamp -> (no new assignments occur) -> end_timestamp
58
66
  def schedule_start_timestamp(timestamp)
59
67
  @schedule_start_timestamp = timestamp
60
68
  end
@@ -63,6 +71,10 @@ class Verdict::Experiment
63
71
  @schedule_end_timestamp = timestamp
64
72
  end
65
73
 
74
+ def schedule_stop_new_assignment_timestamp(timestamp)
75
+ @schedule_stop_new_assignment_timestamp = timestamp
76
+ end
77
+
66
78
  def rollout_percentage(percentage, rollout_group_name = :enabled)
67
79
  groups(Verdict::Segmenters::RolloutSegmenter) do
68
80
  group rollout_group_name, percentage
@@ -130,18 +142,18 @@ class Verdict::Experiment
130
142
  raise unless disqualify_empty_identifier?
131
143
  end
132
144
 
133
- def assign(subject, context = nil)
145
+ def assign(subject, context = nil, dynamic_qualifiers: [])
134
146
  previous_assignment = lookup(subject)
135
147
 
136
148
  subject_identifier = retrieve_subject_identifier(subject)
137
149
  assignment = if previous_assignment
138
- previous_assignment
139
- elsif subject_qualifies?(subject, context)
140
- group = segmenter.assign(subject_identifier, subject, context)
141
- subject_assignment(subject, group, nil, group.nil?)
142
- else
143
- nil_assignment(subject)
144
- end
150
+ previous_assignment
151
+ elsif dynamic_subject_qualifies?(subject, dynamic_qualifiers, context) && is_make_new_assignments?
152
+ group = segmenter.assign(subject_identifier, subject, context)
153
+ subject_assignment(subject, group, nil, group.nil?)
154
+ else
155
+ nil_assignment(subject)
156
+ end
145
157
 
146
158
  store_assignment(assignment)
147
159
  rescue Verdict::StorageError
@@ -182,9 +194,11 @@ class Verdict::Experiment
182
194
  @storage.remove_assignment(self, subject)
183
195
  end
184
196
 
185
- def switch(subject, context = nil)
197
+ # The qualifiers param accepts an array of procs.
198
+ # This is intended for qualification logic that cannot be defined in the experiment definition
199
+ def switch(subject, context = nil, qualifiers: [])
186
200
  return unless is_scheduled?
187
- assign(subject, context).to_sym
201
+ assign(subject, context, dynamic_qualifiers: qualifiers).to_sym
188
202
  end
189
203
 
190
204
  def lookup(subject)
@@ -229,8 +243,9 @@ class Verdict::Experiment
229
243
  @disqualify_empty_identifier
230
244
  end
231
245
 
232
- def subject_qualifies?(subject, context = nil)
246
+ def subject_qualifies?(subject, context = nil, dynamic_qualifiers: [])
233
247
  ensure_experiment_has_started
248
+ return false unless dynamic_qualifiers.all? { |qualifier| qualifier.call(subject) }
234
249
  everybody_qualifies? || @qualifiers.all? { |qualifier| qualifier.call(subject, context) }
235
250
  end
236
251
 
@@ -268,12 +283,27 @@ class Verdict::Experiment
268
283
  private
269
284
 
270
285
  def is_scheduled?
271
- if @schedule_start_timestamp and @schedule_start_timestamp > Time.now
286
+ if @schedule_start_timestamp && @schedule_start_timestamp > Time.now
272
287
  return false
273
288
  end
274
- if @schedule_end_timestamp and @schedule_end_timestamp < Time.now
289
+ if @schedule_end_timestamp && @schedule_end_timestamp <= Time.now
275
290
  return false
276
291
  end
277
292
  return true
278
293
  end
294
+
295
+ def is_make_new_assignments?
296
+ return !(@schedule_stop_new_assignment_timestamp && @schedule_stop_new_assignment_timestamp <= Time.now)
297
+ end
298
+
299
+ # Used when a Experiment class has overridden the subject_qualifies? method prior to v0.15.0
300
+ # The previous version of subject_qualifies did not accept dynamic qualifiers, thus this is used to
301
+ # determine how many parameters to pass
302
+ def dynamic_subject_qualifies?(subject, dynamic_qualifiers, context)
303
+ if method(:subject_qualifies?).parameters.include?([:key, :dynamic_qualifiers])
304
+ subject_qualifies?(subject, context, dynamic_qualifiers: dynamic_qualifiers)
305
+ else
306
+ subject_qualifies?(subject, context)
307
+ end
308
+ end
279
309
  end
@@ -6,33 +6,56 @@ module Verdict
6
6
  attr_accessor :redis, :key_prefix
7
7
 
8
8
  def initialize(redis = nil, options = {})
9
- @redis = redis
9
+ if !redis.nil? && !redis.respond_to?(:with)
10
+ @redis = ConnectionPoolLike.new(redis)
11
+ else
12
+ @redis = redis
13
+ end
14
+
10
15
  @key_prefix = options[:key_prefix] || 'experiments/'
11
16
  end
12
17
 
13
18
  protected
14
19
 
20
+ class ConnectionPoolLike
21
+ def initialize(redis)
22
+ @redis = redis
23
+ end
24
+
25
+ def with
26
+ yield @redis
27
+ end
28
+ end
29
+
15
30
  def get(scope, key)
16
- redis.hget(scope_key(scope), key)
31
+ redis.with do |conn|
32
+ conn.hget(scope_key(scope), key)
33
+ end
17
34
  rescue ::Redis::BaseError => e
18
35
  raise Verdict::StorageError, "Redis error: #{e.message}"
19
36
  end
20
37
 
21
38
  def set(scope, key, value)
22
- redis.hset(scope_key(scope), key, value)
39
+ redis.with do |conn|
40
+ conn.hset(scope_key(scope), key, value)
41
+ end
23
42
  rescue ::Redis::BaseError => e
24
43
  raise Verdict::StorageError, "Redis error: #{e.message}"
25
44
  end
26
45
 
27
46
  def remove(scope, key)
28
- redis.hdel(scope_key(scope), key)
47
+ redis.with do |conn|
48
+ conn.hdel(scope_key(scope), key)
49
+ end
29
50
  rescue ::Redis::BaseError => e
30
51
  raise Verdict::StorageError, "Redis error: #{e.message}"
31
52
  end
32
53
 
33
54
  def clear(scope, options)
34
55
  scrub(scope)
35
- redis.del(scope_key(scope))
56
+ redis.with do |conn|
57
+ conn.del(scope_key(scope))
58
+ end
36
59
  rescue ::Redis::BaseError => e
37
60
  raise Verdict::StorageError, "Redis error: #{e.message}"
38
61
  end
@@ -44,10 +67,14 @@ module Verdict
44
67
  end
45
68
 
46
69
  def scrub(scope, cursor: 0)
47
- cursor, results = redis.hscan(scope_key(scope), cursor, count: PAGE_SIZE)
70
+ cursor, results = redis.with do |conn|
71
+ conn.hscan(scope_key(scope), cursor, count: PAGE_SIZE)
72
+ end
73
+
48
74
  results.map(&:first).each do |key|
49
75
  remove(scope, key)
50
76
  end
77
+
51
78
  scrub(scope, cursor: cursor) unless cursor.to_i.zero?
52
79
  end
53
80
  end
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.13.0"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -565,9 +565,134 @@ class ExperimentTest < Minitest::Test
565
565
  end
566
566
  end
567
567
 
568
+ def test_is_stop_new_assignments
569
+ e = Verdict::Experiment.new('test') do
570
+ groups do
571
+ group :a, :half
572
+ group :b, :half
573
+ end
574
+ schedule_stop_new_assignment_timestamp Time.new(2020, 1, 15)
575
+ end
576
+
577
+ # new assignments stopped after the stop timestamp
578
+ Timecop.freeze(Time.new(2020, 1, 16)) do
579
+ assert !e.send(:is_make_new_assignments?)
580
+ assert_nil e.switch(1)
581
+ end
582
+ # new assignments didn't stop before the stop timestamp
583
+ Timecop.freeze(Time.new(2020, 1, 3)) do
584
+ assert e.send(:is_make_new_assignments?)
585
+ assert :a, e.switch(2)
586
+ end
587
+ end
588
+
589
+ def test_switch_preserves_old_assignments_after_stop_new_assignments_timestamp
590
+ e = Verdict::Experiment.new('test') do
591
+ groups do
592
+ group :a, :half
593
+ group :b, :half
594
+ end
595
+ end
596
+
597
+ assert_equal :a, e.switch(1)
598
+
599
+ e.schedule_stop_new_assignment_timestamp Time.new(2020, 4, 15)
600
+
601
+ # switch respects to stop new assignment timestamp, old assignment preserves, new assignment returns nil
602
+ Timecop.freeze(Time.new(2020, 4, 16)) do
603
+ assert !e.send(:is_make_new_assignments?)
604
+ # old assignment stay the same
605
+ assert_equal :a, e.switch(1)
606
+ # new assignment returns nil
607
+ assert_nil e.switch(2)
608
+ end
609
+ end
610
+
611
+ def test_schedule_start_timestamp_and_stop_new_assignemnt_timestamp_are_inclusive_but_end_timestamp_is_exclusive
612
+ e = Verdict::Experiment.new('test') do
613
+ groups do
614
+ group :a, :half
615
+ group :b, :half
616
+ end
617
+
618
+ schedule_start_timestamp Time.new(2020, 1, 1)
619
+ schedule_stop_new_assignment_timestamp Time.new(2020, 1, 15)
620
+ schedule_end_timestamp Time.new(2020, 1, 31)
621
+ end
622
+
623
+ # start_timestamp is included
624
+ Timecop.freeze(Time.new(2020, 1, 1)) do
625
+ assert e.send(:is_scheduled?)
626
+ assert_equal :a, e.switch(1)
627
+ end
628
+
629
+ # stop_new_assignment_timestamp is included
630
+ Timecop.freeze(Time.new(2020, 1, 15)) do
631
+ assert !e.send(:is_make_new_assignments?)
632
+ # old assignment preserved
633
+ assert_equal :a, e.switch(1)
634
+ # new assignment returns nil
635
+ assert_nil e.switch(2)
636
+ end
637
+
638
+ # end_timestamp is excluded
639
+ Timecop.freeze(Time.new(2020, 1, 31)) do
640
+ assert !e.send(:is_scheduled?)
641
+ assert_nil e.switch(1)
642
+ end
643
+ end
644
+
645
+ def test_custom_qualifiers_success
646
+ e = Verdict::Experiment.new('test') do
647
+ groups do
648
+ group :all, 100
649
+ end
650
+ end
651
+
652
+ subject = 2
653
+ custom_qualifier_a = Proc.new { |subject| subject.even? }
654
+ custom_qualifier_b = Proc.new { |subject| subject > 0 }
655
+
656
+ group = e.switch(subject, qualifiers: [custom_qualifier_a, custom_qualifier_b])
657
+ assert_equal e.group(:all).to_sym, group
658
+ end
659
+
660
+ def test_custom_qualifiers_failure
661
+ e = Verdict::Experiment.new('test') do
662
+ groups do
663
+ group :all, 100
664
+ end
665
+ end
666
+
667
+ subject = 3
668
+ custom_qualifier_a = Proc.new { |subject| subject.even? }
669
+ custom_qualifier_b = Proc.new { |subject| subject > 0 }
670
+
671
+ group = e.switch(subject, qualifiers: [custom_qualifier_a, custom_qualifier_b])
672
+ assert_nil group
673
+ end
674
+
675
+ def test_dynamic_subject_qualifies_call_overridden_method
676
+ e = MyExperiment.new('test') do
677
+ groups do
678
+ group :all, 100
679
+ end
680
+ end
681
+
682
+ group = e.switch(4)
683
+ assert_nil group
684
+ end
685
+
568
686
  private
569
687
 
570
688
  def redis
571
689
  @redis ||= ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
572
690
  end
573
691
  end
692
+
693
+ class MyExperiment < Verdict::Experiment
694
+ def subject_qualifies?(subject, context = nil)
695
+ return false if subject.even?
696
+ super
697
+ end
698
+ end
@@ -1,97 +1,75 @@
1
1
  require 'test_helper'
2
2
 
3
- class RedisStorageTest < Minitest::Test
4
3
 
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
- @redis.del('experiments/redis_storage')
17
- end
4
+ module SharedRedisStorageTests
5
+ attr_reader :redis, :experiment, :storage
18
6
 
19
7
  def test_store_and_retrieve_qualified_assignment
20
- refute @redis.hexists(experiment_key, 'assignment_subject_1')
8
+ refute redis.hexists(experiment_key, 'assignment_subject_1')
21
9
 
22
- new_assignment = @experiment.assign('subject_1')
10
+ new_assignment = experiment.assign('subject_1')
23
11
  assert new_assignment.qualified?
24
12
  refute new_assignment.returning?
25
13
 
26
- assert @redis.hexists(experiment_key, 'assignment_subject_1')
14
+ assert redis.hexists(experiment_key, 'assignment_subject_1')
27
15
 
28
- returning_assignment = @experiment.assign('subject_1')
16
+ returning_assignment = experiment.assign('subject_1')
29
17
  assert returning_assignment.returning?
30
18
  assert_equal new_assignment.experiment, returning_assignment.experiment
31
19
  assert_equal new_assignment.group, returning_assignment.group
32
20
  end
33
21
 
34
22
  def test_store_and_retrieve_unqualified_assignment
35
- refute @redis.hexists(experiment_key, 'assignment_subject_2')
23
+ refute redis.hexists(experiment_key, 'assignment_subject_2')
36
24
 
37
- new_assignment = @experiment.assign('subject_2')
25
+ new_assignment = experiment.assign('subject_2')
38
26
 
39
27
  refute new_assignment.returning?
40
28
  refute new_assignment.qualified?
41
- assert @redis.hexists(experiment_key, 'assignment_subject_2')
29
+ assert redis.hexists(experiment_key, 'assignment_subject_2')
42
30
 
43
- returning_assignment = @experiment.assign('subject_2')
31
+ returning_assignment = experiment.assign('subject_2')
44
32
  assert returning_assignment.returning?
45
33
  assert_equal new_assignment.experiment, returning_assignment.experiment
46
34
  assert_nil new_assignment.group
47
35
  assert_nil returning_assignment.group
48
36
  end
49
37
 
50
- def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
51
- @redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
52
- assert !@experiment.assign('subject_1').qualified?
53
- end
54
-
55
- def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
56
- @redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
57
- assert !@experiment.assign('subject_1').qualified?
58
- end
59
-
60
38
  def test_remove_subject_assignment
61
- @experiment.assign('subject_3')
62
- assert @redis.hexists(experiment_key, 'assignment_subject_3')
63
- @experiment.remove_subject_assignment('subject_3')
64
- refute @redis.hexists(experiment_key, 'assignment_subject_3')
39
+ experiment.assign('subject_3')
40
+ assert redis.hexists(experiment_key, 'assignment_subject_3')
41
+ experiment.remove_subject_assignment('subject_3')
42
+ refute redis.hexists(experiment_key, 'assignment_subject_3')
65
43
  end
66
44
 
67
45
  def test_started_at
68
- refute @redis.hexists(experiment_key, "started_at")
69
- a = @experiment.send(:ensure_experiment_has_started)
70
- assert @redis.hexists(experiment_key, "started_at")
71
- assert_equal a, @experiment.started_at
46
+ refute redis.hexists(experiment_key, "started_at")
47
+ a = experiment.send(:ensure_experiment_has_started)
48
+ assert redis.hexists(experiment_key, "started_at")
49
+ assert_equal a, experiment.started_at
72
50
  end
73
51
 
74
52
  def test_cleanup_with_scope_argument
75
53
  1000.times do |n|
76
- @experiment.assign("something_#{n}")
54
+ experiment.assign("something_#{n}")
77
55
  end
78
56
 
79
- assert_operator @redis, :exists, experiment_key
57
+ assert_operator redis, :exists, experiment_key
80
58
 
81
59
  Verdict.default_logger.expects(:warn).with(regexp_matches(/deprecated/))
82
- @storage.cleanup(:redis_storage)
83
- refute_operator @redis, :exists, experiment_key
60
+ storage.cleanup(:redis_storage)
61
+ refute_operator redis, :exists, experiment_key
84
62
  end
85
63
 
86
64
  def test_cleanup
87
65
  1000.times do |n|
88
- @experiment.assign("something_#{n}")
66
+ experiment.assign("something_#{n}")
89
67
  end
90
68
 
91
- assert_operator @redis, :exists, experiment_key
69
+ assert_operator redis, :exists, experiment_key
92
70
 
93
- @storage.cleanup(@experiment)
94
- refute_operator @redis, :exists, experiment_key
71
+ storage.cleanup(experiment)
72
+ refute_operator redis, :exists, experiment_key
95
73
  end
96
74
 
97
75
  private
@@ -100,3 +78,67 @@ class RedisStorageTest < Minitest::Test
100
78
  "experiments/redis_storage"
101
79
  end
102
80
  end
81
+
82
+ class RedisStorageTest < Minitest::Test
83
+ include SharedRedisStorageTests
84
+
85
+ def setup
86
+ @redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
87
+ @storage = storage = Verdict::Storage::RedisStorage.new(@redis)
88
+ @experiment = Verdict::Experiment.new(:redis_storage) do
89
+ qualify { |s| s == 'subject_1' }
90
+ groups { group :all, 100 }
91
+ storage storage, store_unqualified: true
92
+ end
93
+ end
94
+
95
+ def teardown
96
+ @redis.del('experiments/redis_storage')
97
+ end
98
+
99
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
100
+ redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
101
+ assert !experiment.assign('subject_1').qualified?
102
+ end
103
+
104
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
105
+ redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
106
+ assert !experiment.assign('subject_1').qualified?
107
+ end
108
+ end
109
+
110
+ class RedisStorageConnectionPoolTest < Minitest::Test
111
+ include SharedRedisStorageTests
112
+
113
+ def setup
114
+ @redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
115
+ @redis_pool = ::ConnectionPool.new(size: 3) do
116
+ ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
117
+ end
118
+ @storage = storage = Verdict::Storage::RedisStorage.new(@redis_pool)
119
+ @experiment = Verdict::Experiment.new(:redis_storage) do
120
+ qualify { |s| s == 'subject_1' }
121
+ groups { group :all, 100 }
122
+ storage storage, store_unqualified: true
123
+ end
124
+ end
125
+
126
+ def teardown
127
+ @redis.del('experiments/redis_storage')
128
+ end
129
+
130
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
131
+ redis_mock = stub
132
+ redis_mock.stubs(:hget).returns(nil)
133
+ redis_mock.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
134
+ @redis_pool.stubs(:with).yields(redis_mock)
135
+ assert !@experiment.assign('subject_1').qualified?
136
+ end
137
+
138
+ def test_remove_subject_assignment
139
+ @experiment.assign('subject_3')
140
+ assert @redis.hexists(experiment_key, 'assignment_subject_3')
141
+ @experiment.remove_subject_assignment('subject_3')
142
+ refute @redis.hexists(experiment_key, 'assignment_subject_3')
143
+ end
144
+ end
@@ -13,6 +13,7 @@ require "mocha/setup"
13
13
  require "timecop"
14
14
  require "verdict"
15
15
  require "redis"
16
+ require "connection_pool"
16
17
 
17
18
  REDIS_HOST = ENV['REDIS_HOST'].nil? ? '127.0.0.1' : ENV['REDIS_HOST']
18
19
  REDIS_PORT = (ENV['REDIS_PORT'].nil? ? '6379' : ENV['REDIS_PORT']).to_i
@@ -25,4 +25,5 @@ Gem::Specification.new do |gem|
25
25
  gem.add_development_dependency("timecop")
26
26
  gem.add_development_dependency("redis")
27
27
  gem.add_development_dependency("rails")
28
+ gem.add_development_dependency("connection_pool")
28
29
  end
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.13.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-03 00:00:00.000000000 Z
11
+ date: 2020-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: connection_pool
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: Shopify Experiments classes
98
112
  email:
99
113
  - kevin.mcphillips@shopify.com