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
@@ -0,0 +1,235 @@
1
+ RSpec::Support.require_rspec_support "directory_maker"
2
+
3
+ module RSpec
4
+ module Core
5
+ # Persists example ids and their statuses so that we can filter
6
+ # to just the ones that failed the last time they ran.
7
+ # @private
8
+ class ExampleStatusPersister
9
+ def self.load_from(file_name)
10
+ return [] unless File.exist?(file_name)
11
+ ExampleStatusParser.parse(File.read(file_name))
12
+ end
13
+
14
+ def self.persist(examples, file_name)
15
+ new(examples, file_name).persist
16
+ end
17
+
18
+ def initialize(examples, file_name)
19
+ @examples = examples
20
+ @file_name = file_name
21
+ end
22
+
23
+ def persist
24
+ write dumped_statuses
25
+ end
26
+
27
+ private
28
+
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
35
+ ExampleStatusDumper.dump(merged_statuses)
36
+ end
37
+
38
+ def merged_statuses
39
+ ExampleStatusMerger.merge(statuses_from_this_run, statuses_from_previous_runs)
40
+ end
41
+
42
+ def statuses_from_this_run
43
+ @examples.map do |ex|
44
+ result = ex.execution_result
45
+
46
+ {
47
+ :example_id => ex.id,
48
+ :status => result.status ? result.status.to_s : Configuration::UNKNOWN_STATUS,
49
+ :run_time => result.run_time ? Formatters::Helpers.format_duration(result.run_time) : ""
50
+ }
51
+ end
52
+ end
53
+
54
+ def statuses_from_previous_runs
55
+ self.class.load_from(@file_name)
56
+ end
57
+ end
58
+
59
+ # Merges together a list of example statuses from this run
60
+ # and a list from previous runs (presumably loaded from disk).
61
+ # Each example status object is expected to be a hash with
62
+ # at least an `:example_id` and a `:status` key. Examples that
63
+ # were loaded but not executed (due to filtering, `--fail-fast`
64
+ # or whatever) should have a `:status` of `UNKNOWN_STATUS`.
65
+ #
66
+ # This willl produce a new list that:
67
+ # - Will be missing examples from previous runs that we know for sure
68
+ # no longer exist.
69
+ # - Will have the latest known status for any examples that either
70
+ # definitively do exist or may still exist.
71
+ # - Is sorted by file name and example definition order, so that
72
+ # the saved file is easily scannable if users want to inspect it.
73
+ # @private
74
+ class ExampleStatusMerger
75
+ def self.merge(this_run, from_previous_runs)
76
+ new(this_run, from_previous_runs).merge
77
+ end
78
+
79
+ def initialize(this_run, from_previous_runs)
80
+ @this_run = hash_from(this_run)
81
+ @from_previous_runs = hash_from(from_previous_runs)
82
+ @file_exists_cache = Hash.new { |hash, file| hash[file] = File.exist?(file) }
83
+ end
84
+
85
+ def merge
86
+ delete_previous_examples_that_no_longer_exist
87
+
88
+ @this_run.merge(@from_previous_runs) do |_ex_id, new, old|
89
+ new.fetch(:status) == Configuration::UNKNOWN_STATUS ? old : new
90
+ end.values.sort_by(&method(:sort_value_from))
91
+ end
92
+
93
+ private
94
+
95
+ def hash_from(example_list)
96
+ example_list.inject({}) do |hash, example|
97
+ hash[example.fetch(:example_id)] = example
98
+ hash
99
+ end
100
+ end
101
+
102
+ def delete_previous_examples_that_no_longer_exist
103
+ @from_previous_runs.delete_if do |ex_id, _|
104
+ example_must_no_longer_exist?(ex_id)
105
+ end
106
+ end
107
+
108
+ def example_must_no_longer_exist?(ex_id)
109
+ # Obviously, it exists if it was loaded for this spec run...
110
+ return false if @this_run.key?(ex_id)
111
+
112
+ spec_file = spec_file_from(ex_id)
113
+
114
+ # `this_run` includes examples that were loaded but not executed.
115
+ # Given that, if the spec file for this example was loaded,
116
+ # but the id does not still exist, it's safe to assume that
117
+ # the example must no longer exist.
118
+ return true if loaded_spec_files.include?(spec_file)
119
+
120
+ # The example may still exist as long as the file exists...
121
+ !@file_exists_cache[spec_file]
122
+ end
123
+
124
+ def loaded_spec_files
125
+ @loaded_spec_files ||= Set.new(@this_run.keys.map(&method(:spec_file_from)))
126
+ end
127
+
128
+ def spec_file_from(ex_id)
129
+ ex_id.split("[").first
130
+ end
131
+
132
+ def sort_value_from(example)
133
+ file, scoped_id = example.fetch(:example_id).split(Configuration::ON_SQUARE_BRACKETS)
134
+ [file, *scoped_id.split(":").map(&method(:Integer))]
135
+ end
136
+ end
137
+
138
+ # Dumps a list of hashes in a pretty, human readable format
139
+ # for later parsing. The hashes are expected to have symbol
140
+ # keys and string values, and each hash should have the same
141
+ # set of keys.
142
+ # @private
143
+ class ExampleStatusDumper
144
+ def self.dump(examples)
145
+ new(examples).dump
146
+ end
147
+
148
+ def initialize(examples)
149
+ @examples = examples
150
+ end
151
+
152
+ def dump
153
+ return nil if @examples.empty?
154
+ (formatted_header_rows + formatted_value_rows).join("\n") << "\n"
155
+ end
156
+
157
+ private
158
+
159
+ def formatted_header_rows
160
+ @formatted_header_rows ||= begin
161
+ dividers = column_widths.map { |w| "-" * w }
162
+ [formatted_row_from(headers.map(&:to_s)), formatted_row_from(dividers)]
163
+ end
164
+ end
165
+
166
+ def formatted_value_rows
167
+ @foramtted_value_rows ||= rows.map do |row|
168
+ formatted_row_from(row)
169
+ end
170
+ end
171
+
172
+ def rows
173
+ @rows ||= @examples.map { |ex| ex.values_at(*headers) }
174
+ end
175
+
176
+ def formatted_row_from(row_values)
177
+ padded_values = row_values.each_with_index.map do |value, index|
178
+ value.ljust(column_widths[index])
179
+ end
180
+
181
+ padded_values.join(" | ") << " |"
182
+ end
183
+
184
+ def headers
185
+ @headers ||= @examples.first.keys
186
+ end
187
+
188
+ def column_widths
189
+ @column_widths ||= begin
190
+ value_sets = rows.transpose
191
+
192
+ headers.each_with_index.map do |header, index|
193
+ values = value_sets[index] << header.to_s
194
+ values.map(&:length).max
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ # Parses a string that has been previously dumped by ExampleStatusDumper.
201
+ # Note that this parser is a bit naive in that it does a simple split on
202
+ # "\n" and " | ", with no concern for handling escaping. For now, that's
203
+ # OK because the values we plan to persist (example id, status, and perhaps
204
+ # example duration) are highly unlikely to contain "\n" or " | " -- after
205
+ # all, who puts those in file names?
206
+ # @private
207
+ class ExampleStatusParser
208
+ def self.parse(string)
209
+ new(string).parse
210
+ end
211
+
212
+ def initialize(string)
213
+ @header_line, _, *@row_lines = string.lines.to_a
214
+ end
215
+
216
+ def parse
217
+ @row_lines.map { |line| parse_row(line) }
218
+ end
219
+
220
+ private
221
+
222
+ def parse_row(line)
223
+ Hash[headers.zip(split_line(line))]
224
+ end
225
+
226
+ def headers
227
+ @headers ||= split_line(@header_line).grep(/\S/).map(&:to_sym)
228
+ end
229
+
230
+ def split_line(line)
231
+ line.split(/\s+\|\s+?/, -1)
232
+ end
233
+ end
234
+ end
235
+ end
@@ -16,9 +16,15 @@ module RSpec
16
16
  # locations is a hash of expanded paths to arrays of line
17
17
  # numbers to match against. e.g.
18
18
  # { "path/to/file.rb" => [37, 42] }
19
- locations = inclusions.delete(:locations) || Hash.new { |h, k| h[k] = [] }
20
- locations[File.expand_path(file_path)].push(*line_numbers)
21
- inclusions.add(:locations => locations)
19
+ add_path_to_arrays_filter(:locations, File.expand_path(file_path), line_numbers)
20
+ end
21
+
22
+ def add_ids(rerun_path, scoped_ids)
23
+ # ids is a hash of relative paths to arrays of ids
24
+ # to match against. e.g.
25
+ # { "./path/to/file.rb" => ["1:1", "2:4"] }
26
+ rerun_path = Metadata.relative_path(File.expand_path rerun_path)
27
+ add_path_to_arrays_filter(:ids, rerun_path, scoped_ids)
22
28
  end
23
29
 
24
30
  def empty?
@@ -26,16 +32,23 @@ module RSpec
26
32
  end
27
33
 
28
34
  def prune(examples)
35
+ # Semantically, this is unnecessary (the filtering below will return the empty
36
+ # array unmodified), but for perf reasons it's worth exiting early here. Users
37
+ # commonly have top-level examples groups that do not have any direct examples
38
+ # and instead have nested groups with examples. In that kind of situation,
39
+ # `examples` will be empty.
40
+ return examples if examples.empty?
41
+
29
42
  examples = prune_conditionally_filtered_examples(examples)
