rspec-expectations 3.0.4 → 3.12.3

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