verdict 0.3.2 → 0.4.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 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ڋ