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

Commit e37b7b8

Browse files
committed
Merge pull request #2088 from rspec/find-failed-line-improvement
Improve logic that finds the failure line to print.
2 parents a06d7a2 + 1f2d437 commit e37b7b8

File tree

9 files changed

+228
-10
lines changed

9 files changed

+228
-10
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 << f # the registered files are already expended absolute paths
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/metadata.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,13 @@ def populate_location_attributes
147147
end
148148

149149
relative_file_path = Metadata.relative_path(file_path)
150+
absolute_file_path = File.expand_path(relative_file_path)
150151
metadata[:file_path] = relative_file_path
151152
metadata[:line_number] = line_number.to_i
152153
metadata[:location] = "#{relative_file_path}:#{line_number}"
153-
metadata[:absolute_file_path] = File.expand_path(relative_file_path)
154+
metadata[:absolute_file_path] = absolute_file_path
154155
metadata[:rerun_file_path] ||= relative_file_path
155-
metadata[:scoped_id] = build_scoped_id_for(relative_file_path)
156+
metadata[:scoped_id] = build_scoped_id_for(absolute_file_path)
156157
end
157158

158159
def file_path_and_line_number_from(backtrace)

lib/rspec/core/rake_task.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'rake'
22
require 'rake/tasklib'
3+
require 'rspec/support'
34
require 'rspec/support/ruby_features'
45
require 'rspec/core/shell_escape'
56

lib/rspec/core/world.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,17 @@ 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.
4651
def register(example_group)
4752
example_groups << example_group
48-
@example_group_counts_by_spec_file[example_group.metadata[:file_path]] += 1
53+
@example_group_counts_by_spec_file[example_group.metadata[:absolute_file_path]] += 1
4954
example_group
5055
end
5156

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