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
@@ -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,45 @@ 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
132
- exception.backtrace.find do |line|
218
+ line_regex = RSpec.configuration.in_project_source_dir_regex
219
+ loaded_spec_files = RSpec.configuration.loaded_spec_files
220
+
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
234
+ end
146
235
 
147
- formatted
236
+ def exception_backtrace
237
+ exception.backtrace || []
148
238
  end
149
239
 
150
240
  # @private
@@ -176,9 +266,9 @@ module RSpec
176
266
  def pending_options
177
267
  if @execution_result.pending_fixed?
178
268
  {
179
- :description_formatter => Proc.new { "#{@example.full_description} FIXED" },
180
- :message_color => RSpec.configuration.fixed_color,
181
- :failure_lines => [
269
+ :description => "#{@example.full_description} FIXED",
270
+ :message_color => RSpec.configuration.fixed_color,
271
+ :failure_lines => [
182
272
  "Expected pending '#{@execution_result.pending_message}' to fail. No Error was raised."
183
273
  ]
184
274
  }
@@ -201,8 +291,6 @@ module RSpec
201
291
  options[:message_color])
202
292
  )
203
293
 
204
- options[:description_formatter] &&= Proc.new {}
205
-
206
294
  return options unless exception.aggregation_metadata[:hide_backtrace]
207
295
  options[:backtrace_formatter] = EmptyBacktraceFormatter
208
296
  options
@@ -213,7 +301,7 @@ module RSpec
213
301
  end
214
302
 
215
303
  def multiple_exception_summarizer(exception, prior_detail_formatter, color)
216
- lambda do |example, colorizer, indentation|
304
+ lambda do |example, colorizer|
217
305
  summary = if exception.aggregation_metadata[:hide_backtrace]
218
306
  # Since the backtrace is hidden, the subfailures will come
219
307
  # immediately after this, and using `:` will read well.
@@ -226,27 +314,30 @@ module RSpec
226
314
 
227
315
  summary = colorizer.wrap(summary, color || RSpec.configuration.failure_color)
228
316
  return summary unless prior_detail_formatter
229
- "#{prior_detail_formatter.call(example, colorizer, indentation)}\n#{indentation}#{summary}"
317
+ [
318
+ prior_detail_formatter.call(example, colorizer),
319
+ summary
320
+ ]
230
321
  end
231
322
  end
232
323
 
233
324
  def sub_failure_list_formatter(exception, message_color)
234
325
  common_backtrace_truncater = CommonBacktraceTruncater.new(exception)
235
326
 
236
- lambda do |failure_number, colorizer, indentation|
237
- 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|
238
329
  options = with_multiple_error_options_as_needed(
239
330
  failure,
240
- :description_formatter => :failure_slash_error_line.to_proc,
241
- :indentation => indentation.length,
331
+ :description => nil,
332
+ :indentation => 0,
242
333
  :message_color => message_color || RSpec.configuration.failure_color,
243
334
  :skip_shared_group_trace => true
244
335
  )
245
336
 
246
337
  failure = common_backtrace_truncater.with_truncated_backtrace(failure)
247
338
  presenter = ExceptionPresenter.new(failure, @example, options)
248
- presenter.fully_formatted("#{failure_number}.#{index + 1}", colorizer)
249
- end.join
339
+ presenter.fully_formatted_lines("#{failure_number}.#{index + 1}", colorizer)
340
+ end
250
341
  end
251
342
  end
252
343
 
@@ -92,7 +92,7 @@ module RSpec
92
92
  # Given a list of example ids, organizes them into a compact, ordered list.
93
93
  def self.organize_ids(ids)
94
94
  grouped = ids.inject(Hash.new { |h, k| h[k] = [] }) do |hash, id|
95
- file, id = id.split(Configuration::ON_SQUARE_BRACKETS)
95
+ file, id = Example.parse_id(id)
96
96
  hash[file] << id
97
97
  hash
98
98
  end
@@ -137,12 +137,12 @@ module RSpec
137
137
  # spec. For example, you could output links to images or other files
138
138
  # produced during the specs.
139
139
  def extra_failure_content(failure)
140
- RSpec::Support.require_rspec_core "formatters/snippet_extractor"
141
- backtrace = failure.exception.backtrace.map do |line|
140
+ RSpec::Support.require_rspec_core "formatters/html_snippet_extractor"
141
+ backtrace = (failure.exception.backtrace || []).map do |line|
142
142
  RSpec.configuration.backtrace_formatter.backtrace_line(line)
143
143
  end
144
144
  backtrace.compact!
145
- @snippet_extractor ||= SnippetExtractor.new
145
+ @snippet_extractor ||= HtmlSnippetExtractor.new
146
146
  " <pre class=\"ruby\"><code>#{@snippet_extractor.snippet(backtrace)}</code></pre>"
147
147
  end
148
148
  end
@@ -33,10 +33,10 @@ module RSpec
33
33
  "<span class='duration'>#{formatted_run_time}s</span></dd>"
34
34
  end
35
35
 
36
- # rubocop:disable Style/ParameterLists
36
+ # rubocop:disable Metrics/ParameterLists
37
37
  def print_example_failed(pending_fixed, description, run_time, failure_id,
38
38
  exception, extra_content)
