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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Changelog.md +75 -0
  5. data/README.md +137 -20
  6. data/lib/rspec/autorun.rb +1 -0
  7. data/lib/rspec/core.rb +8 -16
  8. data/lib/rspec/core/backtrace_formatter.rb +1 -3
  9. data/lib/rspec/core/bisect/coordinator.rb +66 -0
  10. data/lib/rspec/core/bisect/example_minimizer.rb +130 -0
  11. data/lib/rspec/core/bisect/runner.rb +139 -0
  12. data/lib/rspec/core/bisect/server.rb +61 -0
  13. data/lib/rspec/core/bisect/subset_enumerator.rb +39 -0
  14. data/lib/rspec/core/configuration.rb +134 -5
  15. data/lib/rspec/core/configuration_options.rb +21 -10
  16. data/lib/rspec/core/example.rb +84 -50
  17. data/lib/rspec/core/example_group.rb +46 -18
  18. data/lib/rspec/core/example_status_persister.rb +235 -0
  19. data/lib/rspec/core/filter_manager.rb +43 -28
  20. data/lib/rspec/core/flat_map.rb +2 -0
  21. data/lib/rspec/core/formatters.rb +30 -20
  22. data/lib/rspec/core/formatters/base_text_formatter.rb +1 -0
  23. data/lib/rspec/core/formatters/bisect_formatter.rb +68 -0
  24. data/lib/rspec/core/formatters/bisect_progress_formatter.rb +115 -0
  25. data/lib/rspec/core/formatters/deprecation_formatter.rb +0 -1
  26. data/lib/rspec/core/formatters/documentation_formatter.rb +0 -4
  27. data/lib/rspec/core/formatters/exception_presenter.rb +389 -0
  28. data/lib/rspec/core/formatters/fallback_message_formatter.rb +28 -0
  29. data/lib/rspec/core/formatters/helpers.rb +22 -2
  30. data/lib/rspec/core/formatters/html_formatter.rb +1 -4
  31. data/lib/rspec/core/formatters/html_printer.rb +2 -6
  32. data/lib/rspec/core/formatters/json_formatter.rb +6 -4
  33. data/lib/rspec/core/formatters/snippet_extractor.rb +12 -7
  34. data/lib/rspec/core/hooks.rb +8 -2
  35. data/lib/rspec/core/memoized_helpers.rb +77 -17
  36. data/lib/rspec/core/metadata.rb +24 -10
  37. data/lib/rspec/core/metadata_filter.rb +16 -3
  38. data/lib/rspec/core/mutex.rb +63 -0
  39. data/lib/rspec/core/notifications.rb +84 -189
  40. data/lib/rspec/core/option_parser.rb +105 -32
  41. data/lib/rspec/core/ordering.rb +28 -25
  42. data/lib/rspec/core/profiler.rb +32 -0
  43. data/lib/rspec/core/project_initializer/spec/spec_helper.rb +6 -1
  44. data/lib/rspec/core/rake_task.rb +6 -20
  45. data/lib/rspec/core/reentrant_mutex.rb +52 -0
  46. data/lib/rspec/core/reporter.rb +65 -17
  47. data/lib/rspec/core/runner.rb +38 -14
  48. data/lib/rspec/core/set.rb +49 -0
  49. data/lib/rspec/core/shared_example_group.rb +3 -1
  50. data/lib/rspec/core/shell_escape.rb +49 -0
  51. data/lib/rspec/core/version.rb +1 -1
  52. data/lib/rspec/core/world.rb +31 -20
  53. metadata +35 -7
  54. metadata.gz.sig +0 -0
  55. data/lib/rspec/core/backport_random.rb +0 -339
@@ -68,6 +68,7 @@ module RSpec
68
68
 
69
69
  output.puts
70
70
 
71
+ output.flush
71
72
  output.close unless output == $stdout
72
73
  end
73
74
  end
@@ -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
@@ -1,5 +1,4 @@
1
1
  RSpec::Support.require_rspec_core "formatters/helpers"
2
- require 'set'
3
2
 
4
3
  module RSpec
5
4
  module Core
@@ -64,10 +64,6 @@ module RSpec
64
64
  def current_indentation
65
65
  ' ' * @group_level
66
66
  end
67
-
68
- def example_group_chain
69
- example_group.parent_groups.reverse
70
- end
71
67
  end
72
68
  end
73
69
  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