verdict 0.3.2 → 0.4.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
  SHA1:
3
- metadata.gz: e7f6af0cf2210f8a66cdb675286728f39a6728d9
4
- data.tar.gz: a9ad8ae9babb3c5bdb01cfce0ae4f9f4cef8cc9d
3
+ metadata.gz: c1f491060e7463a0121982c8a73c55c164e7a5b1
4
+ data.tar.gz: 86ec4f4226aae28170ab89a1f9e3b6f4ef7150e3
5
5
  SHA512:
6
- metadata.gz: 69cb9b3377d9fdbf2345e08e65b194f3e5b5d69d461d3b11a1c92b078f9051cfe153ae356ad4332b15515b0d8ef9c5995d9b567ab9fa0e95b0c0faa5be493c89
7
- data.tar.gz: 4dc4b3c36750840dc3b702ee71c041242d68af3ba359fde4bdb5eea24746c6e3a39cc3837bece1959b18f3ca00d3bd604b9fda9c67535c951cdb34d8d3d3a5ea
6
+ metadata.gz: a139d649a3990f8d2336dbb24ed980edfc344b03c621fee969137b8bd2e063ac0dbfc36801dcda2d044e0a484094da82380dc0e3332e12c9684120e49e7b0e79
7
+ data.tar.gz: d3c25106668841a418b3e5ee9e95dd929964da20f8b82ed5d85345b9e1d29dd22483f2581d622539bcaef9f1d01b5634d078cc821735c5dae5387247dc0341b0
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ dump.rdb
data/.travis.yml CHANGED
@@ -5,7 +5,7 @@ rvm:
5
5
  - 2.0.0
6
6
  - 2.1.0
7
7
  - ruby-head
8
- - rbx
8
+ - rbx-2.2.6
9
9
  - jruby-19mode
10
10
  matrix:
11
11
  allow_failures:
data/README.md CHANGED
@@ -78,7 +78,6 @@ an object that responds to the following methods:
78
78
  * `store_assignment(assignment)`
79
79
  * `retrieve_assignment(experiment, subject_identifier)`
80
80
  * `remove_assignment(experiment, subject_identifier)`
81
- * `clear_experiment(experiment)`
82
81
  * `retrieve_start_timestamp(experiment)`
83
82
  * `store_start_timestamp(experiment, timestamp)`
84
83
 
@@ -2,7 +2,7 @@ class Verdict::Experiment
2
2
 
3
3
  include Verdict::Metadata
4
4
 
5
- attr_reader :handle, :qualifier, :subject_storage, :event_logger
5
+ attr_reader :handle, :qualifier, :storage, :event_logger
6
6
 
7
7
  def self.define(handle, *args, &block)
8
8
  experiment = self.new(handle, *args, &block)
@@ -16,7 +16,7 @@ class Verdict::Experiment
16
16
  options = default_options.merge(options)
17
17
  @qualifier = options[:qualifier]
18
18
  @event_logger = options[:event_logger] || Verdict::EventLogger.new(Verdict.default_logger)
19
- @subject_storage = storage(options[:storage] || :memory)
19
+ @storage = storage(options[:storage] || :memory)
20
20
  @store_unqualified = options[:store_unqualified]
21
21
  @segmenter = options[:segmenter]
22
22
  @subject_type = options[:subject_type]
@@ -57,15 +57,15 @@ class Verdict::Experiment
57
57
  @qualifier = block
58
58
  end
59
59
 
60
- def storage(subject_storage = nil, options = {})
61
- return @subject_storage if subject_storage.nil?
60
+ def storage(storage = nil, options = {})
61
+ return @storage if storage.nil?
62
62
 
63
63
  @store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
64
- @subject_storage = case subject_storage
64
+ @storage = case storage
65
65
  when :memory; Verdict::Storage::MemoryStorage.new
66
66
  when :none; Verdict::Storage::MockStorage.new
67
- when Class; subject_storage.new
68
- else subject_storage
67
+ when Class; storage.new
68
+ else storage
69
69
  end
70
70
  end
71
71
 
@@ -75,7 +75,7 @@ class Verdict::Experiment
75
75
  end
76
76
 
77
77
  def started_at
78
- @started_at ||= @subject_storage.retrieve_start_timestamp(self)
78
+ @started_at ||= @storage.retrieve_start_timestamp(self)
79
79
  end
80
80
 
81
81
  def started?
@@ -147,7 +147,7 @@ class Verdict::Experiment
147
147
  end
148
148
 
149
149
  def store_assignment(assignment)
150
- @subject_storage.store_assignment(assignment) if should_store_assignment?(assignment)
150
+ @storage.store_assignment(assignment) if should_store_assignment?(assignment)
151
151
  event_logger.log_assignment(assignment)
