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
@@ -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