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