152
152
  assignment
153
153
  end
@@ -157,7 +157,7 @@ class Verdict::Experiment
157
157
  end
158
158
 
159
159
  def remove_subject_assignment_by_identifier(subject_identifier)
160
- @subject_storage.remove_assignment(self, subject_identifier)
160
+ @storage.remove_assignment(self, subject_identifier)
161
161
  end
162
162
 
163
163
  def switch(subject, context = nil)
@@ -172,10 +172,6 @@ class Verdict::Experiment
172
172
  fetch_assignment(subject_identifier)
173
173
  end
174
174
 
175
- def wrapup
176
- @subject_storage.clear_experiment(self)
177
- end
178
-
179
175
  def retrieve_subject_identifier(subject)
180
176
  identifier = subject_identifier(subject).to_s
181
177
  raise Verdict::EmptySubjectIdentifier, "Subject resolved to an empty identifier!" if identifier.empty?
@@ -213,7 +209,7 @@ class Verdict::Experiment
213
209
  end
214
210
 
215
211
  def fetch_assignment(subject_identifier)
216
- @subject_storage.retrieve_assignment(self, subject_identifier)
212
+ @storage.retrieve_assignment(self, subject_identifier)
217
213
  end
218
214
 
219
215
  def disqualify_empty_identifier?
@@ -262,11 +258,11 @@ class Verdict::Experiment
262
258
  end
263
259
 
264
260
  def set_start_timestamp
265
- @subject_storage.store_start_timestamp(self, started_now = Time.now.utc)
261
+ @storage.store_start_timestamp(self, started_now = Time.now.utc)
266
262
  started_now
267
263
  end
268
264
 
269
265
  def ensure_experiment_has_started
270
- @started_at ||= @subject_storage.retrieve_start_timestamp(self) || set_start_timestamp
266
+ @started_at ||= @storage.retrieve_start_timestamp(self) || set_start_timestamp
271
267
  end
272
268
  end
@@ -0,0 +1,72 @@
1
+ module Verdict
2
+ module Storage
3
+ class BaseStorage
4
+
5
+ # Should store the assignments to allow quick lookups.
6
+ # - Assignments should be unique on the combination of
7
+ # `assignment.experiment.handle` and `assignment.subject_identifier`.
8
+ # - The main property to store is `group.handle`
9
+ # - Should return true if stored successfully.
10
+ def store_assignment(assignment)
11
+ hash = { group: assignment.handle, created_at: assignment.created_at.strftime('%FT%TZ') }
12
+ set(assignment.experiment.handle.to_s, "assignment_#{assignment.subject_identifier}", JSON.dump(hash))
13
+ end
14
+
15
+ # Should do a fast lookup of an assignment of the subject for the given experiment.
16
+ # - Should return nil if not found in store
17
+ # - Should return an Assignment instance otherwise.
18
+ def retrieve_assignment(experiment, subject_identifier)
19
+ if value = get(experiment.handle.to_s, "assignment_#{subject_identifier}")
20
+ hash = JSON.parse(value)
21
+ experiment.subject_assignment(
22
+ subject_identifier,
23
+ experiment.group(hash['group']),
24
+ Time.xmlschema(hash['created_at'])
25
+ )
26
+ end
27
+ end
28
+
29
+ # Should remove the subject from storage, so it will be reassigned later.
30
+ def remove_assignment(experiment, subject_identifier)
31
+ remove(experiment.handle.to_s, "assignment_#{subject_identifier}")
32
+ end
33
+
34
+ # Retrieves the start timestamp of the experiment
35
+ def retrieve_start_timestamp(experiment)
36
+ if timestamp = get(experiment.handle.to_s, 'started_at')
37
+ Time.parse(timestamp)
38
+ end
39
+ end
40
+
41
+ # Stores the timestamp on which the experiment was started
42
+ def store_start_timestamp(experiment, timestamp)
43
+ set(experiment.handle.to_s, 'started_at', timestamp.utc.strftime('%FT%TZ'))
44
+ end
45
+
46
+
47
+ # Retrieves a key in a given scope from storage.
48
+ # - The scope and key are both provided as string.
49
+ # - Should return a string value if the key is found in the scope, nil otherwise.
50
+ # - Should raise Verdict::StorageError if anything goes wrong.
51
+ def get(scope, key)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ # Retrieves a key in a given scope from storage.
56
+ # - The scope, key, and value are all provided as string.
57
+ # - Should return true if the item was successfully stored.
58
+ # - Should raise Verdict::StorageError if anything goes wrong.
59
+ def set(scope, key, value)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Retrieves a key in a given scope from storage.
64
+ # - The scope and key are both provided as string.
65
+ # - Should return true if the item was successfully removed from storage.
66
+ # - Should raise Verdict::StorageError if anything goes wrong.
67
+ def remove(scope, key)
68
+ raise NotImplementedError
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,57 @@
1
+ module Verdict
2
+ module Storage
3
+ class LegacyRedisStorage
4
+ attr_accessor :redis, :key_prefix
5
+
6
+ def initialize(redis = nil, options = {})
7
+ @redis = redis
8
+ @key_prefix = options[:key_prefix] || 'experiments/'
9
+ end
10
+
11
+ def retrieve_assignment(experiment, subject_identifier)
12
+ if value = redis.hget(generate_experiment_key(experiment), subject_identifier)
13
+ hash = JSON.parse(value)
14
+ experiment.subject_assignment(
15
+ subject_identifier,
16
+ experiment.group(hash['group']),
17
+ DateTime.parse(hash['created_at']).to_time
18
+ )
19
+ end
20
+ rescue ::Redis::BaseError => e
21
+ raise Verdict::StorageError, "Redis error: #{e.message}"
22
+ end
23
+
24
+ def store_assignment(assignment)
25
+ hash = { group: assignment.handle, created_at: assignment.created_at }
26
+ redis.hset(generate_experiment_key(assignment.experiment), assignment.subject_identifier, JSON.dump(hash))
27
+ rescue ::Redis::BaseError => e
28
+ raise Verdict::StorageError, "Redis error: #{e.message}"
29
+ end
30
+
31
+ def remove_assignment(experiment, subject_identifier)
32
+ redis.hdel(generate_experiment_key(experiment), subject_identifier)
33
+ end
34
+
35
+ def retrieve_start_timestamp(experiment)
36
+ if started_at = redis.get(generate_experiment_start_timestamp_key(experiment))
37
+ DateTime.parse(started_at).to_time
38
+ end
39
+ end
40
+
41
+ def store_start_timestamp(experiment, timestamp)
42
+ redis.setnx(generate_experiment_start_timestamp_key(experiment), timestamp.to_s)
43
+ end
44
+
45
+
46
+ private
47
+
48
+ def generate_experiment_key(experiment)
49
+ "#{@key_prefix}#{experiment.handle}"
50
+ end
51
+
52
+ def generate_experiment_start_timestamp_key(experiment)
53
+ "#{@key_prefix}#{experiment.handle}/started_at"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,39 +1,25 @@
1
1
  module Verdict
