rspec-expectations 3.0.4 → 3.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.document +1 -1
  4. data/.yardopts +1 -1
  5. data/Changelog.md +530 -5
  6. data/{License.txt → LICENSE.md} +5 -4
  7. data/README.md +73 -31
  8. data/lib/rspec/expectations/block_snippet_extractor.rb +253 -0
  9. data/lib/rspec/expectations/configuration.rb +96 -1
  10. data/lib/rspec/expectations/expectation_target.rb +82 -38
  11. data/lib/rspec/expectations/fail_with.rb +11 -6
  12. data/lib/rspec/expectations/failure_aggregator.rb +229 -0
  13. data/lib/rspec/expectations/handler.rb +36 -15
  14. data/lib/rspec/expectations/minitest_integration.rb +43 -2
  15. data/lib/rspec/expectations/syntax.rb +5 -5
  16. data/lib/rspec/expectations/version.rb +1 -1
  17. data/lib/rspec/expectations.rb +15 -1
  18. data/lib/rspec/matchers/aliased_matcher.rb +79 -4
  19. data/lib/rspec/matchers/built_in/all.rb +11 -0
  20. data/lib/rspec/matchers/built_in/base_matcher.rb +111 -28
  21. data/lib/rspec/matchers/built_in/be.rb +28 -114
  22. data/lib/rspec/matchers/built_in/be_between.rb +1 -1
  23. data/lib/rspec/matchers/built_in/be_instance_of.rb +5 -1
  24. data/lib/rspec/matchers/built_in/be_kind_of.rb +5 -1
  25. data/lib/rspec/matchers/built_in/be_within.rb +5 -12
  26. data/lib/rspec/matchers/built_in/change.rb +171 -63
  27. data/lib/rspec/matchers/built_in/compound.rb +201 -30
  28. data/lib/rspec/matchers/built_in/contain_exactly.rb +73 -12
  29. data/lib/rspec/matchers/built_in/count_expectation.rb +169 -0
  30. data/lib/rspec/matchers/built_in/eq.rb +3 -38
  31. data/lib/rspec/matchers/built_in/eql.rb +2 -2
  32. data/lib/rspec/matchers/built_in/equal.rb +3 -3
  33. data/lib/rspec/matchers/built_in/exist.rb +7 -3
  34. data/lib/rspec/matchers/built_in/has.rb +93 -30
  35. data/lib/rspec/matchers/built_in/have_attributes.rb +114 -0
  36. data/lib/rspec/matchers/built_in/include.rb +133 -25
  37. data/lib/rspec/matchers/built_in/match.rb +79 -2
  38. data/lib/rspec/matchers/built_in/operators.rb +14 -5
  39. data/lib/rspec/matchers/built_in/output.rb +59 -2
  40. data/lib/rspec/matchers/built_in/raise_error.rb +130 -27
  41. data/lib/rspec/matchers/built_in/respond_to.rb +117 -15
  42. data/lib/rspec/matchers/built_in/satisfy.rb +28 -14
  43. data/lib/rspec/matchers/built_in/{start_and_end_with.rb → start_or_end_with.rb} +20 -8
  44. data/lib/rspec/matchers/built_in/throw_symbol.rb +15 -5
  45. data/lib/rspec/matchers/built_in/yield.rb +129 -156
  46. data/lib/rspec/matchers/built_in.rb +5 -3
  47. data/lib/rspec/matchers/composable.rb +24 -36
  48. data/lib/rspec/matchers/dsl.rb +203 -37
  49. data/lib/rspec/matchers/english_phrasing.rb +58 -0
  50. data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +82 -0
  51. data/lib/rspec/matchers/fail_matchers.rb +42 -0
  52. data/lib/rspec/matchers/generated_descriptions.rb +1 -2
  53. data/lib/rspec/matchers/matcher_delegator.rb +3 -4
  54. data/lib/rspec/matchers/matcher_protocol.rb +105 -0
  55. data/lib/rspec/matchers.rb +267 -144
  56. data.tar.gz.sig +0 -0
  57. metadata +71 -49
  58. metadata.gz.sig +0 -0
  59. data/lib/rspec/matchers/pretty.rb +0 -77
