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

Option for "Caused by" backtraces to print more than just the first line #3027

Closed
mpalmer opened this issue Apr 6, 2023 · 3 comments
Closed

Comments

@mpalmer
Copy link

mpalmer commented Apr 6, 2023

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 (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?
  • 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?

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.

@JonRowe
Copy link
Member

JonRowe commented Apr 11, 2023

👋 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"...

@davidtaylorhq
Copy link
Contributor

davidtaylorhq commented Feb 6, 2024

This was resolved by #3046, and was released in v3.13.0

@JonRowe JonRowe closed this as completed Feb 6, 2024
@JonRowe
Copy link
Member

JonRowe commented Feb 6, 2024

Thank you for the reminder 😹

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants