rspec-expectations 3.2.1 → 3.3.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 (43) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Changelog.md +55 -4
  5. data/README.md +1 -1
  6. data/lib/rspec/expectations.rb +13 -1
  7. data/lib/rspec/expectations/configuration.rb +17 -0
  8. data/lib/rspec/expectations/expectation_target.rb +3 -9
  9. data/lib/rspec/expectations/fail_with.rb +1 -3
  10. data/lib/rspec/expectations/failure_aggregator.rb +194 -0
  11. data/lib/rspec/expectations/minitest_integration.rb +13 -0
  12. data/lib/rspec/expectations/version.rb +1 -1
  13. data/lib/rspec/matchers.rb +59 -5
  14. data/lib/rspec/matchers/built_in/base_matcher.rb +56 -7
  15. data/lib/rspec/matchers/built_in/be.rb +25 -15
  16. data/lib/rspec/matchers/built_in/be_between.rb +1 -1
  17. data/lib/rspec/matchers/built_in/be_within.rb +2 -2
  18. data/lib/rspec/matchers/built_in/contain_exactly.rb +12 -8
  19. data/lib/rspec/matchers/built_in/eq.rb +3 -38
  20. data/lib/rspec/matchers/built_in/eql.rb +2 -2
  21. data/lib/rspec/matchers/built_in/equal.rb +3 -3
  22. data/lib/rspec/matchers/built_in/exist.rb +2 -2
  23. data/lib/rspec/matchers/built_in/has.rb +3 -1
  24. data/lib/rspec/matchers/built_in/have_attributes.rb +5 -4
  25. data/lib/rspec/matchers/built_in/include.rb +44 -19
  26. data/lib/rspec/matchers/built_in/match.rb +9 -1
  27. data/lib/rspec/matchers/built_in/operators.rb +14 -5
  28. data/lib/rspec/matchers/built_in/output.rb +9 -2
  29. data/lib/rspec/matchers/built_in/raise_error.rb +64 -22
  30. data/lib/rspec/matchers/built_in/respond_to.rb +2 -3
  31. data/lib/rspec/matchers/built_in/satisfy.rb +7 -9
  32. data/lib/rspec/matchers/built_in/start_or_end_with.rb +3 -1
  33. data/lib/rspec/matchers/built_in/throw_symbol.rb +1 -1
  34. data/lib/rspec/matchers/built_in/yield.rb +7 -5
  35. data/lib/rspec/matchers/composable.rb +5 -4
  36. data/lib/rspec/matchers/dsl.rb +19 -6
  37. data/lib/rspec/matchers/english_phrasing.rb +42 -0
  38. data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +2 -8
  39. data/lib/rspec/matchers/fail_matchers.rb +42 -0
  40. data/lib/rspec/matchers/matcher_delegator.rb +2 -0
  41. metadata +9 -7
  42. metadata.gz.sig +0 -0
  43. data/lib/rspec/matchers/pretty.rb +0 -77
@@ -42,7 +42,7 @@ module RSpec
42
42
  # @return [String]
43
43
  def description
44
44
  described_items = surface_descriptions_in(expected)
45
- improve_hash_formatting "have attributes #{described_items.inspect}"
45
+ improve_hash_formatting "have attributes #{RSpec::Support::ObjectFormatter.format(described_items)}"
46
46
  end
47
47
 
48
48
  # @api private
@@ -55,14 +55,14 @@ module RSpec
55
55
  # @return [String]
56
56
  def failure_message
57
57
  respond_to_failure_message_or do
58
- "expected #{@actual.inspect} to #{description} but had attributes #{ formatted_values }"
58
+ "expected #{actual_formatted} to #{description} but had attributes #{ formatted_values }"
59
59
  end
60
60
  end
61
61
 
62
62
  # @api private
63
63
  # @return [String]
64
64
  def failure_message_when_negated
65
- respond_to_failure_message_or { "expected #{@actual.inspect} not to #{description}" }
65
+ respond_to_failure_message_or { "expected #{actual_formatted} not to #{description}" }
66
66
  end
67
67
 
68
68
  private
@@ -105,7 +105,8 @@ module RSpec
105
105
  end
106
106
 
107
107
  def formatted_values
108
- improve_hash_formatting(@values.inspect)
108
+ values = RSpec::Support::ObjectFormatter.format(@values)
109
+ improve_hash_formatting(values)
109
110
  end