30
43
 
31
44
  if inclusions.standalone?
32
- examples.select { |e| include?(e) }
45
+ examples.select { |e| inclusions.include_example?(e) }
33
46
  else
34
- locations, other_inclusions = inclusions.partition_locations
47
+ locations, ids, non_scoped_inclusions = inclusions.split_file_scoped_rules
35
48
 
36
- examples.select do |e|
37
- priority_include?(e, locations) do
38
- !exclude?(e) && other_inclusions.include_example?(e)
49
+ examples.select do |ex|
50
+ file_scoped_include?(ex.metadata, ids, locations) do
51
+ !exclusions.include_example?(ex) && non_scoped_inclusions.include_example?(ex)
39
52
  end
40
53
  end
41
54
  end
@@ -67,12 +80,10 @@ module RSpec
67
80
 
68
81
  private
69
82
 
70
- def exclude?(example)
71
- exclusions.include_example?(example)
72
- end
73
-
74
- def include?(example)
75
- inclusions.include_example?(example)
83
+ def add_path_to_arrays_filter(filter_key, path, values)
84
+ filter = inclusions.delete(filter_key) || Hash.new { |h, k| h[k] = [] }
85
+ filter[path].concat(values)
86
+ inclusions.add(filter_key => filter)
76
87
  end
77
88
 
78
89
  def prune_conditionally_filtered_examples(examples)
@@ -87,9 +98,16 @@ module RSpec
87
98
  # and there is a `:slow => true` exclusion filter), but only for specs
88
99
  # defined in the same file as the location filters. Excluded specs in
89
100
  # other files should still be excluded.
90
- def priority_include?(example, locations)
91
- return yield if locations[example.metadata[:absolute_file_path]].empty?
92
- MetadataFilter.filter_applies?(:locations, locations, example.metadata)
101
+ def file_scoped_include?(ex_metadata, ids, locations)
102
+ no_id_filters = ids[ex_metadata[:rerun_file_path]].empty?
103
+ no_location_filters = locations[
104
+ File.expand_path(ex_metadata[:rerun_file_path])
105
+ ].empty?
106
+
107
+ return yield if no_location_filters && no_id_filters
108
+
109
+ MetadataFilter.filter_applies?(:ids, ids, ex_metadata) ||
110
+ MetadataFilter.filter_applies?(:locations, locations, ex_metadata)
93
111
  end
94
112
  end
95
113
 
@@ -174,17 +192,6 @@ module RSpec
174
192
  apply_standalone_filter(*args) || super
175
193
  end
176
194
 
177
- def use(*args)
178
- apply_standalone_filter(*args) || super
179
- end
180
-
181
- def partition_locations
182
- locations = @rules.fetch(:locations) { Hash.new([]) }
183
- other_inclusions = self.class.new(@rules.dup.tap { |r| r.delete(:locations) })
184
-
185
- return locations, other_inclusions
186
- end
187
-
188
195
  def include_example?(example)
189
196
  @rules.empty? || super
190
197
  end
@@ -193,6 +200,14 @@ module RSpec
193
200
  is_standalone_filter?(@rules)
194
201
  end
195
202
 
203
+ def split_file_scoped_rules
204
+ rules_dup = @rules.dup
205
+ locations = rules_dup.delete(:locations) { Hash.new([]) }
206
+ ids = rules_dup.delete(:ids) { Hash.new([]) }
207
+
208
+ return locations, ids, self.class.new(rules_dup)
209
+ end
210
+
196
211
  private
197
212
 
198
213
  def apply_standalone_filter(updated)
@@ -7,9 +7,11 @@ module RSpec
7
7
  array.flat_map(&block)
8
8
  end
9
9
  else # for 1.8.7
10
+ # :nocov:
10
11
  def flat_map(array, &block)
11
12
  array.map(&block).flatten(1)
12
13
  end
14
+ # :nocov:
13
15
  end
14
16
 
15
17
  module_function :flat_map
@@ -66,11 +66,13 @@ RSpec::Support.require_rspec_support "directory_maker"
66
66
  # @see RSpec::Core::Formatters::BaseTextFormatter
67
67
  # @see RSpec::Core::Reporter
68
68
  module RSpec::Core::Formatters
