cachetastic-memcache-pool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README +0 -0
- data/Rakefile +18 -0
- data/cachetastic-memcache-pool.gemspec +26 -0
- data/lib/cachetastic/adapters/memcache_pool.rb +4 -0
- data/lib/cachetastic/adapters/memcache_pool/adapter.rb +149 -0
- data/lib/cachetastic/adapters/memcache_pool/version.rb +7 -0
- data/spec/cachetastic/adapters/memcache_pool/adapter_spec.rb +147 -0
- data/spec/spec_helper.rb +5 -0
- metadata +142 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc "Run specs"
|
6
|
+
RSpec::Core::RakeTask.new do |t|
|
7
|
+
t.pattern = "./spec/**/*_spec.rb"
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Generate code coverage"
|
11
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
12
|
+
t.pattern = "./spec/**/*_spec.rb"
|
13
|
+
t.rcov = true
|
14
|
+
t.rcov_opts = ['--exclude', 'spec']
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Default: run specs.'
|
18
|
+
task :default => :spec
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "cachetastic/adapters/memcache_pool/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "cachetastic-memcache-pool"
|
7
|
+
s.version = Cachetastic::Adapters::MemcachePool::VERSION
|
8
|
+
s.authors = ["Jason Wadsworth"]
|
9
|
+
s.email = ["jason@gazelle.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Cachetastic Memcached Adapter with Connection Pooling}
|
12
|
+
s.description = %q{Cachetastic Memcached Adapter with Connection Pooling}
|
13
|
+
|
14
|
+
s.rubyforge_project = "cachetastic-memcache-pool"
|
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
|
+
|
21
|
+
s.add_development_dependency "rspec", "~> 2.6.0"
|
22
|
+
s.add_development_dependency "rcov", "~> 0.9.0"
|
23
|
+
|
24
|
+
s.add_runtime_dependency "cachetastic", "~> 3.0.0"
|
25
|
+
s.add_runtime_dependency "memcache-client", "~> 1.8.5"
|
26
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Cachetastic # :nodoc:
|
2
|
+
module Adapters
|
3
|
+
module MemcachePool
|
4
|
+
# An adapter to cache objects to the file system.
|
5
|
+
#
|
6
|
+
# This adapter supports the following configuration settings,
|
7
|
+
# in addition to the default settings:
|
8
|
+
#
|
9
|
+
# configatron.cachetastic.defaults.servers = ['127.0.0.1:11211']
|
10
|
+
# configatron.cachetastic.defaults.mc_options = {:c_threshold => 10_000,
|
11
|
+
# :compression => true,
|
12
|
+
# :debug => false,
|
13
|
+
# :readonly => false,
|
14
|
+
# :urlencode => false}
|
15
|
+
# configatron.cachetastic.delete_delay = 0
|
16
|
+
#
|
17
|
+
# The <tt>servers</tt> setting defines an <tt>Array</tt> of Mecached
|
18
|
+
# servers, represented as "<host>:<port>".
|
19
|
+
#
|
20
|
+
# The <tt>mc_options</tt> setting is a <tt>Hash</tt> of settings required
|
21
|
+
# by Memcached. See the Memcached documentation for more information on
|
22
|
+
# what the settings mean.
|
23
|
+
#
|
24
|
+
# The <tt>delete_delay</tt> setting tells Memcached how long to wait
|
25
|
+
# before it deletes the object. This is not the same as <tt>expiry_time</tt>.
|
26
|
+
# It is only used when the <tt>delete</tt> method is called.
|
27
|
+
#
|
28
|
+
# See <tt>Cachetastic::Adapters::Base</tt> for a list of public API
|
29
|
+
# methods.
|
30
|
+
class Adapter < Cachetastic::Adapters::Base
|
31
|
+
|
32
|
+
def initialize(klass) # :nodoc:
|
33
|
+
define_accessor(:servers)
|
34
|
+
define_accessor(:mc_options)
|
35
|
+
define_accessor(:delete_delay)
|
36
|
+
self.delete_delay = 0
|
37
|
+
self.servers = ['127.0.0.1:11211']
|
38
|
+
self.mc_options = {:c_threshold => 10_000,
|
39
|
+
:compression => true,
|
40
|
+
:debug => false,
|
41
|
+
:readonly => false,
|
42
|
+
:urlencode => false}
|
43
|
+
super
|
44
|
+
connection
|
45
|
+
end
|
46
|
+
|
47
|
+
def get(key) # :nodoc:
|
48
|
+
connection.get(transform_key(key), false)
|
49
|
+
end # get
|
50
|
+
|
51
|
+
def set(key, value, expiry_time = configatron.cachetastic.defaults.default_expiry) # :nodoc:
|
52
|
+
connection.set(transform_key(key), marshal(value), expiry_time, false)
|
53
|
+
end # set
|
54
|
+
|
55
|
+
def delete(key) # :nodoc:
|
56
|
+
connection.delete(transform_key(key), self.delete_delay)
|
57
|
+
end # delete
|
58
|
+
|
59
|
+
def expire_all # :nodoc:
|
60
|
+
increment_version
|
61
|
+
return nil
|
62
|
+
end # expire_all
|
63
|
+
|
64
|
+
def transform_key(key) # :nodoc:
|
65
|
+
namespace + ':' + key.to_s.hexdigest
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return <tt>false</tt> if the connection to Memcached is
|
69
|
+
# either <tt>nil</tt> or not active.
|
70
|
+
def valid?
|
71
|
+
return false if @_mc_connection.nil?
|
72
|
+
return false unless @_mc_connection.active?
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def connection
|
78
|
+
self.class.data_connection(self)
|
79
|
+
end
|
80
|
+
|
81
|
+
def ns_connection
|
82
|
+
self.class.ns_connection(self)
|
83
|
+
end
|
84
|
+
|
85
|
+
def increment_version
|
86
|
+
name = self.klass.name
|
87
|
+
v = get_version
|
88
|
+
ns_connection.set(name, v + 1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_version
|
92
|
+
name = self.klass.name
|
93
|
+
v = ns_connection.get(name)
|
94
|
+
if v.nil?
|
95
|
+
ns_connection.set(name, 1)
|
96
|
+
v = 1
|
97
|
+
end
|
98
|
+
v
|
99
|
+
end
|
100
|
+
|
101
|
+
def namespace
|
102
|
+
@_ns_version = get_version
|
103
|
+
"#{self.klass.name}.#{@_ns_version}"
|
104
|
+
end
|
105
|
+
|
106
|
+
class << self
|
107
|
+
# JDW: TODO: Extract all of this into a MemCachePool class, and add the ability to do true thread-safe pooling
|
108
|
+
def reset_connections
|
109
|
+
@_connections_by_klass = {}
|
110
|
+
@_connections_by_digest = {}
|
111
|
+
@_ns_connections_by_klass = {}
|
112
|
+
@_ns_connections_by_digest = {}
|
113
|
+
end
|
114
|
+
|
115
|
+
def connection_digest(adapter)
|
116
|
+
{:servers => adapter.servers, :mc_options => adapter.mc_options}.to_s.hexdigest
|
117
|
+
end
|
118
|
+
|
119
|
+
def data_connection(adapter)
|
120
|
+
@_connections_by_klass ||= {}
|
121
|
+
@_connections_by_digest ||= {}
|
122
|
+
return get_connection(adapter, @_connections_by_klass, @_connections_by_digest, nil)
|
123
|
+
end
|
124
|
+
|
125
|
+
def ns_connection(adapter)
|
126
|
+
@_ns_connections_by_klass ||= {}
|
127
|
+
@_ns_connections_by_digest ||= {}
|
128
|
+
return get_connection(adapter, @_ns_connections_by_klass, @_ns_connections_by_digest, :namespace_versions)
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_connection(adapter, connections_by_class, connections_by_digest, namespace)
|
132
|
+
matching_connection = connections_by_class[adapter.klass.name]
|
133
|
+
if !matching_connection || !matching_connection.active?
|
134
|
+
digest = connection_digest(adapter)
|
135
|
+
matching_connection = connections_by_digest[digest]
|
136
|
+
if !matching_connection || !matching_connection.active?
|
137
|
+
matching_connection = MemCache.new(adapter.servers, adapter.mc_options.merge(:namespace => namespace))
|
138
|
+
end
|
139
|
+
connections_by_digest[digest] = matching_connection
|
140
|
+
end
|
141
|
+
connections_by_class[adapter.klass.name] = matching_connection
|
142
|
+
matching_connection
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end # Adapter
|
147
|
+
end # MemcachePool
|
148
|
+
end # Adapters
|
149
|
+
end # Cachetastic
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cachetastic::Adapters::MemcachePool::Adapter do
|
4
|
+
before(:each) do
|
5
|
+
configatron.temp_start
|
6
|
+
configatron.cachetastic.defaults.adapter = Cachetastic::Adapters::MemcachePool::Adapter
|
7
|
+
clear_test_cache_keys
|
8
|
+
end
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
pending 'JDW: This is an integration test. It requires a memcached server to be running on localhost:11211'
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:each) do
|
15
|
+
Cachetastic::Adapters::MemcachePool::Adapter.reset_connections
|
16
|
+
configatron.temp_end
|
17
|
+
end
|
18
|
+
|
19
|
+
class FakeCacheClass; end
|
20
|
+
class AnotherFakeCacheClass; end
|
21
|
+
|
22
|
+
def clear_test_cache_keys
|
23
|
+
# Clear namespace values
|
24
|
+
cache_classes = [FakeCacheClass, AnotherFakeCacheClass]
|
25
|
+
ns_connection = memcached_connection(:namespace_versions)
|
26
|
+
cache_classes.each do |cache_classs|
|
27
|
+
ns_connection.delete(cache_classs.name)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Clear data values
|
31
|
+
data_connection = memcached_connection(nil)
|
32
|
+
cache_classes.each do |cache_class|
|
33
|
+
%w{key1 key2}.each do |key|
|
34
|
+
(1..2).each do |version|
|
35
|
+
data_connection.delete("#{cache_class.name}.${version}:#{key.to_s.hexdigest}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def expect_connection_creation(namespace)
|
42
|
+
mock_cache = mock(MemCache, :get => nil, :active? => true)
|
43
|
+
MemCache.should_receive(:new) do |servers, options|
|
44
|
+
servers.should == ['127.0.0.1:11211']
|
45
|
+
options[:namespace].should == namespace
|
46
|
+
mock_cache
|
47
|
+
end
|
48
|
+
mock_cache
|
49
|
+
end
|
50
|
+
|
51
|
+
def memcached_connection(namespace)
|
52
|
+
servers = ['127.0.0.1:11211']
|
53
|
+
mc_options = {
|
54
|
+
:c_threshold => 10_000,
|
55
|
+
:compression => true,
|
56
|
+
:debug => true,
|
57
|
+
:readonly => false,
|
58
|
+
:urlencode => false,
|
59
|
+
:namespace => namespace
|
60
|
+
}
|
61
|
+
MemCache.new(servers, mc_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_memcached(key, expected_value, namespace = nil)
|
65
|
+
mc = memcached_connection(namespace)
|
66
|
+
mc.get(key.hexdigest).should == expected_value
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should set a value in memcached" do
|
70
|
+
adapter = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
71
|
+
adapter.set('test-key', 'test-value', 10)
|
72
|
+
|
73
|
+
check_memcached('test-key', 'test-value', "#{FakeCacheClass.name}.1")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should get a value in memcached" do
|
77
|
+
memcached_connection(nil).set("#{FakeCacheClass.name}.1:#{'key1'.hexdigest}", 'value1', 86400, false)
|
78
|
+
|
79
|
+
adapter = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
80
|
+
adapter.get('key1').should == 'value1'
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should delete a value in memcached" do
|
84
|
+
key = "#{FakeCacheClass.name}.1:#{'key1'.hexdigest}"
|
85
|
+
conn = memcached_connection(nil)
|
86
|
+
conn.set(key, 'value1', 86400, false)
|
87
|
+
|
88
|
+
adapter = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
89
|
+
adapter.delete('key1')
|
90
|
+
conn.get('key').should be_nil
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should not see the old value in memcached after expire_all is called" do
|
94
|
+
memcached_connection(nil).set("#{FakeCacheClass.name}.1:#{'key1'.hexdigest}", 'value1', 86400, false)
|
95
|
+
|
96
|
+
adapter = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
97
|
+
adapter.get('key1').should == 'value1'
|
98
|
+
adapter.expire_all
|
99
|
+
adapter.get('key1').should be_nil
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should create a namespace connection and a data connection" do
|
103
|
+
data_connection = expect_connection_creation(nil)
|
104
|
+
ns_connection = expect_connection_creation(:namespace_versions)
|
105
|
+
|
106
|
+
ns_connection.should_receive(:set).with(FakeCacheClass.name, 1)
|
107
|
+
data_connection.should_receive(:set).with("#{FakeCacheClass.name}.1:#{'key1'.hexdigest}", "value1", 86400, false)
|
108
|
+
|
109
|
+
cache = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
110
|
+
cache.set('key1', 'value1')
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should create only one namespace and data connection for caches with the same configuration" do
|
114
|
+
data_connection = expect_connection_creation(nil)
|
115
|
+
ns_connection = expect_connection_creation(:namespace_versions)
|
116
|
+
|
117
|
+
ns_connection.should_receive(:set).with(FakeCacheClass.name, 1)
|
118
|
+
data_connection.should_receive(:set).with("#{FakeCacheClass.name}.1:#{'key1'.hexdigest}", "value1", 86400, false)
|
119
|
+
cache1 = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
120
|
+
cache1.set('key1', 'value1')
|
121
|
+
|
122
|
+
ns_connection.should_receive(:set).with(AnotherFakeCacheClass.name, 1)
|
123
|
+
data_connection.should_receive(:set).with("#{AnotherFakeCacheClass.name}.1:#{'key2'.hexdigest}", "value2", 86400, false)
|
124
|
+
cache2 = Cachetastic::Adapters::MemcachePool::Adapter.new(AnotherFakeCacheClass)
|
125
|
+
cache2.set('key2', 'value2')
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should create a new namespace connection if the configuration is different" do
|
129
|
+
configatron.cachetastic.fake_cache_class.mc_options = {:c_threshold => 20_000}
|
130
|
+
|
131
|
+
data_connection1 = expect_connection_creation(nil)
|
132
|
+
data_connection1.should_receive(:set).with("#{FakeCacheClass.name}.1:#{'key1'.hexdigest}", "value1", 86400, false)
|
133
|
+
ns_connection1 = expect_connection_creation(:namespace_versions)
|
134
|
+
ns_connection1.should_receive(:set).with(FakeCacheClass.name, 1)
|
135
|
+
|
136
|
+
cache1 = Cachetastic::Adapters::MemcachePool::Adapter.new(FakeCacheClass)
|
137
|
+
cache1.set('key1', 'value1')
|
138
|
+
|
139
|
+
|
140
|
+
data_connection2 = expect_connection_creation(nil)
|
141
|
+
data_connection2.should_receive(:set).with("#{AnotherFakeCacheClass.name}.1:#{'key2'.hexdigest}", "value2", 86400, false)
|
142
|
+
ns_connection2 = expect_connection_creation(:namespace_versions)
|
143
|
+
ns_connection2.should_receive(:set).with(AnotherFakeCacheClass.name, 1)
|
144
|
+
cache2 = Cachetastic::Adapters::MemcachePool::Adapter.new(AnotherFakeCacheClass)
|
145
|
+
cache2.set('key2', 'value2')
|
146
|
+
end
|
147
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cachetastic-memcache-pool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jason Wadsworth
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-21 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rspec
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 23
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 6
|
33
|
+
- 0
|
34
|
+
version: 2.6.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rcov
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 59
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 9
|
49
|
+
- 0
|
50
|
+
version: 0.9.0
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: cachetastic
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 7
|
62
|
+
segments:
|
63
|
+
- 3
|
64
|
+
- 0
|
65
|
+
- 0
|
66
|
+
version: 3.0.0
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: memcache-client
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 61
|
78
|
+
segments:
|
79
|
+
- 1
|
80
|
+
- 8
|
81
|
+
- 5
|
82
|
+
version: 1.8.5
|
83
|
+
type: :runtime
|
84
|
+
version_requirements: *id004
|
85
|
+
description: Cachetastic Memcached Adapter with Connection Pooling
|
86
|
+
email:
|
87
|
+
- jason@gazelle.com
|
88
|
+
executables: []
|
89
|
+
|
90
|
+
extensions: []
|
91
|
+
|
92
|
+
extra_rdoc_files: []
|
93
|
+
|
94
|
+
files:
|
95
|
+
- .gitignore
|
96
|
+
- .rspec
|
97
|
+
- Gemfile
|
98
|
+
- README
|
99
|
+
- Rakefile
|
100
|
+
- cachetastic-memcache-pool.gemspec
|
101
|
+
- lib/cachetastic/adapters/memcache_pool.rb
|
102
|
+
- lib/cachetastic/adapters/memcache_pool/adapter.rb
|
103
|
+
- lib/cachetastic/adapters/memcache_pool/version.rb
|
104
|
+
- spec/cachetastic/adapters/memcache_pool/adapter_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
has_rdoc: true
|
107
|
+
homepage: ""
|
108
|
+
licenses: []
|
109
|
+
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
hash: 3
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
hash: 3
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
requirements: []
|
134
|
+
|
135
|
+
rubyforge_project: cachetastic-memcache-pool
|
136
|
+
rubygems_version: 1.4.2
|
137
|
+
signing_key:
|
138
|
+
specification_version: 3
|
139
|
+
summary: Cachetastic Memcached Adapter with Connection Pooling
|
140
|
+
test_files:
|
141
|
+
- spec/cachetastic/adapters/memcache_pool/adapter_spec.rb
|
142
|
+
- spec/spec_helper.rb
|