110
111
  end
111
112
  end
@@ -12,62 +12,78 @@ module RSpec
12
12
  # @api private
13
13
  # @return [Boolean]
14
14
  def matches?(actual)
15
- @actual = actual
16
- perform_match(:all?, :all?)
15
+ perform_match(actual) { |v| v }
17
16
  end
18
17
 
19
18
  # @api private
20
19
  # @return [Boolean]
21
20
  def does_not_match?(actual)
22
- @actual = actual
23
- perform_match(:none?, :any?)
21
+ perform_match(actual) { |v| !v }
24
22
  end
25
23
 
26
24
  # @api private
27
25
  # @return [String]
28
26
  def description
29
- described_items = surface_descriptions_in(expected)
30
- improve_hash_formatting "include#{to_sentence(described_items)}"
27
+ improve_hash_formatting("include#{readable_list_of(expected)}")
31
28
  end
32
29
 
33
30
  # @api private
34
31
  # @return [String]
35
32
  def failure_message
36
- improve_hash_formatting(super) + invalid_object_message
33
+ format_failure_message("to") { super }
37
34
  end
38
35
 
39
36
  # @api private
40
37
  # @return [String]
41
38
  def failure_message_when_negated
42
- improve_hash_formatting(super) + invalid_object_message
39
+ format_failure_message("not to") { super }
43
40
  end
44
41
 
45
42
  # @api private
46
43
  # @return [Boolean]
47
44
  def diffable?
48
- true
45
+ !diff_would_wrongly_highlight_matched_item?
49
46
  end
50
47
 
51
48
  private
52
49
 
53
- def invalid_object_message
54
- return '' if actual.respond_to?(:include?)
55
- ", but it does not respond to `include?`"
50
+ def format_failure_message(preposition)
51
+ if actual.respond_to?(:include?)
52
+ improve_hash_formatting("expected #{description_of @actual} #{preposition} include#{readable_list_of @divergent_items}")
53
+ else
54
+ improve_hash_formatting(yield) + ", but it does not respond to `include?`"
55
+ end
56
56
  end
57
57
 
58
- def perform_match(predicate, hash_subset_predicate)
59
- return false unless actual.respond_to?(:include?)
58
+ def readable_list_of(items)
59
+ described_items = surface_descriptions_in(items)
60
+ if described_items.all? { |item| item.is_a?(Hash) }
61
+ " #{described_items.inject(:merge).inspect}"
62
+ else
63
+ EnglishPhrasing.list(described_items)
64
+ end
65
+ end
60
66
 
61
- expected.__send__(predicate) do |expected_item|
67
+ def perform_match(actual, &block)
68
+ @actual = actual
69
+ @divergent_items = excluded_from_actual(&block)
70
+ actual.respond_to?(:include?) && @divergent_items.empty?
71
+ end
72
+
73
+ def excluded_from_actual
74
+ return [] unless @actual.respond_to?(:include?)
75
+
76
+ expected.inject([]) do |memo, expected_item|
62
77
  if comparing_hash_to_a_subset?(expected_item)
63
- expected_item.__send__(hash_subset_predicate) do |(key, value)|
64
- actual_hash_includes?(key, value)
78
+ expected_item.each do |(key, value)|
79
+ memo << { key => value } unless yield actual_hash_includes?(key, value)
65
80
  end
66
81
  elsif comparing_hash_keys?(expected_item)
67
- actual_hash_has_key?(expected_item)
82
+ memo << expected_item unless yield actual_hash_has_key?(expected_item)
68
83
  else
69
- actual_collection_includes?(expected_item)
84
+ memo << expected_item unless yield actual_collection_includes?(expected_item)
70
85
  end
86
+ memo
71
87
  end
72
88
  end
73
89
 
@@ -99,6 +115,15 @@ module RSpec
99
115
 
100
116
  actual.any? { |value| values_match?(expected_item, value) }
101
117
  end
118
+
119
+ def diff_would_wrongly_highlight_matched_item?
120
+ return false unless actual.is_a?(String) && expected.is_a?(Array)
121
+
122
+ lines = actual.split("\n")
123
+ expected.any? do |str|
124
+ actual.include?(str) && lines.none? { |line| line == str }
125
+ end
126
+ end
102
127
  end
103
128
  end
