verdict 0.14.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f05448f375933cba0d01c10a4e302b684242297e779d96cd3bf45cff2f6fea74
4
- data.tar.gz: d8782ab103377d758a550f36f16fa40cec2caa2662333a99d180aab637985126
3
+ metadata.gz: a6d20d61b4b74ae3cdc111f872f18c6bcedf9165bed983dfc489d826edad4e99
4
+ data.tar.gz: 5a23b9b6b678b0e7c7ccdd8c4df24f6029ee6f5c8a3ba16e9850147974b0a490
5
5
  SHA512:
6
- metadata.gz: 14317430460d1fcb88748507d20fd8be9f0a271d434951c6deb4b89203eeb3a5675a7c88e5e1d79bd6404c275bc3a9a3b35558a09df51284c100613963c4ba51
7
- data.tar.gz: 39ad70804de5c962785117525ccd668155efd228860735de13569316d22e6e3ed65e050842753c7283bc21c299ee237a00885686d6963a94e3130594f5c61b21
6
+ metadata.gz: 3c931d20970cb9a75a8f9780d6648493dbf6ca2a5a9eb61f79f35896063b603b36f611cc1e6175710075d0271afad9f73efd795b09eb55c63848dc67def3a68c
7
+ data.tar.gz: '0599822d437c5e7e512a0a75e64312aaf97008d0ae31b75bacab6bdc8361a0750b22f9e14f31d70c7b41df771c20267a2719e883a0394b161e1d4367901252d7'
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby:
11
+ - 2.5
12
+ - 2.6
13
+ - 2.7
14
+ - head
15
+ - jruby
16
+ services:
17
+ redis:
18
+ image: redis
19
+ options: >-
20
+ --health-cmd "redis-cli ping"
21
+ --health-interval 10s
22
+ --health-timeout 5s
23
+ --health-retries 5
24
+ ports:
25
+ - 6379:6379
26
+ name: Tests Ruby ${{ matrix.ruby }}
27
+ steps:
28
+ - uses: actions/checkout@v1
29
+ - name: Set up Ruby ${{ matrix.ruby }}
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: ${{ matrix.ruby }}
33
+ - name: Run tests
34
+ run: |
35
+ gem install bundler
36
+ bundle install --jobs 4 --retry 3
37
+ bundle exec rake
38
+ env:
39
+ REDIS_HOST: localhost
@@ -1,3 +1,18 @@
1
+ ## v0.16.1
2
+ * Change `RedisStorage` scrub to be iterative to avoid SystemStackError while cleaning big experiments
3
+
4
+ ## v0.16.0
5
+ * Allow configuring the `RedisStorage` with a [`ConnectionPool`](https://github.com/mperham/connection_pool) instead of a raw `Redis` connection.
6
+
7
+ ## v0.15.2
8
+ * Fix edge case where inheriting from `Verdict::Experiment` and overriding `subject_qualifies?` resulted in an error.
9
+
10
+ ## v0.15.1
11
+ * Make the `dynamic_qualifies` parameter in `Verdict::Experiment#subject_qualifies?` optional. This fixes a bug where users that were previously calling this method directly experienced issues after v0.15.0
12
+
13
+ ## v0.15.0
14
+ * Add optional `qualifiers` parameter to the `Verdict::Experiment#switch` method. This parameter accepts an array of procs and is used as additional qualifiers. The purpose of this parameter is to allow users to define qualification logic outside of the experiment definition.
15
+
1
16
  ## v0.14.0
2
17
  * Add optional experiment definition method `schedule_stop_new_assignment_timestamp` to support limiting experiment's assignment lifetime with another pre-determined time interval. It allows users to have an assignment cooldown period for stable analysis of the experiment results. Experiment's lifetime now becomes: start experiment -> stop new assignments -> end experiment.
3
18
 
data/Gemfile CHANGED
@@ -2,7 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in experiments.gemspec
4
4
  gemspec
5
- gem "rubysl", platform: :rbx
6
5
  gem "json", platform: :rbx
7
6
 
8
7
  group :development do
data/dev.yml ADDED
@@ -0,0 +1,11 @@
1
+ name: verdict
2
+
3
+ type: ruby
4
+
5
+ up:
6
+ - ruby: 2.7.1
7
+ - bundler
8
+
9
+ commands:
10
+ test:
11
+ run: rake test
@@ -142,18 +142,18 @@ class Verdict::Experiment
142
142
  raise unless disqualify_empty_identifier?
143
143
  end
144
144
 
145
- def assign(subject, context = nil)
145
+ def assign(subject, context = nil, dynamic_qualifiers: [])
146
146
  previous_assignment = lookup(subject)
147
147
 
148
148
  subject_identifier = retrieve_subject_identifier(subject)
149
149
  assignment = if previous_assignment
150
- previous_assignment
151
- elsif subject_qualifies?(subject, context) && is_make_new_assignments?
152
- group = segmenter.assign(subject_identifier, subject, context)
153
- subject_assignment(subject, group, nil, group.nil?)
154
- else
155
- nil_assignment(subject)
156
- end
150
+ previous_assignment
151
+ elsif dynamic_subject_qualifies?(subject, dynamic_qualifiers, context) && is_make_new_assignments?
152
+ group = segmenter.assign(subject_identifier, subject, context)
153
+ subject_assignment(subject, group, nil, group.nil?)
154
+ else
155
+ nil_assignment(subject)
156
+ end
157
157
 
158
158
  store_assignment(assignment)
159
159
  rescue Verdict::StorageError
@@ -194,9 +194,11 @@ class Verdict::Experiment
194
194
  @storage.remove_assignment(self, subject)
195
195
  end
196
196
 
197
- def switch(subject, context = nil)
197
+ # The qualifiers param accepts an array of procs.
198
+ # This is intended for qualification logic that cannot be defined in the experiment definition
199
+ def switch(subject, context = nil, qualifiers: [])
198
200
  return unless is_scheduled?
199
- assign(subject, context).to_sym
201
+ assign(subject, context, dynamic_qualifiers: qualifiers).to_sym
200
202
  end
201
203
 
202
204
  def lookup(subject)
@@ -241,8 +243,9 @@ class Verdict::Experiment
241
243
  @disqualify_empty_identifier
242
244
  end
243
245
 
244
- def subject_qualifies?(subject, context = nil)
246
+ def subject_qualifies?(subject, context = nil, dynamic_qualifiers: [])
245
247
  ensure_experiment_has_started
248
+ return false unless dynamic_qualifiers.all? { |qualifier| qualifier.call(subject) }
246
249
  everybody_qualifies? || @qualifiers.all? { |qualifier| qualifier.call(subject, context) }
247
250
  end
248
251
 
@@ -292,4 +295,15 @@ class Verdict::Experiment
292
295
  def is_make_new_assignments?
293
296
  return !(@schedule_stop_new_assignment_timestamp && @schedule_stop_new_assignment_timestamp <= Time.now)
294
297
  end
298
+
299
+ # Used when a Experiment class has overridden the subject_qualifies? method prior to v0.15.0
300
+ # The previous version of subject_qualifies did not accept dynamic qualifiers, thus this is used to
301
+ # determine how many parameters to pass
302
+ def dynamic_subject_qualifies?(subject, dynamic_qualifiers, context)
303
+ if method(:subject_qualifies?).parameters.include?([:key, :dynamic_qualifiers])
304
+ subject_qualifies?(subject, context, dynamic_qualifiers: dynamic_qualifiers)
305
+ else
306
+ subject_qualifies?(subject, context)
307
+ end
308
+ end
295
309
  end
@@ -6,33 +6,56 @@ module Verdict
6
6
  attr_accessor :redis, :key_prefix
7
7
 
8
8
  def initialize(redis = nil, options = {})
9
- @redis = redis
9
+ if !redis.nil? && !redis.respond_to?(:with)
10
+ @redis = ConnectionPoolLike.new(redis)
11
+ else
12
+ @redis = redis
13
+ end
14
+
10
15
  @key_prefix = options[:key_prefix] || 'experiments/'
11
16
  end
12
17
 
13
18
  protected
14
19
 
20
+ class ConnectionPoolLike
21
+ def initialize(redis)
22
+ @redis = redis
23
+ end
24
+
25
+ def with
26
+ yield @redis
27
+ end
28
+ end
29
+
15
30
  def get(scope, key)
16
- redis.hget(scope_key(scope), key)
31
+ redis.with do |conn|
32
+ conn.hget(scope_key(scope), key)
33
+ end
17
34
  rescue ::Redis::BaseError => e
18
35
  raise Verdict::StorageError, "Redis error: #{e.message}"
19
36
  end
20
37
 
21
38
  def set(scope, key, value)
22
- redis.hset(scope_key(scope), key, value)
39
+ redis.with do |conn|
40
+ conn.hset(scope_key(scope), key, value)
41
+ end
23
42
  rescue ::Redis::BaseError => e
24
43
  raise Verdict::StorageError, "Redis error: #{e.message}"
25
44
  end
26
45
 
27
46
  def remove(scope, key)
28
- redis.hdel(scope_key(scope), key)
47
+ redis.with do |conn|
48
+ conn.hdel(scope_key(scope), key)
49
+ end
29
50
  rescue ::Redis::BaseError => e
30
51
  raise Verdict::StorageError, "Redis error: #{e.message}"
31
52
  end
32
53
 
33
54
  def clear(scope, options)
34
55
  scrub(scope)
35
- redis.del(scope_key(scope))
56
+ redis.with do |conn|
57
+ conn.del(scope_key(scope))
58
+ end
36
59
  rescue ::Redis::BaseError => e
37
60
  raise Verdict::StorageError, "Redis error: #{e.message}"
38
61
  end
@@ -44,11 +67,17 @@ module Verdict
44
67
  end
45
68
 
46
69
  def scrub(scope, cursor: 0)
47
- cursor, results = redis.hscan(scope_key(scope), cursor, count: PAGE_SIZE)
48
- results.map(&:first).each do |key|
49
- remove(scope, key)
70
+ loop do
71
+ cursor, results = redis.with do |conn|
72
+ conn.hscan(scope_key(scope), cursor, count: PAGE_SIZE)
73
+ end
74
+
75
+ results.map(&:first).each do |key|
76
+ remove(scope, key)
77
+ end
78
+
79
+ break if cursor.to_i.zero?
50
80
  end
51
- scrub(scope, cursor: cursor) unless cursor.to_i.zero?
52
81
  end
53
82
  end
54
83
  end
@@ -1,3 +1,3 @@
1
1
  module Verdict
2
- VERSION = "0.14.0"
2
+ VERSION = "0.16.1"
3
3
  end
@@ -641,9 +641,58 @@ class ExperimentTest < Minitest::Test
641
641
  assert_nil e.switch(1)
642
642
  end
643
643
  end
644
+
645
+ def test_custom_qualifiers_success
646
+ e = Verdict::Experiment.new('test') do
647
+ groups do
648
+ group :all, 100
649
+ end
650
+ end
651
+
652
+ subject = 2
653
+ custom_qualifier_a = Proc.new { |subject| subject.even? }
654
+ custom_qualifier_b = Proc.new { |subject| subject > 0 }
655
+
656
+ group = e.switch(subject, qualifiers: [custom_qualifier_a, custom_qualifier_b])
657
+ assert_equal e.group(:all).to_sym, group
658
+ end
659
+
660
+ def test_custom_qualifiers_failure
661
+ e = Verdict::Experiment.new('test') do
662
+ groups do
663
+ group :all, 100
664
+ end
665
+ end
666
+
667
+ subject = 3
668
+ custom_qualifier_a = Proc.new { |subject| subject.even? }
669
+ custom_qualifier_b = Proc.new { |subject| subject > 0 }
670
+
671
+ group = e.switch(subject, qualifiers: [custom_qualifier_a, custom_qualifier_b])
672
+ assert_nil group
673
+ end
674
+
675
+ def test_dynamic_subject_qualifies_call_overridden_method
676
+ e = MyExperiment.new('test') do
677
+ groups do
678
+ group :all, 100
679
+ end
680
+ end
681
+
682
+ group = e.switch(4)
683
+ assert_nil group
684
+ end
685
+
644
686
  private
645
687
 
646
688
  def redis
647
689
  @redis ||= ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
648
690
  end
649
691
  end
692
+
693
+ class MyExperiment < Verdict::Experiment
694
+ def subject_qualifies?(subject, context = nil)
695
+ return false if subject.even?
696
+ super
697
+ end
698
+ end
@@ -6,7 +6,9 @@ require 'fake_app'
6
6
  class CookieStorageTest < Minitest::Test
7
7
  def setup
8
8
  @storage = Verdict::Storage::CookieStorage.new.tap do |s|
9
- s.cookies = ActionDispatch::Cookies::CookieJar.new(nil)
9
+ request = mock()
10
+ request.stubs(:cookies_same_site_protection).returns(proc { :none })
11
+ s.cookies = ActionDispatch::Cookies::CookieJar.new(request)
10
12
  end
11
13
  @experiment = Verdict::Experiment.new(:cookie_storage_test) do
12
14
  groups { group :all, 100 }
@@ -1,97 +1,75 @@
1
1
  require 'test_helper'
2
2
 
3
- class RedisStorageTest < Minitest::Test
4
3
 
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
4
+ module SharedRedisStorageTests
5
+ attr_reader :redis, :experiment, :storage
18
6
 
19
7
  def test_store_and_retrieve_qualified_assignment
20
- refute @redis.hexists(experiment_key, 'assignment_subject_1')
8
+ refute redis.hexists(experiment_key, 'assignment_subject_1')
21
9
 
22
- new_assignment = @experiment.assign('subject_1')
10
+ new_assignment = experiment.assign('subject_1')
23
11
  assert new_assignment.qualified?
24
12
  refute new_assignment.returning?
25
13
 
26
- assert @redis.hexists(experiment_key, 'assignment_subject_1')
14
+ assert redis.hexists(experiment_key, 'assignment_subject_1')
27
15
 
28
- returning_assignment = @experiment.assign('subject_1')
16
+ returning_assignment = experiment.assign('subject_1')
29
17
  assert returning_assignment.returning?
30
18
  assert_equal new_assignment.experiment, returning_assignment.experiment
31
19
  assert_equal new_assignment.group, returning_assignment.group
32
20
  end
33
21
 
34
22
  def test_store_and_retrieve_unqualified_assignment
35
- refute @redis.hexists(experiment_key, 'assignment_subject_2')
23
+ refute redis.hexists(experiment_key, 'assignment_subject_2')
36
24
 
37
- new_assignment = @experiment.assign('subject_2')
25
+ new_assignment = experiment.assign('subject_2')
38
26
 
39
27
  refute new_assignment.returning?
40
28
  refute new_assignment.qualified?
41
- assert @redis.hexists(experiment_key, 'assignment_subject_2')
29
+ assert redis.hexists(experiment_key, 'assignment_subject_2')
42
30
 
43
- returning_assignment = @experiment.assign('subject_2')
31
+ returning_assignment = experiment.assign('subject_2')
44
32
  assert returning_assignment.returning?
45
33
  assert_equal new_assignment.experiment, returning_assignment.experiment
46
34
  assert_nil new_assignment.group
47
35
  assert_nil returning_assignment.group
48
36
  end
49
37
 
50
- def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
51
- @redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
52
- assert !@experiment.assign('subject_1').qualified?
53
- end
54
-
55
- def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
56
- @redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
57
- assert !@experiment.assign('subject_1').qualified?
58
- end
59
-
60
38
  def test_remove_subject_assignment
61
- @experiment.assign('subject_3')
62
- assert @redis.hexists(experiment_key, 'assignment_subject_3')
63
- @experiment.remove_subject_assignment('subject_3')
64
- refute @redis.hexists(experiment_key, 'assignment_subject_3')
39
+ experiment.assign('subject_3')
40
+ assert redis.hexists(experiment_key, 'assignment_subject_3')
41
+ experiment.remove_subject_assignment('subject_3')
42
+ refute redis.hexists(experiment_key, 'assignment_subject_3')
65
43
  end
66
44
 
67
45
  def test_started_at
68
- refute @redis.hexists(experiment_key, "started_at")
69
- a = @experiment.send(:ensure_experiment_has_started)
70
- assert @redis.hexists(experiment_key, "started_at")
71
- assert_equal a, @experiment.started_at
46
+ refute redis.hexists(experiment_key, "started_at")
47
+ a = experiment.send(:ensure_experiment_has_started)
48
+ assert redis.hexists(experiment_key, "started_at")
49
+ assert_equal a, experiment.started_at
72
50
  end
73
51
 
74
52
  def test_cleanup_with_scope_argument
75
53
  1000.times do |n|
76
- @experiment.assign("something_#{n}")
54
+ experiment.assign("something_#{n}")
77
55
  end
78
56
 
79
- assert_operator @redis, :exists, experiment_key
57
+ assert_operator redis, :exists, experiment_key
80
58
 
81
59
  Verdict.default_logger.expects(:warn).with(regexp_matches(/deprecated/))
82
- @storage.cleanup(:redis_storage)
83
- refute_operator @redis, :exists, experiment_key
60
+ storage.cleanup(:redis_storage)
61
+ refute_operator redis, :exists, experiment_key
84
62
  end
85
63
 
86
64
  def test_cleanup
87
65
  1000.times do |n|
88
- @experiment.assign("something_#{n}")
66
+ experiment.assign("something_#{n}")
89
67
  end
90
68
 
91
- assert_operator @redis, :exists, experiment_key
69
+ assert_operator redis, :exists, experiment_key
92
70
 
93
- @storage.cleanup(@experiment)
94
- refute_operator @redis, :exists, experiment_key
71
+ storage.cleanup(experiment)
72
+ refute_operator redis, :exists, experiment_key
95
73
  end
96
74
 
97
75
  private
@@ -100,3 +78,67 @@ class RedisStorageTest < Minitest::Test
100
78
  "experiments/redis_storage"
101
79
  end
102
80
  end
81
+
82
+ class RedisStorageTest < Minitest::Test
83
+ include SharedRedisStorageTests
84
+
85
+ def setup
86
+ @redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
87
+ @storage = storage = Verdict::Storage::RedisStorage.new(@redis)
88
+ @experiment = Verdict::Experiment.new(:redis_storage) do
89
+ qualify { |s| s == 'subject_1' }
90
+ groups { group :all, 100 }
91
+ storage storage, store_unqualified: true
92
+ end
93
+ end
94
+
95
+ def teardown
96
+ @redis.del('experiments/redis_storage')
97
+ end
98
+
99
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_reads
100
+ redis.stubs(:hget).raises(::Redis::BaseError, "Redis is down")
101
+ assert !experiment.assign('subject_1').qualified?
102
+ end
103
+
104
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
105
+ redis.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
106
+ assert !experiment.assign('subject_1').qualified?
107
+ end
108
+ end
109
+
110
+ class RedisStorageConnectionPoolTest < Minitest::Test
111
+ include SharedRedisStorageTests
112
+
113
+ def setup
114
+ @redis = ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
115
+ @redis_pool = ::ConnectionPool.new(size: 3) do
116
+ ::Redis.new(host: REDIS_HOST, port: REDIS_PORT)
117
+ end
118
+ @storage = storage = Verdict::Storage::RedisStorage.new(@redis_pool)
119
+ @experiment = Verdict::Experiment.new(:redis_storage) do
120
+ qualify { |s| s == 'subject_1' }
121
+ groups { group :all, 100 }
122
+ storage storage, store_unqualified: true
123
+ end
124
+ end
125
+
126
+ def teardown
127
+ @redis.del('experiments/redis_storage')
128
+ end
129
+
130
+ def test_assign_should_return_unqualified_when_redis_is_unavailable_for_writes
131
+ redis_mock = stub
132
+ redis_mock.stubs(:hget).returns(nil)
133
+ redis_mock.stubs(:hset).raises(::Redis::BaseError, "Redis is down")
134
+ @redis_pool.stubs(:with).yields(redis_mock)
135
+ assert !@experiment.assign('subject_1').qualified?
136
+ end
137
+
138
+ def test_remove_subject_assignment
139
+ @experiment.assign('subject_3')
140
+ assert @redis.hexists(experiment_key, 'assignment_subject_3')
141
+ @experiment.remove_subject_assignment('subject_3')
142
+ refute @redis.hexists(experiment_key, 'assignment_subject_3')
143
+ end
144
+ end
@@ -13,6 +13,7 @@ require "mocha/setup"
13
13
  require "timecop"
14
14
  require "verdict"
15
15
  require "redis"
16
+ require "connection_pool"
16
17
 
17
18
  REDIS_HOST = ENV['REDIS_HOST'].nil? ? '127.0.0.1' : ENV['REDIS_HOST']
18
19
  REDIS_PORT = (ENV['REDIS_PORT'].nil? ? '6379' : ENV['REDIS_PORT']).to_i
@@ -25,4 +25,5 @@ Gem::Specification.new do |gem|
25
25
  gem.add_development_dependency("timecop")
26
26
  gem.add_development_dependency("redis")
27
27
  gem.add_development_dependency("rails")
28
+ gem.add_development_dependency("connection_pool")
28
29
  end
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.14.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-29 00:00:00.000000000 Z
11
+ date: 2020-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: connection_pool
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: Shopify Experiments classes
98
112
  email:
99
113
  - kevin.mcphillips@shopify.com
@@ -103,14 +117,15 @@ extensions: []
103
117
  extra_rdoc_files: []
104
118
  files:
105
119
  - ".github/probots.yml"
120
+ - ".github/workflows/ci.yml"
106
121
  - ".gitignore"
107
- - ".travis.yml"
108
122
  - CHANGELOG.md
109
123
  - CONTRIBUTING.md
110
124
  - Gemfile
111
125
  - LICENSE
112
126
  - README.md
113
127
  - Rakefile
128
+ - dev.yml
114
129
  - docs/concepts.md
115
130
  - lib/verdict.rb
116
131
  - lib/verdict/assignment.rb
@@ -1,16 +0,0 @@
1
- language: ruby
2
- cache: bundler
3
- before_install: gem update bundler
4
- script: bundle exec rake
5
- rvm:
6
- - 2.0
7
- - 2.1
8
- - 2.3.3
9
- - ruby-head
10
- - jruby
11
- matrix:
12
- allow_failures:
13
- - rvm: ruby-head
14
- - rvm: jruby
15
- services:
16
- - redis-server