josh-rack-cache 0.5.1
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/CHANGES +167 -0
- data/COPYING +18 -0
- data/README +110 -0
- data/Rakefile +137 -0
- data/TODO +27 -0
- data/doc/configuration.markdown +112 -0
- data/doc/faq.markdown +141 -0
- data/doc/index.markdown +121 -0
- data/doc/layout.html.erb +34 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/server.ru +34 -0
- data/doc/storage.markdown +164 -0
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +45 -0
- data/lib/rack/cache/appengine.rb +52 -0
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +253 -0
- data/lib/rack/cache/entitystore.rb +339 -0
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +407 -0
- data/lib/rack/cache/options.rb +150 -0
- data/lib/rack/cache/request.rb +33 -0
- data/lib/rack/cache/response.rb +267 -0
- data/lib/rack/cache/storage.rb +62 -0
- data/rack-cache.gemspec +70 -0
- data/test/cache_test.rb +38 -0
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +774 -0
- data/test/entitystore_test.rb +230 -0
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +302 -0
- data/test/options_test.rb +77 -0
- data/test/pony.jpg +0 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +178 -0
- data/test/spec_setup.rb +237 -0
- data/test/storage_test.rb +94 -0
- metadata +118 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/options'
|
3
|
+
|
4
|
+
module Rack::Cache::Options
|
5
|
+
option_accessor :foo
|
6
|
+
end
|
7
|
+
|
8
|
+
class MockOptions
|
9
|
+
include Rack::Cache::Options
|
10
|
+
def initialize
|
11
|
+
@env = nil
|
12
|
+
initialize_options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'Rack::Cache::Options' do
|
17
|
+
before { @options = MockOptions.new }
|
18
|
+
|
19
|
+
describe '#set' do
|
20
|
+
it 'sets a Symbol option as rack-cache.symbol' do
|
21
|
+
@options.set :bar, 'baz'
|
22
|
+
@options.options['rack-cache.bar'].should.equal 'baz'
|
23
|
+
end
|
24
|
+
it 'sets a String option as string' do
|
25
|
+
@options.set 'foo.bar', 'bling'
|
26
|
+
@options.options['foo.bar'].should.equal 'bling'
|
27
|
+
end
|
28
|
+
it 'sets all key/value pairs when given a Hash' do
|
29
|
+
@options.set :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling'
|
30
|
+
@options.foo.should.equal 'bar'
|
31
|
+
@options.options['rack-cache.bar'].should.equal 'baz'
|
32
|
+
@options.options['foo.bar'].should.equal 'bling'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'makes options declared with option_accessor available as attributes' do
|
37
|
+
@options.set :foo, 'bar'
|
38
|
+
@options.foo.should.equal 'bar'
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'allows setting multiple options via assignment' do
|
42
|
+
@options.options = { :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling' }
|
43
|
+
@options.foo.should.equal 'bar'
|
44
|
+
@options.options['foo.bar'].should.equal 'bling'
|
45
|
+
@options.options['rack-cache.bar'].should.equal 'baz'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "allows storing the value as a block" do
|
49
|
+
block = Proc.new { "bar block" }
|
50
|
+
@options.set(:foo, &block)
|
51
|
+
@options.options['rack-cache.foo'].should.equal block
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'allows the cache key generator to be configured' do
|
55
|
+
@options.should.respond_to :cache_key
|
56
|
+
@options.should.respond_to :cache_key=
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'allows the meta store to be configured' do
|
60
|
+
@options.should.respond_to :metastore
|
61
|
+
@options.should.respond_to :metastore=
|
62
|
+
@options.metastore.should.not.be nil
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'allows the entity store to be configured' do
|
66
|
+
@options.should.respond_to :entitystore
|
67
|
+
@options.should.respond_to :entitystore=
|
68
|
+
@options.entitystore.should.not.be nil
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'allows log verbosity to be configured' do
|
72
|
+
@options.should.respond_to :verbose
|
73
|
+
@options.should.respond_to :verbose=
|
74
|
+
@options.should.respond_to :verbose?
|
75
|
+
@options.verbose.should.not.be.nil
|
76
|
+
end
|
77
|
+
end
|
data/test/pony.jpg
ADDED
Binary file
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/request'
|
3
|
+
|
4
|
+
describe 'Rack::Cache::Request' do
|
5
|
+
it 'is marked as no_cache when the Cache-Control header includes the no-cache directive' do
|
6
|
+
request = Rack::Cache::Request.new('HTTP_CACHE_CONTROL' => 'public, no-cache')
|
7
|
+
assert request.no_cache?
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'is marked as no_cache when request should not be loaded from cache' do
|
11
|
+
request = Rack::Cache::Request.new('HTTP_PRAGMA' => 'no-cache')
|
12
|
+
assert request.no_cache?
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'is not marked as no_cache when neither no-cache directive is specified' do
|
16
|
+
request = Rack::Cache::Request.new('HTTP_CACHE_CONTROL' => 'public')
|
17
|
+
assert !request.no_cache?
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
|
3
|
+
describe 'Rack::Cache::Response' do
|
4
|
+
before :each do
|
5
|
+
@now = Time.httpdate(Time.now.httpdate)
|
6
|
+
@one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
|
7
|
+
@one_hour_later = Time.httpdate((Time.now + (60**2)).httpdate)
|
8
|
+
@res = Rack::Cache::Response.new(200, {'Date' => @now.httpdate}, [])
|
9
|
+
end
|
10
|
+
|
11
|
+
after :each do
|
12
|
+
@now, @res, @one_hour_ago = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'responds to #to_a with a Rack response tuple' do
|
16
|
+
@res.should.respond_to :to_a
|
17
|
+
@res.to_a.should.equal [200, {'Date' => @now.httpdate}, []]
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#cache_control' do
|
21
|
+
it 'handles multiple name=value pairs' do
|
22
|
+
@res.headers['Cache-Control'] = 'max-age=600, max-stale=300, min-fresh=570'
|
23
|
+
@res.cache_control['max-age'].should.equal '600'
|
24
|
+
@res.cache_control['max-stale'].should.equal '300'
|
25
|
+
@res.cache_control['min-fresh'].should.equal '570'
|
26
|
+
end
|
27
|
+
it 'removes the header when given an empty hash' do
|
28
|
+
@res.headers['Cache-Control'] = 'max-age=600, must-revalidate'
|
29
|
+
@res.cache_control['max-age'].should.equal '600'
|
30
|
+
@res.cache_control = {}
|
31
|
+
@res.headers.should.not.include 'Cache-Control'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#validateable?' do
|
36
|
+
it 'is true when Last-Modified header present' do
|
37
|
+
@res = Rack::Cache::Response.new(200, {'Last-Modified' => @one_hour_ago.httpdate}, [])
|
38
|
+
@res.should.be.validateable
|
39
|
+
end
|
40
|
+
it 'is true when ETag header present' do
|
41
|
+
@res = Rack::Cache::Response.new(200, {'ETag' => '"12345"'}, [])
|
42
|
+
@res.should.be.validateable
|
43
|
+
end
|
44
|
+
it 'is false when no validator is present' do
|
45
|
+
@res = Rack::Cache::Response.new(200, {}, [])
|
46
|
+
@res.should.not.be.validateable
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#date' do
|
51
|
+
it 'uses the Date header if present' do
|
52
|
+
@res = Rack::Cache::Response.new(200, {'Date' => @one_hour_ago.httpdate}, [])
|
53
|
+
@res.date.should.equal @one_hour_ago
|
54
|
+
end
|
55
|
+
it 'uses the current time when no Date header present' do
|
56
|
+
@res = Rack::Cache::Response.new(200, {}, [])
|
57
|
+
@res.date.should.be.close Time.now, 1
|
58
|
+
end
|
59
|
+
it 'returns the correct date when the header is modified directly' do
|
60
|
+
@res = Rack::Cache::Response.new(200, { 'Date' => @one_hour_ago.httpdate }, [])
|
61
|
+
@res.date.should.equal @one_hour_ago
|
62
|
+
@res.headers['Date'] = @now.httpdate
|
63
|
+
@res.date.should.equal @now
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#max_age' do
|
68
|
+
it 'uses s-maxage cache control directive when present' do
|
69
|
+
@res.headers['Cache-Control'] = 's-maxage=600, max-age=0'
|
70
|
+
@res.max_age.should.equal 600
|
71
|
+
end
|
72
|
+
it 'falls back to max-age when no s-maxage directive present' do
|
73
|
+
@res.headers['Cache-Control'] = 'max-age=600'
|
74
|
+
@res.max_age.should.equal 600
|
75
|
+
end
|
76
|
+
it 'falls back to Expires when no max-age or s-maxage directive present' do
|
77
|
+
@res.headers['Cache-Control'] = 'must-revalidate'
|
78
|
+
@res.headers['Expires'] = @one_hour_later.httpdate
|
79
|
+
@res.max_age.should.equal 60 ** 2
|
80
|
+
end
|
81
|
+
it 'gives a #max_age of nil when no freshness information available' do
|
82
|
+
@res.max_age.should.be.nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#private=' do
|
87
|
+
it 'adds the private Cache-Control directive when set true' do
|
88
|
+
@res.headers['Cache-Control'] = 'max-age=100'
|
89
|
+
@res.private = true
|
90
|
+
@res.headers['Cache-Control'].split(', ').sort.
|
91
|
+
should.equal ['max-age=100', 'private']
|
92
|
+
end
|
93
|
+
it 'removes the public Cache-Control directive' do
|
94
|
+
@res.headers['Cache-Control'] = 'public, max-age=100'
|
95
|
+
@res.private = true
|
96
|
+
@res.headers['Cache-Control'].split(', ').sort.
|
97
|
+
should.equal ['max-age=100', 'private']
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#expire!' do
|
102
|
+
it 'sets the Age to be equal to the max-age' do
|
103
|
+
@res.headers['Cache-Control'] = 'max-age=100'
|
104
|
+
@res.expire!
|
105
|
+
@res.headers['Age'].should.equal '100'
|
106
|
+
end
|
107
|
+
it 'sets the Age to be equal to the s-maxage when both max-age and s-maxage present' do
|
108
|
+
@res.headers['Cache-Control'] = 'max-age=100, s-maxage=500'
|
109
|
+
@res.expire!
|
110
|
+
@res.headers['Age'].should.equal '500'
|
111
|
+
end
|
112
|
+
it 'does nothing when the response is already stale/expired' do
|
113
|
+
@res.headers['Cache-Control'] = 'max-age=5, s-maxage=500'
|
114
|
+
@res.headers['Age'] = '1000'
|
115
|
+
@res.expire!
|
116
|
+
@res.headers['Age'].should.equal '1000'
|
117
|
+
end
|
118
|
+
it 'does nothing when the response does not include freshness information' do
|
119
|
+
@res.expire!
|
120
|
+
@res.headers.should.not.include 'Age'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#ttl' do
|
125
|
+
it 'is nil when no Expires or Cache-Control headers present' do
|
126
|
+
@res.ttl.should.be.nil
|
127
|
+
end
|
128
|
+
it 'uses the Expires header when no max-age is present' do
|
129
|
+
@res.headers['Expires'] = (@res.now + (60**2)).httpdate
|
130
|
+
@res.ttl.should.be.close(60**2, 1)
|
131
|
+
end
|
132
|
+
it 'returns negative values when Expires is in part' do
|
133
|
+
@res.ttl.should.be.nil
|
134
|
+
@res.headers['Expires'] = @one_hour_ago.httpdate
|
135
|
+
@res.ttl.should.be < 0
|
136
|
+
end
|
137
|
+
it 'uses the Cache-Control max-age value when present' do
|
138
|
+
@res.headers['Cache-Control'] = 'max-age=60'
|
139
|
+
@res.ttl.should.be.close(60, 1)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#vary' do
|
144
|
+
it 'is nil when no Vary header is present' do
|
145
|
+
@res.vary.should.be.nil
|
146
|
+
end
|
147
|
+
it 'returns the literal value of the Vary header' do
|
148
|
+
@res.headers['Vary'] = 'Foo Bar Baz'
|
149
|
+
@res.vary.should.equal 'Foo Bar Baz'
|
150
|
+
end
|
151
|
+
it 'can be checked for existence using the #vary? method' do
|
152
|
+
@res.should.respond_to :vary?
|
153
|
+
@res.should.not.vary
|
154
|
+
@res.headers['Vary'] = '*'
|
155
|
+
@res.should.vary
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe '#vary_header_names' do
|
160
|
+
it 'returns an empty Array when no Vary header is present' do
|
161
|
+
@res.vary_header_names.should.be.empty
|
162
|
+
end
|
163
|
+
it 'parses a single header name value' do
|
164
|
+
@res.headers['Vary'] = 'Accept-Language'
|
165
|
+
@res.vary_header_names.should.equal ['Accept-Language']
|
166
|
+
end
|
167
|
+
it 'parses multiple header name values separated by spaces' do
|
168
|
+
@res.headers['Vary'] = 'Accept-Language User-Agent X-Foo'
|
169
|
+
@res.vary_header_names.should.equal \
|
170
|
+
['Accept-Language', 'User-Agent', 'X-Foo']
|
171
|
+
end
|
172
|
+
it 'parses multiple header name values separated by commas' do
|
173
|
+
@res.headers['Vary'] = 'Accept-Language,User-Agent, X-Foo'
|
174
|
+
@res.vary_header_names.should.equal \
|
175
|
+
['Accept-Language', 'User-Agent', 'X-Foo']
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/test/spec_setup.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
[ STDOUT, STDERR ].each { |io| io.sync = true }
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'test/spec'
|
9
|
+
rescue LoadError => boom
|
10
|
+
require 'rubygems' rescue nil
|
11
|
+
require 'test/spec'
|
12
|
+
end
|
13
|
+
|
14
|
+
# Set the MEMCACHED environment variable as follows to enable testing
|
15
|
+
# of the MemCached meta and entity stores.
|
16
|
+
ENV['MEMCACHED'] ||= 'localhost:11211'
|
17
|
+
$memcached = nil
|
18
|
+
$memcache = nil
|
19
|
+
|
20
|
+
def have_memcached?(server=ENV['MEMCACHED'])
|
21
|
+
return $memcached unless $memcached.nil?
|
22
|
+
v, $VERBOSE = $VERBOSE, nil # silence warnings from memcached
|
23
|
+
require 'memcached'
|
24
|
+
$VERBOSE = v
|
25
|
+
$memcached = Memcached.new(server)
|
26
|
+
$memcached.set('ping', '')
|
27
|
+
true
|
28
|
+
rescue LoadError => boom
|
29
|
+
$memcached = false
|
30
|
+
false
|
31
|
+
rescue => boom
|
32
|
+
STDERR.puts "memcached not working. related tests will be skipped."
|
33
|
+
$memcached = false
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
have_memcached?
|
38
|
+
|
39
|
+
def have_memcache?(server=ENV['MEMCACHED'])
|
40
|
+
return $memcache unless $memcache.nil?
|
41
|
+
require 'memcache'
|
42
|
+
$memcache = MemCache.new(server)
|
43
|
+
$memcache.set('ping', '')
|
44
|
+
true
|
45
|
+
rescue LoadError => boom
|
46
|
+
$memcache = false
|
47
|
+
false
|
48
|
+
rescue => boom
|
49
|
+
STDERR.puts "memcache not working. related tests will be skipped."
|
50
|
+
$memcache = false
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
54
|
+
have_memcache?
|
55
|
+
|
56
|
+
def need_memcached(forwhat)
|
57
|
+
if have_memcached?
|
58
|
+
yield
|
59
|
+
else
|
60
|
+
STDERR.puts "skipping memcached #{forwhat}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def need_memcache(forwhat)
|
65
|
+
if have_memcache?
|
66
|
+
yield
|
67
|
+
else
|
68
|
+
STDERR.puts "skipping memcache #{forwhat}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def need_java(forwhat)
|
73
|
+
if RUBY_PLATFORM =~ /java/
|
74
|
+
yield
|
75
|
+
else
|
76
|
+
STDERR.puts "skipping app engine #{forwhat}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
# Setup the load path ..
|
82
|
+
$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__)) + '/lib'
|
83
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
84
|
+
|
85
|
+
require 'rack/cache'
|
86
|
+
|
87
|
+
|
88
|
+
# Methods for constructing downstream applications / response
|
89
|
+
# generators.
|
90
|
+
module CacheContextHelpers
|
91
|
+
|
92
|
+
# The Rack::Cache::Context instance used for the most recent
|
93
|
+
# request.
|
94
|
+
attr_reader :cache
|
95
|
+
|
96
|
+
# An Array of Rack::Cache::Context instances used for each request, in
|
97
|
+
# request order.
|
98
|
+
attr_reader :caches
|
99
|
+
|
100
|
+
# The Rack::Response instance result of the most recent request.
|
101
|
+
attr_reader :response
|
102
|
+
|
103
|
+
# An Array of Rack::Response instances for each request, in request order.
|
104
|
+
attr_reader :responses
|
105
|
+
|
106
|
+
# The backend application object.
|
107
|
+
attr_reader :app
|
108
|
+
|
109
|
+
def setup_cache_context
|
110
|
+
# holds each Rack::Cache::Context
|
111
|
+
@app = nil
|
112
|
+
|
113
|
+
# each time a request is made, a clone of @cache_template is used
|
114
|
+
# and appended to @caches.
|
115
|
+
@cache_template = nil
|
116
|
+
@cache = nil
|
117
|
+
@caches = []
|
118
|
+
@errors = StringIO.new
|
119
|
+
@cache_config = nil
|
120
|
+
|
121
|
+
@called = false
|
122
|
+
@request = nil
|
123
|
+
@response = nil
|
124
|
+
@responses = []
|
125
|
+
|
126
|
+
@storage = Rack::Cache::Storage.new
|
127
|
+
end
|
128
|
+
|
129
|
+
def teardown_cache_context
|
130
|
+
@app, @cache_template, @cache, @caches, @called,
|
131
|
+
@request, @response, @responses, @cache_config = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# A basic response with 200 status code and a tiny body.
|
135
|
+
def respond_with(status=200, headers={}, body=['Hello World'])
|
136
|
+
called = false
|
137
|
+
@app =
|
138
|
+
lambda do |env|
|
139
|
+
called = true
|
140
|
+
response = Rack::Response.new(body, status, headers)
|
141
|
+
request = Rack::Request.new(env)
|
142
|
+
yield request, response if block_given?
|
143
|
+
response.finish
|
144
|
+
end
|
145
|
+
@app.meta_def(:called?) { called }
|
146
|
+
@app.meta_def(:reset!) { called = false }
|
147
|
+
@app
|
148
|
+
end
|
149
|
+
|
150
|
+
def cache_config(&block)
|
151
|
+
@cache_config = block
|
152
|
+
end
|
153
|
+
|
154
|
+
def request(method, uri='/', opts={})
|
155
|
+
opts = {
|
156
|
+
'rack.run_once' => true,
|
157
|
+
'rack.errors' => @errors,
|
158
|
+
'rack-cache.storage' => @storage
|
159
|
+
}.merge(opts)
|
160
|
+
|
161
|
+
fail 'response not specified (use respond_with)' if @app.nil?
|
162
|
+
@app.reset! if @app.respond_to?(:reset!)
|
163
|
+
|
164
|
+
@cache_prototype ||= Rack::Cache::Context.new(@app, &@cache_config)
|
165
|
+
@cache = @cache_prototype.clone
|
166
|
+
@caches << @cache
|
167
|
+
@request = Rack::MockRequest.new(@cache)
|
168
|
+
yield @cache if block_given?
|
169
|
+
@response = @request.request(method.to_s.upcase, uri, opts)
|
170
|
+
@responses << @response
|
171
|
+
@response
|
172
|
+
end
|
173
|
+
|
174
|
+
def get(stem, env={}, &b)
|
175
|
+
request(:get, stem, env, &b)
|
176
|
+
end
|
177
|
+
|
178
|
+
def head(stem, env={}, &b)
|
179
|
+
request(:head, stem, env, &b)
|
180
|
+
end
|
181
|
+
|
182
|
+
def post(*args, &b)
|
183
|
+
request(:post, *args, &b)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
module TestHelpers
|
189
|
+
include FileUtils
|
190
|
+
F = File
|
191
|
+
|
192
|
+
@@temp_dir_count = 0
|
193
|
+
|
194
|
+
def create_temp_directory
|
195
|
+
@@temp_dir_count += 1
|
196
|
+
path = F.join(Dir.tmpdir, "rack-cache-#{$$}-#{@@temp_dir_count}")
|
197
|
+
mkdir_p path
|
198
|
+
if block_given?
|
199
|
+
yield path
|
200
|
+
remove_entry_secure path
|
201
|
+
end
|
202
|
+
path
|
203
|
+
end
|
204
|
+
|
205
|
+
def create_temp_file(root, file, data='')
|
206
|
+
path = F.join(root, file)
|
207
|
+
mkdir_p F.dirname(path)
|
208
|
+
F.open(path, 'w') { |io| io.write(data) }
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
class Test::Unit::TestCase
|
214
|
+
include TestHelpers
|
215
|
+
include CacheContextHelpers
|
216
|
+
end
|
217
|
+
|
218
|
+
# Metaid == a few simple metaclass helper
|
219
|
+
# (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
|
220
|
+
class Object
|
221
|
+
# The hidden singleton lurks behind everyone
|
222
|
+
def metaclass; class << self; self; end; end
|
223
|
+
def meta_eval(&blk); metaclass.instance_eval(&blk); end
|
224
|
+
# Adds methods to a metaclass
|
225
|
+
def meta_def name, &blk
|
226
|
+
meta_eval { define_method name, &blk }
|
227
|
+
end
|
228
|
+
# Defines an instance method within a class
|
229
|
+
def class_def name, &blk
|
230
|
+
class_eval { define_method name, &blk }
|
231
|
+
end
|
232
|
+
|
233
|
+
# True when the Object is neither false or nil.
|
234
|
+
def truthy?
|
235
|
+
!!self
|
236
|
+
end
|
237
|
+
end
|