104
129
  end
@@ -21,7 +21,15 @@ module RSpec
21
21
 
22
22
  def match(expected, actual)
23
23
  return true if values_match?(expected, actual)
24
- actual.match(expected) if actual.respond_to?(:match)
24
+ return false unless can_safely_call_match?(expected, actual)
25
+ actual.match(expected)
26
+ end
27
+
28
+ def can_safely_call_match?(expected, actual)
29
+ return false unless actual.respond_to?(:match)
30
+
31
+ !(RSpec::Matchers.is_a_matcher?(expected) &&
32
+ (String === actual || Regexp === actual))
25
33
  end
26
34
  end
27
35
  end
@@ -74,7 +74,7 @@ module RSpec
74
74
  # @api private
75
75
  # @return [String]
76
76
  def description
77
- "#{@operator} #{@expected.inspect}"
77
+ "#{@operator} #{RSpec::Support::ObjectFormatter.format(@expected)}"
78
78
  end
79
79
 
80
80
  private
@@ -98,10 +98,15 @@ module RSpec
98
98
  def __delegate_operator(actual, operator, expected)
99
99
  if actual.__send__(operator, expected)
100
100
  true
101
- elsif ['==', '===', '=~'].include?(operator)
102
- fail_with_message("expected: #{expected.inspect}\n got: #{actual.inspect} (using #{operator})")
103
101
  else
104
- fail_with_message("expected: #{operator} #{expected.inspect}\n got: #{operator.gsub(/./, ' ')} #{actual.inspect}")
102
+ expected_formatted = RSpec::Support::ObjectFormatter.format(expected)
103
+ actual_formatted = RSpec::Support::ObjectFormatter.format(actual)
104
+
105
+ if ['==', '===', '=~'].include?(operator)
106
+ fail_with_message("expected: #{expected_formatted}\n got: #{actual_formatted} (using #{operator})")
107
+ else
108
+ fail_with_message("expected: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}")
109
+ end
105
110
  end
106
111
  end
107
112
  end
@@ -111,7 +116,11 @@ module RSpec
111
116
  class NegativeOperatorMatcher < OperatorMatcher
112
117
  def __delegate_operator(actual, operator, expected)
113
118
  return false unless actual.__send__(operator, expected)
114
- fail_with_message("expected not: #{operator} #{expected.inspect}\n got: #{operator.gsub(/./, ' ')} #{actual.inspect}")
119
+
120
+ expected_formatted = RSpec::Support::ObjectFormatter.format(expected)
121
+ actual_formatted = RSpec::Support::ObjectFormatter.format(actual)
122
+
123
+ fail_with_message("expected not: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}")
115
124
  end
116
125
  end
117
126
  end
@@ -1,5 +1,4 @@
1
1
  require 'stringio'
2
- require 'tempfile'
3
2
 
4
3
  module RSpec
5
4
  module Matchers
@@ -114,7 +113,7 @@ module RSpec
114
113
 
115
114
  def actual_output_description
116
115
  return "nothing" unless captured?
117
- @actual.inspect
116
+ actual_formatted
118
117
  end
119
118
  end
120
119
 
@@ -172,6 +171,14 @@ module RSpec
172
171
  # @private
173
172
  class CaptureStreamToTempfile < Struct.new(:name, :stream)
174
173
  def capture(block)
174
+ # We delay loading tempfile until it is actually needed because
175
+ # we want to minimize stdlibs loaded so that users who use a
176
+ # portion of the stdlib can't have passing specs while forgetting
177
+ # to load it themselves. `CaptureStreamToTempfile` is rarely used
178
+ # and `tempfile` pulls in a bunch of things (delegate, tmpdir,
179
+ # thread, fileutils, etc), so it's worth delaying it until this point.
180
+ require 'tempfile'
181
+
175
182
  original_stream = stream.clone
176
183
  captured_stream = Tempfile.new(name)
177
184
 
@@ -8,11 +8,15 @@ module RSpec
8
8
  class RaiseError
9
9
  include Composable
10
10
 
11
- def initialize(expected_error_or_message=Exception, expected_message=nil, &block)
11
+ def initialize(expected_error_or_message=nil, expected_message=nil, &block)
12
12
  @block = block
13
13
  @actual_error = nil
