Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 89c6471

Browse files
author
mrageh
committed
Include exception that raised failure in backtrace
Problem Sometimes rspec-core does not display the underlying cause of an exception that was raised. Previously if an exception was raised there would sometimes be an underlying cause of the exception. For example an exception that's raised and rescued within an application causes the exception that is displayed in the stacktrace. ``` class BrokenCode def self.shallow_method begin deep_method rescue raise "Something happened and I'm hiding that from the message 'because usability'." end end def self.deep_method raise 'The real cause for the failure is here!' end end RSpec.describe 'some broken code' do it 'works' do BrokenCode.shallow_method end end ``` ``` 1) some broken code works Failure/Error: raise "Something happened and I'm hiding that from the message 'because usability'." RuntimeError: Something happened and I'm hiding that from the message 'because usability'. # ./my_spec.rb:32:in `rescue in shallow_method' # ./my_spec.rb:29:in `shallow_method' # ./my_spec.rb:43:in `block (2 levels) in <top (required)>' ``` The above example demonstrates the problem, the stacktrace above is not very clear as it does not show the initial exception that caused the problem. This makes it difficult to fix their failing test. Solution Ruby 2.1 introduced `Exception#cause` which shows the underlying cause of an exception if there is one. This commit uses that new Ruby method to get the cause of an exception and display it in the stacktrace. The stacktrace for the above code example would change to the below example: ``` 1) some broken code works Failure/Error: raise "Something happened and I'm hiding that from the message 'because usability'." RuntimeError: Something happened and I'm hiding that from the message 'because usability'. # ./my_spec.rb:32:in `rescue in shallow_method' # ./my_spec.rb:29:in `shallow_method' # ./my_spec.rb:43:in `block (2 levels) in <top (required)>' ------------------ --- Caused by: --- RuntimeError: The real cause for the failure is here! # ./my_spec.rb:37:in `deep_method' # ./my_spec.rb:30:in `shallow_method' # ./my_spec.rb:43:in `block (2 levels) in <top (required)>' ``` This makes it a lot easier to fix the main cause of the test failure. Edge case There may be a situation where lots of exceptions are captured and bubble up the stacktrace. In that case I don't think we want to print out the cause of every single exception. Instead what I propose we do is to print out the first exception that caused the other exceptions to bubble up the stack trace.
1 parent 1a9b414 commit 89c6471

File tree

3 files changed

+79
-14
lines changed

3 files changed

+79
-14
lines changed

lib/rspec/core/formatters/exception_presenter.rb

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,36 @@ def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
3131
end
3232
end
3333

34-
def formatted_backtrace
35-
backtrace_formatter.format_backtrace(exception_backtrace, example.metadata)
34+
def formatted_backtrace(exception=@exception)
35+
backtrace_formatter.format_backtrace((exception.backtrace || []), example.metadata) +
36+
formatted_cause(exception)
37+
end
38+
39+
if RSpec::Support::RubyFeatures.supports_exception_cause?
40+
def formatted_cause(exception)
41+
last_cause = final_exception(exception)
42+
cause = []
43+
44+
if exception.cause
45+
cause << '------------------'
46+
cause << '--- Caused by: ---'
47+
cause << "#{exception_class_name(last_cause)}:" unless exception_class_name(last_cause) =~ /RSpec/
48+
49+
encoded_string(last_cause.message.to_s).split("\n").each do |line|
50+
cause << " #{line}"
51+
end
52+
53+
cause << (" #{backtrace_formatter.format_backtrace(last_cause.backtrace, example.metadata).first}")
54+
end
55+
56+
cause
57+
end
58+
else
59+
# :nocov:
60+
def formatted_cause(_)
61+
[]
62+
end
63+
# :nocov:
3664
end
3765

3866
def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
@@ -56,6 +84,14 @@ def failure_slash_error_line
5684

5785
private
5886

87+
def final_exception(exception)
88+
if exception.cause
89+
final_exception(exception.cause)
90+
else
91+
exception
92+
end
93+
end
94+
5995
def description_and_detail(colorizer, indentation)
6096
detail = detail_formatter.call(example, colorizer, indentation)
6197
return (description || detail) unless description && detail
@@ -81,7 +117,7 @@ def encoded_string(string)
81117
# :nocov:
82118
end
83119

84-
def exception_class_name
120+
def exception_class_name(exception=@exception)
85121
name = exception.class.name.to_s
86122
name = "(anonymous error class)" if name == ''
87123
name

spec/rspec/core/formatters/exception_presenter_spec.rb

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module RSpec::Core
1111
before do
1212
allow(example.execution_result).to receive(:exception) { exception }
1313
example.metadata[:absolute_file_path] = __FILE__
14+
allow(exception).to receive(:cause) if RSpec::Support::RubyFeatures.supports_exception_cause?
1415
end
1516

1617
describe "#fully_formatted" do
@@ -49,9 +50,9 @@ module RSpec::Core
4950
end
5051

5152
it "allows the caller to specify additional indentation" do
52-
presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4)
53+
the_presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4)
5354

