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 +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