@@ -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
@@ -8,7 +8,9 @@ module RSpec
8
8
  # Not intended to be instantiated directly.
9
9
  class Output < BaseMatcher
10
10
  def initialize(expected)
11
- @expected = expected
11
+ @expected = expected
12
+ @actual = ""
13
+ @block = nil
12
14
  @stream_capturer = NullCapture
13
15
  end
14
16
 
@@ -25,6 +27,7 @@ module RSpec
25
27
 
26
28
  # @api public
27
29
  # Tells the matcher to match against stdout.
30
+ # Works only when the main Ruby process prints to stdout
28
31
  def to_stdout
29
32
  @stream_capturer = CaptureStdout
30
33
  self
@@ -32,11 +35,30 @@ module RSpec
32
35
 
33
36
  # @api public
34
37
  # Tells the matcher to match against stderr.
38
+ # Works only when the main Ruby process prints to stderr
35
39
  def to_stderr
36
40
  @stream_capturer = CaptureStderr
37
41
  self
38
42
  end
39
43
 
44
+ # @api public
45
+ # Tells the matcher to match against stdout.
46
+ # Works when subprocesses print to stdout as well.
47
+ # This is significantly (~30x) slower than `to_stdout`
48
+ def to_stdout_from_any_process
49
+ @stream_capturer = CaptureStreamToTempfile.new("stdout", $stdout)
50
+ self
51
+ end
52
+
53
+ # @api public
54
+ # Tells the matcher to match against stderr.
55
+ # Works when subprocesses print to stderr as well.
56
+ # This is significantly (~30x) slower than `to_stderr`
57
+ def to_stderr_from_any_process
58
+ @stream_capturer = CaptureStreamToTempfile.new("stderr", $stderr)
59
+ self
60
+ end
61
+
40
62
  # @api private
41
63
  # @return [String]
42
64
  def failure_message
@@ -72,6 +94,13 @@ module RSpec
72
94
  true
73
95
  end
74
96
 
97
+ # @api private
98
+ # Indicates this matcher matches against a block only.
99
+ # @return [False]
100
+ def supports_value_expectations?
101
+ false
102
+ end
103
+
75
104
  private
76
105
 
77
106
  def captured?
@@ -91,7 +120,7 @@ module RSpec
91
120
 
92
121
  def actual_output_description
93
122
  return "nothing" unless captured?
94
- @actual.inspect
123
+ actual_formatted
95
124
  end
96
125
  end
97
126
 
@@ -145,6 +174,34 @@ module RSpec
145
174
  $stderr = original_stream
146
175
  end
147
176
  end
177
+
178
+ # @private
179
+ class CaptureStreamToTempfile < Struct.new(:name, :stream)
180
+ def capture(block)
181
+ # We delay loading tempfile until it is actually needed because
182
+ # we want to minimize stdlibs loaded so that users who use a
183
+ # portion of the stdlib can't have passing specs while forgetting
184
+ # to load it themselves. `CaptureStreamToTempfile` is rarely used
185
+ # and `tempfile` pulls in a bunch of things (delegate, tmpdir,
186
+ # thread, fileutils, etc), so it's worth delaying it until this point.
187
+ require 'tempfile'
188
+
189
+ original_stream = stream.clone
190
+ captured_stream = Tempfile.new(name)
191
+
192
+ begin
193
+ captured_stream.sync = true
194
+ stream.reopen(captured_stream)
195
+ block.call
196
+ captured_stream.rewind
197
+ captured_stream.read
198
+ ensure
199
+ stream.reopen(original_stream)
200
+ captured_stream.close
201
+ captured_stream.unlink
202
+ end
203
+ end
204
+ end
148
205
  end
149
206
  end
150
207
  end
