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,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
@@ -73,6 +73,7 @@ module RSpec::Core::Formatters
73
73
  autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter'
74
74
  autoload :JsonFormatter, 'rspec/core/formatters/json_formatter'
75
75
  autoload :BisectFormatter, 'rspec/core/formatters/bisect_formatter'
76
+ autoload :ExceptionPresenter, 'rspec/core/formatters/exception_presenter'
76
77
 
77
78
  # Register the formatter class
78
79
  # @param formatter_class [Class] formatter class to register
@@ -182,7 +183,7 @@ module RSpec::Core::Formatters
182
183
 
183
184
  def duplicate_formatter_exists?(new_formatter)
184
185
  @formatters.any? do |formatter|
185
- formatter.class === new_formatter && formatter.output == new_formatter.output
186
+ formatter.class == new_formatter.class && formatter.output == new_formatter.output
186
187
  end
187
188
  end
188
189
 
@@ -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)
@@ -147,12 +147,13 @@ module RSpec
147
147
  end
148
148
 
149
149
  relative_file_path = Metadata.relative_path(file_path)
150
+ absolute_file_path = File.expand_path(relative_file_path)
150
151
  metadata[:file_path] = relative_file_path
151
152
  metadata[:line_number] = line_number.to_i
152
153
  metadata[:location] = "#{relative_file_path}:#{line_number}"
153
- metadata[:absolute_file_path] = File.expand_path(relative_file_path)
154
+ metadata[:absolute_file_path] = absolute_file_path
154
155
  metadata[:rerun_file_path] ||= relative_file_path
155
- metadata[:scoped_id] = build_scoped_id_for(relative_file_path)
156
+ metadata[:scoped_id] = build_scoped_id_for(absolute_file_path)
156
157
  end
157
158
 
158
159
  def file_path_and_line_number_from(backtrace)
@@ -15,22 +15,19 @@ module RSpec
15
15
  # @private
16
16
  def filter_applies?(key, value, metadata)
17
17
  silence_metadata_example_group_deprecations do
18
- return filter_applies_to_any_value?(key, value, metadata) if Array === metadata[key] && !(Proc === value)
19
18
  return location_filter_applies?(value, metadata) if key == :locations
20
19
  return id_filter_applies?(value, metadata) if key == :ids
21
20
  return filters_apply?(key, value, metadata) if Hash === value
22
21
 
23
22
  return false unless metadata.key?(key)
23
+ return true if TrueClass === value && !!metadata[key]
24
+ return filter_applies_to_any_value?(key, value, metadata) if Array === metadata[key] && !(Proc === value)
24
25
 
25
26
  case value
26
27
  when Regexp
27
28
  metadata[key] =~ value
28
29
  when Proc
29
- case value.arity
30
- when 0 then value.call
31
- when 2 then value.call(metadata[key], metadata)
32
- else value.call(metadata[key])
33
- end
30
+ proc_filter_applies?(key, value, metadata)
34
31
  else
35
32
  metadata[key].to_s == value.to_s
36
33
  end
@@ -61,6 +58,14 @@ module RSpec
61
58
  !(relevant_line_numbers(metadata) & preceding_declaration_lines).empty?
62
59
  end
63
60
 
61
+ def proc_filter_applies?(key, proc, metadata)
62
+ case proc.arity
63
+ when 0 then proc.call
64
+ when 2 then proc.call(metadata[key], metadata)
65
+ else proc.call(metadata[key])
66
+ end
67
+ end
68
+
64
69
  def relevant_line_numbers(metadata)
65
70
  Metadata.ascend(metadata).map { |meta| meta[:line_number] }
66
71
  end
@@ -1,7 +1,6 @@
1
1
  RSpec::Support.require_rspec_core "formatters/exception_presenter"
2
2
  RSpec::Support.require_rspec_core "formatters/helpers"
3
3
  RSpec::Support.require_rspec_core "shell_escape"
4
- RSpec::Support.require_rspec_support "encoded_string"
5
4
 
6
5
  module RSpec::Core
7
6
  # Notifications are value objects passed to formatters to provide them
@@ -10,6 +9,7 @@ module RSpec::Core
10
9
  # @private
11
10
  module NullColorizer
12
11
  module_function
12
+
13
13
  def wrap(line, _code_or_symbol)
14
14
  line
15
15
  end
@@ -281,7 +281,8 @@ module RSpec::Core
281
281
  # @attr pending_examples [Array<RSpec::Core::Example>] the pending examples
282
282
  # @attr load_time [Float] the number of seconds taken to boot RSpec
283
283
  # and load the spec files
284
- SummaryNotification = Struct.new(:duration, :examples, :failed_examples, :pending_examples, :load_time)
284
+ SummaryNotification = Struct.new(:duration, :examples, :failed_examples,
285
+ :pending_examples, :load_time)
285
286
  class SummaryNotification
286
287
  # @api
287
288
  # @return [Fixnum] the number of examples run