14
+ @warn_about_bare_error = warn_about_potential_false_positives? && expected_error_or_message.nil?
15
+
14
16
  case expected_error_or_message
15
- when String, Regexp
17
+ when nil
18
+ @expected_error, @expected_message = Exception, expected_message
19
+ when String
16
20
  @expected_error, @expected_message = Exception, expected_error_or_message
17
21
  else
18
22
  @expected_error, @expected_message = expected_error_or_message, expected_message
@@ -23,6 +27,7 @@ module RSpec
23
27
  # Specifies the expected error message.
24
28
  def with_message(expected_message)
25
29
  raise_message_already_set if @expected_message
30
+ @warn_about_bare_error = false
26
31
  @expected_message = expected_message
27
32
  self
28
33
  end
@@ -37,20 +42,20 @@ module RSpec
37
42
  @eval_block = false
38
43
  @eval_block_passed = false
39
44
 
45
+ warn_about_bare_error if warning_about_bare_error && !negative_expectation
40
46
  return false unless Proc === given_proc
41
47
 
42
48
  begin
43
49
  given_proc.call
44
50
  rescue Exception => @actual_error
45
- if values_match?(@expected_error, @actual_error)
51
+ if values_match?(@expected_error, @actual_error) ||
52
+ values_match?(@expected_error, @actual_error.message)
46
53
  @raised_expected_error = true
47
54
  @with_expected_message = verify_message
48
55
  end
49
56
  end
50
57
 
51
- unless negative_expectation
52
- eval_block if @raised_expected_error && @with_expected_message && @block
53
- end
58
+ eval_block if !negative_expectation && ready_to_eval_block?
54
59
 
55
60
  expectation_matched?
56
61
  end
@@ -58,7 +63,7 @@ module RSpec
58
63
 
59
64
  # @private
60
65
  def does_not_match?(given_proc)
61
- prevent_invalid_expectations
66
+ warn_for_false_positives
62
67
  !matches?(given_proc, :negative_expectation) && Proc === given_proc
63
68
  end
64
69
 
@@ -103,6 +108,10 @@ module RSpec
103
108
  @eval_block ? @eval_block_passed : true
104
109
  end
105
110
 
111
+ def ready_to_eval_block?
112
+ @raised_expected_error && @with_expected_message && @block
113
+ end
114
+
106
115
  def eval_block
107
116
  @eval_block = true
108
117
  begin
@@ -118,29 +127,62 @@ module RSpec
118
127
  values_match?(@expected_message, @actual_error.message.to_s)
119
128
  end
120
129
 
121
- def prevent_invalid_expectations
122
- what_to_raise = if expecting_specific_exception? && @expected_message
123
- "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
124
- elsif expecting_specific_exception?
125
- "`expect { }.not_to raise_error(SpecificErrorClass)`"
126
- elsif @expected_message
127
- "`expect { }.not_to raise_error(message)`"
128
- end
130
+ def warn_for_false_positives
131
+ return unless warn_about_potential_false_positives?
132
+ expression = if expecting_specific_exception? && @expected_message
133
+ "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
134
+ elsif expecting_specific_exception?
135
+ "`expect { }.not_to raise_error(SpecificErrorClass)`"
136
+ elsif @expected_message
137
+ "`expect { }.not_to raise_error(message)`"
138
+ end
139
+
140
+ return unless expression
141
+
142
+ warn_about_negative_false_positive expression
143
+ end
144
+
145
+ def warn_about_potential_false_positives?
146
+ RSpec::Expectations.configuration.warn_about_potential_false_positives?
147
+ end
129
148
 
130
- return unless what_to_raise
149
+ def warning_about_bare_error
150
+ @warn_about_bare_error && @block.nil?
151
+ end
131
152
 
