rspec-core 3.3.2 → 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.tar.gz.sig +0 -0
  4. data/.document +1 -1
  5. data/.yardopts +1 -1
  6. data/Changelog.md +69 -0
  7. data/{License.txt → LICENSE.md} +6 -5
  8. data/README.md +18 -3
  9. data/lib/rspec/core/bisect/example_minimizer.rb +78 -39
  10. data/lib/rspec/core/configuration.rb +87 -25
  11. data/lib/rspec/core/configuration_options.rb +1 -1
  12. data/lib/rspec/core/example.rb +54 -7
  13. data/lib/rspec/core/example_group.rb +28 -8
  14. data/lib/rspec/core/example_status_persister.rb +16 -16
  15. data/lib/rspec/core/formatters.rb +1 -0
  16. data/lib/rspec/core/formatters/bisect_progress_formatter.rb +44 -15
  17. data/lib/rspec/core/formatters/exception_presenter.rb +146 -59
  18. data/lib/rspec/core/formatters/helpers.rb +1 -1
  19. data/lib/rspec/core/formatters/html_formatter.rb +2 -2
  20. data/lib/rspec/core/formatters/html_printer.rb +2 -3
  21. data/lib/rspec/core/formatters/html_snippet_extractor.rb +116 -0
  22. data/lib/rspec/core/formatters/protocol.rb +9 -0
  23. data/lib/rspec/core/formatters/snippet_extractor.rb +124 -97
  24. data/lib/rspec/core/hooks.rb +2 -2
  25. data/lib/rspec/core/memoized_helpers.rb +2 -2
  26. data/lib/rspec/core/metadata.rb +3 -2
  27. data/lib/rspec/core/metadata_filter.rb +11 -6
  28. data/lib/rspec/core/notifications.rb +3 -2
  29. data/lib/rspec/core/option_parser.rb +22 -4
  30. data/lib/rspec/core/project_initializer/spec/spec_helper.rb +2 -2
  31. data/lib/rspec/core/rake_task.rb +12 -3
  32. data/lib/rspec/core/reporter.rb +18 -2
  33. data/lib/rspec/core/ruby_project.rb +1 -1
  34. data/lib/rspec/core/shared_example_group.rb +2 -0
  35. data/lib/rspec/core/source.rb +76 -0
  36. data/lib/rspec/core/source/location.rb +13 -0
  37. data/lib/rspec/core/source/node.rb +93 -0
  38. data/lib/rspec/core/source/syntax_highlighter.rb +71 -0
  39. data/lib/rspec/core/source/token.rb +43 -0
  40. data/lib/rspec/core/version.rb +1 -1
  41. data/lib/rspec/core/world.rb +25 -6
  42. metadata +12 -9
  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
@@ -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"
140
+ RSpec::Support.require_rspec_core "formatters/html_snippet_extractor"
141
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
@@ -1,114 +1,141 @@
1
+ RSpec::Support.require_rspec_core "source"
2
+
1
3
  module RSpec
2
4
  module Core
3
5
  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.
6
+ # @private
8
7
  class SnippetExtractor
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
8
+ NoSuchFileError = Class.new(StandardError)
9
+ NoSuchLineError = Class.new(StandardError)
15
10
 
16
- # @private
17
- module CoderayConverter
18
- def self.convert(code)
19
- CodeRay.scan(code, :ruby).html(:line_numbers => false)
20
- end
11
+ def self.extract_line_at(file_path, line_number)
12
+ source = source_from_file(file_path)
13
+ line = source.lines[line_number - 1]
14
+ raise NoSuchLineError unless line
15
+ line
21
16
  end
22
17
 
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
18
+ def self.source_from_file(path)
19
+ raise NoSuchFileError unless File.exist?(path)
20
+ RSpec.world.source_cache.source_from_file(path)
32
21
  end
33
22
 
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]
23
+ if RSpec::Support::RubyFeatures.ripper_supported?
24
+ NoExpressionAtLineError = Class.new(StandardError)
25
+
26
+ PAREN_TOKEN_TYPE_PAIRS = {
27
+ :on_lbracket => :on_rbracket,
28
+ :on_lparen => :on_rparen,
29
+ :on_lbrace => :on_rbrace,
30
+ :on_heredoc_beg => :on_heredoc_end
31
+ }
32
+
33
+ attr_reader :source, :beginning_line_number, :max_line_count
34
+
35
+ def self.extract_expression_lines_at(file_path, beginning_line_number, max_line_count=nil)
36
+ if max_line_count == 1
37
+ [extract_line_at(file_path, beginning_line_number)]
38
+ else
39
+ source = source_from_file(file_path)
40
+ new(source, beginning_line_number, max_line_count).expression_lines
41
+ end
68
42
  end
69
- end
70
43
 
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}"
44
+ def initialize(source, beginning_line_number, max_line_count=nil)
45
+ @source = source
46
+ @beginning_line_number = beginning_line_number
47
+ @max_line_count = max_line_count
90
48
  end
