From 18bff165d17af8a5e3283227317d627b2e1f873e Mon Sep 17 00:00:00 2001 From: st0012 Date: Thu, 26 Feb 2026 13:00:56 +0000 Subject: [PATCH 1/2] Fix formatter crash on backtrace lines without file:line format When a test failure includes chained exceptions (Caused by), the formatted backtrace contains lines that don't follow the file:line format (e.g. empty strings, "Caused by:" headers). Splitting these by ":" produces fewer than 2 parts, causing a NoMethodError or TypeError in adjust_backtrace. Skip the file:// rewriting for backtrace lines that don't match the expected format, returning them unchanged. Fixes #93 Fixes #97 Co-Authored-By: Claude Opus 4.6 --- lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb | 2 ++ spec/fixtures/rspec_example_spec.rb | 6 ++++++ spec/rspec_formatter_spec.rb | 16 ++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb b/lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb index 959675d..cc8324b 100644 --- a/lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb +++ b/lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb @@ -80,6 +80,8 @@ def generate_id(example) def adjust_backtrace(backtrace) # Correct the backtrace entry so that vscode recognized it as a link to open parts = backtrace.split(":", 3) + return backtrace if parts.length < 2 + parts[0].sub(/^\./, "file://" + File.expand_path(".")) + ":" + parts[1] + " : " + (parts[2] || "") end end diff --git a/spec/fixtures/rspec_example_spec.rb b/spec/fixtures/rspec_example_spec.rb index 46b5e03..506a082 100644 --- a/spec/fixtures/rspec_example_spec.rb +++ b/spec/fixtures/rspec_example_spec.rb @@ -34,5 +34,11 @@ pending expect { raise "error" }.to raise_error end + + it "fails with a chained error" do + raise "secondary error" + rescue + raise "primary error" + end end end diff --git a/spec/rspec_formatter_spec.rb b/spec/rspec_formatter_spec.rb index 010986b..c1778a2 100644 --- a/spec/rspec_formatter_spec.rb +++ b/spec/rspec_formatter_spec.rb @@ -148,6 +148,22 @@ "uri" => "file://#{fixture_path}", }, }, + { + "method" => "start", + "params" => { + "id" => "./spec/fixtures/rspec_example_spec.rb:11::./spec/fixtures/rspec_example_spec.rb:12::./spec/fixtures/rspec_example_spec.rb:38", + "line" => "38", + "uri" => "file://#{fixture_path}", + }, + }, + { + "method" => "fail", + "params" => { + "id" => "./spec/fixtures/rspec_example_spec.rb:11::./spec/fixtures/rspec_example_spec.rb:12::./spec/fixtures/rspec_example_spec.rb:38", + "message" => %r{RuntimeError:\n primary error.*Caused by.*secondary error}m, + "uri" => "file://#{fixture_path}", + }, + }, { "method" => "finish", "params" => {} }, ] From 0ea2610362150736a51fa8b86e2f31a8ff5891e0 Mon Sep 17 00:00:00 2001 From: st0012 Date: Thu, 26 Feb 2026 13:16:15 +0000 Subject: [PATCH 2/2] Replace indirect event assertions with direct crash reproduction Use a dedicated chained_exception fixture and assert stderr does not contain adjust_backtrace crash output, directly reproducing #93. Co-Authored-By: Claude Opus 4.6 --- spec/fixtures/chained_exception_spec.rb | 15 +++++++++++++++ spec/fixtures/rspec_example_spec.rb | 6 ------ spec/rspec_formatter_spec.rb | 25 +++++++++---------------- 3 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 spec/fixtures/chained_exception_spec.rb diff --git a/spec/fixtures/chained_exception_spec.rb b/spec/fixtures/chained_exception_spec.rb new file mode 100644 index 0000000..1d22386 --- /dev/null +++ b/spec/fixtures/chained_exception_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "../../lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter" + +RSpec.configure do |config| + config.formatter = "RubyLsp::RSpec::RSpecFormatter" +end + +RSpec.describe "ChainedExceptionExample" do + it "fails with a chained error" do + raise "secondary error" + rescue + raise "primary error" + end +end diff --git a/spec/fixtures/rspec_example_spec.rb b/spec/fixtures/rspec_example_spec.rb index 506a082..46b5e03 100644 --- a/spec/fixtures/rspec_example_spec.rb +++ b/spec/fixtures/rspec_example_spec.rb @@ -34,11 +34,5 @@ pending expect { raise "error" }.to raise_error end - - it "fails with a chained error" do - raise "secondary error" - rescue - raise "primary error" - end end end diff --git a/spec/rspec_formatter_spec.rb b/spec/rspec_formatter_spec.rb index c1778a2..43cdbaa 100644 --- a/spec/rspec_formatter_spec.rb +++ b/spec/rspec_formatter_spec.rb @@ -148,28 +148,21 @@ "uri" => "file://#{fixture_path}", }, }, - { - "method" => "start", - "params" => { - "id" => "./spec/fixtures/rspec_example_spec.rb:11::./spec/fixtures/rspec_example_spec.rb:12::./spec/fixtures/rspec_example_spec.rb:38", - "line" => "38", - "uri" => "file://#{fixture_path}", - }, - }, - { - "method" => "fail", - "params" => { - "id" => "./spec/fixtures/rspec_example_spec.rb:11::./spec/fixtures/rspec_example_spec.rb:12::./spec/fixtures/rspec_example_spec.rb:38", - "message" => %r{RuntimeError:\n primary error.*Caused by.*secondary error}m, - "uri" => "file://#{fixture_path}", - }, - }, { "method" => "finish", "params" => {} }, ] expect(events).to match(expected) end + it "does not crash when formatting backtrace from chained exceptions" do + fixture_path = File.expand_path("spec/fixtures/chained_exception_spec.rb") + + stdout, stderr, = Open3.capture3("bundle", "exec", "rspec", fixture_path) + + output = stdout + stderr + expect(output).not_to include("adjust_backtrace") + end + describe "RubyLsp::RSpec::RSpecFormatter notifications" do let(:output) { StringIO.new } let(:formatter) { RubyLsp::RSpec::RSpecFormatter.new(output) }