Skip to content
This repository

Include backtrace in errors reported by `raise_error` matcher. #177

Merged
merged 1 commit into from almost 2 years ago

3 participants

Myron Marston David Chelimsky Andy Lindeman
Myron Marston
Owner

It's hard to troubleshoot unexpected errors when the backtrace is silenced,
as it was previously.

Closes #59.

Myron Marston
Owner

Note this depends on rspec/rspec-core#701.

@alindeman / @dchelimsky -- can you review?

lib/rspec/matchers/configuration.rb
((6 lines not shown))
  69 + # implement `#format_backtrace(Array<String>)`. This is used
  70 + # to format backtraces of errors handled by the `raise_error`
  71 + # matcher.
  72 + #
  73 + # If you are using rspec-core, rspec-core's backtrace formatting
  74 + # will be used (including respecting the presence or absence of
  75 + # the `--backtrace` option).
  76 + #
  77 + # @overload backtrace_formatter
  78 + # @return [#format_backtrace] the backtrace formatter
  79 + # @overload backtrace_formatter=
  80 + # @param value [#format_backtrace] sets the backtrace formatter
  81 + attr_writer :backtrace_formatter
  82 + def backtrace_formatter
  83 + @backtrace_formatter ||= if defined?(::RSpec::Core::Formatters::Helpers)
  84 + ::RSpec::Core::Formatters::Helpers
2
David Chelimsky Owner

I'd rather expose this more explicitly in RSpec::Core - something like RSpec::Core::BacktraceFormatters. WDYT?

Myron Marston Owner