@@ -4,17 +4,33 @@ module RSpec
4
4
  # @api private
5
5
  # Provides the implementation for `raise_error`.
6
6
  # Not intended to be instantiated directly.
7
+ # rubocop:disable Metrics/ClassLength
8
+ # rubocop:disable Lint/RescueException
7
9
  class RaiseError
8
10
  include Composable
9
11
 
10
- def initialize(expected_error_or_message=Exception, expected_message=nil, &block)
12
+ # Used as a sentinel value to be able to tell when the user did not pass an
13
+ # argument. We can't use `nil` for that because we need to warn when `nil` is
14
+ # passed in a different way. It's an Object, not a Module, since Module's `===`
15
+ # does not evaluate to true when compared to itself.
16
+ UndefinedValue = Object.new.freeze
17
+
18
+ def initialize(expected_error_or_message, expected_message, &block)
11
19
  @block = block
12
20
  @actual_error = nil
21
+ @warn_about_bare_error = UndefinedValue === expected_error_or_message
22
+ @warn_about_nil_error = expected_error_or_message.nil?
23
+
13
24
  case expected_error_or_message
14
- when String, Regexp
15
- @expected_error, @expected_message = Exception, expected_error_or_message
25
+ when nil, UndefinedValue
26
+ @expected_error = Exception
27
+ @expected_message = expected_message
28
+ when String
29
+ @expected_error = Exception
30
+ @expected_message = expected_error_or_message
16
31
  else
17
- @expected_error, @expected_message = expected_error_or_message, expected_message
32
+ @expected_error = expected_error_or_message
33
+ @expected_message = expected_message
18
34
  end
19
35
  end
20
36
 
@@ -22,11 +38,12 @@ module RSpec
22
38
  # Specifies the expected error message.
23
39
  def with_message(expected_message)
24
40
  raise_message_already_set if @expected_message
41
+ @warn_about_bare_error = false
25
42
  @expected_message = expected_message
26
43
  self
27
44
  end
28
45
 
29
- # rubocop:disable MethodLength
46
+ # rubocop:disable Metrics/MethodLength
30
47
  # @private
31
48
  def matches?(given_proc, negative_expectation=false, &block)
32
49
  @given_proc = given_proc
@@ -41,23 +58,26 @@ module RSpec
41
58
  begin
42
59
  given_proc.call
43
60
  rescue Exception => @actual_error
44
- if values_match?(@expected_error, @actual_error)
61
+ if values_match?(@expected_error, @actual_error) ||
62
+ values_match?(@expected_error, actual_error_message)
45
63
  @raised_expected_error = true
46
64
  @with_expected_message = verify_message
47
65
  end
48
66
  end
49
67
 
50
68
  unless negative_expectation
51
- eval_block if @raised_expected_error && @with_expected_message && @block
69
+ warn_about_bare_error! if warn_about_bare_error?
70
+ warn_about_nil_error! if warn_about_nil_error?
71
+ eval_block if ready_to_eval_block?
52
72
  end
53
73
 
54
74
  expectation_matched?
55
75
  end
56
- # rubocop:enable MethodLength
76
+ # rubocop:enable Metrics/MethodLength
57
77
 
58
78
  # @private
59
79
  def does_not_match?(given_proc)
60
- prevent_invalid_expectations
80
+ warn_for_negative_false_positives!
61
81
  !matches?(given_proc, :negative_expectation) && Proc === given_proc
62
82
  end
63
83
 
@@ -66,10 +86,20 @@ module RSpec
66
86
  true
67
87
  end
68
88
 
89
+ # @private
90
+ def supports_value_expectations?
91
+ false
92
+ end
93
+
94
+ # @private
95
+ def expects_call_stack_jump?
96
+ true
97
+ end
98
+
69
99
  # @api private
70
100
  # @return [String]
71
101
  def failure_message
72
- @eval_block ? @actual_error.message : "expected #{expected_error}#{given_error}"
102
+ @eval_block ? actual_error_message : "expected #{expected_error}#{given_error}"
73
103
  end
