From 99e141f5449fc5e21f6fce1844bd09f37a0d1af7 Mon Sep 17 00:00:00 2001 From: Sam Bostock Date: Mon, 22 Jun 2020 10:23:04 -0400 Subject: [PATCH] Add tests for exotic rescued error capturing The idiomatic way to capture exceptions is to assign to a local variable rescue => local_variable However, other kinds of LVALUEs also work rescue => $global_variable rescue => @@class_variable rescue => @instance_variable rescue => Constant rescue => receiver&.setter_method rescue => receiver.setter_method rescue => receiver[:key] Some of the tests involve side effects to the global state. We can remove the constant and class variable, but the best we can go for the global variable is to nil it out. This is effectively identical to removing it though, as nil is the default when accessing an undefined global. --- language/fixtures/rescue.rb | 104 ++++++++++++++++++++++++++++++++++++ language/rescue_spec.rb | 21 ++++++-- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/language/fixtures/rescue.rb b/language/fixtures/rescue.rb index b906e17a2f..7e3e4240c2 100644 --- a/language/fixtures/rescue.rb +++ b/language/fixtures/rescue.rb @@ -1,4 +1,108 @@ module RescueSpecs + class ClassVariableCaptor + def capture(msg) + raise msg + rescue => @@captured_error + :caught + end + + def captured_error + self.class.remove_class_variable(:@@captured_error) + end + end + + class ConstantCaptor + # Using lambda gets around the dynamic constant assignment warning + CAPTURE = -> msg { + begin + raise msg + rescue => CapturedError + :caught + end + } + + def capture(msg) + CAPTURE.call(msg) + end + + def captured_error + self.class.send(:remove_const, :CapturedError) + end + end + + class GlobalVariableCaptor + def capture(msg) + raise msg + rescue => $captured_error + :caught + end + + def captured_error + $captured_error.tap do + $captured_error = nil # Can't remove globals, only nil them out + end + end + end + + class InstanceVariableCaptor + attr_reader :captured_error + + def capture(msg) + raise msg + rescue => @captured_error + :caught + end + end + + class LocalVariableCaptor + attr_reader :captured_error + + def capture(msg) + raise msg + rescue => captured_error + @captured_error = captured_error + :caught + end + end + + class SafeNavigationSetterCaptor + attr_accessor :captured_error + + def capture(msg) + raise msg + rescue => self&.captured_error + :caught + end + end + + class SetterCaptor + attr_accessor :captured_error + + def capture(msg) + raise msg + rescue => self.captured_error + :caught + end + end + + class SquareBracketsCaptor + def capture(msg) + @hash = {} + + raise msg + rescue => self[:error] + :caught + end + + def []=(key, value) + @hash[key] = value + end + + def captured_error + @hash[:error] + end + end + def self.begin_else(raise_exception) begin ScratchPad << :one diff --git a/language/rescue_spec.rb b/language/rescue_spec.rb index 7fa674d009..a6afd4981c 100644 --- a/language/rescue_spec.rb +++ b/language/rescue_spec.rb @@ -23,11 +23,22 @@ class ArbitraryException < StandardError end.should == :caught end - it "can capture the raised exception in a local variable" do - begin - raise SpecificExampleException, "some text" - rescue SpecificExampleException => e - e.message.should == "some text" + { + # Standard use case + 'can capture the raised exception in a local variable' => RescueSpecs::LocalVariableCaptor, + # Exotic use cases + 'can capture the raised exception in a class variable' => RescueSpecs::ClassVariableCaptor, + 'can capture the raised exception in a constant' => RescueSpecs::ConstantCaptor, + 'can capture the raised exception in a global variable' => RescueSpecs::GlobalVariableCaptor, + 'can capture the raised exception in an instance variable' => RescueSpecs::InstanceVariableCaptor, + 'can capture the raised exception using a safely navigated setter method' => RescueSpecs::SafeNavigationSetterCaptor, + 'can capture the raised exception using a setter method' => RescueSpecs::SetterCaptor, + 'can capture the raised exception using a square brackets setter' => RescueSpecs::SquareBracketsCaptor, + }.each do |description, klass| + it description do + captor = klass.new + captor.capture('some text').should == :caught # Ensure rescue body still runs + captor.captured_error.message.should == 'some text' end end