rspec-core 3.3.2 → 3.4.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 (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.document +1 -1
  5. data/.yardopts +1 -1
  6. data/Changelog.md +69 -0
  7. data/{License.txt → LICENSE.md} +6 -5
  8. data/README.md +18 -3
  9. data/lib/rspec/core/bisect/example_minimizer.rb +78 -39
  10. data/lib/rspec/core/configuration.rb +87 -25
  11. data/lib/rspec/core/configuration_options.rb +1 -1
  12. data/lib/rspec/core/example.rb +54 -7
  13. data/lib/rspec/core/example_group.rb +28 -8
  14. data/lib/rspec/core/example_status_persister.rb +16 -16
  15. data/lib/rspec/core/formatters.rb +1 -0
  16. data/lib/rspec/core/formatters/bisect_progress_formatter.rb +44 -15
  17. data/lib/rspec/core/formatters/exception_presenter.rb +146 -59
  18. data/lib/rspec/core/formatters/helpers.rb +1 -1
  19. data/lib/rspec/core/formatters/html_formatter.rb +2 -2
  20. data/lib/rspec/core/formatters/html_printer.rb +2 -3
  21. data/lib/rspec/core/formatters/html_snippet_extractor.rb +116 -0
  22. data/lib/rspec/core/formatters/protocol.rb +9 -0
  23. data/lib/rspec/core/formatters/snippet_extractor.rb +124 -97
  24. data/lib/rspec/core/hooks.rb +2 -2
  25. data/lib/rspec/core/memoized_helpers.rb +2 -2
  26. data/lib/rspec/core/metadata.rb +3 -2
  27. data/lib/rspec/core/metadata_filter.rb +11 -6
  28. data/lib/rspec/core/notifications.rb +3 -2
  29. data/lib/rspec/core/option_parser.rb +22 -4
  30. data/lib/rspec/core/project_initializer/spec/spec_helper.rb +2 -2
  31. data/lib/rspec/core/rake_task.rb +12 -3
  32. data/lib/rspec/core/reporter.rb +18 -2
  33. data/lib/rspec/core/ruby_project.rb +1 -1
  34. data/lib/rspec/core/shared_example_group.rb +2 -0
  35. data/lib/rspec/core/source.rb +76 -0
  36. data/lib/rspec/core/source/location.rb +13 -0
  37. data/lib/rspec/core/source/node.rb +93 -0
  38. data/lib/rspec/core/source/syntax_highlighter.rb +71 -0
  39. data/lib/rspec/core/source/token.rb +43 -0
  40. data/lib/rspec/core/version.rb +1 -1
  41. data/lib/rspec/core/world.rb +25 -6
  42. metadata +12 -9
  43. metadata.gz.sig +0 -0
  44. data/lib/rspec/core/bisect/subset_enumerator.rb +0 -39
  45. data/lib/rspec/core/mutex.rb +0 -63
  46. data/lib/rspec/core/reentrant_mutex.rb +0 -52
@@ -64,7 +64,7 @@ module RSpec
64
64
  end
65
65
 
66
66
  def order(keys)
67
- OPTIONS_ORDER.reverse.each do |key|
67
+ OPTIONS_ORDER.reverse_each do |key|
68
68
  keys.unshift(key) if keys.delete(key)
69
69
  end
70
70
  keys
@@ -118,6 +118,30 @@ module RSpec
118
118
  @id ||= Metadata.id_from(metadata)
119
119
  end
120
120
 
121
+ # @private
122
+ def self.parse_id(id)
123
+ # http://rubular.com/r/OMZSAPcAfn
124
+ id.match(/\A(.*?)(?:\[([\d\s:,]+)\])?\z/).captures
125
+ end
126
+
127
+ # Duplicates the example and overrides metadata with the provided
128
+ # hash.
129
+ #
130
+ # @param metadata_overrides [Hash] the hash to override the example metadata
131
+ # @return [Example] a duplicate of the example with modified metadata
132
+ def duplicate_with(metadata_overrides={})
133
+ new_metadata = metadata.clone.merge(metadata_overrides)
134
+
135
+ RSpec::Core::Metadata::RESERVED_KEYS.each do |reserved_key|
136
+ new_metadata.delete reserved_key
137
+ end
138
+
139
+ # don't clone the example group because the new example
140
+ # must belong to the same example group (not a clone).
141
+ Example.new(example_group, description.clone,
142
+ new_metadata, new_metadata[:block])
143
+ end
144
+
121
145
  # @attr_reader
122
146
  #
123
147
  # Returns the first exception raised in the context of running this
@@ -170,6 +194,12 @@ module RSpec
170
194
  @reporter = RSpec::Core::NullReporter
171
195
  end
172
196
 
197
+ # Provide a human-readable representation of this class
198
+ def inspect
199
+ "#<#{self.class.name} #{description.inspect}>"
200
+ end
201
+ alias to_s inspect
202
+
173
203
  # @return [RSpec::Core::Reporter] the current reporter for the example
174
204
  attr_reader :reporter
175
205
 
@@ -215,14 +245,14 @@ module RSpec
215
245
  rescue Pending::SkipDeclaredInExample
216
246
  # no-op, required metadata has already been set by the `skip`
217
247
  # method.
218
- rescue Exception => e
248
+ rescue AllExceptionsExcludingDangerousOnesOnRubiesThatAllowIt => e
219
249
  set_exception(e)
220
250
  ensure
221
251
  run_after_example
222
252
  end
223
253
  end
224
254
  end
225
- rescue Exception => e
255
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
226
256
  set_exception(e)
227
257
  ensure
228
258
  @example_group_instance = nil # if you love something... let it go
@@ -233,6 +263,20 @@ module RSpec
233
263
  RSpec.current_example = nil
234
264
  end
235
265
 
266
+ if RSpec::Support::Ruby.jruby? || RUBY_VERSION.to_f < 1.9
267
+ # :nocov:
268
+ # For some reason, rescuing `Support::AllExceptionsExceptOnesWeMustNotRescue`
269
+ # in place of `Exception` above can cause the exit status to be the wrong
270
+ # thing. I have no idea why. See:
271
+ # https://github.com/rspec/rspec-core/pull/2063#discussion_r38284978
272
+ # @private
273
+ AllExceptionsExcludingDangerousOnesOnRubiesThatAllowIt = Exception
274
+ # :nocov:
275
+ else
276
+ # @private
277
+ AllExceptionsExcludingDangerousOnesOnRubiesThatAllowIt = Support::AllExceptionsExceptOnesWeMustNotRescue
278
+ end
279
+
236
280
  # Wraps both a `Proc` and an {Example} for use in {Hooks#around
237
281
  # around} hooks. In around hooks we need to yield this special
238
282
  # kind of object (rather than the raw {Example}) because when
@@ -256,13 +300,15 @@ module RSpec
256
300
  attr_reader :example
257
301
 
258
302
  Example.public_instance_methods(false).each do |name|
259
- next if name.to_sym == :run || name.to_sym == :inspect
303
+ name_sym = name.to_sym
304
+ next if name_sym == :run || name_sym == :inspect || name_sym == :to_s
260
305
 
261
306
  define_method(name) { |*a, &b| @example.__send__(name, *a, &b) }
262
307
  end
263
308
 
264
309
  Proc.public_instance_methods(false).each do |name|
265
- next if name.to_sym == :call || name.to_sym == :inspect || name.to_sym == :to_proc
310
+ name_sym = name.to_sym
311
+ next if name_sym == :call || name_sym == :inspect || name_sym == :to_s || name_sym == :to_proc
266
312
 
267
313
  define_method(name) { |*a, &b| @proc.__send__(name, *a, &b) }
268
314
  end
@@ -386,7 +432,7 @@ module RSpec
386
432
 
387
433
  def with_around_example_hooks
388
434
  hooks.run(:around, :example, self) { yield }
389
- rescue Exception => e
435
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
390
436
  set_exception(e)
391
437
  end
392
438
 
@@ -398,6 +444,7 @@ module RSpec
398
444
  def finish(reporter)
399
445
  pending_message = execution_result.pending_message
400
446
 
447
+ reporter.example_finished(self)
401
448
  if @exception
402
449
  record_finished :failed
403
450
  execution_result.exception = @exception
@@ -442,7 +489,7 @@ module RSpec
442
489
 
443
490
  def verify_mocks
444
491
  @example_group_instance.verify_mocks_for_rspec if mocks_need_verification?
445
- rescue Exception => e
492
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
446
493
  set_exception(e)
447
494
  end
448
495
 
@@ -461,7 +508,7 @@ module RSpec
461
508
 
462
509
  def generate_description
463
510
  RSpec::Matchers.generated_description
464
- rescue Exception => e
511
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
465
512
  location_description + " (Got an error when generating description " \
466
513
  "from matcher: #{e.class}: #{e.message} -- #{e.backtrace.first})"
467
514
  end
@@ -2,6 +2,7 @@ RSpec::Support.require_rspec_support 'recursive_const_methods'
2
2
 
3
3
  module RSpec
4
4
  module Core
5
+ # rubocop:disable Metrics/ClassLength
5
6
  # ExampleGroup and {Example} are the main structural elements of
6
7
  # rspec-core. Consider this example:
7
8
  #
@@ -340,6 +341,27 @@ module RSpec
340
341
  find_and_eval_shared("examples", name, caller.first, *args, &block)
341
342
  end
342
343
 
344
+ # Clear memoized values when adding/removing examples
345
+ # @private
346
+ def self.reset_memoized
347
+ @descendant_filtered_examples = nil
348
+ @_descendants = nil
349
+ @parent_groups = nil
350
+ @declaration_line_numbers = nil
351
+ end
352
+
353
+ # Adds an example to the example group
354
+ def self.add_example(example)
355
+ reset_memoized
356
+ examples << example
357
+ end
358
+
359
+ # Removes an example from the example group
360
+ def self.remove_example(example)
361
+ reset_memoized
362
+ examples.delete example
363
+ end
364
+
343
365
  # @private
344
366
  def self.find_and_eval_shared(label, name, inclusion_location, *args, &customization_block)
345
367
  shared_block = RSpec.world.shared_example_group_registry.find(parent_groups, name)
@@ -524,9 +546,9 @@ module RSpec
524
546
  rescue Pending::SkipDeclaredInExample => ex
525
547
  for_filtered_examples(reporter) { |example| example.skip_with_exception(reporter, ex) }
526
548
  true
527
- rescue Exception => ex
528
- RSpec.world.wants_to_quit = true if fail_fast?
549
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => ex
529
550
  for_filtered_examples(reporter) { |example| example.fail_with_exception(reporter, ex) }
551
+ RSpec.world.wants_to_quit = true if reporter.fail_fast_limit_met?
530
552
  false
531
553
  ensure
532
554
  run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks
@@ -557,7 +579,9 @@ module RSpec
557
579
  instance = new(example.inspect_output)
558
580
  set_ivars(instance, before_context_ivars)
559
581
  succeeded = example.run(instance, reporter)
560
- RSpec.world.wants_to_quit = true if fail_fast? && !succeeded
582
+ if !succeeded && reporter.fail_fast_limit_met?
583
+ RSpec.world.wants_to_quit = true
584
+ end
561
585
  succeeded
562
586
  end.all?
563
587
  end
@@ -574,11 +598,6 @@ module RSpec
574
598
  false
575
599
  end
576
600
 
577
- # @private
578
- def self.fail_fast?
579
- RSpec.configuration.fail_fast?
580
- end
581
-
582
601
  # @private
583
602
  def self.declaration_line_numbers
584
603
  @declaration_line_numbers ||= [metadata[:line_number]] +
@@ -671,6 +690,7 @@ module RSpec
671
690
  super
672
691
  end
673
692
  end
693
+ # rubocop:enable Metrics/ClassLength
674
694
 
675
695
  # @private
676
696
  # Unnamed example group used by `SuiteHookContext`.
@@ -21,24 +21,28 @@ module RSpec
21
21
  end
22
22
 
23
23
  def persist
24
- write dumped_statuses
24
+ RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name))
25
+ File.open(@file_name, File::RDWR | File::CREAT) do |f|
26
+ # lock the file while reading / persisting to avoid a race
27
+ # condition where parallel or unrelated spec runs race to
28
+ # update the same file
29
+ f.flock(File::LOCK_EX)
30
+ unparsed_previous_runs = f.read
31
+ f.rewind
32
+ f.write(dump_statuses(unparsed_previous_runs))
33
+ f.flush
34
+ f.truncate(f.pos)
35
+ end
25
36
  end
26
37
 
27
38
  private
28
39
 
29
- def write(statuses)
30
- RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name))
31
- File.open(@file_name, "w") { |f| f.write(statuses) }
32
- end
33
-
34
- def dumped_statuses
40
+ def dump_statuses(unparsed_previous_runs)
41
+ statuses_from_previous_runs = ExampleStatusParser.parse(unparsed_previous_runs)
42
+ merged_statuses = ExampleStatusMerger.merge(statuses_from_this_run, statuses_from_previous_runs)
35
43
  ExampleStatusDumper.dump(merged_statuses)
36
44
  end
37
45
 
38
- def merged_statuses
39
- ExampleStatusMerger.merge(statuses_from_this_run, statuses_from_previous_runs)
40
- end
41
-
42
46
  def statuses_from_this_run
43
47
  @examples.map do |ex|
44
48
  result = ex.execution_result
@@ -50,10 +54,6 @@ module RSpec
50
54
  }
51
55
  end
52
56
  end
53
-
54
- def statuses_from_previous_runs
55
- self.class.load_from(@file_name)
56
- end
57
57
  end
58
58
 
59
59
  # Merges together a list of example statuses from this run
@@ -130,7 +130,7 @@ module RSpec
130
130
  end
131
131
 
132
132
  def sort_value_from(example)