74
104
 
75
105
  # @api private
@@ -86,6 +116,12 @@ module RSpec
86
116
 
87
117
  private
88
118
 
119
+ def actual_error_message
120
+ return nil unless @actual_error
121
+
122
+ @actual_error.respond_to?(:original_message) ? @actual_error.original_message : @actual_error.message
123
+ end
124
+
89
125
  def expectation_matched?
90
126
  error_and_message_match? && block_matches?
91
127
  end
@@ -98,6 +134,10 @@ module RSpec
98
134
  @eval_block ? @eval_block_passed : true
99
135
  end
100
136
 
137
+ def ready_to_eval_block?
138
+ @raised_expected_error && @with_expected_message && @block
139
+ end
140
+
101
141
  def eval_block
102
142
  @eval_block = true
103
143
  begin
@@ -110,32 +150,87 @@ module RSpec
110
150
 
111
151
  def verify_message
112
152
  return true if @expected_message.nil?
113
- values_match?(@expected_message, @actual_error.message)
153
+ values_match?(@expected_message, actual_error_message.to_s)
114
154
  end
115
155
 
116
- def prevent_invalid_expectations
117
- what_to_raise = if expecting_specific_exception? && @expected_message
118
- "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
119
- elsif expecting_specific_exception?
120
- "`expect { }.not_to raise_error(SpecificErrorClass)`"
121
- elsif @expected_message
122
- "`expect { }.not_to raise_error(message)`"
123
- end
156
+ def warn_for_negative_false_positives!
157
+ expression = if expecting_specific_exception? && @expected_message
158
+ "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
159
+ elsif expecting_specific_exception?
160
+ "`expect { }.not_to raise_error(SpecificErrorClass)`"
161
+ elsif @expected_message
162
+ "`expect { }.not_to raise_error(message)`"
163
+ elsif @warn_about_nil_error
164
+ "`expect { }.not_to raise_error(nil)`"
165
+ end
124
166
 
125
- return unless what_to_raise
167
+ return unless expression
126
168
 
127
- specific_class_error = ArgumentError.new("#{what_to_raise} is not valid, use `expect { }.not_to raise_error` (with no args) instead")
128
- raise specific_class_error
169
+ warn_about_negative_false_positive! expression
170
+ end
171
+
172
+ def handle_warning(message)
173
+ RSpec::Expectations.configuration.false_positives_handler.call(message)
174
+ end
175
+
176
+ def warn_about_bare_error?
177
+ @warn_about_bare_error && @block.nil?
178
+ end
179
+
180
+ def warn_about_nil_error?
181
+ @warn_about_nil_error
182
+ end
183
+
184
+ def warn_about_bare_error!
185
+ handle_warning("Using the `raise_error` matcher without providing a specific " \
186
+ "error or message risks false positives, since `raise_error` " \
187
+ "will match when Ruby raises a `NoMethodError`, `NameError` or " \
188
+ "`ArgumentError`, potentially allowing the expectation to pass " \
189
+ "without even executing the method you are intending to call. " \
190
+ "#{warning}"\
191
+ "Instead consider providing a specific error class or message. " \
192
+ "This message can be suppressed by setting: " \
193
+ "`RSpec::Expectations.configuration.on_potential_false" \
194
+ "_positives = :nothing`")
195
+ end
196
+
197
+ def warn_about_nil_error!
198
+ handle_warning("Using the `raise_error` matcher with a `nil` error is probably " \
199
+ "unintentional, it risks false positives, since `raise_error` " \
200
+ "will match when Ruby raises a `NoMethodError`, `NameError` or " \
201
+ "`ArgumentError`, potentially allowing the expectation to pass " \
202
+ "without even executing the method you are intending to call. " \
203
+ "#{warning}"\
204
+ "Instead consider providing a specific error class or message. " \
205
+ "This message can be suppressed by setting: " \
206
+ "`RSpec::Expectations.configuration.on_potential_false" \
207
+ "_positives = :nothing`")
208
+ end
209
+
210
+ def warn_about_negative_false_positive!(expression)
211
+ handle_warning("Using #{expression} risks false positives, since literally " \
212
+ "any other error would cause the expectation to pass, " \
213
+ "including those raised by Ruby (e.g. `NoMethodError`, `NameError` " \
214
+ "and `ArgumentError`), meaning the code you are intending to test " \
215
+ "may not even get reached. Instead consider using " \
216
+ "`expect { }.not_to raise_error` or `expect { }.to raise_error" \
217
+ "(DifferentSpecificErrorClass)`. This message can be suppressed by " \
218
+ "setting: `RSpec::Expectations.configuration.on_potential_false" \
219
+ "_positives = :nothing`")
129
220
  end
