Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adds support for optionally-handled (or "non-serious") exceptions.

  • Loading branch information...
commit 4d22757ac7822ee16a8fc68b5f56683cdb051bbe 1 parent 0123a59
Michael Bishop authored
23 lib/mulligan.rb
View
@@ -7,6 +7,28 @@ def self.supported?
def self.using_extension?
supported? && RUBY_VERSION < "2.0.0"
end
+
+
+ # Defines a scope inside of which Kernel#signal can raise exceptions which can be
+ # optionally rescued.
+ if self.supported?
+ def self.with_signal_activated
+ Mulligan::Kernel.__send__(:__increment_automatic_continuing_scope_count__)
+ yield if block_given?
+ rescue => e
+ unless recovery(IgnoringRecovery).nil?
+ recover IgnoringRecovery
+ end
+ mg_raise
+ ensure
+ Mulligan::Kernel.__send__(:__decrement_automatic_continuing_scope_count__)
+ end
+ else
+ def self.with_signal_activated
+ yield if block_given?
+ end
+ end
+
end
require "mulligan/exception"
@@ -19,3 +41,4 @@ class Exception
class Object
include Mulligan::Kernel
end
+
57 lib/mulligan/kernel.rb
View
@@ -68,6 +68,63 @@ def recovery(choice = nil)
$!.send(:__chosen_recovery__, choice)
end
+ # Raises an exception that can be optionally handled.
+ #
+ # The closest analogy would be a "non-serious" condition in common-lisp.
+ #
+ # #signal is the same as Kernel#raise with two very important caveats:
+ # 1. #signal means that the calling code can optionally handle the exception.
+ # Calling code can still handle the exception with the goal of offering
+ # additional direction to the called code but it's not required to.
+ # It does this by ensuring the raised exception has an IgnoringRecovery
+ # attached so higher code can invoke it.
+ # 2. #signal only functions when called somewhere inside a block passed to
+ # Mulligan::with_signal_activated. Mulligan::with_signal_activated can
+ # be anywhere in the stack as long as it's before the call to #signal.
+ # It's recommended that you put Mulligan::with_signal_activated at the top
+ # of your program.
+ # When #signal is not activated but is called, it doesn't raise an
+ # exception, but instead immediately calls the IgnoringRecovery, (whether
+ # explicitly attached to the exception or implicitly, through the #signal
+ # call itself).
+ #
+ # @params (the same as you would pass to Kernel#raise)
+ def signal(*args)
+ # Mulligan::Kernel.__last_recovery_collector__ ||= Mulligan::Collector.new
+ collector = Mulligan::Kernel.send(:__last_recovery_collector__)
+ if (collector.nil?)
+ Mulligan::Kernel.send(:"__last_recovery_collector__=", collector = Mulligan::Collector.new)
+ end
+
+ last_ignoring_recovery = collector.send(:__recovery__, IgnoringRecovery)
+ is_inside_automatic_continuing_scope = Mulligan::Kernel.send(:__is_inside_automatic_continuing_scope__)
+
+ # If the collector already has an ignoring recovery...
+ if (!!last_ignoring_recovery)
+ if is_inside_automatic_continuing_scope
+ # We can safely raise, knowing it is guaranteed to at least be ignored
+ mg_raise *args
+ else
+ # we are not within an automatic continuing scope so it's not safe to
+ # raise. Instead, we call the existing continue recovery
+ last_ignoring_recovery.invoke
+ end
+ end
+
+ # There is no continue recovery and it's not safe to raise so we just continue.
+ # Why is this not the first thing we test? Because we need to first check
+ # if there is an attached continue recovery that we need to call.
+ return if !is_inside_automatic_continuing_scope
+
+ # We can safely raise, adding a continue recovery.
+ case collector
+ when IgnoringRecovery
+ return
+ else
+ mg_raise *args
+ end
+ end
+
private
class << self
def __last_recovery_collector__
68 spec/kernel_spec.rb
View
@@ -151,10 +151,10 @@
result = begin
case recovery
when IgnoringRecovery
- 5
else
mg_raise
end
+ 5
rescue => e
begin
recover Recovery
@@ -168,10 +168,10 @@
result = begin
case recovery
when IgnoringRecovery
- 5
else
mg_raise
end
+ 5
rescue => e
begin
recover RetryingRecovery
@@ -266,5 +266,69 @@
end
end
end
+
+ describe "#signal" do
+ context "when inside an #with_signal_activated block" do
+ it "is continued" do
+ Mulligan.with_signal_activated do
+ result = begin
+ signal
+ 6
+ end
+ expect(result).to eq(6)
+ end
+ end
+
+ it "raises a continue recovery" do
+ Mulligan.with_signal_activated do
+ begin
+ signal
+ rescue RuntimeError
+ expect(recovery(IgnoringRecovery)).to_not be_nil
+ end
+ end
+ end
+
+ it "does not override an existing continue recovery" do
+ Mulligan.with_signal_activated do
+ result = begin
+ case recovery
+ when Mulligan::IgnoringRecovery
+ 5
+ else
+ signal
+ # This is not something that should be written normally, but for this
+ # test we want to differentiate results
+ 6
+ end
+ rescue RuntimeError => e
+ recover IgnoringRecovery
+ end
+ expect(result).to Mulligan.supported? ? eq(5) : eq(6)
+ end
+ end
+ end
+
+ context "when NOT inside an #with_signal_activated block" do
+ it "doesn't raise" do
+ expect{ signal }.to_not raise_error
+ end
+
+ it "calls a pre-existing continue recovery" do
+ result = begin
+ case recovery
+ when Mulligan::IgnoringRecovery
+ 5
+ else
+ signal
+ # This is not something that should be written normally, but for this
+ # test we want to differentiate results
+ 6
+ end
+ end
+ expect(result).to Mulligan.supported? ? eq(5) : eq(6)
+ end
+ end
+ end
end
38 spec/mulligan_spec.rb
View
@@ -20,4 +20,42 @@ class SubclassRecovery < RootRecovery
it 'should have a version number' do
Mulligan::VERSION.should_not be_nil
end
+
+ describe "#with_signal_activated" do
+ it "raises exceptions without continuing recoveries" do
+ expect do
+ Mulligan.with_signal_activated do
+ raise
+ end
+ end.to raise_error(RuntimeError)
+ end
+
+ it "handles exception with a continue recovery" do
+ test_case = proc do
+ Mulligan.with_signal_activated do
+ case recovery
+ when IgnoringRecovery
+ else ; mg_raise ; end
+ end
+ end
+ if Mulligan.supported?
+ expect(&test_case).to_not raise_error
+ else
+ expect(&test_case).to raise_error
+ end
+ end
+
+ pending "starts a new scope when starting a thread" do
+ Mulligan.with_signal_activated do
+ t = Thread.start do
+ case recovery
+ when IgnoringRecovery
+ else ; mg_raise ; end
+ 5
+ end
+ expect(t.value).to eq (5)
+ end
+ end
+ end
+
end
Please sign in to comment.
Something went wrong with that request. Please try again.