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
@@ -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,76 @@
1
+ RSpec::Support.require_rspec_core 'source/node'
2
+ RSpec::Support.require_rspec_core 'source/syntax_highlighter'
3
+ RSpec::Support.require_rspec_core 'source/token'
4
+
5
+ module RSpec
6
+ module Core
7
+ # @private
8
+ # Represents a Ruby source file and provides access to AST and tokens.
9
+ class Source
10
+ attr_reader :source, :path
11
+
12
+ def self.from_file(path)
13
+ source = File.read(path)
14
+ new(source, path)
15
+ end
16
+
17
+ def initialize(source_string, path=nil)
18
+ @source = source_string
19
+ @path = path ? File.expand_path(path) : '(string)'
20
+ end
21
+
22
+ def lines
23
+ @lines ||= source.split("\n")
24
+ end
25
+
26
+ def ast
27
+ @ast ||= begin
28
+ require 'ripper'
29
+ sexp = Ripper.sexp(source)
30
+ raise SyntaxError unless sexp
31
+ Node.new(sexp)
32
+ end
33
+ end
34
+
35
+ def tokens
36
+ @tokens ||= begin
37
+ require 'ripper'
38
+ tokens = Ripper.lex(source)
39
+ Token.tokens_from_ripper_tokens(tokens)
40
+ end
41
+ end
42
+
43
+ def nodes_by_line_number
44
+ @nodes_by_line_number ||= begin
45
+ nodes_by_line_number = ast.select(&:location).group_by { |node| node.location.line }
46
+ Hash.new { |hash, key| hash[key] = [] }.merge(nodes_by_line_number)
47
+ end
48
+ end
49
+
50
+ def tokens_by_line_number
51
+ @tokens_by_line_number ||= begin
52
+ nodes_by_line_number = tokens.group_by { |token| token.location.line }
53
+ Hash.new { |hash, key| hash[key] = [] }.merge(nodes_by_line_number)
54
+ end
55
+ end
56
+
57
+ def inspect
58
+ "#<#{self.class} #{path}>"
59
+ end
60
+
61
+ # @private
62
+ class Cache
63
+ attr_reader :syntax_highlighter
64
+
65
+ def initialize(configuration)
66
+ @sources_by_path = {}
67
+ @syntax_highlighter = SyntaxHighlighter.new(configuration)
68
+ end
69
+
70
+ def source_from_file(path)
71
+ @sources_by_path[path] ||= Source.from_file(path)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -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
@@ -0,0 +1,71 @@
1
+ module RSpec
2
+ module Core
3
+ class Source
4
+ # @private
5
+ # Provides terminal syntax highlighting of code snippets
6
+ # when coderay is available.
7
+ class SyntaxHighlighter
8
+ def initialize(configuration)
9
+ @configuration = configuration
10
+ end
11
+
12
+ def highlight(lines)
13
+ implementation.highlight_syntax(lines)
14
+ end
15
+
16
+ private
17
+
18
+ if RSpec::Support::OS.windows?
19
+ # :nocov:
20
+ def implementation
21
+ WindowsImplementation
22
+ end
23
+ # :nocov:
24
+ else
25
+ def implementation
26
+ return color_enabled_implementation if @configuration.color_enabled?
27
+ NoSyntaxHighlightingImplementation
28
+ end
29
+ end
30
+
31
+ def color_enabled_implementation
32
+ @color_enabled_implementation ||= begin
33
+ ::Kernel.require 'coderay'
34
+ CodeRayImplementation
35
+ rescue LoadError
36
+ NoSyntaxHighlightingImplementation
37
+ end
38
+ end
39
+
40
+ # @private
41
+ module CodeRayImplementation
42
+ RESET_CODE = "\e[0m"
43
+
44
+ def self.highlight_syntax(lines)
45
+ highlighted = begin
46
+ CodeRay.encode(lines.join("\n"), :ruby, :terminal)
47
+ rescue Support::AllExceptionsExceptOnesWeMustNotRescue
48
+ return lines
49
+ end
50
+
51
+ highlighted.split("\n").map do |line|
52
+ line.sub(/\S/) { |char| char.insert(0, RESET_CODE) }
53
+ end
54
+ end
55
+ end
56
+
57
+ # @private
58
+ module NoSyntaxHighlightingImplementation
59
+ def self.highlight_syntax(lines)
60
+ lines
61
+ end
62
+ end
63
+
64
+ # @private
65
+ # Not sure why, but our code above (and/or coderay itself) does not work
66
+ # on Windows, so we disable the feature on Windows.
67
+ WindowsImplementation = NoSyntaxHighlightingImplementation
68
+ end
69
+ end
70
+ end
71
+ end