rspec-core 3.3.0 → 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/.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)