Agreed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
spec/rspec/matchers/raise_error_spec.rb
... ... @@ -51,6 +51,27 @@
51 51 lambda { raise RuntimeError, "example message" }.should_not raise_error
52 52 }.should fail_with(/expected no Exception, got #<RuntimeError: example message>/)
53 53 end
  54 +
  55 + it 'includes the backtrace of the error that was raised in the error message' do
  56 + expect {
  57 + expect { raise "boom" }.not_to raise_error
  58 + }.to raise_error { |e|
  59 + backtrace_line = "#{File.basename(__FILE__)}:#{__LINE__ - 2}"
  60 + e.message.should include("with backtrace", backtrace_line)
  61 + }
  62 + end
  63 +
  64 + it 'formats the backtrace using the configured backtrace formatter' do
  65 + RSpec::Matchers.configuration.backtrace_formatter.
  66 + should_receive(:format_backtrace).
3
David Chelimsky Owner

I prefer to reserve message expectations for cases where any side effects or state changes happen upstream. In this example, a stub would tell the story just as well since the literal "formatted-backtrace" appears twice in the example. Subtle, and subjective, I realize. Don't change it if you feel strongly about it.

Myron Marston Owner

I'm on the fence about this...mocks are best used with commands, not queries (and this is a query), but given the focus of the example is on "using the configured backtrace formatter", should_receive seemed appropriate here. The return value still needed to be used because format_backtrace could be called and then the matcher could throw the return value away and not use it.

But you're right, the use of the return value (if it was just a stub) demonstrates it is used just fine.

@alindeman -- do you have a preference?

Andy Lindeman Owner

I'd probably personally use a stub here, for the same reasons as you and @dchelimsky mention. That said, I don't feel strongly about it in this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
spec/rspec/matchers/configuration_spec.rb
((7 lines not shown))
  19 +
  20 + let(:formatted_backtrace) do
  21 + config.backtrace_formatter.format_backtrace(original_backtrace)
  22 + end
  23 +
  24 + before do
  25 + RSpec.configuration.stub(:backtrace_clean_patterns) { [/clean-me/] }
  26 + end
  27 +
  28 + it "defaults to rspec-core's backtrace formatter when rspec-core is loaded" do
  29 + expect(config.backtrace_formatter).to be(RSpec::Core::Formatters::Helpers)
  30 + expect(formatted_backtrace).to eq(cleaned_backtrace)
  31 + end
  32 +
  33 + it "defaults to a null formatter when rspec-core is not loaded" do
  34 + stub_const("RSpec::Core::Formatters", nil) # so the formatter module is not loaded
2
David Chelimsky Owner

Nice example of stub_const. It's a big hammer, but this is a perfect case for it!

Myron Marston Owner

Actually, this would be a perfect case for rspec/rspec-mocks#183, but we haven't implemented that yet, and this works OK here :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Myron Marston
Owner

One other thing to be fixed here:

expect {
  blah
}.to raise_error { |e|
  expect(e.some_attribute).to eq(some_value)
}

In a case like this, if the expectation in the error-matching block fails, the backtrace is not being included in the failure. It'd be cool to find a solution for this (thought it might be complex, in which case I might skip it).

Myron Marston myronmarston Include backtrace in errors reported by `raise_error` matcher.
It's hard to troubleshoot unexpected errors when the backtrace is silenced,
as it was previously.

Closes #59.
4a919ab
Myron Marston myronmarston merged commit 16a133f into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Oct 09, 2012
Myron Marston myronmarston Include backtrace in errors reported by `raise_error` matcher.
It's hard to troubleshoot unexpected errors when the backtrace is silenced,
as it was previously.

Closes #59.
4a919ab
This page is out of date. Refresh to see the latest.
2  Changelog.md
Source Rendered
@@ -4,6 +4,8 @@
4 4 Enhancements
5 5
6 6 * Colorize diffs if the `--color` option is configured. (Alex Coplan)
  7 +* Include backtraces in unexpected errors handled by `raise_error`
  8 + matcher (Myron Marston)
7 9
8 10 Bug fixes
9 11
13 lib/rspec/matchers/built_in/raise_error.rb
@@ -87,8 +87,19 @@ def expected_error
87 87 end
88 88 end
89 89
  90 + def format_backtrace(backtrace)
  91 + formatter = Matchers.configuration.backtrace_formatter
  92 + formatter.format_backtrace(backtrace)
  93 + end
  94 +
90 95 def given_error
91   - @actual_error.nil? ? " but nothing was raised" : ", got #{@actual_error.inspect}"
  96 + return " but nothing was raised" unless @actual_error
  97 +
  98 + backtrace = format_backtrace(@actual_error.backtrace)
  99 + [
  100 + ", got #{@actual_error.inspect} with backtrace:",
  101 + *backtrace
  102 + ].join("\n # ")
92 103 end
93 104 end
94 105 end
29 lib/rspec/matchers/configuration.rb
@@ -64,6 +64,35 @@ def add_should_and_should_not_to(*modules)
64 64 Expectations::Syntax.enable_should(mod)
65 65 end
66 66 end
  67 +
  68 + # Sets or gets the backtrace formatter. The backtrace formatter should
  69 + # implement `#format_backtrace(Array<String>)`. This is used
  70 + # to format backtraces of errors handled by the `raise_error`
  71 + # matcher.
  72 + #
  73 + # If you are using rspec-core, rspec-core's backtrace formatting
  74 + # will be used (including respecting the presence or absence of
  75 + # the `--backtrace` option).
  76 + #
  77 + # @overload backtrace_formatter
  78 + # @return [#format_backtrace] the backtrace formatter
  79 + # @overload backtrace_formatter=
  80 + # @param value [#format_backtrace] sets the backtrace formatter
  81 + attr_writer :backtrace_formatter
  82 + def backtrace_formatter
  83 + @backtrace_formatter ||= if defined?(::RSpec::Core::BacktraceFormatter)
  84 + ::RSpec::Core::BacktraceFormatter
  85 + else
  86 + NullBacktraceFormatter
  87 + end
  88 + end
  89 +
  90 + # @api private
  91 + NullBacktraceFormatter = Module.new do
  92 + def self.format_backtrace(backtrace)
  93 + backtrace
  94 + end
  95 + end
67 96 end
68 97
69 98 # The configuration object
28 spec/rspec/matchers/configuration_spec.rb
@@ -13,6 +13,34 @@ module Matchers
13 13 describe Configuration do
14 14 let(:config) { Configuration.new }
15 15
  16 + describe "#backtrace_formatter" do
  17 + let(:original_backtrace) { %w[ clean-me/a.rb other/file.rb clean-me/b.rb ] }
  18 + let(:cleaned_backtrace) { %w[ other/file.rb ] }
  19 +
  20 + let(:formatted_backtrace) do
  21 + config.backtrace_formatter.format_backtrace(original_backtrace)
  22 + end
  23 +
  24 + before do
  25 + RSpec.configuration.stub(:backtrace_clean_patterns) { [/clean-me/] }
  26 + end
  27 +
  28 + it "defaults to rspec-core's backtrace formatter when rspec-core is loaded" do
  29 + expect(config.backtrace_formatter).to be(RSpec::Core::BacktraceFormatter)
  30 + expect(formatted_backtrace).to eq(cleaned_backtrace)
  31 + end
  32 +
  33 + it "defaults to a null formatter when rspec-core is not loaded" do
  34 + stub_const("RSpec::Core", nil) # so the formatter module is not loaded
  35 + expect(formatted_backtrace).to eq(original_backtrace)
  36 + end
  37 +
  38 + it "can be set to another backtrace formatter" do
  39 + config.backtrace_formatter = stub(:format_backtrace => ['a'])
  40 + expect(formatted_backtrace).to eq(['a'])
  41 + end
  42 + end
  43 +
16 44 context 'on an interpreter that does not provide BasicObject', :unless => defined?(::BasicObject) do
17 45 before { RSpec::Expectations::Syntax.disable_should(Delegator) }
18 46
30 spec/rspec/matchers/raise_error_spec.rb
@@ -51,6 +51,27 @@
51 51 lambda { raise RuntimeError, "example message" }.should_not raise_error
52 52 }.should fail_with(/expected no Exception, got #<RuntimeError: example message>/)
53 53 end
  54 +
  55 + it 'includes the backtrace of the error that was raised in the error message' do
  56 + expect {
  57 + expect { raise "boom" }.not_to raise_error
  58 + }.to raise_error { |e|
  59 + backtrace_line = "#{File.basename(__FILE__)}:#{__LINE__ - 2}"
  60 + e.message.should include("with backtrace", backtrace_line)
  61 + }
  62 + end
  63 +
  64 + it 'formats the backtrace using the configured backtrace formatter' do
  65 + RSpec::Matchers.configuration.backtrace_formatter.
  66 + stub(:format_backtrace).
  67 + and_return("formatted-backtrace")
  68 +
  69 + expect {
  70 + expect { raise "boom" }.not_to raise_error
  71 + }.to raise_error { |e|
  72 + e.message.should include("with backtrace", "formatted-backtrace")
  73 + }
  74 + end
54 75 end
55 76
56 77 describe "should raise_error(message)" do
@@ -77,6 +98,15 @@
77 98 lambda {raise NameError.new('blarg')}.should raise_error('blah')
78 99 end.should fail_with(/expected Exception with \"blah\", got #<NameError: blarg>/)
79 100 end
  101 +
  102 + it 'includes the backtrace of any other error in the failure message' do
  103 + expect {
  104 + expect { raise "boom" }.to raise_error(ArgumentError)
  105 + }.to raise_error { |e|
  106 + backtrace_line = "#{File.basename(__FILE__)}:#{__LINE__ - 2}"
  107 + e.message.should include("with backtrace", backtrace_line)
  108 + }
  109 + end
80 110 end
81 111
82 112 describe "should_not raise_error(message)" do

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.