diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4040c6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.gem +.bundle +Gemfile.lock +pkg/* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3ecb7fe --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "http://rubygems.org" + +# Specify your gem's dependencies in rspec_caching_test.gemspec +gemspec \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/lib/rspec-rails-caching.rb b/lib/rspec-rails-caching.rb new file mode 100644 index 0000000..cd12967 --- /dev/null +++ b/lib/rspec-rails-caching.rb @@ -0,0 +1,52 @@ +require 'rspec-rails-caching/version' +require 'rspec-rails-caching/matchers' +require 'rspec-rails-caching/test_store' +require 'rspec-rails-caching/extensions/action_controller' + +module RSpecRailsCaching + RSpec::Rails::ControllerExampleGroup.class_eval do + include Matchers + end + + RSpec.configure do |config| + config.alias_it_should_behave_like_to :with_configuration, "with" + config.before(:all, :type => :controller, :caching => true) do |example| + silence_warnings do + @_orig_cache_store = RAILS_CACHE + Object.const_set "RAILS_CACHE", TestStore.new + end + + ActionController::Base.cache_store = RAILS_CACHE + ActionController::Base.perform_caching = true + + # The controller needs to be reloaded to metaprogram the caches_page + # callback with the perform_caching option turned on. There is no + # reloading if the example controller isn't in the load paths, likely + # because it was defined inline in the spec. + if ctrl_class_file = + ActiveSupport::Dependencies.search_for_file(example.class.controller_class.to_s.underscore) + then + ActiveSupport::Dependencies.load(ctrl_class_file) + end + + example.class.controller_class.class_eval do + self.perform_caching = true + extend Extensions::ActionController::ClassMethods + end + end + + config.after(:all) do |example| + silence_warnings do + Object.const_set "RAILS_CACHE", @_orig_cache_store + end + ActionController::Base.cache_store = RAILS_CACHE + ActionController::Base.perform_caching = false + end + + config.around(:each, :type => :controller, :caching => true) do |example| + # This block does some voodoo to ensure the controller class gets + # reloaded in the right context. I don't understand why it's necessary. + end + end + +end diff --git a/lib/rspec-rails-caching/extensions.rb b/lib/rspec-rails-caching/extensions.rb new file mode 100644 index 0000000..9a02dfc --- /dev/null +++ b/lib/rspec-rails-caching/extensions.rb @@ -0,0 +1 @@ +require 'rspec-rails-caching/extensions/action_controller/base' diff --git a/lib/rspec-rails-caching/extensions/action_controller.rb b/lib/rspec-rails-caching/extensions/action_controller.rb new file mode 100644 index 0000000..37ef8b9 --- /dev/null +++ b/lib/rspec-rails-caching/extensions/action_controller.rb @@ -0,0 +1,20 @@ +module RSpecRailsCaching::Extensions + module ActionController + + module ClassMethods + def cache_page(content, path, extension = nil, *) + instrument_page_cache :write_page, path do + cache_store.cached_pages << path + end + end + + def expire_page(path) + instrument_page_cache :expire_page, path do + cache_store.cached_pages.delete path + cache_store.expired_pages << path + end + end + end + + end +end diff --git a/lib/rspec-rails-caching/matchers.rb b/lib/rspec-rails-caching/matchers.rb new file mode 100644 index 0000000..c041e1a --- /dev/null +++ b/lib/rspec-rails-caching/matchers.rb @@ -0,0 +1,9 @@ +module RSpecRailsCaching + module Matchers + end +end + +require 'rspec-rails-caching/matchers/cache_page' +require 'rspec-rails-caching/matchers/expire_page' +require 'rspec-rails-caching/matchers/test_cache_caches' +require 'rspec-rails-caching/matchers/test_cache_expires' diff --git a/lib/rspec-rails-caching/matchers/cache_page.rb b/lib/rspec-rails-caching/matchers/cache_page.rb new file mode 100644 index 0000000..6b37fd7 --- /dev/null +++ b/lib/rspec-rails-caching/matchers/cache_page.rb @@ -0,0 +1,38 @@ +module RSpecRailsCaching::Matchers + extend RSpec::Matchers::DSL + + matcher :cache_page do |*expected| + match do |actual| + actual = actual.call if actual.respond_to?(:call) + unless actual.is_a? ActionController::TestResponse + raise ArgumentError("cache_page matcher expects a callable Proc or a TestResponse") + end + + Array(expected).all? { |e| cache_store.page_cached?(e) } + end + + failure_message_for_should do |actual| + "expected #{controller.class} to cache: #{expected.inspect} but got #{cache_results.inspect}" + end + + failure_message_for_should_not do |actual| + "expected #{controller.class} not to cache: #{expected.inspect} but got #{cache_results.inspect}" + end + + description do + "cache page #{expected.inspect}" + end + + def controller + matcher_execution_context.controller + end + + def cache_store + controller.cache_store + end + + def cache_results + cache_store.cached_pages + end + end +end diff --git a/lib/rspec-rails-caching/matchers/expire_page.rb b/lib/rspec-rails-caching/matchers/expire_page.rb new file mode 100644 index 0000000..c083611 --- /dev/null +++ b/lib/rspec-rails-caching/matchers/expire_page.rb @@ -0,0 +1,38 @@ +module RSpecRailsCaching::Matchers + extend RSpec::Matchers::DSL + + matcher :expire_page do |*expected| + match do |actual| + actual = actual.call if actual.respond_to?(:call) + unless actual.is_a? ActionController::TestResponse + raise ArgumentError("cache_page matcher expects a callable Proc or a TestResponse") + end + + Array(expected).all? { |e| cache_store.page_expired?(e) } + end + + failure_message_for_should do |actual| + "expected #{controller.class} to expire: #{expected.inspect} but got #{cache_results.inspect}" + end + + failure_message_for_should_not do |actual| + "expected #{controller.class} not to expire: #{expected.inspect} but got #{cache_results.inspect}" + end + + description do + "expire page #{expected.inspect}" + end + + def controller + matcher_execution_context.controller + end + + def cache_store + controller.cache_store + end + + def cache_results + cache_store.expired_pages + end + end +end diff --git a/lib/rspec-rails-caching/test_store.rb b/lib/rspec-rails-caching/test_store.rb new file mode 100644 index 0000000..f81bfdb --- /dev/null +++ b/lib/rspec-rails-caching/test_store.rb @@ -0,0 +1,70 @@ +module RSpecRailsCaching + class TestStore < ActiveSupport::Cache::Store + + attr_reader :cached + attr_reader :expired + attr_reader :expiration_patterns + attr_reader :data + attr_accessor :read_cache + attr_reader :cached_pages + attr_reader :expired_pages + + attr_accessor :context + + def initialize(do_read_cache = false) + @data = {} + @cached = [] + @expired = [] + @expiration_patterns = [] + @read_cache = do_read_cache + @cached_pages = [] + @expired_pages = [] + end + + def reset + @data.clear + @cached.clear + @expired.clear + @expiration_patterns.clear + end + + def read_entry(name, options = nil) + read_cache ? @data[name] : nil + end + + def write_entry(name, value, options = nil) + @data[name] = value if read_cache + @cached << name + end + + def delete_entry(name, options = nil) + @expired << name + end + + def delete_matched(matcher, options = nil) + @expiration_patterns << matcher + end + + def cached?(name) + @cached.include?(name) + end + + def expired?(name) + @expired.include?(name) || @expiration_patterns.detect { |matcher| name =~ matcher } + end + + def page_cached?(options = {}) + @cached_pages.include?(test_cache_url(options)) + end + + def page_expired?(options = {}) + @expired_pages.include?(test_cache_url(options)) + end + + private + def test_cache_url(options) + return options if options.is_a?(String) + url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })) + end + end +end diff --git a/lib/rspec-rails-caching/version.rb b/lib/rspec-rails-caching/version.rb new file mode 100644 index 0000000..50f369a --- /dev/null +++ b/lib/rspec-rails-caching/version.rb @@ -0,0 +1,3 @@ +module RSpecRailsCaching + VERSION = "0.1.0" +end diff --git a/rspec-caching.gemspec b/rspec-caching.gemspec new file mode 100644 index 0000000..16666b2 --- /dev/null +++ b/rspec-caching.gemspec @@ -0,0 +1,22 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "rspec-rails-caching/version" + +Gem::Specification.new do |s| + s.name = "rspec-rails-caching" + s.version = RSpecRailsCaching::VERSION + s.authors = ["Andrew Vit"] + s.email = ["andrew@avit.ca"] + s.homepage = "https://github.com/avit/rspec-rails-caching" + s.summary = %q{RSpec Rails Caching} + s.description = %q{RSpec helper for testing page and action caching in Rails} + + s.rubyforge_project = "rspec-rails-caching" + s.add_dependency "rails", ">=3.0.0" + s.add_dependency "rspec", ">=2.8.0" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ["lib"] +end