garner 0.4.5 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|