Skip to content

Commit

Permalink
Add tests for ActiveSupport::Rescuable. Use ActiveSupport::Rescuable …
Browse files Browse the repository at this point in the history
…in ActionController::Base.
  • Loading branch information
lifo committed Oct 4, 2008
1 parent 964dfc1 commit 259a7a8
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 119 deletions.
102 changes: 3 additions & 99 deletions actionpack/lib/action_controller/rescue.rb
Expand Up @@ -41,10 +41,9 @@ def self.included(base) #:nodoc:
base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE)
base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES

base.class_inheritable_array :rescue_handlers
base.rescue_handlers = []

base.extend(ClassMethods)
base.send :include, ActiveSupport::Rescuable

base.class_eval do
alias_method_chain :perform_action, :rescue
end
Expand All @@ -54,65 +53,12 @@ module ClassMethods
def process_with_exception(request, response, exception) #:nodoc:
new.process(request, response, :rescue_action, exception)
end

# Rescue exceptions raised in controller actions.
#
# <tt>rescue_from</tt> receives a series of exception classes or class
# names, and a trailing <tt>:with</tt> option with the name of a method
# or a Proc object to be called to handle them. Alternatively a block can
# be given.
#
# Handlers that take one argument will be called with the exception, so
# that the exception can be inspected when dealing with it.
#
# Handlers are inherited. They are searched from right to left, from
# bottom to top, and up the hierarchy. The handler of the first class for
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
# any.
#
# class ApplicationController < ActionController::Base
# rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
# rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
#
# rescue_from 'MyAppError::Base' do |exception|
# render :xml => exception, :status => 500
# end
#
# protected
# def deny_access
# ...
# end
#
# def show_errors(exception)
# exception.record.new_record? ? ...
# end
# end
def rescue_from(*klasses, &block)
options = klasses.extract_options!
unless options.has_key?(:with)
block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.")
end

klasses.each do |klass|
key = if klass.is_a?(Class) && klass <= Exception
klass.name
elsif klass.is_a?(String)
klass
else
raise(ArgumentError, "#{klass} is neither an Exception nor a String")
end

# Order is important, we put the pair at the end. When dealing with an
# exception we will follow the documented order going from right to left.
rescue_handlers << [key, options[:with]]
end
end
end

protected
# Exception handler called when the performance of an action raises an exception.
def rescue_action(exception)
rescue_action_with_handler(exception) || rescue_action_without_handler(exception)
rescue_with_handler(exception) || rescue_action_without_handler(exception)
end

# Overwrite to implement custom logging of errors. By default logs as fatal.
Expand Down Expand Up @@ -168,18 +114,6 @@ def rescue_action_locally(exception)
render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
end

# Tries to rescue the exception by looking up and calling a registered handler.
def rescue_action_with_handler(exception)
if handler = handler_for_rescue(exception)
if handler.arity != 0
handler.call(exception)
else
handler.call
end
true # don't rely on the return value of the handler
end
end

def rescue_action_without_handler(exception)
log_error(exception) if logger
erase_results if performed?
Expand Down Expand Up @@ -216,36 +150,6 @@ def response_code_for_rescue(exception)
rescue_responses[exception.class.name]
end

def handler_for_rescue(exception)
# We go from right to left because pairs are pushed onto rescue_handlers
# as rescue_from declarations are found.
_, handler = *rescue_handlers.reverse.detect do |klass_name, handler|
# The purpose of allowing strings in rescue_from is to support the
# declaration of handler associations for exception classes whose
# definition is yet unknown.
#
# Since this loop needs the constants it would be inconsistent to
# assume they should exist at this point. An early raised exception
# could trigger some other handler and the array could include
# precisely a string whose corresponding constant has not yet been
# seen. This is why we are tolerant to unknown constants.
#
# Note that this tolerance only matters if the exception was given as
# a string, otherwise a NameError will be raised by the interpreter
# itself when rescue_from CONSTANT is executed.
klass = self.class.const_get(klass_name) rescue nil
klass ||= klass_name.constantize rescue nil
exception.is_a?(klass) if klass
end

case handler
when Symbol
method(handler)
when Proc
handler.bind(self)
end
end

def clean_backtrace(exception)
if backtrace = exception.backtrace
if defined?(RAILS_ROOT)
Expand Down
2 changes: 2 additions & 0 deletions activesupport/CHANGELOG
@@ -1,5 +1,7 @@
*Edge*

* Add ActiveSupport::Rescuable module abstracting ActionController::Base rescue_from features. [Norbert Crombach, Pratik]

* Switch from String#chars to String#mb_chars for the unicode proxy. [Manfred Stienstra]

This helps with 1.8.7 compatibility and also improves performance for some operations by reducing indirection.
Expand Down
2 changes: 2 additions & 0 deletions activesupport/lib/active_support.rb
Expand Up @@ -56,6 +56,8 @@

