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

Commit 3a38db5

Browse files
committed
Improve logic that finds the failure line to print.
- Introduce `project_source_dirs` config setting. - Look for the first backtrace line in one of the `project_source_dirs` rather than the first line from your spec file. This helps in situations where you define a helper method in a support file that has a failing expectation, and call it from your spec. Previously it would have shown the helper method call site rather than the expectation in the helper method itself. - If no backtrace line can be found in a `project_source_dirs`, pick the first backtrace line. While we don’t generally want to show lines from gems, it’s better than showing no line at all. Fixes #1991.
1 parent 90fca7e commit 3a38db5

File tree

6 files changed

+171
-8
lines changed

6 files changed

+171
-8
lines changed

Changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ Enhancements:
2626
output when a `cause` is available. (Adam Magan)
2727
* Stop rescuing `NoMemoryError`, `SignalExcepetion`, `Interrupt` and
2828
`SystemExit`. It is dangerous to interfere with these. (Myron Marston, #2063)
29+
* Add `config.project_source_dirs` setting which RSpec uses to determine
30+
if a backtrace line comes from your project source or from some
31+
external library. It defaults to `spec`, `lib` and `app` but can be
32+
configured differently. (Myron Marston, #2088)
33+
* Improve failure line detection so that it looks for the failure line
34+
in any project source directory instead of just in the spec file.
35+
In addition, if no backtrace lines can be found from a project source
36+
file, we fall back to displaying the source of the first backtrace
37+
line. This should virtually eliminate the "Unable to find matching
38+
line from backtrace" messages. (Myron Marston, #2088)
2939

3040
Bug Fixes:
3141

lib/rspec/core/configuration.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ def self.add_read_only_setting(name, opts={})
100100
#
101101
# @note Other scripts invoking `rspec` indirectly will ignore this
102102
# setting.
103-
add_setting :default_path
103+
add_read_only_setting :default_path
104+
def default_path=(path)
105+
project_source_dirs << path
106+
@default_path = path
107+
end
104108

105109
# @macro add_setting
106110
# Run examples over DRb (default: `false`). RSpec doesn't supply the DRb
@@ -241,6 +245,16 @@ def exclude_pattern=(value)
241245
update_pattern_attr :exclude_pattern, value
242246
end
243247

248+
# @macro add_setting
249+
# Specifies which directories contain the source code for your project.
250+
# When a failure occurs, RSpec looks through the backtrace to find a
251+
# a line of source to print. It first looks for a line coming from
252+
# one of the project source directories so that, for example, it prints
253+
# the expectation or assertion call rather than the source code from
254+
# the expectation or assertion framework.
255+
# @return [Array<String>]
256+
add_setting :project_source_dirs
257+
244258
# @macro add_setting
245259
# Report the times for the slowest examples (default: `false`).
246260
# Use this to specify the number of examples to include in the profile.
@@ -353,6 +367,7 @@ def initialize
353367
@backtrace_formatter = BacktraceFormatter.new
354368

355369
@default_path = 'spec'
370+
@project_source_dirs = %w[ spec lib app ]
356371
@deprecation_stream = $stderr
357372
@output_stream = $stdout
358373
@reporter = nil
@@ -1274,6 +1289,15 @@ def requires=(paths)
12741289
@requires += paths
12751290
end
12761291

1292+
# @private
1293+
def in_project_source_dir_regex
1294+
regexes = project_source_dirs.map do |dir|
1295+
/\A#{Regexp.escape(File.expand_path(dir))}\//
1296+
end
1297+
1298+
Regexp.union(regexes)
1299+
end
1300+
12771301
# @private
12781302
if RUBY_VERSION.to_f >= 1.9
12791303
# @private

lib/rspec/core/formatters/exception_presenter.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@ def read_failed_line
165165
end
166166

167167
def find_failed_line
168-
example_path = example.metadata[:absolute_file_path].downcase
168+
line_regex = RSpec.configuration.in_project_source_dir_regex
169+
169170
exception_backtrace.find do |line|
170-
next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1])
171-
File.expand_path(line_path).downcase == example_path
172-
end
171+
File.expand_path(line) =~ line_regex
172+
end || exception_backtrace.first
173173
end
174174

