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

Commit aadd339

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 a06d7a2 commit aadd339

File tree

7 files changed

+222
-7
lines changed

7 files changed

+222
-7
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: 35 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
@@ -1315,6 +1339,16 @@ def configure_expectation_framework
13151339

13161340
# @private
13171341
def load_spec_files
1342+
# Note which spec files world is already aware of.
1343+
# This is generally only needed for when the user runs
1344+
# `ruby path/to/spec.rb` (and loads `rspec/autorun`) --
1345+
# in that case, the spec file was loaded by `ruby` and
1346+
# isn't loaded by us here so we only know about it because
1347+
# of an example group being registered in it.
1348+
RSpec.world.registered_example_group_files.each do |f|
1349+
loaded_spec_files << File.expand_path(f)
1350+
end
1351+
13181352
files_to_run.uniq.each do |f|
13191353
file = File.expand_path(f)
13201354
load file

lib/rspec/core/formatters/exception_presenter.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,14 @@ 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+
loaded_spec_files = RSpec.configuration.loaded_spec_files
170+
169171
exception_backtrace.find do |line|
170172
next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1])
171-
File.expand_path(line_path).downcase == example_path
172-
end
173+
path = File.expand_path(line_path)
174+
loaded_spec_files.include?(path) || path =~ line_regex
175+
end || exception_backtrace.first
173176
end
174177

175178
def formatted_message_and_backtrace(colorizer, indentation)

lib/rspec/core/world.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ def filter_manager
4040
@configuration.filter_manager
4141
end
4242

43+
# @private
44+
def registered_example_group_files
45+
@example_group_counts_by_spec_file.keys
46+
end
47+
4348
# @api private
4449
#
4550
# Register an example group.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 source of a failure in a spec file that is defined at the current directory instead of in the normal `spec` subdir" do
8+
write_file "the_spec.rb", "
9+
RSpec.describe do
10+
it 'fails via expect' do
11+
expect(1).to eq(2)
12+
end
13+
end
14+
"
15+
16+
run_command "the_spec.rb"
17+
expect(last_cmd_stdout).to include("expect(1).to eq(2)")
18+
end
19+
20+
it "finds the source of a failure in a spec file loaded by running `ruby file` rather than loaded directly by RSpec" do
21+
write_file "passing_spec.rb", "
22+
RSpec.describe do
23+
example { }
24+
end
25+
"
26+
27+
write_file "failing_spec.rb", "
28+
RSpec.describe do
29+
it 'fails via expect' do
30+
expect(1).to eq(2)
31+
end
32+
end
33+
"
34+
35+
in_current_dir { load "failing_spec.rb" }
36+
run_command "passing_spec.rb"
37+
38+
expect(last_cmd_stdout).to include("expect(1).to eq(2)")
39+
end
40+
41+
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
42+
write_file "lib/lib_mod.rb", "
43+
module LibMod
44+
def self.trigger_failure
45+
raise 'LibMod failure'
46+
end
47+
end
48+
"
49+
50+
write_file "app/app_mod.rb", "
51+
module AppMod
52+
def self.trigger_failure
53+
raise 'AppMod failure'
54+
end
55+
end
56+
"
57+
58+
write_file "spec/support/spec_support.rb", "
59+
module SpecSupport
60+
def self.trigger_failure
61+
raise 'SpecSupport failure'
62+
end
63+
end
64+
"
65+
66+
write_file "spec/default_config_spec.rb", "
67+
require './lib/lib_mod'
68+
require './spec/support/spec_support'
69+
require './app/app_mod'
70+
71+
RSpec.describe do
72+
example('1') { LibMod.trigger_failure }
73+
example('2') { AppMod.trigger_failure }
74+
example('3') { SpecSupport.trigger_failure }
75+
end
76+
"
77+
78+
run_command "./spec/default_config_spec.rb"
79+
80+
expect(last_cmd_stdout).to include("raise 'LibMod failure'").
81+
and include("raise 'AppMod failure'").
82+
and include("raise 'SpecSupport failure'").
83+
and exclude("AppMod.trigger_failure")
84+
85+
write_file "spec/change_config_spec.rb", "
86+
require './app/app_mod'
87+
88+
RSpec.configure do |c|
89+
c.project_source_dirs = %w[ lib spec ]
90+
end
91+
92+
RSpec.describe do
93+
example('1') { AppMod.trigger_failure }
94+
end
95+
"
96+
97+
run_command "./spec/change_config_spec.rb"
98+
99+
expect(last_cmd_stdout).to include("AppMod.trigger_failure").
100+
and exclude("raise 'AppMod failure'")
101+
end
102+
103+
it "finds the callsite of a method provided by a gem that fails (rather than the line in the gem)" do
104+
write_file "vendor/gems/assertions/lib/assertions.rb", "
105+
module Assertions
106+
AssertionFailed = Class.new(StandardError)
107+
108+
def assert(value, msg)
109+
raise(AssertionFailed, msg) unless value
110+
end
111+
end
112+
"
113+
114+
write_file "spec/unit/the_spec.rb", "
115+
require './vendor/gems/assertions/lib/assertions'
116+
117+
RSpec.describe do
118+
include Assertions
119+
120+
it 'fails via assert' do
121+
assert false, 'failed assertion'
122+
end
123+
124+
it 'fails via expect' do
125+
expect(1).to eq(2)
126+
end
127+
end
128+
"
129+
130+
run_command ""
131+
132+
expect(last_cmd_stdout).to include("assert false, 'failed assertion'").
133+
and include("expect(1).to eq(2)").
134+
and exclude("raise(AssertionFailed, msg)")
135+
end
136+
137+
it "falls back to finding a line in a gem when there are no backtrace lines in the app, lib or spec directories" do
138+
write_file "vendor/gems/before_failure/lib/before_failure.rb", "
139+
RSpec.configure do |c|
140+
c.before { raise 'before failure!' }
141+
end
142+
"
143+
144+
write_file "spec/unit/the_spec.rb", "
145+
require './vendor/gems/before_failure/lib/before_failure'
146+
147+
RSpec.describe do
148+
example('1') { }
149+
end
150+
"
151+
152+
run_command ""
153+
154+
expect(last_cmd_stdout).to include("c.before { raise 'before failure!' }").
155+
and exclude("Unable to find matching line from backtrace")
156+
end
157+
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)