2
2
  module Storage
3
- class MemoryStorage
4
- attr_reader :assignments, :start_timestamps
3
+ class MemoryStorage < BaseStorage
4
+ attr_reader :storage
5
5
 
6
6
  def initialize
7
- @assignments = {}
8
- @start_timestamps = {}
7
+ @storage = {}
9
8
  end
10
9
 
11
- def store_assignment(assignment)
12
- @assignments[assignment.experiment.handle] ||= {}
13
- @assignments[assignment.experiment.handle][assignment.subject_identifier] = assignment.returning
14
- true
10
+ def get(scope, key)
11
+ @storage[scope] ||= {}
12
+ @storage[scope][key]
15
13
  end
16
14
 
17
- def retrieve_assignment(experiment, subject_identifier)
18
- experiment_store = @assignments[experiment.handle] || {}
19
- experiment_store[subject_identifier]
15
+ def set(scope, key, value)
16
+ @storage[scope] ||= {}
17
+ @storage[scope][key] = value
20
18
  end
21
19
 
22
- def remove_assignment(experiment, subject_identifier)
23
- @assignments[experiment.handle] ||= {}
24
- @assignments[experiment.handle].delete(subject_identifier)
25
- end
26
-
27
- def clear_experiment(experiment)
28
- @assignments.delete(experiment.handle)
29
- end
30
-
31
- def retrieve_start_timestamp(experiment)
32
- @start_timestamps[experiment.handle]
33
- end
34
-
35
- def store_start_timestamp(experiment, timestamp)
36
- @start_timestamps[experiment.handle] = timestamp
20
+ def remove(scope, key)
21
+ @storage[scope] ||= {}
22
+ @storage[scope].delete(key)
37
23
  end
38
24
  end
39
25
  end
@@ -1,37 +1,15 @@
1
1
  module Verdict
2
2
  module Storage
3
- class MockStorage
4
- # Should store the assignments to allow quick lookups.
5
- # - Assignments should be unique on the combination of
6
- # `assignment.experiment.handle` and `assignment.subject_identifier`.
7
- # - The main property to store is `group.handle`
8
- # - Should return true if stored successfully.
9
- def store_assignment(assignment)
3
+ class MockStorage < BaseStorage
4
+ def set(scope, key, value)
10
5
  false