133
- file, scoped_id = example.fetch(:example_id).split(Configuration::ON_SQUARE_BRACKETS)
133
+ file, scoped_id = Example.parse_id(example.fetch(:example_id))
134
134
  [file, *scoped_id.split(":").map(&method(:Integer))]
135
135
  end
136
136
  end
@@ -73,6 +73,7 @@ module RSpec::Core::Formatters
73
73
  autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter'
74
74
  autoload :JsonFormatter, 'rspec/core/formatters/json_formatter'
75
75
  autoload :BisectFormatter, 'rspec/core/formatters/bisect_formatter'
76
+ autoload :ExceptionPresenter, 'rspec/core/formatters/exception_presenter'
76
77
 
77
78
  # Register the formatter class
78
79
  # @param formatter_class [Class] formatter class to register
@@ -9,10 +9,14 @@ module RSpec
9
9
  # We've named all events with a `bisect_` prefix to prevent naming collisions.
10
10
  Formatters.register self, :bisect_starting, :bisect_original_run_complete,
11
11
  :bisect_round_started, :bisect_individual_run_complete,
12
- :bisect_round_finished, :bisect_complete, :bisect_repro_command,
13
- :bisect_failed, :bisect_aborted
12
+ :bisect_complete, :bisect_repro_command,
13
+ :bisect_failed, :bisect_aborted,
14
+ :bisect_round_ignoring_ids, :bisect_round_detected_multiple_culprits,
15
+ :bisect_dependency_check_started, :bisect_dependency_check_passed,
16
+ :bisect_dependency_check_failed
14
17
 
