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

Commit eedf9a9

Browse files
committed
Merge pull request #2083 from rspec/extract-failed-lines-by-parsing-ruby
Extract multiple failed lines by parsing Ruby
2 parents 5507b69 + f37093b commit eedf9a9

23 files changed

+1489
-213
lines changed

Changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ Enhancements:
4444
hooks to be invoked when example groups are created. (bootstraponline, #2094)
4545
* Add `add_example` and `remove_example` to `RSpec::Core::ExampleGroup` to
4646
allow manipulating an example groups examples. (bootstraponline, #2095)
47+
* Display multiline failure source lines in failure output when Ripper is
48+
available (MRI >= 1.9.2, and JRuby >= 1.7.5 && < 9.0.0.0.rc1).
49+
(Yuji Nakayama, #2083)
50+
* Add `max_displayed_failure_line_count` configuration option
51+
(defaults to 10). (Yuji Nakayama, #2083)
4752

4853
Bug Fixes:
4954

lib/rspec/core/configuration.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
332332
# Currently this will place a mutex around memoized values such as let blocks.
333333
add_setting :threadsafe
334334

335+
# @macro add_setting
336+
# Maximum count of failed source lines to display in the failure reports.
337+
# (default `10`).
338+
add_setting :max_displayed_failure_line_count
339+
335340
# @private
336341
add_setting :tty
337342
# @private
@@ -387,6 +392,7 @@ def initialize
387392
@libs = []
388393
@derived_metadata_blocks = FilterableItemRepository::QueryOptimized.new(:any?)
389394
@threadsafe = true
395+
@max_displayed_failure_line_count = 10
390396

391397
define_built_in_hooks
392398
end

lib/rspec/core/formatters.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module RSpec::Core::Formatters
7373
autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter'
7474
autoload :JsonFormatter, 'rspec/core/formatters/json_formatter'
7575
autoload :BisectFormatter, 'rspec/core/formatters/bisect_formatter'
76+
autoload :ExceptionPresenter, 'rspec/core/formatters/exception_presenter'
7677

7778
# Register the formatter class
7879
# @param formatter_class [Class] formatter class to register

lib/rspec/core/formatters/exception_presenter.rb

Lines changed: 101 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# encoding: utf-8
2+
RSpec::Support.require_rspec_core "formatters/snippet_extractor"
3+
RSpec::Support.require_rspec_support "encoded_string"
4+
25
module RSpec
36
module Core
47
module Formatters
@@ -12,14 +15,13 @@ def initialize(exception, example, options={})
1215
@exception = exception
1316
@example = example
1417
@message_color = options.fetch(:message_color) { RSpec.configuration.failure_color }
15-
@description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self)
18+
@description = options.fetch(:description) { example.full_description }
1619
@detail_formatter = options.fetch(:detail_formatter) { Proc.new {} }
1720
@extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} }
1821
@backtrace_formatter = options.fetch(:backtrace_formatter) { RSpec.configuration.backtrace_formatter }
1922
@indentation = options.fetch(:indentation, 2)
2023
@skip_shared_group_trace = options.fetch(:skip_shared_group_trace, false)
2124
@failure_lines = options[:failure_lines]
22-
@extra_failure_lines = Array(example.metadata[:extra_failure_lines])
2325
end
2426

2527
def message_lines
@@ -71,16 +73,21 @@ def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCo
7173
end
7274

7375
def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
74-
alignment_basis = "#{' ' * @indentation}#{failure_number}) "
75-
indentation = ' ' * alignment_basis.length
76-
77-
"\n#{alignment_basis}#{description_and_detail(colorizer, indentation)}" \
78-
"\n#{formatted_message_and_backtrace(colorizer, indentation)}" \
79-
"#{extra_detail_formatter.call(failure_number, colorizer, indentation)}"
76+
lines = fully_formatted_lines(failure_number, colorizer)
77+
lines.join("\n") << "\n"
8078
end
8179

82-
def failure_slash_error_line
83-
@failure_slash_error_line ||= "Failure/Error: #{read_failed_line.strip}"
80+
def fully_formatted_lines(failure_number, colorizer)
81+
lines = [
82+
description,
83+
detail_formatter.call(example, colorizer),
84+
formatted_message_and_backtrace(colorizer),
85+
extra_detail_formatter.call(failure_number, colorizer),
86+
].compact.flatten
87+
88+
lines = indent_lines(lines, failure_number)
89+
lines.unshift("")
90+
lines
8491
end
8592

8693
private
@@ -93,12 +100,6 @@ def final_exception(exception)
93100
end
94101
end
95102

96-
def description_and_detail(colorizer, indentation)
97-
detail = detail_formatter.call(example, colorizer, indentation)
98-
return (description || detail) unless description && detail
99-
"#{description}\n#{indentation}#{detail}"
100-
end
101-
102103
if String.method_defined?(:encoding)
103104
def encoding_of(string)
104105
string.encoding
@@ -118,28 +119,71 @@ def encoded_string(string)
118119
# :nocov:
119120
end
120121

122+
def indent_lines(lines, failure_number)
123+
alignment_basis = "#{' ' * @indentation}#{failure_number}) "
124+
indentation = ' ' * alignment_basis.length
125+
126+
lines.each_with_index.map do |line, index|
127+
if index == 0
128+
"#{alignment_basis}#{line}"
129+
elsif line.empty?
130+
line
131+
else
132+
"#{indentation}#{line}"
133+
end
134+
end
135+
end
136+
121137
def exception_class_name(exception=@exception)
122138
name = exception.class.name.to_s
123139
name = "(anonymous error class)" if name == ''
124140
name
125141
end
126142

127143
def failure_lines
128-
@failure_lines ||=
129-
begin
130-
lines = []
131-
lines << failure_slash_error_line unless (description == failure_slash_error_line)
132-
lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/
133-
encoded_string(exception.message.to_s).split("\n").each do |line|
134-
lines << " #{line}"
135-
end
136-
unless @extra_failure_lines.empty?
137-
lines << ''
138-
lines.concat(@extra_failure_lines)
139-
lines << ''
140-
end
141-
lines
144+
@failure_lines ||= [].tap do |lines|
145+
lines.concat(failure_slash_error_lines)
146+
147+
sections = [failure_slash_error_lines, exception_lines]
148+
if sections.any? { |section| section.size > 1 } && !exception_lines.first.empty?
149+
lines << ''
142150
end
151+
152+
lines.concat(exception_lines)
153+
lines.concat(extra_failure_lines)
154+
end
155+
end
156+
157+
def failure_slash_error_lines
158+
lines = read_failed_lines
159+
if lines.count == 1
160+
lines[0] = "Failure/Error: #{lines[0].strip}"
161+
else
162+
least_indentation = lines.map { |line| line[/^[ \t]*/] }.min
163+
lines = lines.map { |line| line.sub(/^#{least_indentation}/, ' ') }
164+
lines.unshift('Failure/Error:')
165+
end
166+
lines
167+
end
168+
169+
def exception_lines
170+
lines = []
171+
lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/
172+
encoded_string(exception.message.to_s).split("\n").each do |line|
173+
lines << (line.empty? ? line : " #{line}")
174+
end
175+
lines
176+
end
177+
178+
def extra_failure_lines
179+
@extra_failure_lines ||= begin
180+
lines = Array(example.metadata[:extra_failure_lines])
181+
unless lines.empty?
182+
lines.unshift('')
183+
lines.push('')
184+
end
185+
lines
186+
end
143187
end
144188

145189
def add_shared_group_lines(lines, colorizer)
@@ -152,22 +196,21 @@ def add_shared_group_lines(lines, colorizer)
152196
lines
153197
end
154198

155-
def read_failed_line
199+
def read_failed_lines
156200
matching_line = find_failed_line
157201
unless matching_line
158-
return "Unable to find matching line from backtrace"
202+
return ["Unable to find matching line from backtrace"]
159203
end
160204

161205
file_path, line_number = matching_line.match(/(.+?):(\d+)(|:\d+)/)[1..2]
162-
163-
if File.exist?(file_path)
164-
File.readlines(file_path)[line_number.to_i - 1] ||
165-
"Unable to find matching line in #{file_path}"
166-
else
167-
"Unable to find #{file_path} to read failed line"
168-
end
206+
max_line_count = RSpec.configuration.max_displayed_failure_line_count
207+
SnippetExtractor.extract_expression_lines_at(file_path, line_number.to_i, max_line_count)
208+
rescue SnippetExtractor::NoSuchFileError
209+
["Unable to find #{file_path} to read failed line"]
210+
rescue SnippetExtractor::NoSuchLineError
211+
["Unable to find matching line in #{file_path}"]
169212
rescue SecurityError
170-
"Unable to read failed line"
213+
["Unable to read failed line"]
171214
end
172215

173216
def find_failed_line
@@ -181,16 +224,12 @@ def find_failed_line
181224
end || exception_backtrace.first
182225
end
183226

184-
def formatted_message_and_backtrace(colorizer, indentation)
227+
def formatted_message_and_backtrace(colorizer)
185228
lines = colorized_message_lines(colorizer) + colorized_formatted_backtrace(colorizer)
186-
187-
formatted = ""
188-
189-
lines.each do |line|
190-
formatted << RSpec::Support::EncodedString.new("#{indentation}#{line}\n", encoding_of(formatted))
229+
encoding = encoding_of("")
230+
lines.map do |line|
231+
RSpec::Support::EncodedString.new(line, encoding)
191232
end
192-
193-
formatted
194233
end
195234

196235
def exception_backtrace
@@ -226,9 +265,9 @@ def options
226265
def pending_options
227266
if @execution_result.pending_fixed?
228267
{
229-
:description_formatter => Proc.new { "#{@example.full_description} FIXED" },
230-
:message_color => RSpec.configuration.fixed_color,
231-
:failure_lines => [
268+
:description => "#{@example.full_description} FIXED",
269+
:message_color => RSpec.configuration.fixed_color,
270+
:failure_lines => [
232271
"Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised."
233272
]
234273
}
@@ -251,8 +290,6 @@ def with_multiple_error_options_as_needed(exception, options)
251290
options[:message_color])
252291
)
253292

254-
options[:description_formatter] &&= Proc.new {}
255-
256293
return options unless exception.aggregation_metadata[:hide_backtrace]
257294
options[:backtrace_formatter] = EmptyBacktraceFormatter
258295
options
@@ -263,7 +300,7 @@ def multiple_exceptions_error?(exception)
263300
end
264301

265302
def multiple_exception_summarizer(exception, prior_detail_formatter, color)
266-
lambda do |example, colorizer, indentation|
303+
lambda do |example, colorizer|
267304
summary = if exception.aggregation_metadata[:hide_backtrace]
268305
# Since the backtrace is hidden, the subfailures will come
269306
# immediately after this, and using `:` will read well.
@@ -276,27 +313,30 @@ def multiple_exception_summarizer(exception, prior_detail_formatter, color)
276313

277314
summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color)
278315
return summary unless prior_detail_formatter
279-
"#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}"
316+
[
317+
prior_detail_formatter.call(example, colorizer),
318+
summary
319+
]
280320
end
281321
end
282322

283323
def sub_failure_list_formatter(exception, message_color)
284324
common_backtrace_truncater = CommonBacktraceTruncater.new(exception)
285325

286-
lambda do |failure_number, colorizer, indentation|
287-
exception.all_exceptions.each_with_index.map do |failure, index|
326+
lambda do |failure_number, colorizer|
327+
FlatMap.flat_map(exception.all_exceptions.each_with_index) do |failure, index|
288328
options = with_multiple_error_options_as_needed(
289329
failure,
290-
:description_formatter => :failure_slash_error_line.to_proc,
291-
:indentation => indentation.length,
330+
:description => nil,
331+
:indentation => 0,
292332
:message_color => message_color || RSpec.configuration.failure_color,
293333
:skip_shared_group_trace => true
294334
)
295335

296336
failure = common_backtrace_truncater.with_truncated_backtrace(failure)
297337
presenter = ExceptionPresenter.new(failure, @example, options)
298-
presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer)
299-
end.join
338+
presenter.fully_formatted_lines("#{failure_number}.#{index + 1}", colorizer)
339+
end
300340
end
301341
end
302342

lib/rspec/core/formatters/html_formatter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,12 @@ def percent_done
137137
# spec. For example, you could output links to images or other files
138138
# produced during the specs.
139139
def extra_failure_content(failure)
140-
RSpec::Support.require_rspec_core "formatters/snippet_extractor"
140+
RSpec::Support.require_rspec_core "formatters/html_snippet_extractor"
141141
backtrace = (failure.exception.backtrace || []).map do |line|
142142
RSpec.configuration.backtrace_formatter.backtrace_line(line)
143143
end
144144
backtrace.compact!
145-
@snippet_extractor ||= SnippetExtractor.new
145+
@snippet_extractor ||= HtmlSnippetExtractor.new
146146
" <pre class=\"ruby\"><code>#{@snippet_extractor.snippet(backtrace)}</code></pre>"
147147
end
148148
end

0 commit comments

Comments
 (0)