11
6
  end
12
7
 
13
- # Should do a fast lookup of an assignment of the subject for the given experiment.
14
- # - Should return nil if not found in store
15
- # - Should return an Assignment instance otherwise.
16
- def retrieve_assignment(experiment, subject_identifier)
8
+ def get(scope, key)
17
9
  nil
18
10
  end
19
11
 
20
- # Should remove the subject from storage, so it will be reassigned later.
21
- def remove_assignment(experiment, subject_identifier)
22
- end
23
-
24
- # Should clear out the storage used for this experiment
25
- def clear_experiment(experiment)
26
- end
27
-
28
- # Retrieves the start timestamp of the experiment
29
- def retrieve_start_timestamp(experiment)
30
- nil
31
- end
32
-
33
- # Stores the timestamp on which the experiment was started
34
- def store_start_timestamp(experiment, timestamp)
12
+ def remove(scope, key)
35
13
  end
36
14
  end
37
15
  end
@@ -1,6 +1,6 @@
1
1
  module Verdict
2
2
  module Storage
3
- class RedisStorage
3
+ class RedisStorage < BaseStorage
4
4
  attr_accessor :redis, :key_prefix
5
5
 
6
6
  def initialize(redis = nil, options = {})
@@ -8,54 +8,28 @@ module Verdict
8
8
  @key_prefix = options[:key_prefix] || 'experiments/'
9
9
  end
10
10
 
11
- def retrieve_assignment(experiment, subject_identifier)
12
- if value = redis.hget(generate_experiment_key(experiment), subject_identifier)
13
- hash = JSON.parse(value)
14
- experiment.subject_assignment(
15
- subject_identifier,
16
- experiment.group(hash['group']),
17
- DateTime.parse(hash['created_at']).to_time
18
- )
19
- end
11
+ def get(scope, key)
12
+ redis.hget("#{@key_prefix}#{scope}", key)
20
13
  rescue ::Redis::BaseError => e
21
14
  raise Verdict::StorageError, "Redis error: #{e.message}"
22
15
  end
23
16
 
24
- def store_assignment(assignment)
25
- hash = { group: assignment.handle, created_at: assignment.created_at }
26
- redis.hset(generate_experiment_key(assignment.experiment), assignment.subject_identifier, JSON.dump(hash))
17
+ def set(scope, key, value)
18
+ redis.hset("#{@key_prefix}#{scope}", key, value)
27
19
  rescue ::Redis::BaseError => e
28
20
  raise Verdict::StorageError, "Redis error: #{e.message}"
29
21
  end
30
22
 
31
- def remove_assignment(experiment, subject_identifier)
32
- redis.hdel(generate_experiment_key(experiment), subject_identifier)
33
- end
34
-
35
- def clear_experiment(experiment)
36
- redis.del(generate_experiment_key(experiment))
37
- redis.del(generate_experiment_start_timestamp_key(experiment))
38
- end
39
-
40
- def retrieve_start_timestamp(experiment)
41
- if started_at = redis.get(generate_experiment_start_timestamp_key(experiment))
42
- DateTime.parse(started_at).to_time
43
- end
44
- end
45
-
46
- def store_start_timestamp(experiment, timestamp)
47
- redis.setnx(generate_experiment_start_timestamp_key(experiment), timestamp.to_s)
23
+ def remove(scope, key)
24
+ redis.hdel("#{@key_prefix}#{scope}", key)
25
+ rescue ::Redis::BaseError => e
26
+ raise Verdict::StorageError, "Redis error: #{e.message}"
48
27
  end
49
28
 
50
-
51
29
  private
52
30
 
53
- def generate_experiment_key(experiment)
54
- "#{@key_prefix}#{experiment.handle}"
55
- end
56
-
57
- def generate_experiment_start_timestamp_key(experiment)
58
- "#{@key_prefix}#{experiment.handle}/started_at"
31
+ def generate_scope_key(scope)
32
+ "#{@key_prefix}#{scope}"
59
33
  end
60
34
  end
61
35
  end
@@ -1,3 +1,5 @@
1
+ require 'verdict/storage/base_storage'
1
2
  require 'verdict/storage/mock_storage'
2
3
  require 'verdict/storage/memory_storage'
3
4
  require 'verdict/storage/redis_storage'
5
+ require 'verdict/storage/legacy_redis_storage'
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1 @@
1
+ # using the default shipit config
@@ -19,7 +19,7 @@ class ConversionTest < Minitest::Test
19
19
  end
20
20
 