15
18
  def bisect_starting(notification)
19
+ @round_count = 0
16
20
  options = notification.original_cli_args.join(' ')
17
21
  output.puts "Bisect started using options: #{options.inspect}"
18
22
  output.print "Running suite to find failures..."
@@ -26,17 +30,35 @@ module RSpec
26
30
  output.puts "Starting bisect with #{failures} and #{non_failures}."
27
31
  end
28
32
 
33
+ def bisect_dependency_check_started(_notification)
34
+ output.print "Checking that failure(s) are order-dependent.."
35
+ end
36
+
37
+ def bisect_dependency_check_passed(_notification)
38
+ output.puts " failure appears to be order-dependent"
39
+ end
40
+
41
+ def bisect_dependency_check_failed(_notification)
42
+ output.puts " failure is not order-dependent"
43
+ end
44
+
29
45
  def bisect_round_started(notification, include_trailing_space=true)
30
- search_desc = Helpers.pluralize(
31
- notification.subset_size, "non-failing example"
32
- )
46
+ @round_count += 1
47
+ range_desc = notification.candidate_range.description
33
48
 
34
- output.print "\nRound #{notification.round}: searching for #{search_desc}" \
35
- " (of #{notification.remaining_count}) to ignore:"
49
+ output.print "\nRound #{@round_count}: bisecting over non-failing #{range_desc}"
36
50
  output.print " " if include_trailing_space
