rspec-core 3.2.3 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/Changelog.md +75 -0
- data/README.md +137 -20
- data/lib/rspec/autorun.rb +1 -0
- data/lib/rspec/core.rb +8 -16
- data/lib/rspec/core/backtrace_formatter.rb +1 -3
- data/lib/rspec/core/bisect/coordinator.rb +66 -0
- data/lib/rspec/core/bisect/example_minimizer.rb +130 -0
- data/lib/rspec/core/bisect/runner.rb +139 -0
- data/lib/rspec/core/bisect/server.rb +61 -0
- data/lib/rspec/core/bisect/subset_enumerator.rb +39 -0
- data/lib/rspec/core/configuration.rb +134 -5
- data/lib/rspec/core/configuration_options.rb +21 -10
- data/lib/rspec/core/example.rb +84 -50
- data/lib/rspec/core/example_group.rb +46 -18
- data/lib/rspec/core/example_status_persister.rb +235 -0
- data/lib/rspec/core/filter_manager.rb +43 -28
- data/lib/rspec/core/flat_map.rb +2 -0
- data/lib/rspec/core/formatters.rb +30 -20
- data/lib/rspec/core/formatters/base_text_formatter.rb +1 -0
- data/lib/rspec/core/formatters/bisect_formatter.rb +68 -0
- data/lib/rspec/core/formatters/bisect_progress_formatter.rb +115 -0
- data/lib/rspec/core/formatters/deprecation_formatter.rb +0 -1
- data/lib/rspec/core/formatters/documentation_formatter.rb +0 -4
- data/lib/rspec/core/formatters/exception_presenter.rb +389 -0
- data/lib/rspec/core/formatters/fallback_message_formatter.rb +28 -0
- data/lib/rspec/core/formatters/helpers.rb +22 -2
- data/lib/rspec/core/formatters/html_formatter.rb +1 -4
- data/lib/rspec/core/formatters/html_printer.rb +2 -6
- data/lib/rspec/core/formatters/json_formatter.rb +6 -4
- data/lib/rspec/core/formatters/snippet_extractor.rb +12 -7
- data/lib/rspec/core/hooks.rb +8 -2
- data/lib/rspec/core/memoized_helpers.rb +77 -17
- data/lib/rspec/core/metadata.rb +24 -10
- data/lib/rspec/core/metadata_filter.rb +16 -3
- data/lib/rspec/core/mutex.rb +63 -0
- data/lib/rspec/core/notifications.rb +84 -189
- data/lib/rspec/core/option_parser.rb +105 -32
- data/lib/rspec/core/ordering.rb +28 -25
- data/lib/rspec/core/profiler.rb +32 -0
- data/lib/rspec/core/project_initializer/spec/spec_helper.rb +6 -1
- data/lib/rspec/core/rake_task.rb +6 -20
- data/lib/rspec/core/reentrant_mutex.rb +52 -0
- data/lib/rspec/core/reporter.rb +65 -17
- data/lib/rspec/core/runner.rb +38 -14
- data/lib/rspec/core/set.rb +49 -0
- data/lib/rspec/core/shared_example_group.rb +3 -1
- data/lib/rspec/core/shell_escape.rb +49 -0
- data/lib/rspec/core/version.rb +1 -1
- data/lib/rspec/core/world.rb +31 -20
- metadata +35 -7
- metadata.gz.sig +0 -0
- data/lib/rspec/core/backport_random.rb +0 -339
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'drb/drb'
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
module Formatters
|
6
|
+
# Used by `--bisect`. When it shells out and runs a portion of the suite, it uses
|
7
|
+
# this formatter as a means to have the status reported back to it, via DRb.
|
8
|
+
#
|
9
|
+
# Note that since DRb calls carry considerable overhead compared to normal
|
10
|
+
# method calls, we try to minimize the number of DRb calls for perf reasons,
|
11
|
+
# opting to communicate only at the start and the end of the run, rather than
|
12
|
+
# after each example.
|
13
|
+
# @private
|
14
|
+
class BisectFormatter
|
15
|
+
Formatters.register self, :start, :start_dump, :example_started,
|
16
|
+
:example_failed, :example_passed, :example_pending
|
17
|
+
|
18
|
+
def initialize(_output)
|
19
|
+
port = RSpec.configuration.drb_port
|
20
|
+
drb_uri = "druby://localhost:#{port}"
|
21
|
+
@all_example_ids = []
|
22
|
+
@failed_example_ids = []
|
23
|
+
@bisect_server = DRbObject.new_with_uri(drb_uri)
|
24
|
+
@remaining_failures = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def start(_notification)
|
28
|
+
@remaining_failures = Set.new(@bisect_server.expected_failures)
|
29
|
+
end
|
30
|
+
|
31
|
+
def example_started(notification)
|
32
|
+
@all_example_ids << notification.example.id
|
33
|
+
end
|
34
|
+
|
35
|
+
def example_failed(notification)
|
36
|
+
@failed_example_ids << notification.example.id
|
37
|
+
example_finished(notification, :failed)
|
38
|
+
end
|
39
|
+
|
40
|
+
def example_passed(notification)
|
41
|
+
example_finished(notification, :passed)
|
42
|
+
end
|
43
|
+
|
44
|
+
def example_pending(notification)
|
45
|
+
example_finished(notification, :pending)
|
46
|
+
end
|
47
|
+
|
48
|
+
def start_dump(_notification)
|
49
|
+
@bisect_server.latest_run_results = RunResults.new(
|
50
|
+
@all_example_ids, @failed_example_ids
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
RunResults = Struct.new(:all_example_ids, :failed_example_ids)
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def example_finished(notification, status)
|
59
|
+
return unless @remaining_failures.include?(notification.example.id)
|
60
|
+
@remaining_failures.delete(notification.example.id)
|
61
|
+
|
62
|
+
return if status == :failed && !@remaining_failures.empty?
|
63
|
+
RSpec.world.wants_to_quit = true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
module Formatters
|
6
|
+
# @private
|
7
|
+
# Produces progress output while bisecting.
|
8
|
+
class BisectProgressFormatter < BaseTextFormatter
|
9
|
+
# We've named all events with a `bisect_` prefix to prevent naming collisions.
|
10
|
+
Formatters.register self, :bisect_starting, :bisect_original_run_complete,
|
11
|
+
:bisect_round_started, :bisect_individual_run_complete,
|
12
|
+
:bisect_round_finished, :bisect_complete, :bisect_repro_command,
|
13
|
+
:bisect_failed, :bisect_aborted
|
14
|
+
|
15
|
+
def bisect_starting(notification)
|
16
|
+
options = notification.original_cli_args.join(' ')
|
17
|
+
output.puts "Bisect started using options: #{options.inspect}"
|
18
|
+
output.print "Running suite to find failures..."
|
19
|
+
end
|
20
|
+
|
21
|
+
def bisect_original_run_complete(notification)
|
22
|
+
failures = Helpers.pluralize(notification.failed_example_ids.size, "failing example")
|
23
|
+
non_failures = Helpers.pluralize(notification.non_failing_example_ids.size, "non-failing example")
|
24
|
+
|
25
|
+
output.puts " (#{Helpers.format_duration(notification.duration)})"
|
26
|
+
output.puts "Starting bisect with #{failures} and #{non_failures}."
|
27
|
+
end
|
28
|
+
|
29
|
+
def bisect_round_started(notification, include_trailing_space=true)
|
30
|
+
search_desc = Helpers.pluralize(
|
31
|
+
notification.subset_size, "non-failing example"
|
32
|
+
)
|
33
|
+
|
34
|
+
output.print "\nRound #{notification.round}: searching for #{search_desc}" \
|
35
|
+
" (of #{notification.remaining_count}) to ignore:"
|
36
|
+
output.print " " if include_trailing_space
|
37
|
+
end
|
38
|
+
|
39
|
+
def bisect_round_finished(notification)
|
40
|
+
output.print " (#{Helpers.format_duration(notification.duration)})"
|
41
|
+
end
|
42
|
+
|
43
|
+
def bisect_individual_run_complete(_)
|
44
|
+
output.print '.'
|
45
|
+
end
|
46
|
+
|
47
|
+
def bisect_complete(notification)
|
48
|
+
output.puts "\nBisect complete! Reduced necessary non-failing examples " \
|
49
|
+
"from #{notification.original_non_failing_count} to " \
|
50
|
+
"#{notification.remaining_count} in " \
|
51
|
+
"#{Helpers.format_duration(notification.duration)}."
|
52
|
+
end
|
53
|
+
|
54
|
+
def bisect_repro_command(notification)
|
55
|
+
output.puts "\nThe minimal reproduction command is:\n #{notification.repro}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def bisect_failed(notification)
|
59
|
+
output.puts "\nBisect failed! #{notification.failure_explanation}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def bisect_aborted(notification)
|
63
|
+
output.puts "\n\nBisect aborted!"
|
64
|
+
output.puts "\nThe most minimal reproduction command discovered so far is:\n #{notification.repro}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
# Produces detailed debug output while bisecting. Used when
|
70
|
+
# bisect is performed while the `DEBUG_RSPEC_BISECT` ENV var is used.
|
71
|
+
# Designed to provide details for us when we need to troubleshoot bisect bugs.
|
72
|
+
class BisectDebugFormatter < BisectProgressFormatter
|
73
|
+
Formatters.register self, :bisect_original_run_complete, :bisect_individual_run_start,
|
74
|
+
:bisect_individual_run_complete, :bisect_round_finished, :bisect_ignoring_ids
|
75
|
+
|
76
|
+
def bisect_original_run_complete(notification)
|
77
|
+
output.puts " (#{Helpers.format_duration(notification.duration)})"
|
78
|
+
|
79
|
+
output.puts " - #{describe_ids 'Failing examples', notification.failed_example_ids}"
|
80
|
+
output.puts " - #{describe_ids 'Non-failing examples', notification.non_failing_example_ids}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def bisect_individual_run_start(notification)
|
84
|
+
output.print "\n - Running: #{notification.command}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def bisect_individual_run_complete(notification)
|
88
|
+
output.print " (#{Helpers.format_duration(notification.duration)})"
|
89
|
+
end
|
90
|
+
|
91
|
+
def bisect_round_started(notification)
|
92
|
+
super(notification, false)
|
93
|
+
end
|
94
|
+
|
95
|
+
def bisect_round_finished(notification)
|
96
|
+
output.print "\n - Round finished"
|
97
|
+
super
|
98
|
+
end
|
99
|
+
|
100
|
+
def bisect_ignoring_ids(notification)
|
101
|
+
output.print "\n - #{describe_ids 'Examples we can safely ignore', notification.ids_to_ignore}"
|
102
|
+
output.print "\n - #{describe_ids 'Remaining non-failing examples', notification.remaining_ids}"
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def describe_ids(description, ids)
|
108
|
+
organized_ids = Formatters::Helpers.organize_ids(ids)
|
109
|
+
formatted_ids = organized_ids.map { |id| " - #{id}" }.join("\n")
|
110
|
+
"#{description} (#{ids.size}):\n#{formatted_ids}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,389 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Core
|
3
|
+
module Formatters
|
4
|
+
# @private
|
5
|
+
class ExceptionPresenter
|
6
|
+
attr_reader :exception, :example, :description, :message_color,
|
7
|
+
:detail_formatter, :extra_detail_formatter, :backtrace_formatter
|
8
|
+
private :message_color, :detail_formatter, :extra_detail_formatter, :backtrace_formatter
|
9
|
+
|
10
|
+
def initialize(exception, example, options={})
|
11
|
+
@exception = exception
|
12
|
+
@example = example
|
13
|
+
@message_color = options.fetch(:message_color) { RSpec.configuration.failure_color }
|
14
|
+
@description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self)
|
15
|
+
@detail_formatter = options.fetch(:detail_formatter) { Proc.new {} }
|
16
|
+
@extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} }
|
17
|
+
@backtrace_formatter = options.fetch(:backtrace_formatter) { RSpec.configuration.backtrace_formatter }
|
18
|
+
@indentation = options.fetch(:indentation, 2)
|
19
|
+
@skip_shared_group_trace = options.fetch(:skip_shared_group_trace, false)
|
20
|
+
@failure_lines = options[:failure_lines]
|
21
|
+
end
|
22
|
+
|
23
|
+
def message_lines
|
24
|
+
add_shared_group_lines(failure_lines, Notifications::NullColorizer)
|
25
|
+
end
|
26
|
+
|
27
|
+
def colorized_message_lines(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
|
28
|
+
add_shared_group_lines(failure_lines, colorizer).map do |line|
|
29
|
+
colorizer.wrap line, message_color
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def formatted_backtrace
|
34
|
+
backtrace_formatter.format_backtrace(exception.backtrace, example.metadata)
|
35
|
+
end
|
36
|
+
|
37
|
+
def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
|
38
|
+
formatted_backtrace.map do |backtrace_info|
|
39
|
+
colorizer.wrap "# #{backtrace_info}", RSpec.configuration.detail_color
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def fully_formatted(failure_number, colorizer=::RSpec::Core::Formatters::ConsoleCodes)
|
44
|
+
alignment_basis = "#{' ' * @indentation}#{failure_number}) "
|
45
|
+
indentation = ' ' * alignment_basis.length
|
46
|
+
|
47
|
+
"\n#{alignment_basis}#{description_and_detail(colorizer, indentation)}" \
|
48
|
+
"\n#{formatted_message_and_backtrace(colorizer, indentation)}" \
|
49
|
+
"#{extra_detail_formatter.call(failure_number, colorizer, indentation)}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def failure_slash_error_line
|
53
|
+
@failure_slash_error_line ||= "Failure/Error: #{read_failed_line.strip}"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def description_and_detail(colorizer, indentation)
|
59
|
+
detail = detail_formatter.call(example, colorizer, indentation)
|
60
|
+
return (description || detail) unless description && detail
|
61
|
+
"#{description}\n#{indentation}#{detail}"
|
62
|
+
end
|
63
|
+
|
64
|
+
if String.method_defined?(:encoding)
|
65
|
+
def encoding_of(string)
|
66
|
+
string.encoding
|
67
|
+
end
|
68
|
+
|
69
|
+
def encoded_string(string)
|
70
|
+
RSpec::Support::EncodedString.new(string, Encoding.default_external)
|
71
|
+
end
|
72
|
+
else # for 1.8.7
|
73
|
+
# :nocov:
|
74
|
+
def encoding_of(_string)
|
75
|
+
end
|
76
|
+
|
77
|
+
def encoded_string(string)
|
78
|
+
RSpec::Support::EncodedString.new(string)
|
79
|
+
end
|
80
|
+
# :nocov:
|
81
|
+
end
|
82
|
+
|
83
|
+
def exception_class_name
|
84
|
+
name = exception.class.name.to_s
|
85
|
+
name = "(anonymous error class)" if name == ''
|
86
|
+
name
|
87
|
+
end
|
88
|
+
|
89
|
+
def failure_lines
|
90
|
+
@failure_lines ||=
|
91
|
+
begin
|
92
|
+
lines = []
|
93
|
+
lines << failure_slash_error_line unless (description == failure_slash_error_line)
|
94
|
+
lines << "#{exception_class_name}:" unless exception_class_name =~ /RSpec/
|
95
|
+
encoded_string(exception.message.to_s).split("\n").each do |line|
|
96
|
+
lines << " #{line}"
|
97
|
+
end
|
98
|
+
lines
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_shared_group_lines(lines, colorizer)
|
103
|
+
return lines if @skip_shared_group_trace
|
104
|
+
|
105
|
+
example.metadata[:shared_group_inclusion_backtrace].each do |frame|
|
106
|
+
lines << colorizer.wrap(frame.description, RSpec.configuration.default_color)
|
107
|
+
end
|
108
|
+
|
109
|
+
lines
|
110
|
+
end
|
111
|
+
|
112
|
+
def read_failed_line
|
113
|
+
matching_line = find_failed_line
|
114
|
+
unless matching_line
|
115
|
+
return "Unable to find matching line from backtrace"
|
116
|
+
end
|
117
|
+
|
118
|
+
file_path, line_number = matching_line.match(/(.+?):(\d+)(|:\d+)/)[1..2]
|
119
|
+
|
120
|
+
if File.exist?(file_path)
|
121
|
+
File.readlines(file_path)[line_number.to_i - 1] ||
|
122
|
+
"Unable to find matching line in #{file_path}"
|
123
|
+
else
|
124
|
+
"Unable to find #{file_path} to read failed line"
|
125
|
+
end
|
126
|
+
rescue SecurityError
|
127
|
+
"Unable to read failed line"
|
128
|
+
end
|
129
|
+
|
130
|
+
def find_failed_line
|
131
|
+
example_path = example.metadata[:absolute_file_path].downcase
|
132
|
+
exception.backtrace.find do |line|
|
133
|
+
next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1])
|
134
|
+
File.expand_path(line_path).downcase == example_path
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def formatted_message_and_backtrace(colorizer, indentation)
|
139
|
+
lines = colorized_message_lines(colorizer) + colorized_formatted_backtrace(colorizer)
|
140
|
+
|
141
|
+
formatted = ""
|
142
|
+
|
143
|
+
lines.each do |line|
|
144
|
+
formatted << RSpec::Support::EncodedString.new("#{indentation}#{line}\n", encoding_of(formatted))
|
145
|
+
end
|
146
|
+
|
147
|
+
formatted
|
148
|
+
end
|
149
|
+
|
150
|
+
# @private
|
151
|
+
# Configuring the `ExceptionPresenter` with the right set of options to handle
|
152
|
+
# pending vs failed vs skipped and aggregated (or not) failures is not simple.
|
153
|
+
# This class takes care of building an appropriate `ExceptionPresenter` for the
|
154
|
+
# provided example.
|
155
|
+
class Factory
|
156
|
+
def build
|
157
|
+
ExceptionPresenter.new(@exception, @example, options)
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def initialize(example)
|
163
|
+
@example = example
|
164
|
+
@execution_result = example.execution_result
|
165
|
+
@exception = if @execution_result.status == :pending
|
166
|
+
@execution_result.pending_exception
|
167
|
+
else
|
168
|
+
@execution_result.exception
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def options
|
173
|
+
with_multiple_error_options_as_needed(@exception, pending_options || {})
|
174
|
+
end
|
175
|
+
|
176
|
+
def pending_options
|
177
|
+
if @execution_result.pending_fixed?
|
178
|
+
{
|
179
|
+
:description_formatter => Proc.new { "#{@example.full_description} FIXED" },
|
180
|
+
:message_color => RSpec.configuration.fixed_color,
|
181
|
+
:failure_lines => [
|
182
|
+
"Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised."
|
183
|
+
]
|
184
|
+
}
|
185
|
+
elsif @execution_result.status == :pending
|
186
|
+
{
|
187
|
+
:message_color => RSpec.configuration.pending_color,
|
188
|
+
:detail_formatter => PENDING_DETAIL_FORMATTER
|
189
|
+
}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def with_multiple_error_options_as_needed(exception, options)
|
194
|
+
return options unless multiple_exceptions_error?(exception)
|
195
|
+
|
196
|
+
options = options.merge(
|
197
|
+
:failure_lines => [],
|
198
|
+
:extra_detail_formatter => sub_failure_list_formatter(exception, options[:message_color]),
|
199
|
+
:detail_formatter => multiple_exception_summarizer(exception,
|
200
|
+
options[:detail_formatter],
|
201
|
+
options[:message_color])
|
202
|
+
)
|
203
|
+
|
204
|
+
options[:description_formatter] &&= Proc.new {}
|
205
|
+
|
206
|
+
return options unless exception.aggregation_metadata[:hide_backtrace]
|
207
|
+
options[:backtrace_formatter] = EmptyBacktraceFormatter
|
208
|
+
options
|
209
|
+
end
|
210
|
+
|
211
|
+
def multiple_exceptions_error?(exception)
|
212
|
+
MultipleExceptionError::InterfaceTag === exception
|
213
|
+
end
|
214
|
+
|
215
|
+
def multiple_exception_summarizer(exception, prior_detail_formatter, color)
|
216
|
+
lambda do |example, colorizer, indentation|
|
217
|
+
summary = if exception.aggregation_metadata[:hide_backtrace]
|
218
|
+
# Since the backtrace is hidden, the subfailures will come
|
219
|
+
# immediately after this, and using `:` will read well.
|
220
|
+
"Got #{exception.exception_count_description}:"
|
221
|
+
else
|
222
|
+
# The backtrace comes after this, so using a `:` doesn't make sense
|
223
|
+
# since the failures may be many lines below.
|
224
|
+
"#{exception.summary}."
|
225
|
+
end
|
226
|
+
|
227
|
+
summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color)
|
228
|
+
return summary unless prior_detail_formatter
|
229
|
+
"#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def sub_failure_list_formatter(exception, message_color)
|
234
|
+
common_backtrace_truncater = CommonBacktraceTruncater.new(exception)
|
235
|
+
|
236
|
+
lambda do |failure_number, colorizer, indentation|
|
237
|
+
exception.all_exceptions.each_with_index.map do |failure, index|
|
238
|
+
options = with_multiple_error_options_as_needed(
|
239
|
+
failure,
|
240
|
+
:description_formatter => :failure_slash_error_line.to_proc,
|
241
|
+
:indentation => indentation.length,
|
242
|
+
:message_color => message_color || RSpec.configuration.failure_color,
|
243
|
+
:skip_shared_group_trace => true
|
244
|
+
)
|
245
|
+
|
246
|
+
failure = common_backtrace_truncater.with_truncated_backtrace(failure)
|
247
|
+
presenter = ExceptionPresenter.new(failure, @example, options)
|
248
|
+
presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer)
|
249
|
+
end.join
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# @private
|
254
|
+
# Used to prevent a confusing backtrace from showing up from the `aggregate_failures`
|
255
|
+
# block declared for `:aggregate_failures` metadata.
|
256
|
+
module EmptyBacktraceFormatter
|
257
|
+
def self.format_backtrace(*)
|
258
|
+
[]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# @private
|
263
|
+
class CommonBacktraceTruncater
|
264
|
+
def initialize(parent)
|
265
|
+
@parent = parent
|
266
|
+
end
|
267
|
+
|
268
|
+
def with_truncated_backtrace(child)
|
269
|
+
child_bt = child.backtrace
|
270
|
+
parent_bt = @parent.backtrace
|
271
|
+
return child if child_bt.nil? || child_bt.empty? || parent_bt.nil?
|
272
|
+
|
273
|
+
index_before_first_common_frame = -1.downto(-child_bt.size).find do |index|
|
274
|
+
parent_bt[index] != child_bt[index]
|
275
|
+
end
|
276
|
+
|
277
|
+
return child if index_before_first_common_frame == -1
|
278
|
+
|
279
|
+
child = child.dup
|
280
|
+
child.set_backtrace(child_bt[0..index_before_first_common_frame])
|
281
|
+
child
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# @private
|
287
|
+
PENDING_DETAIL_FORMATTER = Proc.new do |example, colorizer|
|
288
|
+
colorizer.wrap("# #{example.execution_result.pending_message}", :detail)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Provides a single exception instance that provides access to
|
294
|
+
# multiple sub-exceptions. This is used in situations where a single
|
295
|
+
# individual spec has multiple exceptions, such as one in the `it` block
|
296
|
+
# and one in an `after` block.
|
297
|
+
class MultipleExceptionError < StandardError
|
298
|
+
# @private
|
299
|
+
# Used so there is a common module in the ancestor chain of this class
|
300
|
+
# and `RSpec::Expectations::MultipleExpectationsNotMetError`, which allows
|
301
|
+
# code to detect exceptions that are instances of either, without first
|
302
|
+
# checking to see if rspec-expectations is loaded.
|
303
|
+
module InterfaceTag
|
304
|
+
# Appends the provided exception to the list.
|
305
|
+
# @param exception [Exception] Exception to append to the list.
|
306
|
+
# @private
|
307
|
+
def add(exception)
|
308
|
+
# `PendingExampleFixedError` can be assigned to an example that initially has no
|
309
|
+
# failures, but when the `aggregate_failures` around hook completes, it notifies of
|
310
|
+
# a failure. If we do not ignore `PendingExampleFixedError` it would be surfaced to
|
311
|
+
# the user as part of a multiple exception error, which is undesirable. While it's
|
312
|
+
# pretty weird we handle this here, it's the best solution I've been able to come
|
313
|
+
# up with, and `PendingExampleFixedError` always represents the _lack_ of any exception
|
314
|
+
# so clearly when we are transitioning to a `MultipleExceptionError`, it makes sense to
|
315
|
+
# ignore it.
|
316
|
+
return if Pending::PendingExampleFixedError === exception
|
317
|
+
|
318
|
+
all_exceptions << exception
|
319
|
+
|
320
|
+
if exception.class.name =~ /RSpec/
|
321
|
+
failures << exception
|
322
|
+
else
|
323
|
+
other_errors << exception
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Provides a way to force `ex` to be something that satisfies the multiple
|
328
|
+
# exception error interface. If it already satisfies it, it will be returned;
|
329
|
+
# otherwise it will wrap it in a `MultipleExceptionError`.
|
330
|
+
# @private
|
331
|
+
def self.for(ex)
|
332
|
+
return ex if self === ex
|
333
|
+
MultipleExceptionError.new(ex)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
include InterfaceTag
|
338
|
+
|
339
|
+
# @return [Array<Exception>] The list of failures.
|
340
|
+
attr_reader :failures
|
341
|
+
|
342
|
+
# @return [Array<Exception>] The list of other errors.
|
343
|
+
attr_reader :other_errors
|
344
|
+
|
345
|
+
# @return [Array<Exception>] The list of failures and other exceptions, combined.
|
346
|
+
attr_reader :all_exceptions
|
347
|
+
|
348
|
+
# @return [Hash] Metadata used by RSpec for formatting purposes.
|
349
|
+
attr_reader :aggregation_metadata
|
350
|
+
|
351
|
+
# @return [nil] Provided only for interface compatibility with
|
352
|
+
# `RSpec::Expectations::MultipleExpectationsNotMetError`.
|
353
|
+
attr_reader :aggregation_block_label
|
354
|
+
|
355
|
+
# @param exceptions [Array<Exception>] The initial list of exceptions.
|
356
|
+
def initialize(*exceptions)
|
357
|
+
super()
|
358
|
+
|
359
|
+
@failures = []
|
360
|
+
@other_errors = []
|
361
|
+
@all_exceptions = []
|
362
|
+
@aggregation_metadata = { :hide_backtrace => true }
|
363
|
+
@aggregation_block_label = nil
|
364
|
+
|
365
|
+
exceptions.each { |e| add e }
|
366
|
+
end
|
367
|
+
|
368
|
+
# @return [String] Combines all the exception messages into a single string.
|
369
|
+
# @note RSpec does not actually use this -- instead it formats each exception
|
370
|
+
# individually.
|
371
|
+
def message
|
372
|
+
all_exceptions.map(&:message).join("\n\n")
|
373
|
+
end
|
374
|
+
|
375
|
+
# @return [String] A summary of the failure, including the block label and a count of failures.
|
376
|
+
def summary
|
377
|
+
"Got #{exception_count_description}"
|
378
|
+
end
|
379
|
+
|
380
|
+
# return [String] A description of the failure/error counts.
|
381
|
+
def exception_count_description
|
382
|
+
failure_count = Formatters::Helpers.pluralize(failures.size, "failure")
|
383
|
+
return failure_count if other_errors.empty?
|
384
|
+
error_count = Formatters::Helpers.pluralize(other_errors.size, "other error")
|
385
|
+
"#{failure_count} and #{error_count}"
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|