rspec-core 3.3.2 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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