37
51
  end
38
52
 
39
- def bisect_round_finished(notification)
53
+ def bisect_round_ignoring_ids(notification)
54
+ range_desc = notification.ignore_range.description
55
+
56
+ output.print " ignoring #{range_desc}"
57
+ output.print " (#{Helpers.format_duration(notification.duration)})"
58
+ end
59
+
60
+ def bisect_round_detected_multiple_culprits(notification)
61
+ output.print " multiple culprits detected - splitting candidates"
40
62
  output.print " (#{Helpers.format_duration(notification.duration)})"
41
63
  end
42
64
 
@@ -71,7 +93,7 @@ module RSpec
71
93
  # Designed to provide details for us when we need to troubleshoot bisect bugs.
72
94
  class BisectDebugFormatter < BisectProgressFormatter
73
95
  Formatters.register self, :bisect_original_run_complete, :bisect_individual_run_start,
74
- :bisect_individual_run_complete, :bisect_round_finished, :bisect_ignoring_ids
96
+ :bisect_individual_run_complete, :bisect_round_ignoring_ids
75
97
 
76
98
  def bisect_original_run_complete(notification)
77
99
  output.puts " (#{Helpers.format_duration(notification.duration)})"
@@ -88,20 +110,27 @@ module RSpec
88
110
  output.print " (#{Helpers.format_duration(notification.duration)})"
89
111
  end
90
112
 
91
- def bisect_round_started(notification)
92
- super(notification, false)
113
+ def bisect_dependency_check_passed(_notification)
114
+ output.print "\n - Failure appears to be order-dependent"
93
115
  end
94
116
 
95
- def bisect_round_finished(notification)
96
- output.print "\n - Round finished"
97
- super
117
+ def bisect_dependency_check_failed(_notification)
118
+ output.print "\n - Failure is not order-dependent"
98
119
  end
99
120
 
100
- def bisect_ignoring_ids(notification)
121
+ def bisect_round_started(notification)
122
+ super(notification, false)
123
+ end
124
+
125
+ def bisect_round_ignoring_ids(notification)
101
126
  output.print "\n - #{describe_ids 'Examples we can safely ignore', notification.ids_to_ignore}"
102
127
  output.print "\n - #{describe_ids 'Remaining non-failing examples', notification.remaining_ids}"
103
128
  end
104
129
 
130
+ def bisect_round_detected_multiple_culprits(_notification)
131
+ output.print "\n - Multiple culprits detected - splitting candidates"
132
+ end
133
+
105
134
  private
106
135
 
107
136
  def describe_ids(description, ids)
@@ -1,3 +1,7 @@
1
+ # encoding: utf-8
2
+ RSpec::Support.require_rspec_core "formatters/snippet_extractor"
3
+ RSpec::Support.require_rspec_support "encoded_string"
4
+
1
5
  module RSpec
2
6
  module Core
3
7
  module Formatters
@@ -11,7 +15,7 @@ module RSpec
11
15
  @exception = exception
12
16
  @example = example
13
17
  @message_color = options.fetch(:message_color) { RSpec.configuration.failure_color }