@@ -34,6 +34,8 @@ module RSpec::Core
34
34
  private
35
35
 
36
36
  # rubocop:disable MethodLength
37
+ # rubocop:disable Metrics/AbcSize
38
+ # rubocop:disable CyclomaticComplexity
37
39
  def parser(options)
38
40
  OptionParser.new do |parser|
39
41
  parser.banner = "Usage: rspec [options] [files or directories]\n\n"
@@ -69,7 +71,18 @@ module RSpec::Core
69
71
  bisect_and_exit(argument)
70
72
  end
71
73
 
72
- parser.on('--[no-]fail-fast', 'Abort the run on first failure.') do |value|
74
+ parser.on('--[no-]fail-fast[=COUNT]', 'Abort the run after a certain number of failures (1 by default).') do |argument|
75
+ if argument == true
76
+ value = 1
77
+ elsif argument == false || argument == 0
78
+ value = false
79
+ else
80
+ begin
81
+ value = Integer(argument)
82
+ rescue ArgumentError
83
+ RSpec.warning "Expected an integer value for `--fail-fast`, got: #{argument.inspect}", :call_site => nil
84
+ end
85
+ end
73
86
  set_fail_fast(options, value)
74
87
  end
75
88
 
@@ -174,12 +187,16 @@ FILTERING
174
187
  parser.on("--next-failure", "Apply `--only-failures` and abort after one failure.",
175
188
  " (Equivalent to `--only-failures --fail-fast --order defined`)") do
176
189
  configure_only_failures(options)
177
- set_fail_fast(options, true)
190
+ set_fail_fast(options, 1)
178
191
  options[:order] ||= 'defined'
179
192
  end
180
193
 
181
194
  parser.on('-P', '--pattern PATTERN', 'Load files matching pattern (default: "spec/**/*_spec.rb").') do |o|
182
- options[:pattern] = o
195
+ if options[:pattern]
196
+ options[:pattern] += ',' + o
197
+ else
198
+ options[:pattern] = o
199
+ end
183
200
  end
184
201
 
185
202
  parser.on('--exclude-pattern PATTERN',
@@ -246,10 +263,11 @@ FILTERING
246
263
  raise OptionParser::InvalidOption.new
247
264
  end
248
265
  end
249
-
250
266
  end
251
267
  end
268
+ # rubocop:enable Metrics/AbcSize
252
269
  # rubocop:enable MethodLength
270
+ # rubocop:enable CyclomaticComplexity
253
271
 
254
272
  def add_tag_filter(options, filter_type, tag_name, value=true)
255
273
  (options[filter_type] ||= {})[tag_name] = value
@@ -57,9 +57,9 @@ RSpec.configure do |config|
57
57
 
58
58
  # Limits the available syntax to the non-monkey patched syntax that is
59
59
  # recommended. For more details, see:
60
- # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
60
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
61
61
  # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
62
- # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
62
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
63
63
  config.disable_monkey_patching!
64
64
 
65
65
  # This setting enables warnings. It's recommended, but in some cases may
@@ -1,7 +1,16 @@
1
1
  require 'rake'
2
2
  require 'rake/tasklib'
3
- require 'rspec/support/ruby_features'
4
- require 'rspec/core/shell_escape'
3
+ require 'rspec/support'
4
+
5
+ RSpec::Support.require_rspec_support "ruby_features"
6
+
7
+ # :nocov:
8
+ unless RSpec::Support.respond_to?(:require_rspec_core)
9
+ RSpec::Support.define_optimized_require_for_rspec(:core) { |f| require_relative "../#{f}" }
10
+ end
11
+ # :nocov:
12
+
13
+ RSpec::Support.require_rspec_core "shell_escape"
5
14
 
6
15
  module RSpec
7
16
  module Core
@@ -91,7 +100,7 @@ module RSpec
91
100
 
92
101
  def file_inclusion_specification
93
102
  if ENV['SPEC']
94
- FileList[ ENV['SPEC']].sort
103
+ FileList[ENV['SPEC']].sort
95
104
  elsif String === pattern && !File.exist?(pattern)
96
105
  "--pattern #{escape pattern}"
97
106
  else
@@ -8,7 +8,7 @@ module RSpec::Core
8
8
  :close, :deprecation, :deprecation_summary, :dump_failures, :dump_pending,
9
9
  :dump_profile, :dump_summary, :example_failed, :example_group_finished,
10
10
  :example_group_started, :example_passed, :example_pending, :example_started,
11
- :message, :seed, :start, :start_dump, :stop
11
+ :message, :seed, :start, :start_dump, :stop, :example_finished
12
12
  ])
13
13
 
14
14
  def initialize(configuration)
@@ -124,6 +124,11 @@ module RSpec::Core
124
124
  notify :example_started, Notifications::ExampleNotification.for(example)
125
125
  end
126
126
 
127
+ # @private
128
+ def example_finished(example)
129
+ notify :example_finished, Notifications::ExampleNotification.for(example)
130
+ end
131
+
127
132
  # @private