69
- autoload :DocumentationFormatter, 'rspec/core/formatters/documentation_formatter'
70
- autoload :HtmlFormatter, 'rspec/core/formatters/html_formatter'
71
- autoload :ProgressFormatter, 'rspec/core/formatters/progress_formatter'
72
- autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter'
73
- autoload :JsonFormatter, 'rspec/core/formatters/json_formatter'
69
+ autoload :DocumentationFormatter, 'rspec/core/formatters/documentation_formatter'
70
+ autoload :HtmlFormatter, 'rspec/core/formatters/html_formatter'
71
+ autoload :FallbackMessageFormatter, 'rspec/core/formatters/fallback_message_formatter'
72
+ autoload :ProgressFormatter, 'rspec/core/formatters/progress_formatter'
73
+ autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter'
74
+ autoload :JsonFormatter, 'rspec/core/formatters/json_formatter'
75
+ autoload :BisectFormatter, 'rspec/core/formatters/bisect_formatter'
74
76
 
75
77
  # Register the formatter class
76
78
  # @param formatter_class [Class] formatter class to register
@@ -120,7 +122,15 @@ module RSpec::Core::Formatters
120
122
  add DeprecationFormatter, deprecation_stream, output_stream
121
123
  end
122
124
 
123
- return unless RSpec.configuration.profile_examples? && !existing_formatter_implements?(:dump_profile)
125
+ unless existing_formatter_implements?(:message)
126
+ add FallbackMessageFormatter, output_stream
127
+ end
128
+
129
+ return unless RSpec.configuration.profile_examples?
130
+
131
+ @reporter.setup_profiler
132
+
133
+ return if existing_formatter_implements?(:dump_profile)
124
134
 
125
135
  add RSpec::Core::Formatters::ProfileFormatter, output_stream
126
136
  end
@@ -133,18 +143,12 @@ module RSpec::Core::Formatters
133
143
 
134
144
  if !Loader.formatters[formatter_class].nil?
135
145
  formatter = formatter_class.new(*args)
136
- @reporter.register_listener formatter, *notifications_for(formatter_class)
146
+ register formatter, notifications_for(formatter_class)
137
147
  elsif defined?(RSpec::LegacyFormatters)
138
148
  formatter = RSpec::LegacyFormatters.load_formatter formatter_class, *args
139
- @reporter.register_listener formatter, *formatter.notifications
149
+ register formatter, formatter.notifications
140
150
  else
141
- line = ::RSpec::CallerFilter.first_non_rspec_line
142
- if line
143
- call_site = "Formatter added at: #{line}"
144
- else
145
- call_site = "The formatter was added via command line flag or your "\
146
- "`.rspec` file."
147
- end
151
+ call_site = "Formatter added at: #{::RSpec::CallerFilter.first_non_rspec_line}"
148
152
 
149
153
  RSpec.warn_deprecation <<-WARNING.gsub(/\s*\|/, ' ')
150
154
  |The #{formatter_class} formatter uses the deprecated formatter
@@ -157,10 +161,7 @@ module RSpec::Core::Formatters
157
161
  |
158
162
  |#{call_site}
159
163
  WARNING
160
- return
161
164
  end
162
- @formatters << formatter unless duplicate_formatter_exists?(formatter)
163
- formatter
164
165
  end
165
166
 
166
167
  private
@@ -172,6 +173,13 @@ module RSpec::Core::Formatters
172
173
  "maybe you meant 'documentation' or 'progress'?.")
173
174
  end
174
175
 
176
+ def register(formatter, notifications)
177
+ return if duplicate_formatter_exists?(formatter)
178
+ @reporter.register_listener formatter, *notifications
179
+ @formatters << formatter
180
+ formatter
181
+ end
182
+
175
183
  def duplicate_formatter_exists?(new_formatter)
176
184
  @formatters.any? do |formatter|
177
185
  formatter.class === new_formatter && formatter.output == new_formatter.output
@@ -192,12 +200,14 @@ module RSpec::Core::Formatters
192
200
  ProgressFormatter
193
201
  when 'j', 'json'
194
202
  JsonFormatter
203
+ when 'bisect'
204
+ BisectFormatter
195
205
  end
196
206
  end
197
207
 
198
208
  def notifications_for(formatter_class)
199
- formatter_class.ancestors.inject(Set.new) do |notifications, klass|
200
- notifications + Loader.formatters.fetch(klass) { Set.new }
209
+ formatter_class.ancestors.inject(::RSpec::Core::Set.new) do |notifications, klass|
210
+ notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::Set.new }
201
211
  end
202
212
  end
203
213