14
- @description = options.fetch(:description_formatter) { Proc.new { example.full_description } }.call(self)
18
+ @description = options.fetch(:description) { example.full_description }
15
19
  @detail_formatter = options.fetch(:detail_formatter) { Proc.new {} }
16
20
  @extra_detail_formatter = options.fetch(:extra_detail_formatter) { Proc.new {} }
17
21
  @backtrace_formatter = options.fetch(:backtrace_formatter) { RSpec.configuration.backtrace_formatter }
@@ -30,8 +34,36 @@ module RSpec
30
34
  end
31
35
  end
32
36
 
33
- def formatted_backtrace
34
- backtrace_formatter.format_backtrace(exception_backtrace, example.metadata)
37
+ def formatted_backtrace(exception=@exception)
38
+ backtrace_formatter.format_backtrace((exception.backtrace || []), example.metadata) +
39
+ formatted_cause(exception)
40
+ end
41
+
42
+ if RSpec::Support::RubyFeatures.supports_exception_cause?
43
+ def formatted_cause(exception)
44
+ last_cause = final_exception(exception)
45
+ cause = []
46
+
47
+ if exception.cause
48
+ cause << '------------------'
49
+ cause << '--- Caused by: ---'
50
+ cause << "#{exception_class_name(last_cause)}:" unless exception_class_name(last_cause) =~ /RSpec/
51
+
52
+ encoded_string(last_cause.message.to_s).split("\n").each do |line|
53
+ cause << " #{line}"
54
+ end
55
+
56
+ cause << (" #{backtrace_formatter.format_backtrace(last_cause.backtrace, example.metadata).first}")
57
+ end
58
+
59
+ cause
60
+ end
61
+ else
62
+ # :nocov:
63
+ def formatted_cause(_)
64
+ []
65
+ end
66
+ # :nocov:
35
67
  end
36
68
 
37
69
  def colorized_formatted_backtrace(colorizer=::RSpec::Core::Formatters::ConsoleCodes)
@@ -41,24 +73,31 @@ module RSpec
41
73
  end
42
74
 
43
75
  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)}"
76
+ lines = fully_formatted_lines(failure_number, colorizer)
77
+ lines.join("\n") << "\n"
50
78
  end
51
79
 
52
- def failure_slash_error_line
53
- @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
54
91
  end
55
92
 
56
93
  private
57
94
 
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}"
95
+ def final_exception(exception)
96
+ if exception.cause
97
+ final_exception(exception.cause)
98
+ else
99
+ exception
100
+ end
62
101
  end
63
102
 
64
103
  if String.method_defined?(:encoding)
@@ -80,23 +119,71 @@ module RSpec
80
119
  # :nocov:
81
120
  end
82
121
 
83
- def exception_class_name
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
+
137
+ def exception_class_name(exception=@exception)
84
138
  name = exception.class.name.to_s
85
139
  name = "(anonymous error class)" if name == ''
86
140
  name
87
141
  end
88
142
 
89
143
  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
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 << ''
99
150
  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 = SnippetExtractor.least_indentation_from(lines)
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
100
187
  end
101
188
 
102
189
  def add_shared_group_lines(lines, colorizer)
@@ -109,42 +196,41 @@ module RSpec
109
196
  lines
110
197
  end
111
198
 
112
- def read_failed_line
199
+ def read_failed_lines
113
200
  matching_line = find_failed_line
114
201
  unless matching_line
115
- return "Unable to find matching line from backtrace"
202
+ return ["Unable to find matching line from backtrace"]
116
203
  end
117
204
 
118
205
  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
206
+ max_line_count = RSpec.configuration.max_displayed_failure_line_count
207
+ lines = SnippetExtractor.extract_expression_lines_at(file_path, line_number.to_i, max_line_count)
208
+ RSpec.world.source_cache.syntax_highlighter.highlight(lines)
209
+ rescue SnippetExtractor::NoSuchFileError
210
+ ["Unable to find #{file_path} to read failed line"]
211
+ rescue SnippetExtractor::NoSuchLineError
212
+ ["Unable to find matching line in #{file_path}"]
126
213
  rescue SecurityError
