riak-client 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +74 -0
- data/lib/riak.rb +49 -0
- data/lib/riak/bucket.rb +176 -0
- data/lib/riak/cache_store.rb +82 -0
- data/lib/riak/client.rb +139 -0
- data/lib/riak/client/curb_backend.rb +82 -0
- data/lib/riak/client/http_backend.rb +209 -0
- data/lib/riak/client/net_http_backend.rb +49 -0
- data/lib/riak/failed_request.rb +37 -0
- data/lib/riak/i18n.rb +20 -0
- data/lib/riak/invalid_response.rb +25 -0
- data/lib/riak/link.rb +73 -0
- data/lib/riak/locale/en.yml +37 -0
- data/lib/riak/map_reduce.rb +248 -0
- data/lib/riak/map_reduce_error.rb +20 -0
- data/lib/riak/robject.rb +267 -0
- data/lib/riak/util/escape.rb +12 -0
- data/lib/riak/util/fiber1.8.rb +48 -0
- data/lib/riak/util/headers.rb +44 -0
- data/lib/riak/util/multipart.rb +52 -0
- data/lib/riak/util/translation.rb +29 -0
- data/lib/riak/walk_spec.rb +117 -0
- data/spec/fixtures/cat.jpg +0 -0
- data/spec/fixtures/multipart-blank.txt +7 -0
- data/spec/fixtures/multipart-with-body.txt +16 -0
- data/spec/integration/riak/cache_store_spec.rb +129 -0
- data/spec/riak/bucket_spec.rb +247 -0
- data/spec/riak/client_spec.rb +174 -0
- data/spec/riak/curb_backend_spec.rb +53 -0
- data/spec/riak/escape_spec.rb +21 -0
- data/spec/riak/headers_spec.rb +34 -0
- data/spec/riak/http_backend_spec.rb +131 -0
- data/spec/riak/link_spec.rb +82 -0
- data/spec/riak/map_reduce_spec.rb +352 -0
- data/spec/riak/multipart_spec.rb +36 -0
- data/spec/riak/net_http_backend_spec.rb +28 -0
- data/spec/riak/object_spec.rb +538 -0
- data/spec/riak/walk_spec_spec.rb +208 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/http_backend_implementation_examples.rb +215 -0
- data/spec/support/mock_server.rb +61 -0
- data/spec/support/mocks.rb +3 -0
- metadata +187 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
module Riak
|
2
|
+
module Util
|
3
|
+
module Escape
|
4
|
+
# URI-escapes bucket or key names that may contain slashes for use in URLs.
|
5
|
+
# @param [String] bucket_or_key the bucket or key name
|
6
|
+
# @return [String] the escaped path segment
|
7
|
+
def escape(bucket_or_key)
|
8
|
+
URI.escape(bucket_or_key.to_s).gsub("/", "%2F")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Poor Man's Fiber (API compatible Thread based Fiber implementation for Ruby 1.8)
|
2
|
+
# (c) 2008 Aman Gupta (tmm1)
|
3
|
+
|
4
|
+
unless defined? Fiber
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
class FiberError < StandardError; end
|
8
|
+
|
9
|
+
class Fiber
|
10
|
+
def initialize
|
11
|
+
raise ArgumentError, 'new Fiber requires a block' unless block_given?
|
12
|
+
|
13
|
+
@yield = Queue.new
|
14
|
+
@resume = Queue.new
|
15
|
+
|
16
|
+
@thread = Thread.new{ @yield.push [ *yield(*@resume.pop) ] }
|
17
|
+
@thread.abort_on_exception = true
|
18
|
+
@thread[:fiber] = self
|
19
|
+
end
|
20
|
+
attr_reader :thread
|
21
|
+
|
22
|
+
def resume *args
|
23
|
+
raise FiberError, 'dead fiber called' unless @thread.alive?
|
24
|
+
@resume.push(args)
|
25
|
+
result = @yield.pop
|
26
|
+
result.size > 1 ? result : result.first
|
27
|
+
end
|
28
|
+
|
29
|
+
def yield *args
|
30
|
+
@yield.push(args)
|
31
|
+
result = @resume.pop
|
32
|
+
result.size > 1 ? result : result.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.yield *args
|
36
|
+
raise FiberError, "can't yield from root fiber" unless fiber = Thread.current[:fiber]
|
37
|
+
fiber.yield(*args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.current
|
41
|
+
Thread.current[:fiber] or raise FiberError, 'not inside a fiber'
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}>"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
module Util
|
18
|
+
# Represents headers from an HTTP response
|
19
|
+
class Headers
|
20
|
+
include Net::HTTPHeader
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
initialize_http_header({})
|
24
|
+
end
|
25
|
+
|
26
|
+
# Parse a single header line into its key and value
|
27
|
+
# @param [String] chunk a single header line
|
28
|
+
def self.parse(chunk)
|
29
|
+
line = chunk.strip
|
30
|
+
# thanks Net::HTTPResponse
|
31
|
+
return [nil,nil] if chunk =~ /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in
|
32
|
+
m = /\A([^:]+):\s*/.match(line)
|
33
|
+
[m[1], m.post_match] rescue [nil, nil]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parses a header line and adds it to the header collection
|
37
|
+
# @param [String] chunk a single header line
|
38
|
+
def parse(chunk)
|
39
|
+
key, value = self.class.parse(chunk)
|
40
|
+
add_field(key, value) if key && value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
module Util
|
18
|
+
# Utility methods for handling multipart/mixed responses
|
19
|
+
module Multipart
|
20
|
+
extend self
|
21
|
+
# Parses a multipart/mixed body into its constituent parts, including nested multipart/mixed sections
|
22
|
+
# @param [String] data the multipart body data
|
23
|
+
# @param [String] boundary the boundary string given in the Content-Type header
|
24
|
+
def parse(data, boundary)
|
25
|
+
contents = data.match(/\r?\n--#{Regexp.escape(boundary)}--\r?\n/).pre_match rescue ""
|
26
|
+
contents.split(/\r?\n--#{Regexp.escape(boundary)}\r?\n/).reject(&:blank?).map do |part|
|
27
|
+
headers = Headers.new
|
28
|
+
if md = part.match(/\r?\n\r?\n/)
|
29
|
+
body = md.post_match
|
30
|
+
md.pre_match.split(/\r?\n/).each do |line|
|
31
|
+
headers.parse(line)
|
32
|
+
end
|
33
|
+
|
34
|
+
if headers["content-type"] =~ /multipart\/mixed/
|
35
|
+
boundary = extract_boundary(headers.to_hash["content-type"].first)
|
36
|
+
parse(body, boundary)
|
37
|
+
else
|
38
|
+
{:headers => headers.to_hash, :body => body}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end.compact
|
42
|
+
end
|
43
|
+
|
44
|
+
# Extracts the boundary string from a Content-Type header that is a multipart type
|
45
|
+
# @param [String] header_string the Content-Type header
|
46
|
+
# @return [String] the boundary string separating each part
|
47
|
+
def extract_boundary(header_string)
|
48
|
+
$1 if header_string =~ /boundary=([A-Za-z0-9\'()+_,-.\/:=?]+)/
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
module Util
|
18
|
+
module Translation
|
19
|
+
def i18n_scope
|
20
|
+
:riak
|
21
|
+
end
|
22
|
+
|
23
|
+
def t(message, options={})
|
24
|
+
I18n.t("#{i18n_scope}.#{message}", options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
|
18
|
+
# The specification of how to follow links from one object to another in Riak,
|
19
|
+
# when using the link-walker resource.
|
20
|
+
# Example link-walking operation:
|
21
|
+
# GET /riak/artists/REM/albums,_,_/tracks,_,1
|
22
|
+
# This operation would have two WalkSpecs:
|
23
|
+
# Riak::WalkSpec.new({:bucket => 'albums'})
|
24
|
+
# Riak::WalkSpec.new({:bucket => 'tracks', :result => true})
|
25
|
+
class WalkSpec
|
26
|
+
include Util::Translation
|
27
|
+
include Util::Escape
|
28
|
+
|
29
|
+
# @return [String] The bucket followed links should be restricted to. "_" represents all buckets.
|
30
|
+
attr_accessor :bucket
|
31
|
+
|
32
|
+
# @return [String] The "riaktag" or "rel" that followed links should be restricted to. "_" represents all tags.
|
33
|
+
attr_accessor :tag
|
34
|
+
|
35
|
+
# @return [Boolean] Whether objects should be returned from this phase of link walking. Default is false.
|
36
|
+
attr_accessor :keep
|
37
|
+
|
38
|
+
# Normalize a list of walk specs into WalkSpec objects.
|
39
|
+
def self.normalize(*params)
|
40
|
+
params.flatten!
|
41
|
+
specs = []
|
42
|
+
while params.length > 0
|
43
|
+
param = params.shift
|
44
|
+
case param
|
45
|
+
when Hash
|
46
|
+
specs << new(param)
|
47
|
+
when WalkSpec
|
48
|
+
specs << param
|
49
|
+
else
|
50
|
+
if params.length >= 2
|
51
|
+
specs << new(param, params.shift, params.shift)
|
52
|
+
else
|
53
|
+
raise ArgumentError, t("too_few_arguments", :params => params.inspect)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
specs
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a walk-spec for use in finding other objects in Riak.
|
61
|
+
# @overload initialize(hash)
|
62
|
+
# Creates a walk-spec from a hash.
|
63
|
+
# @param [Hash] hash options for the walk-spec
|
64
|
+
# @option hash [String] :bucket ("_") the bucket the links should point to (default '_' is all)
|
65
|
+
# @option hash [String] :tag ("_") the tag to filter links by (default '_' is all)
|
66
|
+
# @option hash [Boolean] :keep (false) whether to return results from following this link specification
|
67
|
+
# @overload initialize(bucket, tag, keep)
|
68
|
+
# Creates a walk-spec from a bucket-tag-result triple.
|
69
|
+
# @param [String] bucket the bucket the links should point to (default '_' is all)
|
70
|
+
# @param [String] tag the tag to filter links by (default '_' is all)
|
71
|
+
# @param [Boolean] keep whether to return results from following this link specification
|
72
|
+
# @see {Riak::RObject#walk}
|
73
|
+
def initialize(*args)
|
74
|
+
args.flatten!
|
75
|
+
case args.size
|
76
|
+
when 1
|
77
|
+
hash = args.first
|
78
|
+
raise ArgumentError, t("hash_type", :hash => hash.inspect) unless Hash === hash
|
79
|
+
assign(hash[:bucket], hash[:tag], hash[:keep])
|
80
|
+
when 3
|
81
|
+
assign(*args)
|
82
|
+
else
|
83
|
+
raise ArgumentError, t("wrong_argument_count_walk_spec")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Converts the walk-spec into the form required by the link-walker resource URL
|
88
|
+
def to_s
|
89
|
+
b = @bucket && escape(@bucket) || '_'
|
90
|
+
t = @tag && escape(@tag) || '_'
|
91
|
+
"#{b},#{t},#{@keep ? '1' : '_'}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def ==(other)
|
95
|
+
other.is_a?(WalkSpec) && other.bucket == bucket && other.tag == tag && other.keep == keep
|
96
|
+
end
|
97
|
+
|
98
|
+
def ===(other)
|
99
|
+
self == other || case other
|
100
|
+
when WalkSpec
|
101
|
+
other.keep == keep &&
|
102
|
+
(bucket == "_" || bucket == other.bucket) &&
|
103
|
+
(tag == "_" || tag == other.tag)
|
104
|
+
when Link
|
105
|
+
(bucket == "_" || bucket == other.url.split("/")[2]) &&
|
106
|
+
(tag == "_" || tag == other.rel)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def assign(bucket, tag, result)
|
112
|
+
@bucket = bucket || "_"
|
113
|
+
@tag = tag || "_"
|
114
|
+
@keep = result || false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
Binary file
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
--5EiMOjuGavQ2IbXAqsJPLLfJNlA
|
3
|
+
Content-Type: multipart/mixed; boundary=7extjTzvYIKVMVHowUiTn0LfvSs
|
4
|
+
|
5
|
+
--7extjTzvYIKVMVHowUiTn0LfvSs
|
6
|
+
X-Riak-Vclock: a85hYGBgyWDKBVHMr9s3ZzAlMuaxMtyZcPAIH1RYyObHDqiwxIZjcOG1M98chAq3bUQIz7SSFQEKM4FUbwMKZwEA
|
7
|
+
Location: /riak/foo/baz
|
8
|
+
Content-Type: text/plain
|
9
|
+
Link: </riak/foo>; rel="up"
|
10
|
+
Etag: 6JdI51eFrvv5lDwY6un7a2
|
11
|
+
Last-Modified: Sat, 16 Jan 2010 22:13:44 GMT
|
12
|
+
|
13
|
+
SCP sloooow....
|
14
|
+
--7extjTzvYIKVMVHowUiTn0LfvSs--
|
15
|
+
|
16
|
+
--5EiMOjuGavQ2IbXAqsJPLLfJNlA--
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require File.expand_path("../../spec_helper", File.dirname(__FILE__))
|
15
|
+
|
16
|
+
describe Riak::CacheStore do
|
17
|
+
before do
|
18
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store)
|
19
|
+
@cleanup = true
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
@cache.bucket.keys(:force => true).each do |k|
|
24
|
+
@cache.bucket.delete(k, :rw => 1) unless k.blank?
|
25
|
+
end if @cleanup
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "Riak integration" do
|
29
|
+
before do
|
30
|
+
@cleanup = false
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should have a client" do
|
34
|
+
@cache.should respond_to(:client)
|
35
|
+
@cache.client.should be_kind_of(Riak::Client)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have a bucket to store entries in" do
|
39
|
+
@cache.bucket.should be_kind_of(Riak::Bucket)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should configure the client according to the initialized options" do
|
43
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store, :port => 10000)
|
44
|
+
@cache.client.port.should == 10000
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should choose the bucket according to the initializer option" do
|
48
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store, :bucket => "foobar")
|
49
|
+
@cache.bucket.name.should == "foobar"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should set the N value to 2 by default" do
|
53
|
+
@cache.bucket.n_value.should == 2
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should set the N value to the specified value" do
|
57
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store, :n_value => 1)
|
58
|
+
@cache.bucket.n_value.should == 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
it "should read and write strings" do
|
64
|
+
@cache.write('foo', 'bar')
|
65
|
+
@cache.read('foo').should == 'bar'
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should read and write hashes" do
|
69
|
+
@cache.write('foo', {:a => "b"})
|
70
|
+
@cache.read('foo').should == {:a => "b"}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should read and write integers" do
|
74
|
+
@cache.write('foo', 1)
|
75
|
+
@cache.read('foo').should == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should read and write nil" do
|
79
|
+
@cache.write('foo', nil)
|
80
|
+
@cache.read('foo').should be_nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should return the stored value when fetching on hit" do
|
84
|
+
@cache.write('foo', 'bar')
|
85
|
+
@cache.fetch('foo'){'baz'}.should == 'bar'
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should return the default value when fetching on miss" do
|
89
|
+
@cache.fetch('foo'){'baz'}.should == 'baz'
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should return the default value when forcing a miss" do
|
93
|
+
@cache.fetch('foo', :force => true){'bar'}.should == 'bar'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should increment an integer value in the cache" do
|
97
|
+
@cache.write('foo', 1, :raw => true)
|
98
|
+
@cache.read('foo', :raw => true).to_i.should == 1
|
99
|
+
@cache.increment('foo')
|
100
|
+
@cache.read('foo', :raw => true).to_i.should == 2
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should decrement an integer value in the cache" do
|
104
|
+
@cache.write('foo', 1, :raw => true)
|
105
|
+
@cache.read('foo', :raw => true).to_i.should == 1
|
106
|
+
@cache.decrement('foo')
|
107
|
+
@cache.read('foo', :raw => true).to_i.should == 0
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should detect if a value exists in the cache" do
|
111
|
+
@cache.write('foo', 'bar')
|
112
|
+
@cache.exist?('foo').should be_true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should delete matching keys from the cache" do
|
116
|
+
@cache.write('foo', 'bar')
|
117
|
+
@cache.write('green', 'thumb')
|
118
|
+
@cache.delete_matched(/foo/)
|
119
|
+
@cache.read('foo').should be_nil
|
120
|
+
@cache.read('green').should == 'thumb'
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should delete a single key from the cache" do
|
124
|
+
@cache.write('foo', 'bar')
|
125
|
+
@cache.read('foo').should == 'bar'
|
126
|
+
@cache.delete('foo')
|
127
|
+
@cache.read('foo').should be_nil
|
128
|
+
end
|
129
|
+
end
|