Option for "Caused by" backtraces to print more than just the first line
mpalmer opened this issue ยท 3 comments
Subject of the issue
When debugging a failed spec where the proximate exception is caused by another exception, I've found that it is very rarely the case that the single line of the causing exception backtrace is sufficient to determine the fault. RSpec prints the backtrace of the proximate exception with all (relevant) lines, but the causing exception backtrace is hard-coded to only present the first line of the causing exception's backtrace.
It would be really handy (for me, at any rate) if there were an option to turn on full(er) backtraces for causing exceptions. Personally, I'd be fine if it were tied to the --backtrace CLI option (aka the RSpec.configure full_backtrace option), because I'm willing to wade through long backtraces if necessary, but I can imagine it'd be a better UX if new config options were introduced to control the degree to which the causing exception backtrace is truncated.
I'd also be quite happy if the default were changed, although I assume that the current behaviour (which dates back to the initial introduction of the feature) was done for a good reason. Interestingly, the example provided in the commit message for the original change shows a full backtrace for the causing exception, even though the code behaves differently. ๐ค
I'm more than happy to whip up a PR if someone would like to give guidance on the various undecided questions raised:
- Should the default behaviour change at all?
- Should the behaviour changes be gated behind new config options, or just use
full_backtrace? If the former, should there be one option (sayconfig.cause_backtrace = [:oneline|:regular|:full]) or multiple options (config.cause_backtrace_oneline = true/false,config.full_cause_backtrace = true/false), or something else? - Should these options be exposed to the CLI, or is just supporting it in
RSpec.configuresufficient for something that is, by the look of it, a very niche feature?
Your environment
- Ruby version: 3.2.1
- rspec-core version: 3.12.1
Steps to reproduce
This script produces a nested exception with a multi-level causing-exception backtrace:
# frozen_string_literal: true
begin
require "bundler/inline"
rescue LoadError => e
$stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
raise e
end
gemfile(true) do
source "https://rubygems.org"
gem "rspec", "3.12.0"
end
puts "Ruby version is: #{RUBY_VERSION}"
require 'rspec/autorun'
def foo
bar
end
def bar
baz
end
def baz
raise RuntimeError
end
def nest
begin
foo
rescue
raise ArgumentError
end
end
RSpec.describe 'nested exceptions' do
it 'assplodes' do
nest
end
end
Expected behavior
This is the output of a lightly-mangled rspec-core that at least prints out a slightly fuller causing backtrace:
Ruby version is: 3.2.1
F
Failures:
1) nested exceptions assplodes
Failure/Error: raise ArgumentError
ArgumentError:
ArgumentError
# t.rb:35:in `rescue in nest'
# t.rb:31:in `nest'
# t.rb:41:in `block (2 levels) in <main>'
# ------------------
# --- Caused by: ---
# RuntimeError:
# RuntimeError
# t.rb:28:in `baz'
t.rb:24:in `bar'
t.rb:20:in `foo'
t.rb:33:in `nest'
t.rb:41:in `block (2 levels) in <main>'
Finished in 0.00333 seconds (files took 0.12685 seconds to load)
1 example, 1 failure
Failed examples:
rspec t.rb:40 # nested exceptions assplodes
The formatting could be (a lot) better, but it gives me enough information to be able to determine how the exception-raising function baz was called, which is often relevant information when I'm debugging.
Actual behavior
This is the output of the repro script when run against a stock rspec-core-3.12.1:
Ruby version is: 3.2.1
F
Failures:
1) nested exceptions assplodes
Failure/Error: raise ArgumentError
ArgumentError:
ArgumentError
# t.rb:35:in `rescue in nest'
# t.rb:31:in `nest'
# t.rb:41:in `block (2 levels) in <main>'
# ------------------
# --- Caused by: ---
# RuntimeError:
# RuntimeError
# t.rb:28:in `baz'
Finished in 0.00296 seconds (files took 0.13643 seconds to load)
1 example, 1 failure
Failed examples:
rspec t.rb:40 # nested exceptions assplodes
If the exception raised in baz were somehow dependent on how it was called (which, let's face it, is pretty likely), the lack of backtrace makes it very hard to figure out what's going on.
๐ I'd happily look at PR to improve this, the original behaviour is largely because when it was introduced
Should the default behaviour change at all?
No (with the caveat of I'm on the fence as to whether --backtrace should trigger this new behaviour).
Should the behaviour changes be gated behind new config options, or just use full_backtrace? If the former, should there be one option (say config.cause_backtrace = [:oneline|:regular|:full]) or multiple options (config.cause_backtrace_oneline = true/false, config.full_cause_backtrace = true/false), or something else?
I like the idea of this being a seperate config, what would the three options do? I'm happy for this to be a single "full_cause_backtrace = true/false" or "cause_backtrace = [:oneline|:full]"
Should these options be exposed to the CLI, or is just supporting it in RSpec.configure sufficient for something that is, by the look of it, a very niche feature?
I'd support making the backtrace option take options on the cli e.g. --backtrace=cause is full cause backtrace only, --backtrace=all is max for both, I don't know what to call the current behaviour and I'm on the fence if --backtrace alone should set both to full? Sort of seems like "yes"...
This was resolved by #3046, and was released in v3.13.0
Thank you for the reminder ๐น