91
- rescue SecurityError
92
- "# Couldn't get snippet for #{file}"
93
- end
94
49
 
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
50
+ def expression_lines
51
+ line_range = line_range_of_expression
52
+
53
+ if max_line_count && line_range.count > max_line_count
54
+ line_range = (line_range.begin)..(line_range.begin + max_line_count - 1)
55
+ end
56
+
57
+ source.lines[(line_range.begin - 1)..(line_range.end - 1)]
58
+ rescue SyntaxError, NoExpressionAtLineError
59
+ [self.class.extract_line_at(source.path, beginning_line_number)]
60
+ end
61
+
62
+ private
63
+
64
+ def line_range_of_expression
65
+ @line_range_of_expression ||= begin
66
+ line_range = line_range_of_location_nodes_in_expression
67
+ initial_unclosed_parens = unclosed_paren_tokens_in_line_range(line_range)
68
+ unclosed_parens = initial_unclosed_parens
69
+
70
+ until (initial_unclosed_parens & unclosed_parens).empty?
71
+ line_range = (line_range.begin)..(line_range.end + 1)
72
+ unclosed_parens = unclosed_paren_tokens_in_line_range(line_range)
73
+ end
74
+
75
+ line_range
76
+ end
110
77
  end
111
- new_lines.join("\n")
78
+
79
+ def unclosed_paren_tokens_in_line_range(line_range)
80
+ tokens = FlatMap.flat_map(line_range) do |line_number|
81
+ source.tokens_by_line_number[line_number]
82
+ end
83
+
84
+ tokens.each_with_object([]) do |token, unclosed_tokens|
85
+ if PAREN_TOKEN_TYPE_PAIRS.keys.include?(token.type)
86
+ unclosed_tokens << token
87
+ else
88
+ index = unclosed_tokens.rindex do |unclosed_token|
89
+ PAREN_TOKEN_TYPE_PAIRS[unclosed_token.type] == token.type
90
+ end
91
+ unclosed_tokens.delete_at(index) if index
92
+ end
93
+ end
94
+ end
95
+
96
+ def line_range_of_location_nodes_in_expression
97
+ line_numbers = expression_node.each_with_object(Set.new) do |node, set|
98
+ set << node.location.line if node.location
99
+ end
100
+
101
+ line_numbers.min..line_numbers.max
102
+ end
103
+
104
+ def expression_node
105
+ raise NoExpressionAtLineError if location_nodes_at_beginning_line.empty?
106
+
107
+ @expression_node ||= begin
108
+ common_ancestor_nodes = location_nodes_at_beginning_line.map do |node|
109
+ node.each_ancestor.to_a
110
+ end.reduce(:&)
111
+
112
+ common_ancestor_nodes.find { |node| expression_outmost_node?(node) }
113
+ end
114
+ end
115
+
116
+ def expression_outmost_node?(node)
117
+ return true unless node.parent
118
+ return false if node.type.to_s.start_with?('@')
119
+ ![node, node.parent].all? do |n|
120
+ # See `Ripper::PARSER_EVENTS` for the complete list of sexp types.
121
+ type = n.type.to_s
122
+ type.end_with?('call') || type.start_with?('method_add_')
123
+ end
124
+ end
125
+
126
+ def location_nodes_at_beginning_line
127
+ source.nodes_by_line_number[beginning_line_number]
128
+ end
129
+ else
130
+ # :nocov:
131
+ def self.extract_expression_lines_at(file_path, beginning_line_number, *)
132
+ [extract_line_at(file_path, beginning_line_number)]
133
+ end
134
+ # :nocov:
135
+ end
136
+
137
+ def self.least_indentation_from(lines)
138
+ lines.map { |line| line[/^[ \t]*/] }.min
112
139
  end
113
140
  end
114
141
  end
@@ -362,7 +362,7 @@ module RSpec
362
362
  class AfterHook < Hook
363
363
  def run(example)
364
364
  example.instance_exec(example, &block)
365
- rescue Exception => ex
365
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => ex
366
366
  example.set_exception(ex)
367
367
  end
368
368
  end
@@ -371,7 +371,7 @@ module RSpec
371
371
  class AfterContextHook < Hook
372
372
  def run(example)
373
373
  example.instance_exec(example, &block)
374
- rescue Exception => e
374
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
375
375
  # TODO: Come up with a better solution for this.
376
376
  RSpec.configuration.reporter.message <<-EOS
377
377
 
@@ -1,4 +1,4 @@
1
- require 'rspec/core/reentrant_mutex'
1
+ RSpec::Support.require_rspec_support 'reentrant_mutex'
2
2
 
3
3
  module RSpec
4
4
  module Core
@@ -148,7 +148,7 @@ module RSpec
148
148
  class ThreadsafeMemoized
149
149
  def initialize
150
150
  @memoized = {}
151
- @mutex = ReentrantMutex.new
151
+ @mutex = Support::ReentrantMutex.new
152
152
  end
153
153
 
154
154
  def fetch_or_store(key)