garner 0.4.5 → 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +130 -0
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +3 -0
- data/README.md +1 -0
- data/Rakefile +39 -0
- data/UPGRADING.md +118 -0
- data/garner.gemspec +44 -0
- data/lib/garner.rb +21 -21
- data/lib/garner/cache.rb +13 -6
- data/lib/garner/cache/binding.rb +6 -14
- data/lib/garner/cache/context.rb +11 -12
- data/lib/garner/cache/identity.rb +1 -1
- data/lib/garner/config.rb +12 -7
- data/lib/garner/mixins/active_record.rb +3 -3
- data/lib/garner/mixins/active_record/base.rb +2 -2
- data/lib/garner/mixins/mongoid.rb +4 -4
- data/lib/garner/mixins/mongoid/document.rb +8 -12
- data/lib/garner/mixins/mongoid/identity.rb +5 -6
- data/lib/garner/mixins/rack.rb +1 -2
- data/lib/garner/strategies/binding/invalidation/base.rb +2 -4
- data/lib/garner/strategies/binding/invalidation/binding_index.rb +1 -3
- data/lib/garner/strategies/binding/invalidation/touch.rb +0 -2
- data/lib/garner/strategies/binding/key/base.rb +1 -3
- data/lib/garner/strategies/binding/key/binding_index.rb +3 -4
- data/lib/garner/strategies/binding/key/cache_key.rb +0 -2
- data/lib/garner/strategies/binding/key/safe_cache_key.rb +2 -3
- data/lib/garner/strategies/context/key/base.rb +1 -3
- data/lib/garner/strategies/context/key/caller.rb +9 -12
- data/lib/garner/strategies/context/key/jsonp.rb +3 -6
- data/lib/garner/strategies/context/key/request_get.rb +2 -4
- data/lib/garner/strategies/context/key/request_path.rb +1 -3
- data/lib/garner/strategies/context/key/request_post.rb +2 -4
- data/lib/garner/version.rb +1 -1
- data/spec/garner/cache/context_spec.rb +38 -0
- data/spec/garner/cache/identity_spec.rb +68 -0
- data/spec/garner/cache_spec.rb +49 -0
- data/spec/garner/config_spec.rb +17 -0
- data/spec/garner/mixins/mongoid/document_spec.rb +80 -0
- data/spec/garner/mixins/mongoid/identity_spec.rb +140 -0
- data/spec/garner/mixins/rack_spec.rb +48 -0
- data/spec/garner/strategies/binding/invalidation/binding_index_spec.rb +14 -0
- data/spec/garner/strategies/binding/invalidation/touch_spec.rb +23 -0
- data/spec/garner/strategies/binding/key/binding_index_spec.rb +245 -0
- data/spec/garner/strategies/binding/key/cache_key_spec.rb +29 -0
- data/spec/garner/strategies/binding/key/safe_cache_key_spec.rb +61 -0
- data/spec/garner/strategies/context/key/caller_spec.rb +106 -0
- data/spec/garner/strategies/context/key/jsonp_spec.rb +22 -0
- data/spec/garner/strategies/context/key/request_get_spec.rb +33 -0
- data/spec/garner/strategies/context/key/request_path_spec.rb +28 -0
- data/spec/garner/strategies/context/key/request_post_spec.rb +34 -0
- data/spec/garner/version_spec.rb +11 -0
- data/spec/integration/active_record_spec.rb +43 -0
- data/spec/integration/grape_spec.rb +33 -0
- data/spec/integration/mongoid_spec.rb +355 -0
- data/spec/integration/rack_spec.rb +77 -0
- data/spec/integration/sinatra_spec.rb +29 -0
- data/spec/performance/strategy_benchmark.rb +59 -0
- data/spec/performance/support/benchmark_context.rb +31 -0
- data/spec/performance/support/benchmark_context_wrapper.rb +67 -0
- data/spec/shared/binding_invalidation_strategy.rb +17 -0
- data/spec/shared/binding_key_strategy.rb +35 -0
- data/spec/shared/conditional_get.rb +48 -0
- data/spec/shared/context_key_strategy.rb +24 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/spec_support.rb +5 -0
- data/spec/support/active_record.rb +36 -0
- data/spec/support/cache.rb +15 -0
- data/spec/support/garner.rb +5 -0
- data/spec/support/mongoid.rb +71 -0
- metadata +155 -157
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'garner/mixins/rack'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
describe 'Rack integration' do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
let(:app) do
|
9
|
+
class TestRackApp
|
10
|
+
include Garner::Mixins::Rack
|
11
|
+
|
12
|
+
attr_accessor :request
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
@request = Rack::Request.new(env)
|
16
|
+
random1 = garner { SecureRandom.hex }
|
17
|
+
random2 = garner { SecureRandom.hex }
|
18
|
+
[
|
19
|
+
200,
|
20
|
+
{ 'Content-Type' => 'application/json' },
|
21
|
+
[random1, random2].to_json
|
22
|
+
]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
TestRackApp.new
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with the RequestGet strategy disabled' do
|
30
|
+
before(:each) do
|
31
|
+
Garner.configure do |config|
|
32
|
+
config.rack_context_key_strategies -= [Garner::Strategies::Context::Key::RequestGet]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
it 'co-caches requests to the same path with different query strings' do
|
36
|
+
get '/foo?q=1'
|
37
|
+
response1 = JSON.parse(last_response.body)[0]
|
38
|
+
get '/foo?q=2'
|
39
|
+
response2 = JSON.parse(last_response.body)[0]
|
40
|
+
response1.should eq response2
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with default configuration' do
|
45
|
+
it 'bypasses cache if cache_enabled? returns false' do
|
46
|
+
TestRackApp.any_instance.stub(:cache_enabled?) { false }
|
47
|
+
get '/'
|
48
|
+
response1 = JSON.parse(last_response.body)[0]
|
49
|
+
get '/'
|
50
|
+
response2 = JSON.parse(last_response.body)[0]
|
51
|
+
response1.should_not == response2
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'caches different results for different paths' do
|
55
|
+
get '/foo'
|
56
|
+
response1 = JSON.parse(last_response.body)[0]
|
57
|
+
get '/bar'
|
58
|
+
response2 = JSON.parse(last_response.body)[0]
|
59
|
+
response1.should_not == response2
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'caches different results for different query strings' do
|
63
|
+
get '/foo?q=1'
|
64
|
+
response1 = JSON.parse(last_response.body)[0]
|
65
|
+
get '/foo?q=2'
|
66
|
+
response2 = JSON.parse(last_response.body)[0]
|
67
|
+
response1.should_not == response2
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'caches multiple blocks separately within an endpoint' do
|
71
|
+
get '/'
|
72
|
+
random1 = JSON.parse(last_response.body)[0]
|
73
|
+
random2 = JSON.parse(last_response.body)[1]
|
74
|
+
random1.should_not == random2
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'garner/mixins/rack'
|
3
|
+
require 'sinatra'
|
4
|
+
|
5
|
+
describe 'Sinatra integration' do
|
6
|
+
|
7
|
+
let(:app) do
|
8
|
+
class TestSinatraApp < Sinatra::Base
|
9
|
+
helpers Garner::Mixins::Rack
|
10
|
+
use Rack::ConditionalGet
|
11
|
+
use Rack::ETag
|
12
|
+
|
13
|
+
before do
|
14
|
+
headers 'Expires' => Time.at(0).utc.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
get '/' do
|
18
|
+
garner do
|
19
|
+
{ meaning_of_life: 42 }.to_json
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
TestSinatraApp.new
|
25
|
+
end
|
26
|
+
|
27
|
+
it_behaves_like 'Rack::ConditionalGet server'
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_support'
|
2
|
+
require 'method_profiler'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
require 'support/benchmark_context'
|
6
|
+
require 'support/benchmark_context_wrapper'
|
7
|
+
|
8
|
+
class StrategyBenchmark
|
9
|
+
attr_accessor :n, :d, :r
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@n = options[:n] || 1000 # Number of iterations
|
13
|
+
@d = options[:d] || 1024 # Document payload size
|
14
|
+
@r = options[:r] || 4 # Recursive database calls per garner block
|
15
|
+
end
|
16
|
+
|
17
|
+
def run!
|
18
|
+
profiler = MethodProfiler.observe(BenchmarkContext)
|
19
|
+
self.class.strategy_pairs.each do |key_strategy, invalidation_strategy|
|
20
|
+
Garner.configure do |config|
|
21
|
+
config.mongoid_identity_fields = [:_id, :_slugs]
|
22
|
+
config.binding_key_strategy = key_strategy
|
23
|
+
config.binding_invalidation_strategy = invalidation_strategy
|
24
|
+
end
|
25
|
+
|
26
|
+
proxy = BenchmarkContextWrapper.new(d: d, r: r)
|
27
|
+
|
28
|
+
# Workaround for MethodProfiler bug
|
29
|
+
profiler.instance_variable_set(:@data, Hash.new { |h, k| h[k] = [] })
|
30
|
+
n.times do
|
31
|
+
# Randomize order of calls
|
32
|
+
[
|
33
|
+
:warm_virtual_fetch,
|
34
|
+
:cold_virtual_fetch,
|
35
|
+
:warm_class_fetch,
|
36
|
+
:cold_class_fetch,
|
37
|
+
:force_invalidate,
|
38
|
+
:soft_invalidate
|
39
|
+
].shuffle.each { |method| proxy.send(method) }
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "Key: #{key_strategy.to_s.split('::')[-1]}"
|
43
|
+
puts "Invalidation: #{invalidation_strategy.to_s.split('::')[-1]}"
|
44
|
+
puts profiler.report.sort_by(:method).order(:ascending)
|
45
|
+
puts "\n\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.strategy_pairs
|
50
|
+
{
|
51
|
+
Garner::Strategies::Binding::Key::Base =>
|
52
|
+
Garner::Strategies::Binding::Invalidation::Base,
|
53
|
+
Garner::Strategies::Binding::Key::SafeCacheKey =>
|
54
|
+
Garner::Strategies::Binding::Invalidation::Touch,
|
55
|
+
Garner::Strategies::Binding::Key::BindingIndex =>
|
56
|
+
Garner::Strategies::Binding::Invalidation::BindingIndex
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class BenchmarkContext
|
2
|
+
include Garner::Cache::Context
|
3
|
+
|
4
|
+
def virtual_fetch(klass, handle, r)
|
5
|
+
garner.bind(klass.identify(handle)).key(caller: nil) do
|
6
|
+
(r - 1).times { klass.find(handle).to_json }
|
7
|
+
klass.find(handle).to_json
|
8
|
+
end
|
9
|
+
end
|
10
|
+
alias_method :a_warm_virtual_fetch, :virtual_fetch
|
11
|
+
alias_method :b_cold_virtual_fetch, :virtual_fetch
|
12
|
+
|
13
|
+
def class_fetch(klass, handle, r)
|
14
|
+
garner.bind(klass).key(caller: nil) do
|
15
|
+
(r - 1).times { klass.find(handle).to_json }
|
16
|
+
klass.find(handle).to_json
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias_method :c_warm_class_fetch, :class_fetch
|
20
|
+
alias_method :d_cold_class_fetch, :class_fetch
|
21
|
+
|
22
|
+
def force_invalidate(binding)
|
23
|
+
binding.invalidate_garner_caches
|
24
|
+
end
|
25
|
+
alias_method :e_force_invalidate, :force_invalidate
|
26
|
+
|
27
|
+
def soft_invalidate(binding)
|
28
|
+
binding.send(:_garner_after_update)
|
29
|
+
end
|
30
|
+
alias_method :f_soft_invalidate, :soft_invalidate
|
31
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
class BenchmarkContextWrapper
|
4
|
+
include Garner::Cache::Context
|
5
|
+
attr_accessor :binding, :context, :r
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
setup!(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup!(options)
|
12
|
+
Monger.destroy_all
|
13
|
+
Garner.config.cache.clear
|
14
|
+
@binding = Monger.create!(name: 'M1')
|
15
|
+
@binding.update_attributes!(
|
16
|
+
subdocument: SecureRandom.hex(options[:d])
|
17
|
+
) if options[:d]
|
18
|
+
@r = options[:r]
|
19
|
+
@context = BenchmarkContext.new
|
20
|
+
|
21
|
+
# Prime MongoDB cache
|
22
|
+
Monger.find(@binding.slug)
|
23
|
+
end
|
24
|
+
|
25
|
+
def warm_virtual_fetch
|
26
|
+
warm_caches
|
27
|
+
context.a_warm_virtual_fetch(binding.class, binding.slug, r)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cold_virtual_fetch
|
31
|
+
update_binding
|
32
|
+
context.b_cold_virtual_fetch(binding.class, binding.slug, r)
|
33
|
+
end
|
34
|
+
|
35
|
+
def warm_class_fetch
|
36
|
+
warm_caches
|
37
|
+
context.c_warm_class_fetch(binding.class, binding.slug, r)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cold_class_fetch
|
41
|
+
update_binding
|
42
|
+
context.d_cold_class_fetch(binding.class, binding.slug, r)
|
43
|
+
end
|
44
|
+
|
45
|
+
def force_invalidate
|
46
|
+
context.e_force_invalidate(binding)
|
47
|
+
end
|
48
|
+
|
49
|
+
def soft_invalidate
|
50
|
+
context.f_soft_invalidate(binding)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def warm_caches
|
56
|
+
klass, handle = binding.class, binding.slug
|
57
|
+
json = binding.reload.to_json
|
58
|
+
garner.bind(klass.identify(handle)).key(caller: nil) { json }
|
59
|
+
garner.bind(klass).key(caller: nil) { json }
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_binding
|
63
|
+
# Randomly shuffle name between M1, M2, M3
|
64
|
+
name = (%w(M1 M2 M3) - [binding.reload.name]).sample
|
65
|
+
binding.update_attributes!(name: name)
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Shared examples for binding invalidation strategies. A valid strategy must
|
2
|
+
# implement apply(binding) and force_apply(binding)
|
3
|
+
shared_examples_for 'Garner::Strategies::Binding::Invalidation strategy' do
|
4
|
+
it 'inherits from Garner::Strategies::Binding::Invalidation::Base' do
|
5
|
+
subject.new.should be_a(Garner::Strategies::Binding::Invalidation::Base)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'apply' do
|
9
|
+
it 'requires an argument' do
|
10
|
+
expect { subject.apply }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'operates on any binding' do
|
14
|
+
expect { subject.apply(double('foo')) }.not_to raise_error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Shared examples for binding strategies. A valid binding strategy must implement
|
2
|
+
# apply(binding).
|
3
|
+
shared_examples_for 'Garner::Strategies::Binding::Key strategy' do
|
4
|
+
it 'requires an argument' do
|
5
|
+
expect { subject.apply }.to raise_error
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'inherits from Garner::Strategies::Binding::Key::Base' do
|
9
|
+
subject.new.should be_a(Garner::Strategies::Binding::Key::Base)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'given a known binding' do
|
13
|
+
it 'returns a valid cache key' do
|
14
|
+
known_bindings.each do |binding|
|
15
|
+
subject.apply(binding).should be_a(String)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns the same cache key for an unchanged object' do
|
20
|
+
known_bindings.each do |binding|
|
21
|
+
key1 = subject.apply(binding)
|
22
|
+
key2 = subject.apply(binding)
|
23
|
+
key1.should eq key2
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'given an unknown binding' do
|
29
|
+
it 'returns a nil cache key' do
|
30
|
+
unknown_bindings.each do |binding|
|
31
|
+
subject.apply(binding).should be_nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Shared examples for a proper Conditional GET server.
|
2
|
+
# To test a new Rack framework, define an app returned by app, with a
|
3
|
+
# single endpoint, "/".
|
4
|
+
shared_examples_for 'Rack::ConditionalGet server' do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
def etag_for(body_str)
|
8
|
+
Rack::ETag.new(nil).send(:digest_body, [body_str]).first
|
9
|
+
end
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
Garner.config.cache.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
it "writes the cached object's ETag from binding" do
|
16
|
+
get '/'
|
17
|
+
last_response.headers['ETag'].length.should eq 32 + 2
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'sends a 304 response if content has not changed (If-None-Match)' do
|
21
|
+
get '/'
|
22
|
+
last_response.status.should eq 200
|
23
|
+
last_response.headers['ETag'].should eq %Q("#{etag_for(last_response.body)}")
|
24
|
+
get '/', {}, 'HTTP_IF_NONE_MATCH' => last_response.headers['ETag']
|
25
|
+
last_response.status.should eq 304
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sends a 200 response if content has changed (If-None-Match)' do
|
29
|
+
get '/'
|
30
|
+
last_response.status.should eq 200
|
31
|
+
get '/', {}, 'HTTP_IF_NONE_MATCH' => %Q("#{etag_for('foobar')}")
|
32
|
+
last_response.status.should eq 200
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'sends a 200 response if content has changed (valid If-Modified-Since but invalid If-None-Match)' do
|
36
|
+
get '/'
|
37
|
+
last_response.status.should eq 200
|
38
|
+
get '/', {}, 'HTTP_IF_MODIFIED_SINCE' => (Time.now + 1).httpdate, 'HTTP_IF_NONE_MATCH' => %Q("#{etag_for(last_response.body)}")
|
39
|
+
last_response.status.should eq 200
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'adds Cache-Control, Pragma and Expires headers' do
|
43
|
+
get '/'
|
44
|
+
last_response.headers['Cache-Control'].split(', ').sort.should eq %w(max-age=0 must-revalidate private)
|
45
|
+
last_response.headers['Pragma'].should be_nil
|
46
|
+
Time.parse(last_response.headers['Expires']).should be < Time.now
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Shared examples for key strategies. A valid key strategy must implement
|
2
|
+
# apply(identity, ruby_context = self).
|
3
|
+
shared_examples_for 'Garner::Strategies::Context::Key strategy' do
|
4
|
+
before(:each) do
|
5
|
+
@cache_identity = Garner::Cache::Identity.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'inherits from Garner::Strategies::Context::Key::Base' do
|
9
|
+
subject.new.should be_a(Garner::Strategies::Context::Key::Base)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'requires a Garner::Cache::Identity' do
|
13
|
+
expect { subject.apply }.to raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'does not require an explicit context, defaulting to self' do
|
17
|
+
expect { subject.apply(@cache_identity) }.to_not raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns a Garner::Cache::Identity' do
|
21
|
+
modified_identity = subject.apply(@cache_identity, self)
|
22
|
+
modified_identity.should eq @cache_identity
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'coveralls'
|
5
|
+
Coveralls.wear!('test_frameworks') if ENV['CI']
|
6
|
+
|
7
|
+
require 'rspec'
|
8
|
+
require 'rack/test'
|
9
|
+
|
10
|
+
require 'garner'
|
11
|
+
|
12
|
+
# Load support files
|
13
|
+
require 'spec_support'
|
14
|
+
|
15
|
+
# Load shared examples
|
16
|
+
Dir["#{File.dirname(__FILE__)}/shared/*.rb"].each do |file|
|
17
|
+
require file
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec.configure do |rspec|
|
21
|
+
rspec.mock_with :rspec do |mocks|
|
22
|
+
mocks.patch_marshal_to_support_partial_doubles = true
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'garner/mixins/active_record'
|
3
|
+
|
4
|
+
# Set up in-memory SQLite connection for ActiveRecord
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
adapter: 'sqlite3',
|
7
|
+
database: ':memory:'
|
8
|
+
)
|
9
|
+
|
10
|
+
# Include mixin
|
11
|
+
module ActiveRecord
|
12
|
+
class Base
|
13
|
+
include Garner::Mixins::ActiveRecord::Base
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Stub classes
|
18
|
+
ActiveRecord::Migration.verbose = false
|
19
|
+
ActiveRecord::Migration.create_table :activists do |t|
|
20
|
+
t.string :name
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
|
24
|
+
class Activist < ActiveRecord::Base
|
25
|
+
end
|
26
|
+
|
27
|
+
# Wrap each test example in a failing transaction to ensure a clean
|
28
|
+
# database for each run.
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.around do |example|
|
31
|
+
ActiveRecord::Base.transaction do
|
32
|
+
example.run
|
33
|
+
fail ActiveRecord::Rollback
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|