130
221
 
131
222
  def expected_error
132
223
  case @expected_message
133
224
  when nil
134
- description_of(@expected_error)
225
+ if RSpec::Support.is_a_matcher?(@expected_error)
226
+ "Exception with #{description_of(@expected_error)}"
227
+ else
228
+ description_of(@expected_error)
229
+ end
135
230
  when Regexp
136
- "#{@expected_error} with message matching #{@expected_message.inspect}"
231
+ "#{@expected_error} with message matching #{description_of(@expected_message)}"
137
232
  else
138
- "#{@expected_error} with #{description_of @expected_message}"
233
+ "#{@expected_error} with #{description_of(@expected_message)}"
139
234
  end
140
235
  end
141
236
 
@@ -150,7 +245,7 @@ module RSpec
150
245
 
151
246
  backtrace = format_backtrace(@actual_error.backtrace)
152
247
  [
153
- ", got #{@actual_error.inspect} with backtrace:",
248
+ ", got #{description_of(@actual_error)} with backtrace:",
154
249
  *backtrace
155
250
  ].join("\n # ")
156
251
  end
@@ -160,9 +255,17 @@ module RSpec
160
255
  end
161
256
 
162
257
  def raise_message_already_set
163
- raise "`expect { }.to raise_error(message).with_message(message)` is not valid. The matcher only allows the expected message to be specified once"
258
+ raise "`expect { }.to raise_error(message).with_message(message)` is not valid. " \
259
+ 'The matcher only allows the expected message to be specified once'
260
+ end
261
+
262
+ def warning
263
+ warning = "Actual error raised was #{description_of(@actual_error)}. "
264
+ warning if @actual_error
164
265
  end
165
266
  end
267
+ # rubocop:enable Lint/RescueException
268
+ # rubocop:enable Metrics/ClassLength
166
269
  end
167
270
  end
168
271
  end
@@ -6,12 +6,14 @@ module RSpec
6
6
  # @api private
7
7
  # Provides the implementation for `respond_to`.
8
8
  # Not intended to be instantiated directly.
9
- class RespondTo
10
- include Composable
11
-
9
+ class RespondTo < BaseMatcher
12
10
  def initialize(*names)
13
11
  @names = names
14
12
  @expected_arity = nil
13
+ @expected_keywords = []
14
+ @ignoring_method_signature_failure = false
15
+ @unlimited_arguments = nil
16
+ @arbitrary_keywords = nil
15
17
  end
16
18
 
17
19
  # @api public
@@ -24,6 +26,43 @@ module RSpec
24
26
  self
25
27
  end
26
28
 