54-
expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
55+
expect(the_presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
5556
|
5657
| 1) Example
5758
| Failure/Error: # The failure happened here!#{ encoding_check }
@@ -64,9 +65,9 @@ module RSpec::Core
6465
it 'passes the indentation on to the `:detail_formatter` lambda so it can align things' do
6566
detail_formatter = Proc.new { "Some Detail" }
6667

67-
presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4,
68+
the_presenter = Formatters::ExceptionPresenter.new(exception, example, :indentation => 4,
6869
:detail_formatter => detail_formatter)
69-
expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
70+
expect(the_presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
7071
|
7172
| 1) Example
7273
| Some Detail
@@ -78,11 +79,11 @@ module RSpec::Core
7879
end
7980

8081
it 'allows the caller to omit the description' do
81-
presenter = Formatters::ExceptionPresenter.new(exception, example,
82+
the_presenter = Formatters::ExceptionPresenter.new(exception, example,
8283
:detail_formatter => Proc.new { "Detail!" },
8384
:description_formatter => Proc.new { })
8485

85-
expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
86+
expect(the_presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
8687
|
8788
| 1) Detail!
8889
| Failure/Error: # The failure happened here!#{ encoding_check }
@@ -93,9 +94,9 @@ module RSpec::Core
9394
end
9495

9596
it 'allows the failure/error line to be used as the description' do
96-
presenter = Formatters::ExceptionPresenter.new(exception, example, :description_formatter => lambda { |p| p.failure_slash_error_line })
97+
the_presenter = Formatters::ExceptionPresenter.new(exception, example, :description_formatter => lambda { |p| p.failure_slash_error_line })
9798

98-
expect(presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
99+
expect(the_presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
99100
|
100101
| 1) Failure/Error: # The failure happened here!#{ encoding_check }
101102
| Boom
@@ -105,13 +106,13 @@ module RSpec::Core
105106
end
106107

107108
it 'allows a caller to specify extra details that are added to the bottom' do
108-
presenter = Formatters::ExceptionPresenter.new(
109+
the_presenter = Formatters::ExceptionPresenter.new(
109110
exception, example, :extra_detail_formatter => lambda do |failure_number, colorizer, indentation|
110111
"#{indentation}extra detail for failure: #{failure_number}\n"
111112
end
112113
)
113114

114-
expect(presenter.fully_formatted(2)).to eq(<<-EOS.gsub(/^ +\|/, ''))
115+
expect(the_presenter.fully_formatted(2)).to eq(<<-EOS.gsub(/^ +\|/, ''))
115116
|
116117
| 2) Example
117118
| Failure/Error: # The failure happened here!#{ encoding_check }
@@ -121,8 +122,35 @@ module RSpec::Core
121122
| extra detail for failure: 2
122123
EOS
123124
end
124-
end
125125

126+
let(:the_exception) { instance_double(Exception, :cause => second_exception, :message => "Boom\nBam", :backtrace => [ "#{__FILE__}:#{line_num}"]) }
127+
128+
let(:second_exception) do
129+
instance_double(Exception, :cause => first_exception, :message => "Second\nexception", :backtrace => ["#{__FILE__}:#{__LINE__}"])
130+
end
131+
132+
let(:first_exception) do
133+
instance_double(Exception, :cause => nil, :message => "Real\nculprit", :backtrace => ["#{__FILE__}:#{__LINE__}"])
134+
end
135+
136+
it 'includes the first exception that caused the failure', :if => RSpec::Support::RubyFeatures.supports_exception_cause? do
137+
the_presenter = Formatters::ExceptionPresenter.new(the_exception, example)
138+
139+
expect(the_presenter.fully_formatted(1)).to eq(<<-EOS.gsub(/^ +\|/, ''))
140+
|
141+
| 1) Example
142+
| Failure/Error: # The failure happened here!#{ encoding_check }
143+
| Boom
144+
| Bam
145+
| # ./spec/rspec/core/formatters/exception_presenter_spec.rb:#{line_num}
146+
| # ------------------
147+
| # --- Caused by: ---
148+
| # Real
149+
| # culprit
150+
| # ./spec/rspec/core/formatters/exception_presenter_spec.rb:133
151+
EOS
152+
end
153+
end
126154
describe "#read_failed_line" do
127155
def read_failed_line
128156
presenter.send(:read_failed_line)

spec/rspec/core/notifications_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
end
1919

2020
it 'provides `colorized_formatted_backtrace`, which formats the backtrace and colorizes it' do
21+
allow(exception).to receive(:cause) if RSpec::Support::RubyFeatures.supports_exception_cause?
2122
allow(RSpec.configuration).to receive(:color_enabled?).and_return(true)
2223
expect(notification.colorized_formatted_backtrace).to eq(["\e[36m# #{RSpec::Core::Metadata.relative_path(__FILE__)}:#{exception_line}\e[0m"])
2324
end

0 commit comments

Comments
 (0)