diff options
Diffstat (limited to 'spec/ruby/shared/kernel')
| -rw-r--r-- | spec/ruby/shared/kernel/at_exit.rb | 76 | ||||
| -rw-r--r-- | spec/ruby/shared/kernel/complex.rb | 133 | ||||
| -rw-r--r-- | spec/ruby/shared/kernel/equal.rb | 54 | ||||
| -rw-r--r-- | spec/ruby/shared/kernel/fixtures/END.rb | 3 | ||||
| -rw-r--r-- | spec/ruby/shared/kernel/fixtures/at_exit.rb | 3 | ||||
| -rw-r--r-- | spec/ruby/shared/kernel/object_id.rb | 100 | ||||
| -rw-r--r-- | spec/ruby/shared/kernel/raise.rb | 398 |
7 files changed, 767 insertions, 0 deletions
diff --git a/spec/ruby/shared/kernel/at_exit.rb b/spec/ruby/shared/kernel/at_exit.rb new file mode 100644 index 0000000000..29db79bb39 --- /dev/null +++ b/spec/ruby/shared/kernel/at_exit.rb @@ -0,0 +1,76 @@ +describe :kernel_at_exit, shared: true do + it "runs after all other code" do + ruby_exe("#{@method} { print 5 }; print 6").should == "65" + end + + it "runs in reverse order of registration" do + code = "#{@method} { print 4 }; #{@method} { print 5 }; print 6; #{@method} { print 7 }" + ruby_exe(code).should == "6754" + end + + it "allows calling exit inside a handler" do + code = "#{@method} { print 3 }; #{@method} { print 4; exit; print 5 }; #{@method} { print 6 }" + ruby_exe(code).should == "643" + end + + it "gives access to the last raised exception - global variables $! and $@" do + code = <<-EOC + #{@method} { + puts "The exception matches: \#{$! == $exception && $@ == $exception.backtrace} (message=\#{$!.message})" + } + + begin + raise "foo" + rescue => $exception + raise + end + EOC + + result = ruby_exe(code, args: "2>&1", exit_status: 1) + result.lines.should.include?("The exception matches: true (message=foo)\n") + end + + it "gives access to an exception raised in a previous handler" do + code = "#{@method} { print '$!.message = ' + $!.message }; #{@method} { raise 'foo' }" + result = ruby_exe(code, args: "2>&1", exit_status: 1) + result.lines.should.include?("$!.message = foo") + end + + it "both exceptions in a handler and in the main script are printed" do + code = "#{@method} { raise 'at_exit_error' }; raise 'main_script_error'" + result = ruby_exe(code, args: "2>&1", exit_status: 1) + result.should.include?('at_exit_error (RuntimeError)') + result.should.include?('main_script_error (RuntimeError)') + end + + it "decides the exit status if both at_exit and the main script raise SystemExit" do + ruby_exe("#{@method} { exit 43 }; exit 42", args: "2>&1", exit_status: 43) + $?.exitstatus.should == 43 + end + + it "runs all handlers even if some raise exceptions" do + code = "#{@method} { STDERR.puts 'last' }; #{@method} { exit 43 }; #{@method} { STDERR.puts 'first' }; exit 42" + result = ruby_exe(code, args: "2>&1", exit_status: 43) + result.should == "first\nlast\n" + $?.exitstatus.should == 43 + end + + it "runs handlers even if the main script fails to parse" do + script = fixture(__FILE__, "#{@method}.rb") + result = ruby_exe('{', options: "-r#{script}", args: "2>&1", exit_status: 1) + $?.should_not.success? + result.should.include?("handler ran\n") + + # it's tempting not to rely on error message and rely only on exception class name, + # but CRuby before 3.2 doesn't print class name for syntax error + result.should include_any_of("syntax error", "SyntaxError") + end + + it "calls the nested handler right after the outer one if a handler is nested into another handler" do + ruby_exe(<<~ruby).should == "last\nbefore\nafter\nnested\nfirst\n" + #{@method} { puts :first } + #{@method} { puts :before; #{@method} { puts :nested }; puts :after }; + #{@method} { puts :last } + ruby + end +end diff --git a/spec/ruby/shared/kernel/complex.rb b/spec/ruby/shared/kernel/complex.rb new file mode 100644 index 0000000000..98ee0b2b3f --- /dev/null +++ b/spec/ruby/shared/kernel/complex.rb @@ -0,0 +1,133 @@ +# Specs shared by Kernel#Complex() and String#to_c() +describe :kernel_complex, shared: true do + + it "returns a Complex object" do + @object.send(@method, '9').should be_an_instance_of(Complex) + end + + it "understands integers" do + @object.send(@method, '20').should == Complex(20) + end + + it "understands negative integers" do + @object.send(@method, '-3').should == Complex(-3) + end + + it "understands fractions (numerator/denominator) for the real part" do + @object.send(@method, '2/3').should == Complex(Rational(2, 3)) + end + + it "understands fractions (numerator/denominator) for the imaginary part" do + @object.send(@method, '4+2/3i').should == Complex(4, Rational(2, 3)) + end + + it "understands negative fractions (-numerator/denominator) for the real part" do + @object.send(@method, '-2/3').should == Complex(Rational(-2, 3)) + end + + it "understands negative fractions (-numerator/denominator) for the imaginary part" do + @object.send(@method, '7-2/3i').should == Complex(7, Rational(-2, 3)) + end + + it "understands floats (a.b) for the real part" do + @object.send(@method, '2.3').should == Complex(2.3) + end + + it "understands floats (a.b) for the imaginary part" do + @object.send(@method, '4+2.3i').should == Complex(4, 2.3) + end + + it "understands negative floats (-a.b) for the real part" do + @object.send(@method, '-2.33').should == Complex(-2.33) + end + + it "understands negative floats (-a.b) for the imaginary part" do + @object.send(@method, '7-28.771i').should == Complex(7, -28.771) + end + + it "understands an integer followed by 'i' to mean that integer is the imaginary part" do + @object.send(@method, '35i').should == Complex(0,35) + end + + it "understands a negative integer followed by 'i' to mean that negative integer is the imaginary part" do + @object.send(@method, '-29i').should == Complex(0,-29) + end + + it "understands an 'i' by itself as denoting a complex number with an imaginary part of 1" do + @object.send(@method, 'i').should == Complex(0,1) + end + + it "understands a '-i' by itself as denoting a complex number with an imaginary part of -1" do + @object.send(@method, '-i').should == Complex(0,-1) + end + + it "understands 'a+bi' to mean a complex number with 'a' as the real part, 'b' as the imaginary" do + @object.send(@method, '79+4i').should == Complex(79,4) + end + + it "understands 'a-bi' to mean a complex number with 'a' as the real part, '-b' as the imaginary" do + @object.send(@method, '79-4i').should == Complex(79,-4) + end + + it "understands 'a+i' to mean a complex number with 'a' as the real part, 1i as the imaginary" do + @object.send(@method, '79+i').should == Complex(79, 1) + end + + it "understands 'a-i' to mean a complex number with 'a' as the real part, -1i as the imaginary" do + @object.send(@method, '79-i').should == Complex(79, -1) + end + + it "understands i, I, j, and J imaginary units" do + @object.send(@method, '79+4i').should == Complex(79, 4) + @object.send(@method, '79+4I').should == Complex(79, 4) + @object.send(@method, '79+4j').should == Complex(79, 4) + @object.send(@method, '79+4J').should == Complex(79, 4) + end + + it "understands scientific notation for the real part" do + @object.send(@method, '2e3+4i').should == Complex(2e3,4) + end + + it "understands negative scientific notation for the real part" do + @object.send(@method, '-2e3+4i').should == Complex(-2e3,4) + end + + it "understands scientific notation for the imaginary part" do + @object.send(@method, '4+2e3i').should == Complex(4, 2e3) + end + + it "understands negative scientific notation for the imaginary part" do + @object.send(@method, '4-2e3i').should == Complex(4, -2e3) + end + + it "understands scientific notation for the real and imaginary part in the same String" do + @object.send(@method, '2e3+2e4i').should == Complex(2e3,2e4) + end + + it "understands negative scientific notation for the real and imaginary part in the same String" do + @object.send(@method, '-2e3-2e4i').should == Complex(-2e3,-2e4) + end + + it "understands scientific notation with e and E" do + @object.send(@method, '2e3+2e4i').should == Complex(2e3, 2e4) + @object.send(@method, '2E3+2E4i').should == Complex(2e3, 2e4) + end + + it "understands 'm@a' to mean a complex number in polar form with 'm' as the modulus, 'a' as the argument" do + @object.send(@method, '79@4').should == Complex.polar(79, 4) + @object.send(@method, '-79@4').should == Complex.polar(-79, 4) + @object.send(@method, '79@-4').should == Complex.polar(79, -4) + end + + it "ignores leading whitespaces" do + @object.send(@method, ' 79+4i').should == Complex(79, 4) + end + + it "ignores trailing whitespaces" do + @object.send(@method, '79+4i ').should == Complex(79, 4) + end + + it "understands _" do + @object.send(@method, '7_9+4_0i').should == Complex(79, 40) + end +end diff --git a/spec/ruby/shared/kernel/equal.rb b/spec/ruby/shared/kernel/equal.rb new file mode 100644 index 0000000000..0a70aec639 --- /dev/null +++ b/spec/ruby/shared/kernel/equal.rb @@ -0,0 +1,54 @@ +# These examples hold for BasicObject#equal?, BasicObject#== and Kernel#eql? +describe :object_equal, shared: true do + it "returns true if other is identical to self" do + obj = Object.new + obj.__send__(@method, obj).should be_true + end + + it "returns false if other is not identical to self" do + a = Object.new + b = Object.new + a.__send__(@method, b).should be_false + end + + it "returns true only if self and other are the same object" do + o1 = mock('o1') + o2 = mock('o2') + o1.__send__(@method, o1).should == true + o2.__send__(@method, o2).should == true + o1.__send__(@method, o2).should == false + end + + it "returns true for the same immediate object" do + o1 = 1 + o2 = :hola + 1.__send__(@method, o1).should == true + :hola.__send__(@method, o2).should == true + end + + it "returns false for nil and any other object" do + o1 = mock('o1') + nil.__send__(@method, nil).should == true + o1.__send__(@method, nil).should == false + nil.__send__(@method, o1).should == false + end + + it "returns false for objects of different classes" do + :hola.__send__(@method, 1).should == false + end + + it "returns true only if self and other are the same boolean" do + true.__send__(@method, true).should == true + false.__send__(@method, false).should == true + + true.__send__(@method, false).should == false + false.__send__(@method, true).should == false + end + + it "returns true for integers of initially different ranges" do + big42 = (bignum_value * 42 / bignum_value) + 42.__send__(@method, big42).should == true + long42 = (1 << 35) * 42 / (1 << 35) + 42.__send__(@method, long42).should == true + end +end diff --git a/spec/ruby/shared/kernel/fixtures/END.rb b/spec/ruby/shared/kernel/fixtures/END.rb new file mode 100644 index 0000000000..cc8ac17c36 --- /dev/null +++ b/spec/ruby/shared/kernel/fixtures/END.rb @@ -0,0 +1,3 @@ +END { + STDERR.puts "handler ran" +} diff --git a/spec/ruby/shared/kernel/fixtures/at_exit.rb b/spec/ruby/shared/kernel/fixtures/at_exit.rb new file mode 100644 index 0000000000..e7bc8baf52 --- /dev/null +++ b/spec/ruby/shared/kernel/fixtures/at_exit.rb @@ -0,0 +1,3 @@ +at_exit do + STDERR.puts "handler ran" +end diff --git a/spec/ruby/shared/kernel/object_id.rb b/spec/ruby/shared/kernel/object_id.rb new file mode 100644 index 0000000000..099df8ff94 --- /dev/null +++ b/spec/ruby/shared/kernel/object_id.rb @@ -0,0 +1,100 @@ +# These examples hold for both BasicObject#__id__ and Kernel#object_id. +describe :object_id, shared: true do + it "returns an integer" do + o1 = @object.new + o1.__send__(@method).should be_kind_of(Integer) + end + + it "returns the same value on all calls to id for a given object" do + o1 = @object.new + o1.__send__(@method).should == o1.__send__(@method) + end + + it "returns different values for different objects" do + o1 = @object.new + o2 = @object.new + o1.__send__(@method).should_not == o2.__send__(@method) + end + + it "returns the same value for two Fixnums with the same value" do + o1 = 1 + o2 = 1 + o1.send(@method).should == o2.send(@method) + end + + it "returns the same value for two Symbol literals" do + o1 = :hello + o2 = :hello + o1.send(@method).should == o2.send(@method) + end + + it "returns the same value for two true literals" do + o1 = true + o2 = true + o1.send(@method).should == o2.send(@method) + end + + it "returns the same value for two false literals" do + o1 = false + o2 = false + o1.send(@method).should == o2.send(@method) + end + + it "returns the same value for two nil literals" do + o1 = nil + o2 = nil + o1.send(@method).should == o2.send(@method) + end + + it "returns a different value for two Bignum literals" do + o1 = 2e100.to_i + o2 = 2e100.to_i + o1.send(@method).should_not == o2.send(@method) + end + + guard -> { "test".frozen? && "test".equal?("test") } do # --enable-frozen-string-literal in $RUBYOPT + it "returns the same value for two identical String literals" do + o1 = "hello" + o2 = "hello" + o1.send(@method).should == o2.send(@method) + end + end + + guard -> { "test".frozen? && !"test".equal?("test") } do # chilled string literals + it "returns a different frozen value for two String literals" do + o1 = "hello" + o2 = "hello" + o1.send(@method).should_not == o2.send(@method) + o1.frozen?.should == true + o2.frozen?.should == true + end + end + + guard -> { !"test".frozen? } do + it "returns a different value for two String literals" do + o1 = "hello" + o2 = "hello" + o1.send(@method).should_not == o2.send(@method) + end + end + + it "returns a different value for an object and its dup" do + o1 = mock("object") + o2 = o1.dup + o1.send(@method).should_not == o2.send(@method) + end + + it "returns a different value for two numbers near the 32 bit Fixnum limit" do + o1 = -1 + o2 = 2 ** 30 - 1 + + o1.send(@method).should_not == o2.send(@method) + end + + it "returns a different value for two numbers near the 64 bit Fixnum limit" do + o1 = -1 + o2 = 2 ** 62 - 1 + + o1.send(@method).should_not == o2.send(@method) + end +end diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb new file mode 100644 index 0000000000..2be06ea797 --- /dev/null +++ b/spec/ruby/shared/kernel/raise.rb @@ -0,0 +1,398 @@ +describe :kernel_raise, shared: true do + before :each do + ScratchPad.clear + end + + it "aborts execution" do + -> do + @object.raise Exception, "abort" + ScratchPad.record :no_abort + end.should raise_error(Exception, "abort") + + ScratchPad.recorded.should be_nil + end + + it "accepts an exception that implements to_hash" do + custom_error = Class.new(StandardError) do + def to_hash + {} + end + end + error = custom_error.new + -> { @object.raise(error) }.should raise_error(custom_error) + end + + it "allows the message parameter to be a hash" do + data_error = Class.new(StandardError) do + attr_reader :data + def initialize(data) + @data = data + end + end + + -> { @object.raise(data_error, {data: 42}) }.should raise_error(data_error) do |ex| + ex.data.should == {data: 42} + end + end + + # https://bugs.ruby-lang.org/issues/8257#note-36 + it "allows extra keyword arguments for compatibility" do + data_error = Class.new(StandardError) do + attr_reader :data + def initialize(data) + @data = data + end + end + + -> { @object.raise(data_error, data: 42) }.should raise_error(data_error) do |ex| + ex.data.should == {data: 42} + end + end + + it "raises RuntimeError if no exception class is given" do + -> { @object.raise }.should raise_error(RuntimeError, "") + end + + it "raises a given Exception instance" do + error = RuntimeError.new + -> { @object.raise(error) }.should raise_error(error) + end + + it "raises a RuntimeError if string given" do + -> { @object.raise("a bad thing") }.should raise_error(RuntimeError, "a bad thing") + end + + it "passes no arguments to the constructor when given only an exception class" do + klass = Class.new(Exception) do + def initialize + end + end + -> { @object.raise(klass) }.should raise_error(klass) { |e| e.message.should == klass.to_s } + end + + it "raises a TypeError when passed a non-Exception object" do + -> { @object.raise(Object.new) }.should raise_error(TypeError, "exception class/object expected") + -> { @object.raise(Object.new, "message") }.should raise_error(TypeError, "exception class/object expected") + -> { @object.raise(Object.new, "message", []) }.should raise_error(TypeError, "exception class/object expected") + end + + it "raises a TypeError when passed true" do + -> { @object.raise(true) }.should raise_error(TypeError, "exception class/object expected") + end + + it "raises a TypeError when passed false" do + -> { @object.raise(false) }.should raise_error(TypeError, "exception class/object expected") + end + + it "raises a TypeError when passed nil" do + -> { @object.raise(nil) }.should raise_error(TypeError, "exception class/object expected") + end + + it "raises a TypeError when passed a message and an extra argument" do + -> { @object.raise("message", {cause: RuntimeError.new()}) }.should raise_error(TypeError, "exception class/object expected") + end + + it "raises TypeError when passed a non-Exception object but it responds to #exception method that doesn't return an instance of Exception class" do + e = Object.new + def e.exception + Array + end + + -> { + @object.raise e + }.should raise_error(TypeError, "exception object expected") + end + + it "re-raises a previously rescued exception without overwriting the backtrace" do + exception = nil + + begin + raise "raised" + rescue => exception + # Ignore. + end + + backtrace = exception.backtrace + + begin + raised_exception = @object.raise(exception) + rescue => raised_exception + # Ignore. + end + + raised_exception.backtrace.should == backtrace + raised_exception.should == exception + end + + it "allows Exception, message, and backtrace parameters" do + -> do + @object.raise(ArgumentError, "message", caller) + end.should raise_error(ArgumentError, "message") + end + + ruby_version_is "3.4" do + locations = caller_locations(1, 2) + it "allows Exception, message, and backtrace_locations parameters" do + -> do + @object.raise(ArgumentError, "message", locations) + end.should raise_error(ArgumentError, "message") { |error| + error.backtrace_locations.map(&:to_s).should == locations.map(&:to_s) + } + end + end + + ruby_version_is "4.0" do + it "allows cause keyword argument" do + cause = StandardError.new("original error") + result = nil + + -> do + @object.raise("new error", cause: cause) + end.should raise_error(RuntimeError, "new error") do |error| + error.cause.should == cause + end + end + + it "raises an ArgumentError when only cause is given" do + cause = StandardError.new("cause") + -> do + @object.raise(cause: cause) + end.should raise_error(ArgumentError, "only cause is given with no arguments") + end + + it "raises an ArgumentError when only cause is given and is nil" do + -> do + @object.raise(cause: nil) + end.should raise_error(ArgumentError, "only cause is given with no arguments") + end + + it "raises a TypeError when given cause is not an instance of Exception" do + cause = Object.new + -> do + @object.raise("message", cause: cause) + end.should raise_error(TypeError, "exception object expected") + end + + it "doesn't set given cause when it equals the raised exception" do + cause = StandardError.new("cause") + result = nil + + -> do + @object.raise(cause, cause: cause) + end.should raise_error(StandardError, "cause") do |error| + error.should == cause + error.cause.should == nil + end + end + + it "accepts cause equal an exception" do + error = RuntimeError.new("message") + result = nil + + -> do + @object.raise(error, cause: error) + end.should raise_error(RuntimeError, "message") do |e| + e.cause.should == nil + end + end + + it "rejects circular causes" do + -> { + begin + raise "Error 1" + rescue => error1 + begin + raise "Error 2" + rescue => error2 + begin + raise "Error 3" + rescue => error3 + @object.raise(error1, cause: error3) + end + end + end + }.should raise_error(ArgumentError, "circular causes") + end + + it "supports exception class with message and cause" do + cause = StandardError.new("cause message") + result = nil + + -> do + @object.raise(ArgumentError, "argument error message", cause: cause) + end.should raise_error(ArgumentError, "argument error message") do |error| + error.should be_kind_of(ArgumentError) + error.message.should == "argument error message" + error.cause.should == cause + end + end + + it "supports exception class with message, backtrace and cause" do + cause = StandardError.new("cause message") + backtrace = ["line1", "line2"] + result = nil + + -> do + @object.raise(ArgumentError, "argument error message", backtrace, cause: cause) + end.should raise_error(ArgumentError, "argument error message") do |error| + error.should be_kind_of(ArgumentError) + error.message.should == "argument error message" + error.cause.should == cause + error.backtrace.should == backtrace + end + end + + it "supports automatic cause chaining" do + -> do + begin + raise "first error" + rescue + # No explicit cause - should chain automatically: + @object.raise("second error") + end + end.should raise_error(RuntimeError, "second error") do |error| + error.cause.should be_kind_of(RuntimeError) + error.cause.message.should == "first error" + end + end + + it "supports cause: nil to prevent automatic cause chaining" do + -> do + begin + raise "first error" + rescue + # Explicit nil prevents chaining: + @object.raise("second error", cause: nil) + end + end.should raise_error(RuntimeError, "second error") do |error| + error.cause.should == nil + end + end + end +end + +describe :kernel_raise_across_contexts, shared: true do + ruby_version_is "4.0" do + describe "with cause keyword argument" do + it "uses the cause from the calling context" do + original_cause = nil + result = nil + + # We have no cause ($!) and we don't specify one explicitly either: + @object.raise("second error") do |&block| + begin + begin + raise "first error" + rescue => original_cause + # We have a cause here ($!) but we should ignore it: + block.call + end + rescue => result + # Ignore. + end + end + + result.should be_kind_of(RuntimeError) + result.message.should == "second error" + result.cause.should == nil + end + + it "accepts a cause keyword argument that overrides the last exception" do + original_cause = nil + override_cause = StandardError.new("override cause") + result = nil + + begin + raise "outer error" + rescue + # We have an existing cause, but we want to override it: + @object.raise("second error", cause: override_cause) do |&block| + begin + begin + raise "first error" + rescue => original_cause + # We also have an existing cause here: + block.call + end + rescue => result + # Ignore. + end + end + end + + result.should be_kind_of(RuntimeError) + result.message.should == "second error" + result.cause.should == override_cause + end + + it "supports automatic cause chaining from calling context" do + result = nil + + @object.raise("new error") do |&block| + begin + begin + raise "original error" + rescue + block.call # Let the context yield/sleep + end + rescue => result + # Ignore. + end + end + + result.should be_kind_of(RuntimeError) + result.message.should == "new error" + # Calling context has no current exception: + result.cause.should == nil + end + + it "supports explicit cause: nil to prevent cause chaining" do + result = nil + + begin + raise "calling context error" + rescue + @object.raise("new error", cause: nil) do |&block| + begin + begin + raise "target context error" + rescue + block.call # Let the context yield/sleep + end + rescue => result + # Ignore. + end + end + + result.should be_kind_of(RuntimeError) + result.message.should == "new error" + result.cause.should == nil + end + end + + it "raises TypeError when cause is not an Exception" do + -> { + @object.raise("error", cause: "not an exception") do |&block| + begin + block.call # Let the context yield/sleep + rescue + # Ignore - we expect the TypeError to be raised in the calling context + end + end + }.should raise_error(TypeError, "exception object expected") + end + + it "raises ArgumentError when only cause is given with no arguments" do + -> { + @object.raise(cause: StandardError.new("cause")) do |&block| + begin + block.call # Let the context yield/sleep + rescue + # Ignore - we expect the ArgumentError to be raised in the calling context + end + end + }.should raise_error(ArgumentError, "only cause is given with no arguments") + end + end + end +end |