require 'active_support/secure_random'

require 'active_support/rescuable'

I18n.load_path << File.dirname(__FILE__) + '/active_support/locale/en-US.yml'

Inflector = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Inflector', 'ActiveSupport::Inflector')
Expand Down
74 changes: 54 additions & 20 deletions activesupport/lib/active_support/rescuable.rb
@@ -1,28 +1,49 @@
module ActiveSupport
# Rescuable module adds support for easier exception handling.
module Rescuable
def self.included(base) # :nodoc:
base.class_inheritable_array :rescue_handlers
base.class_inheritable_accessor :rescue_handlers
base.rescue_handlers = []

base.extend(ClassMethods)
end

module ClassMethods
def enable_rescue_for(*methods)
methods.each do |method|
class_eval <<-EOS
def #{method}_with_rescue(*args, &block)
#{method}_without_rescue(*args, &block)
rescue Exception => exception
rescue_with_handler(exception)
end
alias_method_chain :#{method}, :rescue
EOS
end
end

# Rescue exceptions raised in controller actions.
#
# <tt>rescue_from</tt> receives a series of exception classes or class
# names, and a trailing <tt>:with</tt> option with the name of a method
# or a Proc object to be called to handle them. Alternatively a block can
# be given.
#
# Handlers that take one argument will be called with the exception, so
# that the exception can be inspected when dealing with it.
#
# Handlers are inherited. They are searched from right to left, from
# bottom to top, and up the hierarchy. The handler of the first class for
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
# any.
#
# class ApplicationController < ActionController::Base
# rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
# rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
#
# rescue_from 'MyAppError::Base' do |exception|
# render :xml => exception, :status => 500
# end
#
# protected
# def deny_access
# ...
# end
#
# def show_errors(exception)
# exception.record.new_record? ? ...
# end
# end
def rescue_from(*klasses, &block)
options = klasses.extract_options!

unless options.has_key?(:with)
if block_given?
options[:with] = block
Expand All @@ -46,18 +67,31 @@ def rescue_from(*klasses, &block)
end
end

# Tries to rescue the exception by looking up and calling a registered handler.
def rescue_with_handler(exception)
if handler = handler_for_rescue(exception)
handler.arity != 0 ? handler.call(exception) : handler.call
else
raise exception
true # don't rely on the return value of the handler
end
end

def handler_for_rescue(exception)
# use reverse so what is added last is found first
_, handler = *rescue_handlers.reverse.detect do |klass_name, handler|
# allow strings to support constants that are not defined yet
# We go from right to left because pairs are pushed onto rescue_handlers
# as rescue_from declarations are found.
_, handler = Array(rescue_handlers).reverse.detect do |klass_name, handler|
# The purpose of allowing strings in rescue_from is to support the
# declaration of handler associations for exception classes whose
# definition is yet unknown.
#
# Since this loop needs the constants it would be inconsistent to
# assume they should exist at this point. An early raised exception
# could trigger some other handler and the array could include
# precisely a string whose corresponding constant has not yet been
# seen. This is why we are tolerant to unknown constants.
#
# Note that this tolerance only matters if the exception was given as
# a string, otherwise a NameError will be raised by the interpreter
# itself when rescue_from CONSTANT is executed.
klass = self.class.const_get(klass_name) rescue nil
klass ||= klass_name.constantize rescue nil
exception.is_a?(klass) if klass
Expand Down
75 changes: 75 additions & 0 deletions activesupport/test/rescuable_test.rb
@@ -0,0 +1,75 @@
require 'abstract_unit'

class WraithAttack < StandardError
end

class NuclearExplosion < StandardError
end

class MadRonon < StandardError
attr_accessor :message

def initialize(message)
@message = message
super()
end
end

class Stargate
attr_accessor :result

include ActiveSupport::Rescuable

rescue_from WraithAttack, :with => :sos

rescue_from NuclearExplosion do
@result = 'alldead'
end

rescue_from MadRonon do |e|
@result = e.message
end

def dispatch(method)
send(method)
rescue Exception => e
rescue_with_handler(e)
end

def attack
raise WraithAttack
end

def nuke
raise NuclearExplosion
end

def ronanize
raise MadRonon.new("dex")
end

def sos
@result = 'killed'
end
end

class RescueableTest < Test::Unit::TestCase
def setup
@stargate = Stargate.new
end

def test_rescue_from_with_method
@stargate.dispatch :attack
assert_equal 'killed', @stargate.result
end

def test_rescue_from_with_block
@stargate.dispatch :nuke
assert_equal 'alldead', @stargate.result
end

def test_rescue_from_with_block_with_args
@stargate.dispatch :ronanize
assert_equal 'dex', @stargate.result
end
end

0 comments on commit 259a7a8

Please sign in to comment.