175175
def formatted_message_and_backtrace(colorizer, indentation)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
require 'support/aruba_support'
2+
3+
RSpec.describe 'Failed line detection' do
4+
include_context "aruba support"
5+
before { clean_current_dir }
6+
7+
it "finds the direct source of failure in any lib, app or spec file, and allows the user to configure what is considered a project source dir" do
8+
write_file "lib/lib_mod.rb", "
9+
module LibMod
10+
def self.trigger_failure
11+
raise 'LibMod failure'
12+
end
13+
end
14+
"
15+
16+
write_file "app/app_mod.rb", "
17+
module AppMod
18+
def self.trigger_failure
19+
raise 'AppMod failure'
20+
end
21+
end
22+
"
23+
24+
write_file "spec/support/spec_support.rb", "
25+
module SpecSupport
26+
def self.trigger_failure
27+
raise 'SpecSupport failure'
28+
end
29+
end
30+
"
31+
32+
write_file "spec/default_config_spec.rb", "
33+
require './lib/lib_mod'
34+
require './spec/support/spec_support'
35+
require './app/app_mod'
36+
37+
RSpec.describe do
38+
example('1') { LibMod.trigger_failure }
39+
example('2') { AppMod.trigger_failure }
40+
example('3') { SpecSupport.trigger_failure }
41+
end
42+
"
43+
44+
run_command "./spec/default_config_spec.rb"
45+
46+
expect(last_cmd_stdout).to include("raise 'LibMod failure'").
47+
and include("raise 'AppMod failure'").
48+
and include("raise 'SpecSupport failure'").
49+
and exclude("AppMod.trigger_failure")
50+
51+
write_file "spec/change_config_spec.rb", "
52+
require './app/app_mod'
53+
54+
RSpec.configure do |c|
55+
c.project_source_dirs = %w[ lib spec ]
56+
end
57+
58+
RSpec.describe do
59+
example('1') { AppMod.trigger_failure }
60+
end
61+
"
62+
63+
run_command "./spec/change_config_spec.rb"
64+
65+
expect(last_cmd_stdout).to include("AppMod.trigger_failure").
66+
and exclude("raise 'AppMod failure'")
67+
end
68+
69+
it "finds the callsite of a method provided by a gem that fails (rather than the line in the gem)" do
70+
write_file "vendor/gems/assertions/lib/assertions.rb", "
71+
module Assertions
72+
AssertionFailed = Class.new(StandardError)
73+
74+
def assert(value, msg)
75+
raise(AssertionFailed, msg) unless value
76+
end
77+
end
78+
"
79+
80+
write_file "spec/unit/the_spec.rb", "
81+
require './vendor/gems/assertions/lib/assertions'
82+
83+
RSpec.describe do
84+
include Assertions
85+
86+
it 'fails via assert' do
87+
assert false, 'failed assertion'
88+
end
89+
90+
it 'fails via expect' do
91+
expect(1).to eq(2)
92+
end
93+
end
94+
"
95+
96+
run_command ""
97+
98+
expect(last_cmd_stdout).to include("assert false, 'failed assertion'").
99+
and include("expect(1).to eq(2)").
100+
and exclude("raise(AssertionFailed, msg)")
101+
end
102+
103+
it "falls back to finding a line in a gem when there are no backtrace lines in the app, lib or spec directories" do
104+
write_file "vendor/gems/before_failure/lib/before_failure.rb", "
105+
RSpec.configure do |c|
106+
c.before { raise 'before failure!' }
107+
end
108+
"
109+
110+
write_file "spec/unit/the_spec.rb", "
111+
require './vendor/gems/before_failure/lib/before_failure'
112+
113+
RSpec.describe do
114+
example('1') { }
115+
end
116+
"
117+
118+
run_command ""
119+
120+
expect(last_cmd_stdout).to include("c.before { raise 'before failure!' }").
121+
and exclude("Unable to find matching line from backtrace")
122+
end
123+
end

spec/rspec/core/configuration_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,12 @@ def specify_consistent_ordering_of_files_to_run
856856
it 'defaults to "spec"' do
857857
expect(config.default_path).to eq('spec')
858858
end
859+
860+
it 'adds to the `project_source_dirs`' do
861+
expect {
862+
config.default_path = 'test'
863+
}.to change { config.project_source_dirs.include?('test') }.from(false).to(true)
864+
end
859865
end
860866

861867
describe "#include" do

spec/support/formatter_support.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ def expected_summary_output_for_example_specs
7979
| # ./spec/support/sandboxing.rb:7
8080
|
8181
| 3) a failing spec with odd backtraces fails with a backtrace that has no file
82-
| Failure/Error: Unable to find matching line from backtrace
82+
| Failure/Error: Unable to find (erb) to read failed line
8383
| RuntimeError:
8484
| foo
8585
| # (erb):1
8686
|
8787
| 4) a failing spec with odd backtraces fails with a backtrace containing an erb file
88-
| Failure/Error: Unable to find matching line from backtrace
88+
| Failure/Error: Unable to find /foo.html.erb to read failed line
8989
| Exception:
9090
| Exception
9191
| # /foo.html.erb:1:in `<main>': foo (RuntimeError)
@@ -159,7 +159,7 @@ def expected_summary_output_for_example_specs
159159
| # ./spec/support/sandboxing.rb:7:in `block (2 levels) in <top (required)>'
160160
|
161161
| 4) a failing spec with odd backtraces fails with a backtrace containing an erb file
162-
| Failure/Error: Unable to find matching line from backtrace
162+
| Failure/Error: Unable to find /foo.html.erb to read failed line
163163
| Exception:
164164
| Exception
165165
| # /foo.html.erb:1:in `<main>': foo (RuntimeError)

0 commit comments

Comments
 (0)