rspec-core 3.3.0 → 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/.document +1 -1
  4. data/.yardopts +1 -1
  5. data/Changelog.md +88 -0
  6. data/{License.txt → LICENSE.md} +6 -5
  7. data/README.md +18 -3
  8. data/lib/rspec/core/bisect/example_minimizer.rb +78 -39
  9. data/lib/rspec/core/configuration.rb +87 -25
  10. data/lib/rspec/core/configuration_options.rb +1 -1
  11. data/lib/rspec/core/example.rb +55 -7
  12. data/lib/rspec/core/example_group.rb +28 -8
  13. data/lib/rspec/core/example_status_persister.rb +16 -16
  14. data/lib/rspec/core/formatters/bisect_progress_formatter.rb +44 -15
  15. data/lib/rspec/core/formatters/exception_presenter.rb +150 -59
  16. data/lib/rspec/core/formatters/helpers.rb +1 -1
  17. data/lib/rspec/core/formatters/html_formatter.rb +3 -3
  18. data/lib/rspec/core/formatters/html_printer.rb +2 -3
  19. data/lib/rspec/core/formatters/html_snippet_extractor.rb +116 -0
  20. data/lib/rspec/core/formatters/protocol.rb +9 -0
  21. data/lib/rspec/core/formatters/snippet_extractor.rb +124 -97
  22. data/lib/rspec/core/formatters.rb +2 -1
  23. data/lib/rspec/core/hooks.rb +2 -2
  24. data/lib/rspec/core/memoized_helpers.rb +2 -2
  25. data/lib/rspec/core/metadata.rb +3 -2
  26. data/lib/rspec/core/metadata_filter.rb +11 -6
  27. data/lib/rspec/core/notifications.rb +3 -2
  28. data/lib/rspec/core/option_parser.rb +22 -4
  29. data/lib/rspec/core/project_initializer/spec/spec_helper.rb +2 -2
  30. data/lib/rspec/core/rake_task.rb +12 -3
  31. data/lib/rspec/core/reporter.rb +18 -2
  32. data/lib/rspec/core/ruby_project.rb +1 -1
  33. data/lib/rspec/core/shared_example_group.rb +2 -0
  34. data/lib/rspec/core/source/location.rb +13 -0
  35. data/lib/rspec/core/source/node.rb +93 -0
  36. data/lib/rspec/core/source/syntax_highlighter.rb +71 -0
  37. data/lib/rspec/core/source/token.rb +43 -0
  38. data/lib/rspec/core/source.rb +76 -0
  39. data/lib/rspec/core/version.rb +1 -1
  40. data/lib/rspec/core/world.rb +25 -6
  41. data.tar.gz.sig +0 -0
  42. metadata +14 -11
  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
@@ -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
@@ -558,6 +605,7 @@ module RSpec
558
605
  class SuiteHookContext < Example
559
606
  def initialize
560
607
  super(AnonymousExampleGroup, "", {})
608
+ @example_group_instance = AnonymousExampleGroup.new
561
609
  end
562
610
 
563
611
  # rubocop:disable Style/AccessorMethodName
@@ -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
@@ -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)