21
21
  def test_assignment_lookup
22
- @experiment.subject_storage.expects(:retrieve_assignment).with(@experiment, 'test_subject_id')
22
+ @experiment.storage.expects(:retrieve_assignment).with(@experiment, 'test_subject_id')
23
23
  conversion = Verdict::Conversion.new(@experiment, 'test_subject_id', :test_goal)
24
24
  conversion.assignment
25
25
  end
@@ -278,8 +278,8 @@ class ExperimentTest < Minitest::Test
278
278
  groups { group :all, 100 }
279
279
  end
280
280
 
281
- e.subject_storage.expects(:retrieve_start_timestamp).returns(nil)
282
- e.subject_storage.expects(:store_start_timestamp).once
281
+ e.storage.expects(:retrieve_start_timestamp).returns(nil)
282
+ e.storage.expects(:store_start_timestamp).once
283
283
  e.send(:ensure_experiment_has_started)
284
284
  end
285
285
 
@@ -289,8 +289,8 @@ class ExperimentTest < Minitest::Test
289
289
  end
290
290
 
291
291
  e.send(:ensure_experiment_has_started)
292
- e.subject_storage.expects(:retrieve_start_timestamp).never
293
- e.subject_storage.expects(:store_start_timestamp).never
292
+ e.storage.expects(:retrieve_start_timestamp).never
293
+ e.storage.expects(:store_start_timestamp).never
294
294
  e.send(:ensure_experiment_has_started)
295
295
  end
296
296
 
@@ -299,8 +299,8 @@ class ExperimentTest < Minitest::Test
299
299
  groups { group :all, 100 }
300
300
  end
301
301
 
302
- e.subject_storage.expects(:retrieve_start_timestamp).returns(Time.now.utc)
303
- e.subject_storage.expects(:store_start_timestamp).never
302
+ e.storage.expects(:retrieve_start_timestamp).returns(Time.now.utc)
303
+ e.storage.expects(:store_start_timestamp).never
304
304
  e.send(:ensure_experiment_has_started)
305
305
  end
306
306
 
File without changes
@@ -1,11 +1,11 @@
1
1
  require 'test_helper'
2
2
 
3
- class RedisSubjectStorageTest < Minitest::Test
3
+ class LegacyRedisStorageTest < Minitest::Test
4
4
 
5
5
  def setup
6
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
7
+ @storage = storage = Verdict::Storage::LegacyRedisStorage.new(@redis)
8
+ @experiment = Verdict::Experiment.new(:legacy_redis_storage) do
9
9
  qualify { |s| s == 'subject_1' }
10
10
  groups { group :all, 100 }
11
11
  storage storage, store_unqualified: true
@@ -13,11 +13,12 @@ class RedisSubjectStorageTest < Minitest::Test
13
13
  end
14
14
 
15
15
  def teardown
16
- @storage.clear_experiment(@experiment)
16
+ @redis.del('experiments/legacy_redis_storage')
17
+ @redis.del('experiments/legacy_redis_storage/started_at')
17
18
  end
18
19
 
19
20
  def test_generate_experiment_key_should_generate_namespaced_key
20
- assert_equal 'experiments/redis_storage', @storage.send(:generate_experiment_key, @experiment)
21
+ assert_equal 'experiments/legacy_redis_storage', @storage.send(:generate_experiment_key, @experiment)
21
22
  end
22
23
 
23
24
  def test_store_and_retrieve_qualified_assignment
@@ -70,14 +71,6 @@ class RedisSubjectStorageTest < Minitest::Test
70
71
  assert !@redis.hexists(experiment_key, 'subject_3')
71
72
  end
72
73
 
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
74
  def test_started_at
82
75
  key = @storage.send(:generate_experiment_start_timestamp_key, @experiment)
83
76
 
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- class MemorySubjectStorageTest < Minitest::Test
3
+ class MemoryStorageTest < Minitest::Test
4
4
 
5
5
  def setup
6
6
  @storage = storage = Verdict::Storage::MemoryStorage.new
@@ -12,12 +12,6 @@ class MemorySubjectStorageTest < Minitest::Test
12
12
  @subject = stub(id: 'bootscale')
13
13
  end
14
14
 
15
- def test_wrapup
16
- @experiment.assign(@subject)
17
- @experiment.wrapup
18
- assert @experiment.lookup(@subject).nil?
19
- end
20
-
21
15
  def test_with_memory_store
22
16
  assignment_1 = @experiment.assign(@subject)
23
17
  assignment_2 = @experiment.assign(@subject)
@@ -32,14 +26,18 @@ class MemorySubjectStorageTest < Minitest::Test
32
26
  end
33
27
 
34
28
  def test_remove_assignment