132
- specific_class_error = ArgumentError.new("#{what_to_raise} is not valid, use `expect { }.not_to raise_error` (with no args) instead")
133
- raise specific_class_error
153
+ def warn_about_bare_error
154
+ RSpec.warning("Using the `raise_error` matcher without providing a specific " \
155
+ "error or message risks false positives, since `raise_error` " \
156
+ "will match when Ruby raises a `NoMethodError`, `NameError` or " \
157
+ "`ArgumentError`, potentially allowing the expectation to pass " \
158
+ "without even executing the method you are intending to call. " \
159
+ "Instead consider providing a specific error class or message. " \
160
+ "This message can be supressed by setting: " \
161
+ "`RSpec::Expectations.configuration.warn_about_potential_false_positives = false`")
162
+ end
163
+
164
+ def warn_about_negative_false_positive(expression)
165
+ RSpec.warning("Using #{expression} risks false positives, since literally " \
166
+ "any other error would cause the expectation to pass, " \
167
+ "including those raised by Ruby (e.g. NoMethodError, NameError " \
168
+ "and ArgumentError), meaning the code you are intending to test " \
169
+ "may not even get reached. Instead consider using " \
170
+ "`expect {}.not_to raise_error`. This message can be supressed by setting: " \
171
+ "`RSpec::Expectations.configuration.warn_about_potential_false_positives = false`")
134
172
  end
135
173
 
136
174
  def expected_error
137
175
  case @expected_message
138
176
  when nil
139
- description_of(@expected_error)
177
+ if RSpec::Support.is_a_matcher?(@expected_error)
178
+ "Exception with #{description_of(@expected_error)}"
179
+ else
180
+ description_of(@expected_error)
181
+ end
140
182
  when Regexp
141
- "#{@expected_error} with message matching #{@expected_message.inspect}"
183
+ "#{@expected_error} with message matching #{description_of(@expected_message)}"
142
184
  else
143
- "#{@expected_error} with #{description_of @expected_message}"
185
+ "#{@expected_error} with #{description_of(@expected_message)}"
144
186
  end
145
187
  end
146
188
 
@@ -155,7 +197,7 @@ module RSpec
155
197
 
156
198
  backtrace = format_backtrace(@actual_error.backtrace)
157
199
  [
158
- ", got #{@actual_error.inspect} with backtrace:",
200
+ ", got #{description_of(@actual_error)} with backtrace:",
159
201
  *backtrace
160
202
  ].join("\n # ")
161
203
  end
@@ -45,7 +45,7 @@ module RSpec
45
45
  # @api private
46
46
  # @return [String]
47
47
  def failure_message
48
- "expected #{@actual.inspect} to respond to #{@failing_method_names.map { |name| name.inspect }.join(', ')}#{with_arity}"
48
+ "expected #{actual_formatted} to respond to #{@failing_method_names.map { |name| description_of(name) }.join(', ')}#{with_arity}"
49
49
  end
50
50
 
51
51
  # @api private
@@ -82,8 +82,7 @@ module RSpec
82
82
  end
83
83
 
84
84
  def pp_names
85
- # Ruby 1.9 returns the same thing for array.to_s as array.inspect, so just use array.inspect here
86
- @names.length == 1 ? "##{@names.first}" : @names.inspect
85
+ @names.length == 1 ? "##{@names.first}" : description_of(@names)
87
86
  end
88
87
  end
89
88
  end
@@ -5,7 +5,11 @@ module RSpec
5
5
  # Provides the implementation for `satisfy`.
6
6
  # Not intended to be instantiated directly.
7
7
  class Satisfy < BaseMatcher
8
- def initialize(&block)
8
+ # @private
9
+ attr_reader :description
10
+
11
+ def initialize(description="satisfy block", &block)
12
+ @description = description
9
13
  @block = block
10
14
  end
11
15
 
@@ -19,19 +23,13 @@ module RSpec
19
23
  # @api private
20
24
  # @return [String]
21
25
  def failure_message
22
- "expected #{@actual} to satisfy block"
26
+ "expected #{actual_formatted} to #{description}"
23
27
  end
24
28
 
25
29
  # @api private
26
30
  # @return [String]
27
31
  def failure_message_when_negated
28
- "expected #{@actual} not to satisfy block"
29
- end
30
-
31
- # @api private
32
- # @return [String]
33
- def description
34
- "satisfy block"
32
+ "expected #{actual_formatted} not to #{description}"
35
33
  end
36
34
  end
37
35
  end
@@ -26,7 +26,9 @@ module RSpec
26
26
  # @return [String]
27
27
  def description
28
28
  return super unless Hash === expected
29
- "#{name_to_sentence} #{surface_descriptions_in(expected).inspect}"
29
+ english_name = EnglishPhrasing.split_words(self.class.matcher_name)
30
+ description_of_expected = surface_descriptions_in(expected).inspect
31
+ "#{english_name} #{description_of_expected}"
30
32
  end
31
33
 
32
34
  private