redis-store-with-cas 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b078891090e8eb34a95b4ee0bc6a8a4f2f9fc179
4
+ data.tar.gz: c80c840495cff70bc4e3b46da42f04d9a5fae9bd
5
+ SHA512:
6
+ metadata.gz: 0b1d51e1aca950890aa2bf18c97b284269c5fbc9352a174e27c277c5678af10bb7d9349a1775bc60b3135bae09e9de2132e8ecb393a405f318dd655cf6b8eb65
7
+ data.tar.gz: 033d735df907553eb5e537c3d8076cd2771079b28aff0ee89e676bb5223ad56305d20f32f7e5113b7c95ae0c1202fcf019ccda6eb04e644355d2ef66014b532f
@@ -0,0 +1,9 @@
1
+ Gemfile.lock
2
+ *.gem
3
+ tmp/
4
+ stdout
5
+ .idea/
6
+ .ruby-version
7
+ .ruby-gemset
8
+ .yardoc/
9
+ doc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Rajko Albrecht
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # Enhance redis store with Compare-and-Swap methods
2
+
3
+ __Redis Store With CAS__ enhance the existing [redis-store for Ruby Frameworks](https://github.com/redis-store/redis-store)
4
+ with so called Compare-And-Swap.
5
+
6
+ This is required for thread-safe-usage of cache systems like [Identity cache](https://github.com/Shopify/identity_cache).
7
+
8
+ The tests and workflow are build like in [Memcached store for ruby](https://github.com/Shopify/memcached_store)
9
+
10
+ ## Installation
11
+
12
+ Add the following to your Gemfile
13
+
14
+ `gem 'redis-store-with-cas`
15
+
16
+ ## Usage
17
+
18
+ Add a
19
+
20
+ `require 'redis-store-with-cas'`
21
+
22
+ line to your source before first call to `Redis::Store.new`. Now each instance of `Redis::Store` has
23
+ two new methods:
24
+
25
+ * `cas`
26
+ * `cas_multi`
27
+
28
+ ## Copyright
29
+
30
+ 2017 Rajko Albrecht - [http://alwins-world.de](http://alwins-world.de)
@@ -0,0 +1,4 @@
1
+ require 'bundler/setup'
2
+ require 'rake'
3
+ require 'bundler/gem_tasks'
4
+ require 'redis-store/testing/tasks'
data/irb.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "irb"
2
+ require 'redis-store-with-cas'
3
+
4
+ IRB.start(__FILE__)
@@ -0,0 +1,3 @@
1
+ require 'redis'
2
+ require 'redis/store'
3
+ require 'redis/store-with-cas'
@@ -0,0 +1,15 @@
1
+ require 'redis'
2
+ require 'redis/store'
3
+ require 'redis/store-with-cas/cas'
4
+ require 'redis/store-with-cas/version'
5
+ require 'redis/store/marshalling'
6
+ require 'redis/store/namespace'
7
+ require 'redis/store-with-cas/namespace'
8
+
9
+ class Redis
10
+
11
+ class Store < self
12
+ include Cas
13
+ end
14
+
15
+ end
@@ -0,0 +1,107 @@
1
+ class Redis
2
+
3
+ # simple enhancement of Redis::Store
4
+ class Store < self
5
+
6
+ # Implements Compare-And-Swap (or as Redis says Compare-And-Save)
7
+ # on top of Redis::Store using Redis::Store::watch. It is designated for simple values, not redis-lists/hashes etc
8
+ module Cas
9
+
10
+ # Single CAS
11
+ #
12
+ # Trys to save change the value of a redis-key when it may not done in a atomic matter. Eg, you have do some
13
+ # checks on old value before setting the new one and so on. If key content changes meanwhile it refuses to
14
+ # set and you will not overwrite changes from other Thread or Process.
15
+ #
16
+ # This method works only on existing keys in redis.
17
+ # It works only with keys holding a value, eg, read/writeable with get/set
18
+ #
19
+ # @yield [value] the current value of given key
20
+ # @yieldparam [String] key the key to get and set the value
21
+ # @yieldreturn [String] the new value to store in key
22
+ # @return [Boolean] true if new value was stored, otherwise false
23
+ # @param key [String] the key to set. Must not be nil and key must exists in Redis
24
+ # @param ttl [Integer] if not nil and integer > 0 set a TTL to the changed key
25
+ # @example
26
+ #
27
+ # storewithcas.cas('examplekey') do |value|
28
+ # # value is the CURRENT value!
29
+ # new_value = do_some_important_stuff_here(value)
30
+ # new_value # write back to redis unless key has changed meanwhile
31
+ # end
32
+ def cas key, ttl=nil
33
+ return false unless exists(key)
34
+ watch(key) do
35
+ value = get(key)
36
+ value = yield value
37
+ ires = multi do |multi|
38
+ multi.set(key,value,ttl.nil? ? {} : {:expire_after => ttl})
39
+ end
40
+ ires.is_a?(Array) && ires[0] == 'OK'
41
+ end
42
+ end
43
+
44
+ # Multi CAS
45
+ #
46
+ # Safe changing multiple keys at once. It works only with keys holding a value, eg, read/writeable with get/set
47
+ #
48
+ # @example
49
+ #
50
+ # storewithcas.cas_multi('key1','key1') do |currenthash|
51
+ # newhashedvalues = make_something_with_current(currenthash)
52
+ # newhashedvalues
53
+ # end
54
+ #
55
+ # @example
56
+ # storewithcas.cas_multi('k1','k2', :expire_in => 1200) do |currenthash| #=> ttl for all keys swapped
57
+ # {'k1' => '1', 'k2' => 2}
58
+ # end
59
+ #
60
+ # @yield [values] a key=>value hash with values of given keys. keys not existing are not yielded
61
+ # @yieldparam [Array] keys the keys to change
62
+ # @yieldreturn [Hash] key-value-pairs to change. Keys not given in parameter or not existing in redis are ignored.
63
+ # @return [Boolean] true if tried making changes, nil when keylist empty
64
+ # @param keys [Array] the keys to set. Must not be nil and keys must exists in Redis. After keys list you may append hash with options for redis.
65
+ def cas_multi *keys
66
+ return if keys.empty?
67
+ options = extract_options keys
68
+ watch(*keys) do
69
+ values = read_multi(*keys,options)
70
+ valuehash = yield values
71
+ valuehash.map do |name,value|
72
+ multi do |multi|
73
+ multi.set(name,value,options)
74
+ end if values.key?(name)
75
+ end
76
+ true
77
+ end
78
+ end
79
+
80
+ # Read list of keys and return values as Hash
81
+ #
82
+ # @param keys Array the keys to read
83
+ # @return [Hash] key-value-pairs of key found in redis, eg, exists.
84
+ #
85
+ # @example
86
+ # values = read_multi("key1","key2","key3") #=> {"key1" => "1", "key2" => "2", "key3" => "3"}
87
+ def read_multi *keys
88
+ options = extract_options keys
89
+ keys = keys.select {|k| exists(k)}
90
+ return {} if keys.empty?
91
+ values = mget(*keys,options)
92
+ values.nil? ? {} : Hash[keys.zip(values)]
93
+ end
94
+
95
+ private
96
+
97
+ def extract_options array
98
+ if array.last.is_a?(Hash) && array.last.instance_of?(Hash)
99
+ array.pop
100
+ else
101
+ {}
102
+ end
103
+ end
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,17 @@
1
+ class Redis
2
+ class Store < self
3
+ # Add namespaced methods missing in #Redis::Store
4
+ module Namespace
5
+ # redis.watch for a list of keys respecting the namespace for keys of the
6
+ # Redis::Store instance
7
+ # @param keys Array a list of keys to watch
8
+ # @see Redis::Store::watch in redis-store-gem
9
+ # @see Redis::watch in redis-gem
10
+ def watch *keys
11
+ if keys.any?
12
+ super(*keys.map {|key| interpolate(key) })
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ require 'redis/store'
2
+
3
+ class Redis
4
+ class StoreWithCas < Store
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'redis/store-with-cas/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'redis-store-with-cas'
7
+ s.version = Redis::StoreWithCas::VERSION
8
+ s.authors = ['Rajko Albrecht']
9
+ s.email = ['ral@alwins-world.de']
10
+ s.homepage = 'https://git.alwin-it.de/alwin/redis-store-with-cas'
11
+ s.summary = %q{Extend redis store for Ruby frameworks with cas}
12
+ s.description = %q{Extend the redis store with CAS (compare-and-swap) functionality for better cache integrity support}
13
+
14
+ s.rubyforge_project = 'redis-store-with-cas'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ s.license = 'MIT'
21
+
22
+ s.add_dependency 'redis', '>= 2.2', '< 4'
23
+ s.add_dependency 'redis-store', '>= 1.1.0', '< 2'
24
+
25
+ s.add_development_dependency 'rake', '~> 10'
26
+ s.add_development_dependency 'bundler', '~> 1.3'
27
+ s.add_development_dependency 'mocha', '~> 0.14.0'
28
+ s.add_development_dependency 'minitest', '~> 5'
29
+ s.add_development_dependency 'git', '~> 1.2'
30
+ s.add_development_dependency 'pry-nav', '~> 0.2.4'
31
+ s.add_development_dependency 'pry', '~> 0.10.4'
32
+ s.add_development_dependency 'redis-store-testing'
33
+
34
+ end
@@ -0,0 +1,127 @@
1
+ require 'test_helper'
2
+
3
+ describe Redis::StoreWithCas do
4
+ def setup
5
+ @store = Redis::Store.new :namespace => 'storetest'
6
+ end
7
+
8
+ def teardown
9
+ @store.flushdb
10
+ @store.quit
11
+ end
12
+
13
+ def test_cas
14
+ @store.set('foo', 'baz')
15
+ assert(@store.cas('foo') do |value|
16
+ assert_equal 'baz', value
17
+ 'bar'
18
+ end)
19
+ assert_equal 'bar', @store.get('foo')
20
+ end
21
+
22
+ def test_cas_with_cache_miss
23
+ refute @store.cas('not_exist') { |_value| flunk }
24
+ end
25
+
26
+ def test_cas_with_conflict
27
+ @store.set('foo', 'bar')
28
+ refute @store.cas('foo') { |_value|
29
+ @store.set('foo', 'baz')
30
+ 'biz'
31
+ }
32
+ assert_equal 'baz', @store.get('foo')
33
+ end
34
+
35
+ # TODO write a Mockup redis with support for watch
36
+ def test_cas_with_ttl
37
+ @store.set('ttlfoo','bar')
38
+ assert(@store.cas('ttlfoo',3600) do |value|
39
+ assert_equal 'bar',value
40
+ 'ttlbar'
41
+ end)
42
+ assert_equal @store.get('ttlfoo'),'ttlbar'
43
+ assert @store.ttl('ttlfoo') > 0
44
+ assert(@store.cas('ttlfoo') do |value|
45
+ 'bar'
46
+ end)
47
+ assert_equal -1,@store.ttl('ttlfoo')
48
+ end
49
+
50
+ def test_cas_multi_with_empty_set
51
+ refute @store.cas_multi { |_hash| flunk }
52
+ end
53
+
54
+
55
+ def test_read_multi
56
+ @store.set('k1','m1')
57
+ @store.set('k2','m2')
58
+ assert_equal({"k1" => "m1","k2" => "m2"},@store.read_multi("k1","k2"))
59
+ end
60
+
61
+ def test_cas_multi
62
+ @store.set('foo', 'bar')
63
+ @store.set('fud', 'biz')
64
+ assert_equal true, (@store.cas_multi('foo', 'fud') do |hash|
65
+ assert_equal({ "foo" => "bar", "fud" => "biz" }, hash)
66
+ { "foo" => "baz", "fud" => "buz" }
67
+ end)
68
+ assert_equal({ "foo" => "baz", "fud" => "buz" }, @store.read_multi('foo', 'fud'))
69
+ end
70
+
71
+ def test_cas_multi_with_ttl
72
+ @store.set('foo', 'bar')
73
+ @store.set('fud', 'biz')
74
+ @store.cas_multi('foo','fud',{:expires_in => 3600}) do |hash|
75
+ { "foo" => "baz", "fud" => "buz" }
76
+ end
77
+ assert @store.ttl('foo') > 0
78
+ assert @store.ttl('fud') > 0
79
+ end
80
+
81
+ def test_cas_multi_with_cache_miss
82
+ assert(@store.cas_multi('not_exist') do |hash|
83
+ assert hash.empty?
84
+ {}
85
+ end)
86
+ end
87
+
88
+ def test_cas_multi_with_altered_key
89
+ @store.set('foo', 'baz')
90
+ assert @store.cas_multi('foo') { |_hash| { 'fu' => 'baz' } }
91
+ assert_nil @store.get('fu')
92
+ assert_equal 'baz', @store.get('foo')
93
+ end
94
+
95
+ def test_cas_multi_with_partial_miss
96
+ @store.set('foo', 'baz')
97
+ assert(@store.cas_multi('foo', 'bar') do |hash|
98
+ assert_equal({ "foo" => "baz" }, hash)
99
+ {}
100
+ end)
101
+ assert_equal 'baz', @store.get('foo')
102
+ end
103
+
104
+ def test_cas_multi_with_partial_update
105
+ @store.set('foo', 'bar')
106
+ @store.set('fud', 'biz')
107
+ assert(@store.cas_multi('foo', 'fud') do |hash|
108
+ assert_equal({ "foo" => "bar", "fud" => "biz" }, hash)
109
+
110
+ { "foo" => "baz" }
111
+ end)
112
+ assert_equal({ "foo" => "baz", "fud" => "biz" }, @store.read_multi('foo', 'fud'))
113
+ end
114
+
115
+ def test_cas_multi_with_partial_conflict
116
+ @store.set('foo', 'bar')
117
+ @store.set('fud', 'biz')
118
+ result = @store.cas_multi('foo', 'fud') do |hash|
119
+ assert_equal({ "foo" => "bar", "fud" => "biz" }, hash)
120
+ @store.set('foo', 'bad')
121
+ { "foo" => "baz", "fud" => "buz" }
122
+ end
123
+ assert result
124
+ assert_equal({ "foo" => "bad", "fud" => "buz" }, @store.read_multi('foo', 'fud'))
125
+ end
126
+
127
+ end
@@ -0,0 +1,6 @@
1
+ require 'bundler/setup'
2
+ require 'minitest/autorun'
3
+ require 'mocha/setup'
4
+
5
+ require 'redis-store-with-cas'
6
+
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ describe Redis::Store::StoreWithCas::VERSION do
4
+ it 'returns current version' do
5
+ Redis::Store::StoreWithCas::VERSION.wont_equal nil
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-store-with-cas
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Rajko Albrecht
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4'
33
+ - !ruby/object:Gem::Dependency
34
+ name: redis-store
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.1.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '2'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 1.1.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '2'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rake
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '10'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '10'
67
+ - !ruby/object:Gem::Dependency
68
+ name: bundler
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.3'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.3'
81
+ - !ruby/object:Gem::Dependency
82
+ name: mocha
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 0.14.0
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: 0.14.0
95
+ - !ruby/object:Gem::Dependency
96
+ name: minitest
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '5'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '5'
109
+ - !ruby/object:Gem::Dependency
110
+ name: git
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '1.2'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '1.2'
123
+ - !ruby/object:Gem::Dependency
124
+ name: pry-nav
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: 0.2.4
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: 0.2.4
137
+ - !ruby/object:Gem::Dependency
138
+ name: pry
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: 0.10.4
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: 0.10.4
151
+ - !ruby/object:Gem::Dependency
152
+ name: redis-store-testing
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ type: :development
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ description: Extend the redis store with CAS (compare-and-swap) functionality for
166
+ better cache integrity support
167
+ email:
168
+ - ral@alwins-world.de
169
+ executables: []
170
+ extensions: []
171
+ extra_rdoc_files: []
172
+ files:
173
+ - ".gitignore"
174
+ - Gemfile
175
+ - MIT-LICENSE
176
+ - README.md
177
+ - Rakefile
178
+ - irb.rb
179
+ - lib/redis-store-with-cas.rb
180
+ - lib/redis/store-with-cas.rb
181
+ - lib/redis/store-with-cas/cas.rb
182
+ - lib/redis/store-with-cas/namespace.rb
183
+ - lib/redis/store-with-cas/version.rb
184
+ - redis-store-with-cas.gemspec
185
+ - test/store_test.rb
186
+ - test/test_helper.rb
187
+ - test/version_test.rb
188
+ homepage: https://git.alwin-it.de/alwin/redis-store-with-cas
189
+ licenses:
190
+ - MIT
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project: redis-store-with-cas
208
+ rubygems_version: 2.6.11
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: Extend redis store for Ruby frameworks with cas
212
+ test_files:
213
+ - test/store_test.rb
214
+ - test/test_helper.rb
215
+ - test/version_test.rb