35
- assert !@experiment.assign(@subject).returning?
36
- @experiment.wrapup
29
+ assignment = @experiment.assign(@subject)
30
+ assert !assignment.returning?
31
+
32
+ assert @experiment.assign(@subject).returning?
33
+ @storage.remove_assignment(@experiment, @subject.id)
37
34
  assert !@experiment.assign(@subject).returning?
38
35
  end
39
36
 
40
37
  def test_started_at
41
- assert @storage.start_timestamps[@experiment.handle].nil?
38
+ assert @storage.get(@experiment.handle.to_s, 'started_at').nil?
42
39
  @experiment.send(:ensure_experiment_has_started)
43
- assert @storage.start_timestamps[@experiment.handle].instance_of?(Time)
40
+ refute @storage.get(@experiment.handle.to_s, 'started_at').nil?
41
+ assert_instance_of Time, @experiment.started_at
44
42
  end
45
43
  end
@@ -0,0 +1,76 @@
1
+ require 'test_helper'
2
+
3
+ class RedisStorageTest < Minitest::Test
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
+ @redis.del('experiments/redis_storage')
17
+ end
18
+
19
+ def experiment_key
20
+ 'experiments/redis_storage'
21
+ end
22
+
23
+ def test_store_and_retrieve_qualified_assignment
24
+ refute @redis.hexists(experiment_key, 'assignment_subject_1')
25
+
26
+ new_assignment = @experiment.assign('subject_1')
27
+ assert new_assignment.qualified?
28
+ refute new_assignment.returning?
29
+
30
+ assert @redis.hexists(experiment_key, 'assignment_subject_1')
31
+
32
+ returning_assignment = @experiment.assign('subject_1')
33
+ assert returning_assignment.returning?
34
+ assert_equal new_assignment.experiment, returning_assignment.experiment
35
+ assert_equal new_assignment.group, returning_assignment.group
36
+ end
37
+
38
+ def test_store_and_retrieve_unqualified_assignment
39
+ refute @redis.hexists(experiment_key, 'assignment_subject_2')
40
+
41
+ new_assignment = @experiment.assign('subject_2')
42
+
43
+ refute new_assignment.returning?
44
+ refute new_assignment.qualified?
45
+ assert @redis.hexists(experiment_key, 'assignment_subject_2')
46
+
47
+ returning_assignment = @experiment.assign('subject_2')
48
+ assert returning_assignment.returning?
49
+ assert_equal new_assignment.experiment, returning_assignment.experiment
50
+ assert_equal new_assignment.group, returning_assignment.group
51
+ end
52
+
53
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
54
+ @redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
55
+ assert !@experiment.assign('subject_1').qualified?
56
+ end
57
+
58
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
59
+ @redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
60
+ assert !@experiment.assign('subject_1').qualified?
61
+ end
62
+
63
+ def test_remove_subject_assignment
64
+ @experiment.assign('subject_3')
65
+ assert @redis.hexists(experiment_key, 'assignment_subject_3')
66
+ @experiment.remove_subject_assignment('subject_3')
67
+ refute @redis.hexists(experiment_key, 'assignment_subject_3')
68
+ end
69
+
70
+ def test_started_at
71
+ refute @redis.hexists(experiment_key, "started_at")
72
+ a = @experiment.send(:ensure_experiment_has_started)
73
+ assert @redis.hexists(experiment_key, "started_at")
74
+ assert_equal a, @experiment.started_at
75
+ end
76
+ end
metadata CHANGED
@@ -1,105 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: verdict
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
- cert_chain:
11
- - |
12
- -----BEGIN CERTIFICATE-----
13
- MIIDcDCCAligAwIBAgIBATANBgkqhkiG9w0BAQUFADA/MQ8wDQYDVQQDDAZhZG1p
14
- bnMxFzAVBgoJkiaJk/IsZAEZFgdzaG9waWZ5MRMwEQYKCZImiZPyLGQBGRYDY29t
15
- MB4XDTE0MDUxNTIwMzM0OFoXDTE1MDUxNTIwMzM0OFowPzEPMA0GA1UEAwwGYWRt
16
- aW5zMRcwFQYKCZImiZPyLGQBGRYHc2hvcGlmeTETMBEGCgmSJomT8ixkARkWA2Nv
17
- bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL0/81O3e1vh5smcwp2G
18
- MpLQ6q0kejQLa65bPYPxdzWA1SYOKyGfw+yR9LdFzsuKpwWzKq6zX35lj1IckWS4
19
- bNBEQzxmufUxU0XPM02haFB8fOfDJzdXsWte9Ge4IFwahwn68gpMqN+BvxL+KMYz
20
- Iut9YmN44d4LZdsENEIO5vmybuG2vYDz7R56qB0PA+Q2P2CdhymsBad2DQs69FBo
21
- uico9V6VMYYctL9lCYdzu9IXrOYNTt88suKIVzzAlHOKeN0Ng5qdztFoTR8sfxDr
22
- Ydg3KHl5n47wlpgd8R0f/4b5gGxW+v9pyJCgQnLlRu7DedVSvv7+GMtj3g9r3nhJ
23
- KqECAwEAAaN3MHUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFI/o
24
- maf34HXbUOQsdoLHacEKQgunMB0GA1UdEQQWMBSBEmFkbWluc0BzaG9waWZ5LmNv
25
- bTAdBgNVHRIEFjAUgRJhZG1pbnNAc2hvcGlmeS5jb20wDQYJKoZIhvcNAQEFBQAD
26
- ggEBADkK9aj5T0HPExsov4EoMWFnO+G7RQ28C30VAfKxnL2UxG6i4XMHVs6Xi94h
27
- qXFw1ec9Y2eDUqaolT3bviOk9BB197+A8Vz/k7MC6ci2NE+yDDB7HAC8zU6LAx8Y
28
- Iqvw7B/PSZ/pz4bUVFlTATif4mi1vO3lidRkdHRtM7UePSn2rUpOi0gtXBP3bLu5
29
- YjHJN7wx5cugMEyroKITG5gL0Nxtu21qtOlHX4Hc4KdE2JqzCPOsS4zsZGhgwhPs
30
- fl3hbtVFTqbOlwL9vy1fudXcolIE/ZTcxQ+er07ZFZdKCXayR9PPs64heamfn0fp
31
- TConQSX2BnZdhIEYW+cKzEC/bLc=
32
- -----END CERTIFICATE-----
33
- date: 2014-05-16 00:00:00.000000000 Z
10
+ cert_chain: []
11
+ date: 2014-09-16 00:00:00.000000000 Z
34
12
  dependencies:
