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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/README.md +0 -1
- data/lib/verdict/experiment.rb +13 -17
- data/lib/verdict/storage/base_storage.rb +72 -0
- data/lib/verdict/storage/legacy_redis_storage.rb +57 -0
- data/lib/verdict/storage/memory_storage.rb +12 -26
- data/lib/verdict/storage/mock_storage.rb +4 -26
- data/lib/verdict/storage/redis_storage.rb +11 -37
- data/lib/verdict/storage.rb +2 -0
- data/lib/verdict/version.rb +1 -1
- data/shipit.rubygems.yml +1 -0
- data/test/conversion_test.rb +1 -1
- data/test/experiment_test.rb +6 -6
- data/test/storage/base_storage_test.rb +0 -0
- data/test/storage/{redis_subject_storage_test.rb → legacy_redis_storage_test.rb} +6 -13
- data/test/storage/{memory_subject_storage_test.rb → memory_storage_test.rb} +9 -11
- data/test/storage/redis_storage_test.rb +76 -0
- metadata +29 -45
- checksums.yaml.gz.sig +0 -0
- data/shipit.yml +0 -15
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1f491060e7463a0121982c8a73c55c164e7a5b1
|
4
|
+
data.tar.gz: 86ec4f4226aae28170ab89a1f9e3b6f4ef7150e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a139d649a3990f8d2336dbb24ed980edfc344b03c621fee969137b8bd2e063ac0dbfc36801dcda2d044e0a484094da82380dc0e3332e12c9684120e49e7b0e79
|
7
|
+
data.tar.gz: d3c25106668841a418b3e5ee9e95dd929964da20f8b82ed5d85345b9e1d29dd22483f2581d622539bcaef9f1d01b5634d078cc821735c5dae5387247dc0341b0
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
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
|
|
data/lib/verdict/experiment.rb
CHANGED
@@ -2,7 +2,7 @@ class Verdict::Experiment
|
|
2
2
|
|
3
3
|
include Verdict::Metadata
|
4
4
|
|
5
|
-
attr_reader :handle, :qualifier, :
|
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
|
-
@
|
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(
|
61
|
-
return @
|
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
|
-
@
|
64
|
+
@storage = case storage
|
65
65
|
when :memory; Verdict::Storage::MemoryStorage.new
|
66
66
|
when :none; Verdict::Storage::MockStorage.new
|
67
|
-
when Class;
|
68
|
-
else
|
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 ||= @
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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 ||= @
|
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 :
|
3
|
+
class MemoryStorage < BaseStorage
|
4
|
+
attr_reader :storage
|
5
5
|
|
6
6
|
def initialize
|
7
|
-
@
|
8
|
-
@start_timestamps = {}
|
7
|
+
@storage = {}
|
9
8
|
end
|
10
9
|
|
11
|
-
def
|
12
|
-
@
|
13
|
-
@
|
14
|
-
true
|
10
|
+
def get(scope, key)
|
11
|
+
@storage[scope] ||= {}
|
12
|
+
@storage[scope][key]
|
15
13
|
end
|
16
14
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
15
|
+
def set(scope, key, value)
|
16
|
+
@storage[scope] ||= {}
|
17
|
+
@storage[scope][key] = value
|
20
18
|
end
|
21
19
|
|
22
|
-
def
|
23
|
-
@
|
24
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
12
|
-
|
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
|
25
|
-
|
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
|
32
|
-
redis.hdel(
|
33
|
-
|
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
|
54
|
-
"#{@key_prefix}#{
|
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
|
data/lib/verdict/storage.rb
CHANGED
data/lib/verdict/version.rb
CHANGED
data/shipit.rubygems.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# using the default shipit config
|
data/test/conversion_test.rb
CHANGED
@@ -19,7 +19,7 @@ class ConversionTest < Minitest::Test
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_assignment_lookup
|
22
|
-
@experiment.
|
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
|
data/test/experiment_test.rb
CHANGED
@@ -278,8 +278,8 @@ class ExperimentTest < Minitest::Test
|
|
278
278
|
groups { group :all, 100 }
|
279
279
|
end
|
280
280
|
|
281
|
-
e.
|
282
|
-
e.
|
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.
|
293
|
-
e.
|
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.
|
303
|
-
e.
|
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
|
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::
|
8
|
-
@experiment = Verdict::Experiment.new(:
|
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
|
-
@
|
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/
|
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
|
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
|
-
|
36
|
-
|
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.
|
38
|
+
assert @storage.get(@experiment.handle.to_s, 'started_at').nil?
|
42
39
|
@experiment.send(:ensure_experiment_has_started)
|
43
|
-
|
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.
|
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
|
-
-
|
114
|
-
-
|
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/
|
153
|
-
- test/storage/
|
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.
|
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/
|
194
|
-
- test/storage/
|
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