127
- "Unable to read failed line"
214
+ ["Unable to read failed line"]
128
215
  end
129
216
 
130
217
  def find_failed_line
131
- example_path = example.metadata[:absolute_file_path].downcase
218
+ line_regex = RSpec.configuration.in_project_source_dir_regex
219
+ loaded_spec_files = RSpec.configuration.loaded_spec_files
220
+
132
221
  exception_backtrace.find do |line|
133
222
  next unless (line_path = line[/(.+?):(\d+)(|:\d+)/, 1])
134
- File.expand_path(line_path).downcase == example_path
135
- end
223
+ path = File.expand_path(line_path)
224
+ loaded_spec_files.include?(path) || path =~ line_regex
225
+ end || exception_backtrace.first
136
226
  end
137
227
 
138
- def formatted_message_and_backtrace(colorizer, indentation)
228
+ def formatted_message_and_backtrace(colorizer)
139
229
  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))
230
+ encoding = encoding_of("")
231
+ lines.map do |line|
232
+ RSpec::Support::EncodedString.new(line, encoding)
145
233
  end
146
-
147
- formatted
148
234
  end
149
235
 
150
236
  def exception_backtrace
@@ -180,9 +266,9 @@ module RSpec
180
266
  def pending_options
181
267
  if @execution_result.pending_fixed?
182
268
  {
183
- :description_formatter => Proc.new { "#{@example.full_description} FIXED" },
184
- :message_color => RSpec.configuration.fixed_color,
185
- :failure_lines => [
269
+ :description => "#{@example.full_description} FIXED",
270
+ :message_color => RSpec.configuration.fixed_color,
271
+ :failure_lines => [
186
272
  "Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised."
187
273
  ]
188
274
  }
@@ -205,8 +291,6 @@ module RSpec
205
291
  options[:message_color])
206
292
  )
207
293
 
208
- options[:description_formatter] &&= Proc.new {}
209
-
210
294
  return options unless exception.aggregation_metadata[:hide_backtrace]
211
295
  options[:backtrace_formatter] = EmptyBacktraceFormatter
212
296
  options
@@ -217,7 +301,7 @@ module RSpec
217
301
  end
218
302
 
219
303
  def multiple_exception_summarizer(exception, prior_detail_formatter, color)
220
- lambda do |example, colorizer, indentation|
304
+ lambda do |example, colorizer|
221
305
  summary = if exception.aggregation_metadata[:hide_backtrace]
222
306
  # Since the backtrace is hidden, the subfailures will come
223
307
  # immediately after this, and using `:` will read well.
@@ -230,27 +314,30 @@ module RSpec
230
314
 
231
315
  summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color)
232
316
  return summary unless prior_detail_formatter
233
- "#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}"
317
+ [
318
+ prior_detail_formatter.call(example, colorizer),
319
+ summary
320
+ ]
234
321
  end
235
322
  end
236
323
 
237
324
  def sub_failure_list_formatter(exception, message_color)
238
325
  common_backtrace_truncater = CommonBacktraceTruncater.new(exception)
239
326
 
240
- lambda do |failure_number, colorizer, indentation|
241
- exception.all_exceptions.each_with_index.map do |failure, index|
327
+ lambda do |failure_number, colorizer|
328
+ FlatMap.flat_map(exception.all_exceptions.each_with_index) do |failure, index|
242
329
  options = with_multiple_error_options_as_needed(
243
330
  failure,
244
- :description_formatter => :failure_slash_error_line.to_proc,
245
- :indentation => indentation.length,
331
+ :description => nil,
332
+ :indentation => 0,
246
333
  :message_color => message_color || RSpec.configuration.failure_color,
247
334
  :skip_shared_group_trace => true
248
335
  )
249
336
 
250
337
  failure = common_backtrace_truncater.with_truncated_backtrace(failure)
251
338
  presenter = ExceptionPresenter.new(failure, @example, options)
252
- presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer)
253
- end.join
339
+ presenter.fully_formatted_lines("#{failure_number}.#{index + 1}", colorizer)
340
+ end
254
341
  end
255
342
  end
256
343