35
13
  - !ruby/object:Gem::Dependency
36
14
  name: minitest
37
15
  requirement: !ruby/object:Gem::Requirement
38
16
  requirements:
39
- - - "~>"
17
+ - - ~>
40
18
  - !ruby/object:Gem::Version
41
19
  version: '5.2'
42
20
  type: :development
43
21
  prerelease: false
44
22
  version_requirements: !ruby/object:Gem::Requirement
45
23
  requirements:
46
- - - "~>"
24
+ - - ~>
47
25
  - !ruby/object:Gem::Version
48
26
  version: '5.2'
49
27
  - !ruby/object:Gem::Dependency
50
28
  name: rake
51
29
  requirement: !ruby/object:Gem::Requirement
52
30
  requirements:
53
- - - ">="
31
+ - - '>='
54
32
  - !ruby/object:Gem::Version
55
33
  version: '0'
56
34
  type: :development
57
35
  prerelease: false
58
36
  version_requirements: !ruby/object:Gem::Requirement
59
37
  requirements:
60
- - - ">="
38
+ - - '>='
61
39
  - !ruby/object:Gem::Version
62
40
  version: '0'
63
41
  - !ruby/object:Gem::Dependency
64
42
  name: mocha
65
43
  requirement: !ruby/object:Gem::Requirement
66
44
  requirements:
67
- - - ">="
45
+ - - '>='
68
46
  - !ruby/object:Gem::Version
69
47
  version: '0'
70
48
  type: :development
71
49
  prerelease: false
72
50
  version_requirements: !ruby/object:Gem::Requirement
73
51
  requirements:
74
- - - ">="
52
+ - - '>='
75
53
  - !ruby/object:Gem::Version
76
54
  version: '0'
77
55
  - !ruby/object:Gem::Dependency
78
56
  name: timecop
79
57
  requirement: !ruby/object:Gem::Requirement
80
58
  requirements:
81
- - - ">="
59
+ - - '>='
82
60
  - !ruby/object:Gem::Version
83
61
  version: '0'
84
62
  type: :development
85
63
  prerelease: false
86
64
  version_requirements: !ruby/object:Gem::Requirement
87
65
  requirements:
88
- - - ">="
66
+ - - '>='
89
67
  - !ruby/object:Gem::Version
90
68
  version: '0'
91
69
  - !ruby/object:Gem::Dependency
92
70
  name: redis
93
71
  requirement: !ruby/object:Gem::Requirement
94
72
  requirements:
95
- - - ">="
73
+ - - '>='
96
74
  - !ruby/object:Gem::Version
97
75
  version: '0'
98
76
  type: :development
99
77
  prerelease: false
