From dc197fd7a862aeb00a9769eab346347d02308394 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 18:19:13 +0300 Subject: [PATCH 01/10] Change output format of failed #expect_raises in specs --- spec/std/spec/expectations_spec.cr | 94 ++++++++++++++++++++++++++++++ src/spec/expectations.cr | 72 ++++++++++++++++++----- 2 files changed, 150 insertions(+), 16 deletions(-) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index 0831bca226ca..0d21e4060af5 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -172,5 +172,99 @@ describe "expectations" do it "pass if raises MyError" do expect_raises(Exception, "Ops") { raise Exception.new("Ops") } end + + describe "failure message format" do + context "given string to compare with message" do + it "contains expected exception, actual exception and backtrace" do + expect_raises(Exception, "digits should be non-negative") do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain(<<-MESSAGE) + Expected Exception with message containing: "digits should be non-negative" + got IndexError with message: "Index out of bounds" + Backtrace: + # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' + MESSAGE + else + raise "nothing is raised" + end + + it "contains expected class, actual exception and backtrace when expected class does not match actual class" do + expect_raises(ArgumentError, "digits should be non-negative") do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain(<<-MESSAGE) + Expected ArgumentError + got IndexError with message: "Index out of bounds" + Backtrace: + # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' + MESSAGE + else + raise "nothing is raised" + end + + it "escapes expected and actual messages in the same way" do + expect_raises(Exception, %q(a\tb\nc)) do + raise %q(a\tb\nc).inspect + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain("Expected Exception with message containing: #{%q(a\tb\nc).inspect}") + e.message.as(String).should contain("got Exception with message: #{%q(a\tb\nc).inspect.inspect}") + else + raise "nothing is raised" + end + end + + context "given regex to match a message" do + it "contains expected exception, actual exception and backtrace" do + expect_raises(Exception, /digits should be non-negative/) do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain(<<-MESSAGE) + Expected Exception with message matching: /digits should be non-negative/ + got IndexError with message: "Index out of bounds" + Backtrace: + # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' + MESSAGE + else + raise "nothing is raised" + end + + it "contains expected class, actual exception and backtrace when expected class does not match actual class" do + expect_raises(ArgumentError, /digits should be non-negative/) do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain(<<-MESSAGE) + Expected ArgumentError + got IndexError with message: "Index out of bounds" + Backtrace: + # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' + MESSAGE + else + raise "nothing is raised" + end + end + + context "given nil to allow any message" do + it "contains expected class, actual exception and backtrace when expected class does not match actual class" do + expect_raises(ArgumentError, nil) do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain(<<-MESSAGE) + Expected ArgumentError + got IndexError with message: "Index out of bounds" + Backtrace: + # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' + MESSAGE + else + raise "nothing is raised" + end + end + end end end diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index 158c82413fc5..d3abb4ca7ff2 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -410,32 +410,72 @@ module Spec raise ex end - ex_to_s = ex.to_s - case message - when Regex - unless (ex_to_s =~ message) - backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass} with message matching #{message.pretty_inspect}, " \ - "got #<#{ex.class}: #{ex_to_s}> with backtrace:\n#{backtrace}", file, line + case {ex.message, message} + when {String, Regex} + unless ex.message =~ message + expectation_failed_message = build_expectation_failed_message(klass, message, ex) + fail expectation_failed_message, file, line end - when String - unless ex_to_s.includes?(message) - backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass} with #{message.pretty_inspect}, got #<#{ex.class}: " \ - "#{ex_to_s}> with backtrace:\n#{backtrace}", file, line + when {String, String} + unless ex.message == message + expectation_failed_message = build_expectation_failed_message(klass, message, ex) + fail expectation_failed_message, file, line end - when Nil + when {_, Nil} # No need to check the message end ex rescue ex - backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass}, got #<#{ex.class}: #{ex}> with backtrace:\n" \ - "#{backtrace}", file, line + expectation_failed_message = build_expectation_failed_message(klass, ex) + fail expectation_failed_message, file, line else fail "Expected #{klass} but nothing was raised", file, line end + + private def build_expectation_failed_message(klass : Class, message : String, exception : Exception) + backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + + <<-MESSAGE + Expected #{klass} with message containing: #{message.inspect} + got #{exception.class} with message: #{exception.message.inspect} + Backtrace: + #{backtrace} + MESSAGE + end + + private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception) + backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + + <<-MESSAGE + Expected #{klass} with message matching: #{message.inspect} + got #{exception.class} with message: #{exception.message.inspect} + Backtrace: + #{backtrace} + MESSAGE + end + + private def build_expectation_failed_message(klass : Class, message : Nil, exception : Exception) + backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + + <<-MESSAGE + Expected #{klass} with any message + got #{exception.class} with message: #{exception.message.inspect} + Backtrace: + #{backtrace} + MESSAGE + end + + private def build_expectation_failed_message(klass : Class, exception : Exception) + backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + + <<-MESSAGE + Expected #{klass} + got #{exception.class} with message: #{exception.message.inspect} + Backtrace: + #{backtrace} + MESSAGE + end {% end %} end From f8fb47c5dd0561291afe0dc9f7fb6f8d588e9ba8 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 18:44:06 +0300 Subject: [PATCH 02/10] Add more specs for #expect_raises --- spec/std/spec/expectations_spec.cr | 64 +++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index 0d21e4060af5..c823ee412e3d 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -169,10 +169,62 @@ describe "expectations" do end describe "expect_raises" do - it "pass if raises MyError" do + it "passes if expected message equals actual message and expected class equals actual class" do expect_raises(Exception, "Ops") { raise Exception.new("Ops") } end + it "passes if expected message equals actual message and expected class is an ancestor of actual class" do + expect_raises(Exception, "Ops") { raise ArgumentError.new("Ops") } + end + + it "passes if expected regex matches actual message and expected class equals actual class" do + expect_raises(Exception, /Ops/) { raise Exception.new("Ops") } + end + + it "passes if expected regex matches actual message and expected class is an ancestor of actual class" do + expect_raises(Exception, /Ops/) { raise ArgumentError.new("Ops") } + end + + it "passes if given no message expectation and expected class equals actual class" do + expect_raises(Exception) { raise Exception.new("Ops") } + end + + it "passes if given no message expectation and expected class is an ancestor of actual class" do + expect_raises(Exception) { raise ArgumentError.new("Ops") } + end + + it "fails if expected message does not equal actual message and expected class equals actual class" do + expect_raises(Exception, "Ops") { raise Exception.new("Hm") } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if expected regex does not match actual message and expected class equals actual class" do + expect_raises(Exception, /Ops/) { raise Exception.new("Hm") } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if given no message expectation and expected class does not equal and is not an ancestor of actual class" do + expect_raises(IndexError) { raise ArgumentError.new("Ops") } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if nothing was raised" do + expect_raises(IndexError) { raise ArgumentError.new("Ops") } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + describe "failure message format" do context "given string to compare with message" do it "contains expected exception, actual exception and backtrace" do @@ -265,6 +317,16 @@ describe "expectations" do raise "nothing is raised" end end + + context "nothing was raises" do + it "contains expected class" do + expect_raises(IndexError) { } + rescue e : Spec::AssertionFailed + e.message.as(String).should contain("Expected IndexError but nothing was raised") + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + end end end end From 31274c3a95f69b8fa07dbb8a1bc61b9affb84e8e Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 19:34:40 +0300 Subject: [PATCH 03/10] Revert partial matching of expected and actual messages --- spec/std/spec/expectations_spec.cr | 12 ++++++++++-- src/spec/expectations.cr | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index c823ee412e3d..3f94fc190003 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -177,12 +177,20 @@ describe "expectations" do expect_raises(Exception, "Ops") { raise ArgumentError.new("Ops") } end + it "passes if expected message is a substring of actual message and expected class equals actual class" do + expect_raises(Exception, "Ops") { raise Exception.new("Black Ops") } + end + + it "passes if expected message is a substring of actual message and expected class is an ancestor of actual class" do + expect_raises(Exception, "Ops") { raise ArgumentError.new("Black Ops") } + end + it "passes if expected regex matches actual message and expected class equals actual class" do - expect_raises(Exception, /Ops/) { raise Exception.new("Ops") } + expect_raises(Exception, /Ops/) { raise Exception.new("Black Ops") } end it "passes if expected regex matches actual message and expected class is an ancestor of actual class" do - expect_raises(Exception, /Ops/) { raise ArgumentError.new("Ops") } + expect_raises(Exception, /Ops/) { raise ArgumentError.new("Black Ops") } end it "passes if given no message expectation and expected class equals actual class" do diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index d3abb4ca7ff2..18eb0de110e8 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -417,7 +417,7 @@ module Spec fail expectation_failed_message, file, line end when {String, String} - unless ex.message == message + unless ex.message.as(String).includes?(message) expectation_failed_message = build_expectation_failed_message(klass, message, ex) fail expectation_failed_message, file, line end From d445a92899ec5b83e3f71831a1c7f2ec24fbe7ae Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 19:42:56 +0300 Subject: [PATCH 04/10] Extract backtrace formatting into a helper method --- src/spec/expectations.cr | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index 18eb0de110e8..ed664dc95023 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -434,7 +434,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, message : String, exception : Exception) - backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + backtrace = format_backtrace(exception.backtrace) <<-MESSAGE Expected #{klass} with message containing: #{message.inspect} @@ -445,7 +445,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception) - backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + backtrace = format_backtrace(exception.backtrace) <<-MESSAGE Expected #{klass} with message matching: #{message.inspect} @@ -456,7 +456,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, message : Nil, exception : Exception) - backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + backtrace = format_backtrace(exception.backtrace) <<-MESSAGE Expected #{klass} with any message @@ -467,7 +467,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, exception : Exception) - backtrace = exception.backtrace.join('\n') { |f| " # #{f}" } + backtrace = format_backtrace(exception.backtrace) <<-MESSAGE Expected #{klass} @@ -476,6 +476,10 @@ module Spec #{backtrace} MESSAGE end + + def format_backtrace(backtrace : Array(String)) + backtrace.join('\n') { |f| " # #{f}" } + end {% end %} end From d6c864d76df980058fcacf05b3df1b0487773f89 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 20:05:32 +0300 Subject: [PATCH 05/10] Fail when actual message is nil and given non-nil expected message --- spec/std/spec/expectations_spec.cr | 32 ++++++++++++++++++++++++++++++ src/spec/expectations.cr | 4 ++++ 2 files changed, 36 insertions(+) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index 3f94fc190003..c9bc6ee2a380 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -201,6 +201,14 @@ describe "expectations" do expect_raises(Exception) { raise ArgumentError.new("Ops") } end + it "passes if given no message expectation, actual message is nil and expected class equals actual class" do + expect_raises(Exception) { raise Exception.new(nil) } + end + + it "passes if given no message expectation, actual message is nil and expected class is an ancestor of actual class" do + expect_raises(Exception) { raise ArgumentError.new(nil) } + end + it "fails if expected message does not equal actual message and expected class equals actual class" do expect_raises(Exception, "Ops") { raise Exception.new("Hm") } rescue Spec::AssertionFailed @@ -209,6 +217,14 @@ describe "expectations" do raise "expected Spec::AssertionFailed but nothing was raised" end + it "fails if given expected message, actual message is nil and expected class equals actual class" do + expect_raises(Exception, "Ops") { raise Exception.new(nil) } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + it "fails if expected regex does not match actual message and expected class equals actual class" do expect_raises(Exception, /Ops/) { raise Exception.new("Hm") } rescue Spec::AssertionFailed @@ -217,6 +233,14 @@ describe "expectations" do raise "expected Spec::AssertionFailed but nothing was raised" end + it "fails if given expected regex, actual message is nil and expected class equals actual class" do + expect_raises(Exception, /Ops/) { raise Exception.new(nil) } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + it "fails if given no message expectation and expected class does not equal and is not an ancestor of actual class" do expect_raises(IndexError) { raise ArgumentError.new("Ops") } rescue Spec::AssertionFailed @@ -225,6 +249,14 @@ describe "expectations" do raise "expected Spec::AssertionFailed but nothing was raised" end + it "fails if given no message expectation, actual message is nil and expected class does not equal and is not an ancestor of actual class" do + expect_raises(IndexError) { raise ArgumentError.new(nil) } + rescue Spec::AssertionFailed + # success + else + raise "expected Spec::AssertionFailed but nothing was raised" + end + it "fails if nothing was raised" do expect_raises(IndexError) { raise ArgumentError.new("Ops") } rescue Spec::AssertionFailed diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index ed664dc95023..d98460f23a85 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -423,6 +423,10 @@ module Spec end when {_, Nil} # No need to check the message + else + # actual message is nil + expectation_failed_message = build_expectation_failed_message(klass, message, ex) + fail expectation_failed_message, file, line end ex From 434e6e8bb582d7d1bb6cea03dab1f8263ff24e1b Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 21:38:51 +0300 Subject: [PATCH 06/10] Don't check backtrace items in specs --- spec/std/spec/expectations_spec.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index c9bc6ee2a380..73c8a0fbf7c3 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -272,11 +272,11 @@ describe "expectations" do raise IndexError.new("Index out of bounds") end rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific e.message.as(String).should contain(<<-MESSAGE) Expected Exception with message containing: "digits should be non-negative" got IndexError with message: "Index out of bounds" Backtrace: - # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' MESSAGE else raise "nothing is raised" @@ -287,11 +287,11 @@ describe "expectations" do raise IndexError.new("Index out of bounds") end rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific e.message.as(String).should contain(<<-MESSAGE) Expected ArgumentError got IndexError with message: "Index out of bounds" Backtrace: - # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' MESSAGE else raise "nothing is raised" @@ -315,11 +315,11 @@ describe "expectations" do raise IndexError.new("Index out of bounds") end rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific e.message.as(String).should contain(<<-MESSAGE) Expected Exception with message matching: /digits should be non-negative/ got IndexError with message: "Index out of bounds" Backtrace: - # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' MESSAGE else raise "nothing is raised" @@ -330,11 +330,11 @@ describe "expectations" do raise IndexError.new("Index out of bounds") end rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific e.message.as(String).should contain(<<-MESSAGE) Expected ArgumentError got IndexError with message: "Index out of bounds" Backtrace: - # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' MESSAGE else raise "nothing is raised" @@ -347,11 +347,11 @@ describe "expectations" do raise IndexError.new("Index out of bounds") end rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific e.message.as(String).should contain(<<-MESSAGE) Expected ArgumentError got IndexError with message: "Index out of bounds" Backtrace: - # spec/std/spec/expectations_spec.cr:#{__LINE__ - 7}:13 in '->' MESSAGE else raise "nothing is raised" From 5e49b716a7e1808e20d205a438c55a5ceb82a5a7 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 23:12:59 +0300 Subject: [PATCH 07/10] Simplified backtrace formatting and inline it back --- src/spec/expectations.cr | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index d98460f23a85..c987e1bc5669 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -438,7 +438,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, message : String, exception : Exception) - backtrace = format_backtrace(exception.backtrace) + backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with message containing: #{message.inspect} @@ -449,7 +449,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception) - backtrace = format_backtrace(exception.backtrace) + backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with message matching: #{message.inspect} @@ -460,7 +460,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, message : Nil, exception : Exception) - backtrace = format_backtrace(exception.backtrace) + backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with any message @@ -471,7 +471,7 @@ module Spec end private def build_expectation_failed_message(klass : Class, exception : Exception) - backtrace = format_backtrace(exception.backtrace) + backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} @@ -480,10 +480,6 @@ module Spec #{backtrace} MESSAGE end - - def format_backtrace(backtrace : Array(String)) - backtrace.join('\n') { |f| " # #{f}" } - end {% end %} end From b0756ece3cacabb36b462761b870d3fa0ff25b5a Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 24 Oct 2025 23:17:19 +0300 Subject: [PATCH 08/10] Use fail instead of raise to fail a test --- spec/std/spec/expectations_spec.cr | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index 73c8a0fbf7c3..754b02d808ab 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -214,7 +214,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end it "fails if given expected message, actual message is nil and expected class equals actual class" do @@ -222,7 +222,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end it "fails if expected regex does not match actual message and expected class equals actual class" do @@ -230,7 +230,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end it "fails if given expected regex, actual message is nil and expected class equals actual class" do @@ -238,7 +238,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end it "fails if given no message expectation and expected class does not equal and is not an ancestor of actual class" do @@ -246,7 +246,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end it "fails if given no message expectation, actual message is nil and expected class does not equal and is not an ancestor of actual class" do @@ -254,7 +254,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end it "fails if nothing was raised" do @@ -262,7 +262,7 @@ describe "expectations" do rescue Spec::AssertionFailed # success else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end describe "failure message format" do @@ -279,7 +279,7 @@ describe "expectations" do Backtrace: MESSAGE else - raise "nothing is raised" + fail "expected Spec::AssertionFailed but nothing is raised" end it "contains expected class, actual exception and backtrace when expected class does not match actual class" do @@ -294,7 +294,7 @@ describe "expectations" do Backtrace: MESSAGE else - raise "nothing is raised" + fail "expected Spec::AssertionFailed but nothing is raised" end it "escapes expected and actual messages in the same way" do @@ -305,7 +305,7 @@ describe "expectations" do e.message.as(String).should contain("Expected Exception with message containing: #{%q(a\tb\nc).inspect}") e.message.as(String).should contain("got Exception with message: #{%q(a\tb\nc).inspect.inspect}") else - raise "nothing is raised" + fail "expected Spec::AssertionFailed but nothing is raised" end end @@ -322,7 +322,7 @@ describe "expectations" do Backtrace: MESSAGE else - raise "nothing is raised" + fail "expected Spec::AssertionFailed but nothing is raised" end it "contains expected class, actual exception and backtrace when expected class does not match actual class" do @@ -337,7 +337,7 @@ describe "expectations" do Backtrace: MESSAGE else - raise "nothing is raised" + fail "expected Spec::AssertionFailed but nothing is raised" end end @@ -354,7 +354,7 @@ describe "expectations" do Backtrace: MESSAGE else - raise "nothing is raised" + fail "expected Spec::AssertionFailed but nothing is raised" end end @@ -364,7 +364,7 @@ describe "expectations" do rescue e : Spec::AssertionFailed e.message.as(String).should contain("Expected IndexError but nothing was raised") else - raise "expected Spec::AssertionFailed but nothing was raised" + fail "expected Spec::AssertionFailed but nothing was raised" end end end From 853f2d9db13a5274271126507dc545762333c2e7 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Sat, 25 Oct 2025 13:22:02 +0300 Subject: [PATCH 09/10] Rollback #to_s semantic --- src/spec/expectations.cr | 43 ++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index c987e1bc5669..79cad0680417 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -410,23 +410,20 @@ module Spec raise ex end - case {ex.message, message} - when {String, Regex} - unless ex.message =~ message - expectation_failed_message = build_expectation_failed_message(klass, message, ex) + exception_as_string = ex.to_s + case message + when Regex + unless exception_as_string =~ message + expectation_failed_message = build_expectation_failed_message(klass, message, ex, exception_as_string) fail expectation_failed_message, file, line end - when {String, String} - unless ex.message.as(String).includes?(message) - expectation_failed_message = build_expectation_failed_message(klass, message, ex) + when String + unless exception_as_string.includes?(message) + expectation_failed_message = build_expectation_failed_message(klass, message, ex, exception_as_string) fail expectation_failed_message, file, line end - when {_, Nil} + when Nil # No need to check the message - else - # actual message is nil - expectation_failed_message = build_expectation_failed_message(klass, message, ex) - fail expectation_failed_message, file, line end ex @@ -437,45 +434,35 @@ module Spec fail "Expected #{klass} but nothing was raised", file, line end - private def build_expectation_failed_message(klass : Class, message : String, exception : Exception) + private def build_expectation_failed_message(klass : Class, message : String, exception : Exception, exception_as_string : String) backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with message containing: #{message.inspect} - got #{exception.class} with message: #{exception.message.inspect} + got #{exception.class} with message: #{exception_as_string.inspect} Backtrace: #{backtrace} MESSAGE end - private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception) + private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception, exception_as_string : String) backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} with message matching: #{message.inspect} - got #{exception.class} with message: #{exception.message.inspect} - Backtrace: - #{backtrace} - MESSAGE - end - - private def build_expectation_failed_message(klass : Class, message : Nil, exception : Exception) - backtrace = " # #{exception.backtrace.join("\n # ")}" - - <<-MESSAGE - Expected #{klass} with any message - got #{exception.class} with message: #{exception.message.inspect} + got #{exception.class} with message: #{exception_as_string.inspect} Backtrace: #{backtrace} MESSAGE end private def build_expectation_failed_message(klass : Class, exception : Exception) + exception_as_string = exception.to_s backtrace = " # #{exception.backtrace.join("\n # ")}" <<-MESSAGE Expected #{klass} - got #{exception.class} with message: #{exception.message.inspect} + got #{exception.class} with message: #{exception_as_string.inspect} Backtrace: #{backtrace} MESSAGE From 32c8f7bb80ed0e013f1da5a974dfe07aaa3a034f Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Sat, 25 Oct 2025 14:02:50 +0300 Subject: [PATCH 10/10] Add specs for the #to_s semantic --- spec/std/spec/expectations_spec.cr | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index 754b02d808ab..2e7e9d8df57f 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -12,6 +12,16 @@ private record NoObjectId, to_unsafe : Int32 do end end +private class ExceptionWithOverriddenToS < Exception + def initialize(message : String, @to_s : String) + super(message) + end + + def to_s + @to_s + end +end + describe "expectations" do describe "accept a custom failure message" do it { 1.should be < 3, "custom message!" } @@ -265,6 +275,14 @@ describe "expectations" do fail "expected Spec::AssertionFailed but nothing was raised" end + it "uses the exception's #to_s output to match a given String" do + expect_raises(Exception, "Hm") { raise ExceptionWithOverriddenToS.new("Ops", to_s: "Hm") } + end + + it "uses the exception's #to_s output to match a given Regex" do + expect_raises(Exception, /Hm/) { raise ExceptionWithOverriddenToS.new("Ops", to_s: "Hm") } + end + describe "failure message format" do context "given string to compare with message" do it "contains expected exception, actual exception and backtrace" do