29
+ # @api public
30
+ # Specifies keyword arguments, if any.
31
+ #
32
+ # @example
33
+ # expect(obj).to respond_to(:message).with_keywords(:color, :shape)
34
+ # @example with an expected number of arguments
35
+ # expect(obj).to respond_to(:message).with(3).arguments.and_keywords(:color, :shape)
36
+ def with_keywords(*keywords)
37
+ @expected_keywords = keywords
38
+ self
39
+ end
40
+ alias :and_keywords :with_keywords
41
+
42
+ # @api public
43
+ # Specifies that the method accepts any keyword, i.e. the method has
44
+ # a splatted keyword parameter of the form **kw_args.
45
+ #
46
+ # @example
47
+ # expect(obj).to respond_to(:message).with_any_keywords
48
+ def with_any_keywords
49
+ @arbitrary_keywords = true
50
+ self
51
+ end
52
+ alias :and_any_keywords :with_any_keywords
53
+
54
+ # @api public
55
+ # Specifies that the number of arguments has no upper limit, i.e. the
56
+ # method has a splatted parameter of the form *args.
57
+ #
58
+ # @example
59
+ # expect(obj).to respond_to(:message).with_unlimited_arguments
60
+ def with_unlimited_arguments
61
+ @unlimited_arguments = true
62
+ self
63
+ end
64
+ alias :and_unlimited_arguments :with_unlimited_arguments
65
+
27
66
  # @api public
28
67
  # No-op. Intended to be used as syntactic sugar when using `with`.
29
68
  #
@@ -47,7 +86,7 @@ module RSpec
47
86
  # @api private
48
87
  # @return [String]
49
88
  def failure_message
50
- "expected #{@actual.inspect} to respond to #{@failing_method_names.map { |name| name.inspect }.join(', ')}#{with_arity}"
89
+ "expected #{actual_formatted} to respond to #{@failing_method_names.map { |name| description_of(name) }.join(', ')}#{with_arity}"
51
90
  end
52
91
 
53
92
  # @api private
@@ -62,9 +101,10 @@ module RSpec
62
101
  "respond to #{pp_names}#{with_arity}"
63
102
  end
64
103
 
65
- # @private
66
- def supports_block_expectations?
67
- false
104
+ # @api private
105
+ # Used by other matchers to suppress a check
106
+ def ignoring_method_signature_failure!
107
+ @ignoring_method_signature_failure = true
68
108
  end
69
109
 
70
110
  private
@@ -77,20 +117,82 @@ module RSpec
77
117
  end
78
118
 
79
119
  def matches_arity?(actual, name)
80
- return true unless @expected_arity
81
-
82
- signature = Support::MethodSignature.new(actual.method(name))
83
- Support::MethodSignatureVerifier.new(signature, Array.new(@expected_arity)).valid?
120
+ ArityCheck.new(@expected_arity, @expected_keywords, @arbitrary_keywords, @unlimited_arguments).matches?(actual, name)
121
+ rescue NameError
122
+ return true if @ignoring_method_signature_failure
123
+ raise ArgumentError, "The #{matcher_name} matcher requires that " \
124
+ "the actual object define the method(s) in " \
125
+ "order to check arity, but the method " \
126
+ "`#{name}` is not defined. Remove the arity " \
127
+ "check or define the method to continue."
84
128
  end
85
129
 
86
130
  def with_arity
87
- return "" unless @expected_arity
88
- " with #{@expected_arity} argument#{@expected_arity == 1 ? '' : 's'}"
131
+ str = ''.dup
132
+ str << " with #{with_arity_string}" if @expected_arity
133
+ str << " #{str.length == 0 ? 'with' : 'and'} #{with_keywords_string}" if @expected_keywords && @expected_keywords.count > 0
134
+ str << " #{str.length == 0 ? 'with' : 'and'} unlimited arguments" if @unlimited_arguments
135
+ str << " #{str.length == 0 ? 'with' : 'and'} any keywords" if @arbitrary_keywords
136
+ str
137
+ end
138
+
139
+ def with_arity_string
140
+ "#{@expected_arity} argument#{@expected_arity == 1 ? '' : 's'}"
141
+ end
142
+
143
+ def with_keywords_string
144
+ kw_str = case @expected_keywords.count
145
+ when 1
146
+ @expected_keywords.first.inspect
147
+ when 2
148
+ @expected_keywords.map(&:inspect).join(' and ')
149
+ else
150
+ "#{@expected_keywords[0...-1].map(&:inspect).join(', ')}, and #{@expected_keywords.last.inspect}"
151
+ end
152
+
153
+ "keyword#{@expected_keywords.count == 1 ? '' : 's'} #{kw_str}"
89
154
  end