128
133
  def example_passed(example)
129
134
  notify :example_passed, Notifications::ExampleNotification.for(example)
@@ -192,6 +197,17 @@ module RSpec::Core
192
197
  exit!(exit_status)
193
198
  end
194
199
 
200
+ # @private
201
+ def fail_fast_limit_met?
202
+ return false unless (fail_fast = @configuration.fail_fast)
203
+
204
+ if fail_fast == true
205
+ @failed_examples.any?
206
+ else
207
+ fail_fast <= @failed_examples.size
208
+ end
209
+ end
210
+
195
211
  private
196
212
 
197
213
  def close
@@ -201,7 +217,7 @@ module RSpec::Core
201
217
  def mute_profile_output?
202
218
  # Don't print out profiled info if there are failures and `--fail-fast` is
203
219
  # used, it just clutters the output.
204
- !@configuration.profile_examples? || (@configuration.fail_fast? && @failed_examples.size > 0)
220
+ !@configuration.profile_examples? || fail_fast_limit_met?
205
221
  end
206
222
 
207
223
  def seed_used?
@@ -6,7 +6,7 @@ module RSpec
6
6
  # @private
7
7
  module RubyProject
8
8
  def add_to_load_path(*dirs)
9
- dirs.map { |dir| add_dir_to_load_path(File.join(root, dir)) }
9
+ dirs.each { |dir| add_dir_to_load_path(File.join(root, dir)) }
10
10
  end
11
11
 
12
12
  def add_dir_to_load_path(dir)
@@ -96,6 +96,7 @@ module RSpec
96
96
  # Shared examples top level DSL.
97
97
  module TopLevelDSL
98
98
  # @private
99
+ # rubocop:disable Lint/NestedMethodDefinition
99
100
  def self.definitions
100
101
  proc do
101
102
  def shared_examples(name, *args, &block)
@@ -105,6 +106,7 @@ module RSpec
105
106
  alias shared_examples_for shared_examples
106
107
  end
107
108
  end
109
+ # rubocop:enable Lint/NestedMethodDefinition
108
110
 
109
111
  # @private
110
112
  def self.exposed_globally?
@@ -0,0 +1,13 @@
1
+ module RSpec
2
+ module Core
3
+ class Source
4
+ # @private
5
+ # Represents a source location of node or token.
6
+ Location = Struct.new(:line, :column) do
7
+ def self.location?(array)
8
+ array.is_a?(Array) && array.size == 2 && array.all? { |e| e.is_a?(Integer) }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,93 @@
1
+ RSpec::Support.require_rspec_core "source/location"
2
+
3
+ module RSpec
4
+ module Core
5
+ class Source
6
+ # @private
7
+ # A wrapper for Ripper AST node which is generated with `Ripper.sexp`.
8
+ class Node
9
+ include Enumerable
10
+
11
+ attr_reader :sexp, :parent
12
+
13
+ def self.sexp?(array)
14
+ array.is_a?(Array) && array.first.is_a?(Symbol)
15
+ end
16
+
17
+ def initialize(ripper_sexp, parent=nil)
18
+ @sexp = ripper_sexp.freeze
19
+ @parent = parent
20
+ end
21
+
22
+ def type
23
+ sexp[0]
24
+ end
25
+
26
+ def args
27
+ @args ||= raw_args.map do |raw_arg|
28
+ if Node.sexp?(raw_arg)
29
+ Node.new(raw_arg, self)
30
+ elsif Location.location?(raw_arg)
31
+ Location.new(*raw_arg)
32
+ elsif raw_arg.is_a?(Array)
33
+ GroupNode.new(raw_arg, self)
34
+ else
35
+ raw_arg
36
+ end
37
+ end.freeze
38
+ end
39
+
40
+ def children
41
+ @children ||= args.select { |arg| arg.is_a?(Node) }.freeze
42
+ end
43
+
44
+ def location
45
+ @location ||= args.find { |arg| arg.is_a?(Location) }
46
+ end
47
+
48
+ def each(&block)
49
+ return to_enum(__method__) unless block_given?
50
+
51
+ yield self
52
+
53
+ children.each do |child|
54
+ child.each(&block)
55
+ end
56
+ end
57
+
58
+ def each_ancestor
59
+ return to_enum(__method__) unless block_given?
60
+
61
+ current_node = self
62
+
63
+ while (current_node = current_node.parent)
64
+ yield current_node
65
+ end
66
+ end
67
+
68
+ def inspect
69
+ "#<#{self.class} #{type}>"
70
+ end
71
+
72
+ private
73
+
74
+ def raw_args
75
+ sexp[1..-1] || []
76
+ end
77
+ end
78
+
79
+ # @private
80
+ class GroupNode < Node
81
+ def type
82
+ :group
83
+ end
84
+
85
+ private
86
+
87
+ def raw_args
88
+ sexp
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end