39
- # rubocop:enable Style/ParameterLists
39
+ # rubocop:enable Metrics/ParameterLists
40
40
  formatted_run_time = "%.5f" % run_time
41
41
 
42
42
  @output.puts " <dd class=\"example #{pending_fixed ? 'pending_fixed' : 'failed'}\">"
@@ -139,7 +139,6 @@ module RSpec
139
139
  EOF
140
140
  # rubocop:enable LineLength
141
141
 
142
- # rubocop:disable LineLength
143
142
  GLOBAL_SCRIPTS = <<-EOF
144
143
 
145
144
  function addClass(element_id, classname) {
@@ -0,0 +1,116 @@
1
+ module RSpec
2
+ module Core
3
+ module Formatters
4
+ # @api private
5
+ #
6
+ # Extracts code snippets by looking at the backtrace of the passed error
7
+ # and applies synax highlighting and line numbers using html.
8
+ class HtmlSnippetExtractor
9
+ # @private
10
+ module NullConverter
11
+ def self.convert(code)
12
+ %Q(#{code}\n<span class="comment"># Install the coderay gem to get syntax highlighting</span>)
13
+ end
14
+ end
15
+
16
+ # @private
17
+ module CoderayConverter
18
+ def self.convert(code)
19
+ CodeRay.scan(code, :ruby).html(:line_numbers => false)
20
+ end
21
+ end
22
+
23
+ # rubocop:disable Style/ClassVars
24
+ @@converter = NullConverter
25
+ begin
26
+ require 'coderay'
27
+ @@converter = CoderayConverter
28
+ # rubocop:disable Lint/HandleExceptions
29
+ rescue LoadError
30
+ # it'll fall back to the NullConverter assigned above
31
+ # rubocop:enable Lint/HandleExceptions
32
+ end
33
+
34
+ # rubocop:enable Style/ClassVars
35
+
36
+ # @api private
37
+ #
38
+ # Extract lines of code corresponding to a backtrace.
39
+ #
40
+ # @param backtrace [String] the backtrace from a test failure
41
+ # @return [String] highlighted code snippet indicating where the test
42
+ # failure occured
43
+ #
44
+ # @see #post_process
45
+ def snippet(backtrace)
46
+ raw_code, line = snippet_for(backtrace[0])
47
+ highlighted = @@converter.convert(raw_code)
48
+ post_process(highlighted, line)
49
+ end
50
+ # rubocop:enable Style/ClassVars
51
+
52
+ # @api private
53
+ #
54
+ # Create a snippet from a line of code.
55
+ #
56
+ # @param error_line [String] file name with line number (i.e.
57
+ # 'foo_spec.rb:12')
58
+ # @return [String] lines around the target line within the file
59
+ #
60
+ # @see #lines_around
61
+ def snippet_for(error_line)
62
+ if error_line =~ /(.*):(\d+)/
63
+ file = Regexp.last_match[1]
64
+ line = Regexp.last_match[2].to_i
65
+ [lines_around(file, line), line]
66
+ else
67
+ ["# Couldn't get snippet for #{error_line}", 1]
68
+ end
69
+ end
70
+
71
+ # @api private
72
+ #
73
+ # Extract lines of code centered around a particular line within a
74
+ # source file.
75
+ #
76
+ # @param file [String] filename
77
+ # @param line [Fixnum] line number
78
+ # @return [String] lines around the target line within the file (2 above
79
+ # and 1 below).
80
+ def lines_around(file, line)
81
+ if File.file?(file)
82
+ lines = File.read(file).split("\n")
83
+ min = [0, line - 3].max
84
+ max = [line + 1, lines.length - 1].min
85
+ selected_lines = []
86
+ selected_lines.join("\n")
87
+ lines[min..max].join("\n")
88
+ else
89
+ "# Couldn't get snippet for #{file}"
90
+ end
91
+ rescue SecurityError
92
+ "# Couldn't get snippet for #{file}"
93
+ end
94
+
95
+ # @api private
96
+ #
97
+ # Adds line numbers to all lines and highlights the line where the
98
+ # failure occurred using html `span` tags.
99
+ #
100
+ # @param highlighted [String] syntax-highlighted snippet surrounding the
101
+ # offending line of code
102
+ # @param offending_line [Fixnum] line where failure occured
103
+ # @return [String] completed snippet
104
+ def post_process(highlighted, offending_line)
105
+ new_lines = []
106
+ highlighted.split("\n").each_with_index do |line, i|
107
+ new_line = "<span class=\"linenum\">#{offending_line + i - 2}</span>#{line}"
108
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
109
+ new_lines << new_line
110
+ end
111
+ new_lines.join("\n")
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -66,6 +66,15 @@ module RSpec
66
66
  # @param notification [ExampleNotification] containing example subclass
67
67
  # of `RSpec::Core::Example`
68
68
 
69
+ # @method example_finished
70
+ # @api public
71
+ # @group Example Notifications
72
+ #
73
+ # Invoked at the end of the execution of each example.
74
+ #
75
+ # @param notification [ExampleNotification] containing example subclass
76
+ # of `RSpec::Core::Example`
77
+
69
78
  # @method example_passed
70
79
  # @api public
71
80
  # @group Example Notifications