90
155
 
91
156
  def pp_names
92
- # Ruby 1.9 returns the same thing for array.to_s as array.inspect, so just use array.inspect here
93
- @names.length == 1 ? "##{@names.first}" : @names.inspect
157
+ @names.length == 1 ? "##{@names.first}" : description_of(@names)
158
+ end
159
+
160
+ # @private
161
+ class ArityCheck
162
+ def initialize(expected_arity, expected_keywords, arbitrary_keywords, unlimited_arguments)
163
+ expectation = Support::MethodSignatureExpectation.new
164
+
165
+ if expected_arity.is_a?(Range)
166
+ expectation.min_count = expected_arity.min
167
+ expectation.max_count = expected_arity.max
168
+ else
169
+ expectation.min_count = expected_arity
170
+ end
171
+
172
+ expectation.keywords = expected_keywords
173
+ expectation.expect_unlimited_arguments = unlimited_arguments
174
+ expectation.expect_arbitrary_keywords = arbitrary_keywords
175
+ @expectation = expectation
176
+ end
177
+
178
+ def matches?(actual, name)
179
+ return true if @expectation.empty?
180
+ verifier_for(actual, name).with_expectation(@expectation).valid?
181
+ end
182
+
183
+ def verifier_for(actual, name)
184
+ Support::StrictSignatureVerifier.new(method_signature_for(actual, name))
185
+ end
186
+
187
+ def method_signature_for(actual, name)
188
+ method_handle = Support.method_handle_for(actual, name)
189
+
190
+ if name == :new && method_handle.owner === ::Class && ::Class === actual
191
+ Support::MethodSignature.new(actual.instance_method(:initialize))
192
+ else
193
+ Support::MethodSignature.new(method_handle)
194
+ end
195
+ end
94
196
  end
95
197
  end
96
198
  end
@@ -4,10 +4,9 @@ module RSpec
4
4
  # @api private
5
5
  # Provides the implementation for `satisfy`.
6
6
  # Not intended to be instantiated directly.
7
- class Satisfy
8
- include Composable
9
-
10
- def initialize(&block)
7
+ class Satisfy < BaseMatcher
8
+ def initialize(description=nil, &block)
9
+ @description = description
11
10
  @block = block
12
11
  end
13
12
 
@@ -18,27 +17,42 @@ module RSpec
18
17
  @block.call(actual)
19
18
  end
20
19
 
20
+ # @private
21
+ def description
22
+ @description ||= "satisfy #{block_representation}"
23
+ end
24
+
21
25
  # @api private
22
26
  # @return [String]
23
27
  def failure_message
24
- "expected #{@actual} to satisfy block"
28
+ "expected #{actual_formatted} to #{description}"
25
29
  end
26
30
 
27
31
  # @api private
28
32
  # @return [String]
29
33
  def failure_message_when_negated
30
- "expected #{@actual} not to satisfy block"
34
+ "expected #{actual_formatted} not to #{description}"
31
35
  end
32
36
 
33
- # @api private
34
- # @return [String]
35
- def description
36
- "satisfy block"
37
- end
37
+ private
38
38
 
39
- # @private
40
- def supports_block_expectations?
41
- false
39
+ if RSpec::Support::RubyFeatures.ripper_supported?
40
+ def block_representation
41
+ if (block_snippet = extract_block_snippet)
42
+ "expression `#{block_snippet}`"
43
+ else
44
+ 'block'
45
+ end
46
+ end
47
+
48
+ def extract_block_snippet
49
+ return nil unless @block
50
+ Expectations::BlockSnippetExtractor.try_extracting_single_line_body_of(@block, matcher_name)
51
+ end
52
+ else
53
+ def block_representation
54
+ 'block'
55
+ end
42
56
  end
43
57
  end
44
58
  end