verdict 0.12.0 → 0.13.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 -0
- data/CHANGELOG.md +7 -1
- data/CONTRIBUTING.md +7 -0
- data/README.md +10 -17
- data/docs/concepts.md +35 -0
- data/lib/verdict/experiment.rb +27 -1
- data/lib/verdict/railtie.rb +8 -3
- data/lib/verdict/storage.rb +1 -0
- data/lib/verdict/storage/cookie_storage.rb +105 -0
- data/lib/verdict/version.rb +1 -1
- data/test/experiment_test.rb +105 -0
- data/test/storage/cookie_storage_test.rb +81 -0
- data/verdict.gemspec +2 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec6df6bdd38dc947ff1072e1dce844ab4d3ddd522701c8de578a35b4c9c51be8
|
4
|
+
data.tar.gz: e6b62b2c579a363f37526079389bea4cb16eb28ed7573dec2d3fa8f8c74ee4ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1399fa37c4575888ffb83aea54469784db11ceef1ebc99f5c1fa5de75b936f460b1f34dc717c28f4f19a6e969c3a85a3faad022119113cd94f754ad61b4fdb85
|
7
|
+
data.tar.gz: 7f1836b8d23ca79a4338f0a4f4ed244ebe7bdbd8b9f03529f2cb00e110c280b1033e112928da900d805112eb0036e0f74278ea021514016541accf01d8c3308c
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
## v0.13.0
|
2
|
+
|
3
|
+
* Add optional experiment definition methods `schedule_start_timestamp` and `schedule_end_timestamp` to support limiting experiment's lifetime in a pre-determined time interval.
|
4
|
+
* Support eager loading from within a Rails app using Zeitwerk.
|
5
|
+
* Add `CookieStorage` storage backend. This backend is a distributed store for Verdict and does not support experiment timestamps. It is designed to be used with Rails applications and requires that `.cookies` be set to the `CookieJar` instance before use.
|
6
|
+
|
1
7
|
## v0.12.0
|
2
8
|
|
3
9
|
* Allow options to be passed to `Experiment#cleanup` so they can be forwarded to storage.
|
4
10
|
|
5
|
-
* Changed `Experiment#cleanup` to accept an argument of type `Verdict::Experiment`.
|
11
|
+
* Changed `Experiment#cleanup` to accept an argument of type `Verdict::Experiment`.
|
6
12
|
Passing a `String`/`Symbol` argument is still supported, but will log a deprecation warning.
|
7
13
|
|
8
14
|
## v0.11.0
|
data/CONTRIBUTING.md
ADDED
data/README.md
CHANGED
@@ -19,6 +19,8 @@ Add this line to your application's Gemfile, and run `bundle install`:
|
|
19
19
|
|
20
20
|
If you're using Rails, the Railtie will handle setting the logger to `Rails.logger` and the experiments directory to `app/experiments`. It will also load the rake tasks for you (run `bundle exec rake -T | grep experiments:` for options).
|
21
21
|
|
22
|
+
You may find the [Concepts](docs/concepts.md) documentation a good place to familiarise yourself with Verdict's nomenclature.
|
23
|
+
|
22
24
|
## Usage
|
23
25
|
|
24
26
|
The `Verdict::Experiment` class is used to create an experiment, define control and experiment groups, and to qualify subjects.
|
@@ -42,8 +44,7 @@ Verdict::Experiment.define :my_experiment do
|
|
42
44
|
end
|
43
45
|
```
|
44
46
|
|
45
|
-
Usually you'll want to place this in a file called **my_experiment.rb** in the
|
46
|
-
**/app/experiments** folder.
|
47
|
+
Usually you'll want to place this in a file called **my_experiment.rb** in the **/app/experiments** folder.
|
47
48
|
|
48
49
|
_We recommend that you subclass `Verdict::Experiment` to set some default options for your app's environment, and call `define` on that class instead._
|
49
50
|
|
@@ -71,9 +72,9 @@ Verdict uses a very simple interface for storing experiment assignments. Out of
|
|
71
72
|
|
72
73
|
* Memory
|
73
74
|
* Redis
|
75
|
+
* Cookies
|
74
76
|
|
75
|
-
You can set up storage for your experiment by calling the `storage` method with
|
76
|
-
an object that responds to the following methods:
|
77
|
+
You can set up storage for your experiment by calling the `storage` method with an object that responds to the following methods:
|
77
78
|
|
78
79
|
* `store_assignment(assignment)`
|
79
80
|
* `retrieve_assignment(experiment, subject)`
|
@@ -83,14 +84,15 @@ an object that responds to the following methods:
|
|
83
84
|
|
84
85
|
Regarding the method signatures above, `experiment` is the Experiment instance, `subject` is the Subject instance, and `assignment` is a `Verdict::Assignment` instance.
|
85
86
|
|
86
|
-
The `subject` instance will be identified internally by its `subject_identifier`
|
87
|
-
By default it will use `subject.id.to_s` as `subject_identifier`, but you can change that by overriding `def subject_identifier(subject)` on the experiment.
|
87
|
+
The `subject` instance will be identified internally by its `subject_identifier`. By default it will use `subject.id.to_s` as `subject_identifier`, but you can change that by overriding `def subject_identifier(subject)` on the experiment.
|
88
88
|
|
89
89
|
Storage providers simply store subject assignments and require quick lookups of subject identifiers. They allow for complex (high CPU) assignments, and for assignments that might not always put the same subject in the same group by storing the assignment for later use.
|
90
90
|
|
91
|
-
Storage providers are intended for operational use and should not be used for data analysis. For data analysis, you should use the logger.
|
91
|
+
Storage providers are intended for operational use and should not be used for data analysis. For data analysis, you should use [the logger](#logging).
|
92
|
+
|
93
|
+
When removing old experiments you might want to clean up corresponding experiment assignments, to reduce the amount of data stored and loaded. By using the logger, this data removal doesn't impact historic data or data analysis.
|
92
94
|
|
93
|
-
For more details about these methods, check out the source code for [Verdict::Storage::MockStorage](lib/verdict/storage/mock_storage.rb)
|
95
|
+
For more details about these methods, check out the source code for [`Verdict::Storage::MockStorage`](lib/verdict/storage/mock_storage.rb)
|
94
96
|
|
95
97
|
## Logging
|
96
98
|
|
@@ -101,12 +103,3 @@ You can override the logging by overriding the `def log_assignment(assignment)`
|
|
101
103
|
Logging (as opposed to storage) should be used for data analysis. The logger requires a write-only / forward-only stream to write to, e.g. a log file, Kafka, or an insert-only database table.
|
102
104
|
|
103
105
|
It's possible to run an experiment without defining any storage, though this comes with several drawbacks. Logging on the other hand is required in order to analyze the results.
|
104
|
-
|
105
|
-
|
106
|
-
## Contributing
|
107
|
-
|
108
|
-
1. Fork it
|
109
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
110
|
-
3. Commit your changes, including tests (`git commit -am 'Added some feature'`)
|
111
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
112
|
-
5. Create new Pull Request, and mention @wvanbergen.
|
data/docs/concepts.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Verdict Concepts
|
2
|
+
|
3
|
+
Understanding the following Verdict concepts can help you implement experiments and discuss them with peers.
|
4
|
+
|
5
|
+
- [Experiment](#experiment)
|
6
|
+
- [Qualifiers](#qualifiers)
|
7
|
+
- [Subject](#subject)
|
8
|
+
- [Group](#group)
|
9
|
+
- [Assignment](#assignment)
|
10
|
+
|
11
|
+
### Experiment
|
12
|
+
|
13
|
+
`Experiment`s are defined in `app/experiments` and encapsulate the details of the test including metadata such as the name, description or owners, as well as any [qualifiers](#qualifiers) or the [groups](#groups) available in the test.
|
14
|
+
|
15
|
+
### Qualifiers
|
16
|
+
|
17
|
+
Qualifiers are methods that take two parameters, `subject` and `context`. They return `true` if the `subject` can take this experiment, or `false` if the `subject` is not eligible. Experiments can have multiple qualifiers which are evaluated in succession. If a qualifier returns `false`, no further qualifiers are checked.
|
18
|
+
|
19
|
+
Examples of qualifiers might be subjects that:
|
20
|
+
|
21
|
+
- Originate from certain browsers or devices
|
22
|
+
- Have a specific `Accept-Language` header
|
23
|
+
- Have added items to their cart.
|
24
|
+
|
25
|
+
### Subject
|
26
|
+
|
27
|
+
A subject is the thing that will be [qualified](#qualifiers), and if successfull, [assigned](#assignment) to a particular [group](#group). For example, a subject might represent a user.
|
28
|
+
|
29
|
+
### Group
|
30
|
+
|
31
|
+
Each [experiment](#experiment) will have two or more groups, usually a control group and a test group. Qualified [subjects](#subject) are assigned to one of the groups in the experiment.
|
32
|
+
|
33
|
+
### Assignment
|
34
|
+
|
35
|
+
An assignment occurs when a qualified [subject](#subject) is placed into a [group](#group). Assignments are persisted so repeat visits by the subject skip [qualification](#qualifiers) and re-assignment and simply returns the existing assignment.
|
data/lib/verdict/experiment.rb
CHANGED
@@ -52,6 +52,17 @@ class Verdict::Experiment
|
|
52
52
|
return self
|
53
53
|
end
|
54
54
|
|
55
|
+
# Optional: Together with the "end timestamp", limits the experiment run timeline within
|
56
|
+
# the given time interval. When experiment is not scheduled, subject switch returns nil.
|
57
|
+
# This is useful when the experimenter requires the experiment to run in a strict timeline.
|
58
|
+
def schedule_start_timestamp(timestamp)
|
59
|
+
@schedule_start_timestamp = timestamp
|
60
|
+
end
|
61
|
+
|
62
|
+
def schedule_end_timestamp(timestamp)
|
63
|
+
@schedule_end_timestamp = timestamp
|
64
|
+
end
|
65
|
+
|
55
66
|
def rollout_percentage(percentage, rollout_group_name = :enabled)
|
56
67
|
groups(Verdict::Segmenters::RolloutSegmenter) do
|
57
68
|
group rollout_group_name, percentage
|
@@ -172,6 +183,7 @@ class Verdict::Experiment
|
|
172
183
|
end
|
173
184
|
|
174
185
|
def switch(subject, context = nil)
|
186
|
+
return unless is_scheduled?
|
175
187
|
assign(subject, context).to_sym
|
176
188
|
end
|
177
189
|
|
@@ -239,10 +251,12 @@ class Verdict::Experiment
|
|
239
251
|
def set_start_timestamp
|
240
252
|
@storage.store_start_timestamp(self, started_now = Time.now.utc)
|
241
253
|
started_now
|
254
|
+
rescue NotImplementedError
|
255
|
+
nil
|
242
256
|
end
|
243
257
|
|
244
258
|
def ensure_experiment_has_started
|
245
|
-
@started_at ||=
|
259
|
+
@started_at ||= started_at || set_start_timestamp
|
246
260
|
rescue Verdict::StorageError
|
247
261
|
@started_at ||= Time.now.utc
|
248
262
|
end
|
@@ -250,4 +264,16 @@ class Verdict::Experiment
|
|
250
264
|
def nil_assignment(subject)
|
251
265
|
subject_assignment(subject, nil, nil)
|
252
266
|
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
def is_scheduled?
|
271
|
+
if @schedule_start_timestamp and @schedule_start_timestamp > Time.now
|
272
|
+
return false
|
273
|
+
end
|
274
|
+
if @schedule_end_timestamp and @schedule_end_timestamp < Time.now
|
275
|
+
return false
|
276
|
+
end
|
277
|
+
return true
|
278
|
+
end
|
253
279
|
end
|
data/lib/verdict/railtie.rb
CHANGED
@@ -5,10 +5,15 @@ class Verdict::Railtie < Rails::Railtie
|
|
5
5
|
Verdict.default_logger = Rails.logger
|
6
6
|
|
7
7
|
Verdict.directory ||= Rails.root.join('app', 'experiments')
|
8
|
-
app.config.eager_load_paths -= Dir[Verdict.directory.to_s]
|
9
8
|
|
10
|
-
|
11
|
-
|
9
|
+
if Rails.gem_version >= Gem::Version.new('6.0.0') && Rails.autoloaders.zeitwerk_enabled?
|
10
|
+
Rails.autoloaders.main.ignore(Verdict.directory)
|
11
|
+
else
|
12
|
+
app.config.eager_load_paths -= Dir[Verdict.directory.to_s]
|
13
|
+
|
14
|
+
# Re-freeze eager load paths to ensure they blow up if modified at runtime, as Rails does
|
15
|
+
app.config.eager_load_paths.freeze
|
16
|
+
end
|
12
17
|
end
|
13
18
|
|
14
19
|
config.to_prepare do
|
data/lib/verdict/storage.rb
CHANGED
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Verdict
|
4
|
+
module Storage
|
5
|
+
# CookieStorage, unlike other Verdict storage classes, is distributed and stored on the client.
|
6
|
+
# Because cookies are opaque to users, we obsfucate information about the test such as the
|
7
|
+
# human readable names for the experiment or assignment group. This means this class assumes
|
8
|
+
# that `name` will be an obsfucated value, or one that we comfortable being "public".
|
9
|
+
class CookieStorage < BaseStorage
|
10
|
+
DEFAULT_COOKIE_LIFESPAN_SECONDS = 15778476 # 6 months
|
11
|
+
KEY = :assignment
|
12
|
+
|
13
|
+
attr_accessor :cookies
|
14
|
+
attr_reader :cookie_lifespan
|
15
|
+
|
16
|
+
def initialize(cookie_lifespan: DEFAULT_COOKIE_LIFESPAN_SECONDS)
|
17
|
+
@cookies = nil
|
18
|
+
@cookie_lifespan = cookie_lifespan
|
19
|
+
end
|
20
|
+
|
21
|
+
def store_assignment(assignment)
|
22
|
+
data = {
|
23
|
+
group: digest_of(assignment.group.to_s),
|
24
|
+
created_at: assignment.created_at.strftime('%FT%TZ')
|
25
|
+
}
|
26
|
+
|
27
|
+
set(assignment.experiment.handle.to_s, KEY, JSON.dump(data))
|
28
|
+
end
|
29
|
+
|
30
|
+
def retrieve_assignment(experiment, subject)
|
31
|
+
if (value = get(experiment.handle.to_s, KEY))
|
32
|
+
data = parse_cookie_value(value)
|
33
|
+
group = find_group_by_digest(experiment, data['group'])
|
34
|
+
|
35
|
+
if group.nil?
|
36
|
+
experiment.remove_subject_assignment(subject)
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
experiment.subject_assignment(subject, group, Time.xmlschema(data['created_at']))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_assignment(experiment, _subject)
|
45
|
+
remove(experiment.handle.to_s, KEY)
|
46
|
+
end
|
47
|
+
|
48
|
+
def retrieve_start_timestamp(_experiment)
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def store_start_timestamp(_experiment, _timestamp)
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def get(scope, key)
|
59
|
+
ensure_cookies_is_set
|
60
|
+
cookies[scope_key(scope, key)]
|
61
|
+
end
|
62
|
+
|
63
|
+
def set(scope, key, value)
|
64
|
+
ensure_cookies_is_set
|
65
|
+
cookies[scope_key(scope, key)] = {
|
66
|
+
value: value,
|
67
|
+
expires: Time.now.utc.advance(seconds: cookie_lifespan),
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def remove(scope, key)
|
72
|
+
ensure_cookies_is_set
|
73
|
+
cookies.delete(scope_key(scope, key))
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def ensure_cookies_is_set
|
79
|
+
raise Verdict::StorageError, 'cookies must be an instance of ActionDispatch::Cookies::CookieJar' if cookies.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
def digest_of(value)
|
83
|
+
Digest::MD5.hexdigest(value)
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_group_by_digest(experiment, digest)
|
87
|
+
experiment.groups.values.find do |group|
|
88
|
+
digest_of(group.to_s) == digest
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_cookie_value(value)
|
93
|
+
value = value[:value] if value.is_a?(Hash)
|
94
|
+
|
95
|
+
JSON.parse(value)
|
96
|
+
rescue JSON::ParserError, TypeError
|
97
|
+
{}
|
98
|
+
end
|
99
|
+
|
100
|
+
def scope_key(scope, key)
|
101
|
+
"#{digest_of(scope)}_#{key}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/verdict/version.rb
CHANGED
data/test/experiment_test.rb
CHANGED
@@ -414,6 +414,16 @@ class ExperimentTest < Minitest::Test
|
|
414
414
|
assert e.started?, "The experiment should have started after the first assignment"
|
415
415
|
end
|
416
416
|
|
417
|
+
def test_experiment_set_start_timestamp_handles_storage_that_does_not_implement_timestamps
|
418
|
+
e = Verdict::Experiment.new('starting_test') do
|
419
|
+
groups { group :all, 100 }
|
420
|
+
end
|
421
|
+
|
422
|
+
e.storage.expects(:store_start_timestamp).raises(NotImplementedError)
|
423
|
+
|
424
|
+
assert_nil e.send(:set_start_timestamp)
|
425
|
+
end
|
426
|
+
|
417
427
|
def test_no_storage
|
418
428
|
e = Verdict::Experiment.new('starting_test') do
|
419
429
|
groups { group :all, 100 }
|
@@ -460,6 +470,101 @@ class ExperimentTest < Minitest::Test
|
|
460
470
|
end
|
461
471
|
end
|
462
472
|
|
473
|
+
def test_is_scheduled
|
474
|
+
e = Verdict::Experiment.new(:json) do
|
475
|
+
groups do
|
476
|
+
group :a, :half
|
477
|
+
group :b, :rest
|
478
|
+
end
|
479
|
+
schedule_start_timestamp Time.new(2020, 1, 1)
|
480
|
+
schedule_end_timestamp Time.new(2020, 1, 3)
|
481
|
+
end
|
482
|
+
|
483
|
+
# Within the interval
|
484
|
+
Timecop.freeze(Time.new(2020, 1, 2)) do
|
485
|
+
assert e.send(:is_scheduled?)
|
486
|
+
end
|
487
|
+
# Too early
|
488
|
+
Timecop.freeze(Time.new(2019, 12, 30)) do
|
489
|
+
assert !e.send(:is_scheduled?)
|
490
|
+
end
|
491
|
+
# Too late
|
492
|
+
Timecop.freeze(Time.new(2020, 1, 4)) do
|
493
|
+
assert !e.send(:is_scheduled?)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
def test_is_scheduled_no_end_timestamp
|
498
|
+
e = Verdict::Experiment.new(:json) do
|
499
|
+
groups do
|
500
|
+
group :a, :half
|
501
|
+
group :b, :rest
|
502
|
+
end
|
503
|
+
schedule_start_timestamp Time.new(2020, 1, 1)
|
504
|
+
end
|
505
|
+
|
506
|
+
# Within the interval because there is no end date
|
507
|
+
Timecop.freeze(Time.new(2030, 1, 1)) do
|
508
|
+
assert e.send(:is_scheduled?)
|
509
|
+
end
|
510
|
+
# Too early
|
511
|
+
Timecop.freeze(Time.new(2019, 12, 30)) do
|
512
|
+
assert !e.send(:is_scheduled?)
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
def test_is_scheduled_no_start_timestamp
|
517
|
+
e = Verdict::Experiment.new(:json) do
|
518
|
+
groups do
|
519
|
+
group :a, :half
|
520
|
+
group :b, :rest
|
521
|
+
end
|
522
|
+
schedule_end_timestamp Time.new(2020, 1, 3)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Within the interval because there is no start date
|
526
|
+
Timecop.freeze(Time.new(2019, 12, 30)) do
|
527
|
+
assert e.send(:is_scheduled?)
|
528
|
+
end
|
529
|
+
# Too late
|
530
|
+
Timecop.freeze(Time.new(2020, 1, 4)) do
|
531
|
+
assert !e.send(:is_scheduled?)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def test_switch_respects_time_schedule
|
536
|
+
e = Verdict::Experiment.new('test') do
|
537
|
+
groups do
|
538
|
+
group :a, :half
|
539
|
+
group :b, :rest
|
540
|
+
end
|
541
|
+
schedule_start_timestamp Time.new(2020, 1, 1)
|
542
|
+
schedule_end_timestamp Time.new(2020, 1, 2)
|
543
|
+
end
|
544
|
+
|
545
|
+
Timecop.freeze(Time.new(2020, 1, 3)) do
|
546
|
+
assert_nil e.switch(1)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def test_switch_respects_time_schedule_even_after_assignment
|
551
|
+
e = Verdict::Experiment.new('test') do
|
552
|
+
groups do
|
553
|
+
group :a, :half
|
554
|
+
group :b, :rest
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
assert_equal :a, e.switch(1)
|
559
|
+
|
560
|
+
e.schedule_start_timestamp Time.new(2020, 1, 1)
|
561
|
+
e.schedule_end_timestamp Time.new(2020, 1, 2)
|
562
|
+
|
563
|
+
Timecop.freeze(Time.new(2020, 1, 3)) do
|
564
|
+
assert_nil e.switch(1)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
463
568
|
private
|
464
569
|
|
465
570
|
def redis
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'fake_app'
|
5
|
+
|
6
|
+
class CookieStorageTest < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@storage = Verdict::Storage::CookieStorage.new.tap do |s|
|
9
|
+
s.cookies = ActionDispatch::Cookies::CookieJar.new(nil)
|
10
|
+
end
|
11
|
+
@experiment = Verdict::Experiment.new(:cookie_storage_test) do
|
12
|
+
groups { group :all, 100 }
|
13
|
+
storage @storage, store_unqualified: true
|
14
|
+
end
|
15
|
+
@subject = stub(id: 'bob')
|
16
|
+
@assignment = Verdict::Assignment.new(@experiment, @subject, @experiment.group(:all), nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_cookie_lifespan_has_a_default
|
20
|
+
cookie_lifespan = Verdict::Storage::CookieStorage.new.cookie_lifespan
|
21
|
+
|
22
|
+
assert_equal Verdict::Storage::CookieStorage::DEFAULT_COOKIE_LIFESPAN_SECONDS, cookie_lifespan
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_cookie_lifespan_can_be_configured
|
26
|
+
storage = Verdict::Storage::CookieStorage.new(cookie_lifespan: 60)
|
27
|
+
|
28
|
+
assert_equal 60, storage.cookie_lifespan
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_raises_storage_error_when_cookies_is_nil
|
32
|
+
storage = Verdict::Storage::CookieStorage.new
|
33
|
+
|
34
|
+
assert_raises(Verdict::StorageError) { storage.store_assignment(@assignment) }
|
35
|
+
assert_raises(Verdict::StorageError) { storage.retrieve_assignment(@experiment, @subject) }
|
36
|
+
assert_raises(Verdict::StorageError) { storage.remove_assignment(@experiment, @subject) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_store_assignment_returns_true_when_an_assignment_is_stored
|
40
|
+
assert @storage.store_assignment(@assignment)
|
41
|
+
refute_nil @storage.retrieve_assignment(@experiment, @subject)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_retrieve_assignment_returns_an_assignment
|
45
|
+
@storage.store_assignment(@assignment)
|
46
|
+
assignment = @storage.retrieve_assignment(@experiment, @subject)
|
47
|
+
|
48
|
+
assert assignment.returning?
|
49
|
+
assert_equal :all, assignment.handle.to_sym
|
50
|
+
assert_equal @experiment, assignment.experiment
|
51
|
+
assert_equal @subject, assignment.subject
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_retrieve_assignment_returns_nil_when_an_assignment_does_not_exist
|
55
|
+
assert_nil @storage.retrieve_assignment(@experiment, @subject)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_retrieve_assignment_returns_nil_when_the_assignment_group_is_invalid
|
59
|
+
invalid_group = Verdict::Group.new(@experiment, :invalid)
|
60
|
+
assignment = Verdict::Assignment.new(@experiment, @subject, invalid_group, nil)
|
61
|
+
|
62
|
+
@storage.store_assignment(assignment)
|
63
|
+
|
64
|
+
assert_nil @storage.retrieve_assignment(@experiment, @subject)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_remove_assignment_returns_true_when_removing_an_assignment
|
68
|
+
@storage.store_assignment(@assignment)
|
69
|
+
|
70
|
+
assert @storage.remove_assignment(@experiment, nil)
|
71
|
+
assert_nil @storage.retrieve_assignment(@experiment, @subject)
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_retrieve_start_timestamp_always_returns_nil
|
75
|
+
assert_nil @storage.retrieve_start_timestamp(nil)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_store_start_timestamp_raises_not_implemented_error
|
79
|
+
assert_raises(NotImplementedError) { @storage.store_start_timestamp(nil, nil) }
|
80
|
+
end
|
81
|
+
end
|
data/verdict.gemspec
CHANGED
@@ -17,6 +17,8 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
|
+
gem.metadata["allowed_push_host"] = "https://rubygems.org"
|
21
|
+
|
20
22
|
gem.add_development_dependency("minitest", '~> 5.2')
|
21
23
|
gem.add_development_dependency("rake")
|
22
24
|
gem.add_development_dependency("mocha")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: verdict
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-04-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -106,10 +106,12 @@ files:
|
|
106
106
|
- ".gitignore"
|
107
107
|
- ".travis.yml"
|
108
108
|
- CHANGELOG.md
|
109
|
+
- CONTRIBUTING.md
|
109
110
|
- Gemfile
|
110
111
|
- LICENSE
|
111
112
|
- README.md
|
112
113
|
- Rakefile
|
114
|
+
- docs/concepts.md
|
113
115
|
- lib/verdict.rb
|
114
116
|
- lib/verdict/assignment.rb
|
115
117
|
- lib/verdict/conversion.rb
|
@@ -126,6 +128,7 @@ files:
|
|
126
128
|
- lib/verdict/segmenters/static_segmenter.rb
|
127
129
|
- lib/verdict/storage.rb
|
128
130
|
- lib/verdict/storage/base_storage.rb
|
131
|
+
- lib/verdict/storage/cookie_storage.rb
|
129
132
|
- lib/verdict/storage/legacy_redis_storage.rb
|
130
133
|
- lib/verdict/storage/memory_storage.rb
|
131
134
|
- lib/verdict/storage/mock_storage.rb
|
@@ -148,6 +151,7 @@ files:
|
|
148
151
|
- test/segmenters/rollout_segmenter_test.rb
|
149
152
|
- test/segmenters/static_segmenter_test.rb
|
150
153
|
- test/storage/base_storage_test.rb
|
154
|
+
- test/storage/cookie_storage_test.rb
|
151
155
|
- test/storage/legacy_redis_storage_test.rb
|
152
156
|
- test/storage/memory_storage_test.rb
|
153
157
|
- test/storage/redis_storage_test.rb
|
@@ -156,7 +160,8 @@ files:
|
|
156
160
|
- verdict.gemspec
|
157
161
|
homepage: http://github.com/Shopify/verdict
|
158
162
|
licenses: []
|
159
|
-
metadata:
|
163
|
+
metadata:
|
164
|
+
allowed_push_host: https://rubygems.org
|
160
165
|
post_install_message:
|
161
166
|
rdoc_options: []
|
162
167
|
require_paths:
|
@@ -193,6 +198,7 @@ test_files:
|
|
193
198
|
- test/segmenters/rollout_segmenter_test.rb
|
194
199
|
- test/segmenters/static_segmenter_test.rb
|
195
200
|
- test/storage/base_storage_test.rb
|
201
|
+
- test/storage/cookie_storage_test.rb
|
196
202
|
- test/storage/legacy_redis_storage_test.rb
|
197
203
|
- test/storage/memory_storage_test.rb
|
198
204
|
- test/storage/redis_storage_test.rb
|