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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.document +1 -1
- data/.yardopts +1 -1
- data/Changelog.md +69 -0
- data/{License.txt → LICENSE.md} +6 -5
- data/README.md +18 -3
- data/lib/rspec/core/bisect/example_minimizer.rb +78 -39
- data/lib/rspec/core/configuration.rb +87 -25
- data/lib/rspec/core/configuration_options.rb +1 -1
- data/lib/rspec/core/example.rb +54 -7
- data/lib/rspec/core/example_group.rb +28 -8
- data/lib/rspec/core/example_status_persister.rb +16 -16
- data/lib/rspec/core/formatters.rb +1 -0
- data/lib/rspec/core/formatters/bisect_progress_formatter.rb +44 -15
- data/lib/rspec/core/formatters/exception_presenter.rb +146 -59
- data/lib/rspec/core/formatters/helpers.rb +1 -1
- data/lib/rspec/core/formatters/html_formatter.rb +2 -2
- data/lib/rspec/core/formatters/html_printer.rb +2 -3
- data/lib/rspec/core/formatters/html_snippet_extractor.rb +116 -0
- data/lib/rspec/core/formatters/protocol.rb +9 -0
- data/lib/rspec/core/formatters/snippet_extractor.rb +124 -97
- data/lib/rspec/core/hooks.rb +2 -2
- data/lib/rspec/core/memoized_helpers.rb +2 -2
- data/lib/rspec/core/metadata.rb +3 -2
- data/lib/rspec/core/metadata_filter.rb +11 -6
- data/lib/rspec/core/notifications.rb +3 -2
- data/lib/rspec/core/option_parser.rb +22 -4
- data/lib/rspec/core/project_initializer/spec/spec_helper.rb +2 -2
- data/lib/rspec/core/rake_task.rb +12 -3
- data/lib/rspec/core/reporter.rb +18 -2
- data/lib/rspec/core/ruby_project.rb +1 -1
- data/lib/rspec/core/shared_example_group.rb +2 -0
- data/lib/rspec/core/source.rb +76 -0
- data/lib/rspec/core/source/location.rb +13 -0
- data/lib/rspec/core/source/node.rb +93 -0
- data/lib/rspec/core/source/syntax_highlighter.rb +71 -0
- data/lib/rspec/core/source/token.rb +43 -0
- data/lib/rspec/core/version.rb +1 -1
- data/lib/rspec/core/world.rb +25 -6
- metadata +12 -9
- metadata.gz.sig +0 -0
- data/lib/rspec/core/bisect/subset_enumerator.rb +0 -39
- data/lib/rspec/core/mutex.rb +0 -63
- 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 =
|
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/
|
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 ||=
|
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
|
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
|
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
|
-
# @
|
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
|
-
|
10
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
data/lib/rspec/core/hooks.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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)
|