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
@@ -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