100
78
  version_requirements: !ruby/object:Gem::Requirement
101
79
  requirements:
102
- - - ">="
80
+ - - '>='
103
81
  - !ruby/object:Gem::Version
104
82
  version: '0'
105
83
  description: Shopify Experiments classes
@@ -110,8 +88,8 @@ executables: []
110
88
  extensions: []
111
89
  extra_rdoc_files: []
112
90
  files:
113
- - ".gitignore"
114
- - ".travis.yml"
91
+ - .gitignore
92
+ - .travis.yml
115
93
  - Gemfile
116
94
  - LICENSE
117
95
  - README.md
@@ -131,12 +109,14 @@ files:
131
109
  - lib/verdict/segmenters/rollout_segmenter.rb
132
110
  - lib/verdict/segmenters/static_segmenter.rb
133
111
  - lib/verdict/storage.rb
112
+ - lib/verdict/storage/base_storage.rb
113
+ - lib/verdict/storage/legacy_redis_storage.rb
134
114
  - lib/verdict/storage/memory_storage.rb
135
115
  - lib/verdict/storage/mock_storage.rb
136
116
  - lib/verdict/storage/redis_storage.rb
137
117
  - lib/verdict/tasks.rake
138
118
  - lib/verdict/version.rb
139
- - shipit.yml
119
+ - shipit.rubygems.yml
140
120
  - test/assignment_test.rb
141
121
  - test/conversion_test.rb
142
122
  - test/event_logger_test.rb
@@ -149,8 +129,10 @@ files:
149
129
  - test/segmenters/random_percentage_segmenter_test.rb
150
130
  - test/segmenters/rollout_segmenter_test.rb
151
131
  - test/segmenters/static_segmenter_test.rb
152
- - test/storage/memory_subject_storage_test.rb
153
- - test/storage/redis_subject_storage_test.rb
132
+ - test/storage/base_storage_test.rb
133
+ - test/storage/legacy_redis_storage_test.rb
134
+ - test/storage/memory_storage_test.rb
135
+ - test/storage/redis_storage_test.rb
154
136
  - test/test_helper.rb
155
137
  - verdict.gemspec
156
138
  homepage: http://github.com/Shopify/verdict
@@ -162,17 +144,17 @@ require_paths:
162
144
  - lib
163
145
  required_ruby_version: !ruby/object:Gem::Requirement
164
146
  requirements:
165
- - - ">="
147
+ - - '>='
166
148
  - !ruby/object:Gem::Version
167
149
  version: '0'
168
150
  required_rubygems_version: !ruby/object:Gem::Requirement
169
151
  requirements:
170
- - - ">="
152
+ - - '>='
171
153
  - !ruby/object:Gem::Version
172
154
  version: '0'
173
155
  requirements: []
174
156
  rubyforge_project:
175
- rubygems_version: 2.2.0
157
+ rubygems_version: 2.0.14
176
158
  signing_key:
177
159
  specification_version: 4
178
160
  summary: A library to centrally define experiments for your application, and collect
@@ -190,6 +172,8 @@ test_files:
190
172
  - test/segmenters/random_percentage_segmenter_test.rb
191
173
  - test/segmenters/rollout_segmenter_test.rb
192
174
  - test/segmenters/static_segmenter_test.rb
193
- - test/storage/memory_subject_storage_test.rb
194
- - test/storage/redis_subject_storage_test.rb
175
+ - test/storage/base_storage_test.rb
176
+ - test/storage/legacy_redis_storage_test.rb
177
+ - test/storage/memory_storage_test.rb
178
+ - test/storage/redis_storage_test.rb
195
179
  - test/test_helper.rb
checksums.yaml.gz.sig DELETED
Binary file
data/shipit.yml DELETED
@@ -1,15 +0,0 @@
1
- dependencies:
2
- override:
3
- - bundle install --path=./data/bundler
4
-
5
- deploy:
6
- override:
7
- - |
8
- BUILD=`bundle exec rake build`
9
- if [ $? != 0 ]; then
10
- echo $BUILD;
11
- exit 1;
12
- fi
13
- PKG=`echo $BUILD | cut -d" " -f5 | sed -e s/\.$//`;
14
- VERSION=`bundle exec rake build | cut -d" " -f2`;
15
- gem push $PKG
data.tar.gz.sig DELETED
Binary file
metadata.gz.sig DELETED
@@ -1,3 +0,0 @@
1
- _����F�HL��oR^�aa�G���K��6W���l�k����o�V<!��?g/Ϲ�;�'J�.�S��l�m����;���B"��#�J��a���jN��ה�Ӈ���[KQP��
2
-
3
- ���7�'O��H��5ڋ