rspec-expectations 3.9.1 → 3.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,17 @@
1
+ require 'rspec/matchers/built_in/count_expectation'
2
+
1
3
  module RSpec
2
4
  module Matchers
3
5
  module BuiltIn
4
6
  # @api private
5
7
  # Provides the implementation for `include`.
6
8
  # Not intended to be instantiated directly.
7
- class Include < BaseMatcher
9
+ class Include < BaseMatcher # rubocop:disable Metrics/ClassLength
10
+ include CountExpectation
8
11
  # @private
9
12
  attr_reader :expecteds
10
13
 
14
+ # @api private
11
15
  def initialize(*expecteds)
12
16
  @expecteds = expecteds
13
17
  end
@@ -15,21 +19,29 @@ module RSpec
15
19
  # @api private
16
20
  # @return [Boolean]
17
21
  def matches?(actual)
18
- actual = actual.to_hash if convert_to_hash?(actual)
19
- perform_match(actual) { |v| v }
22
+ check_actual?(actual) &&
23
+ if check_expected_count?
24
+ expected_count_matches?(count_inclusions)
25
+ else
26
+ perform_match { |v| v }
27
+ end
20
28
  end
21
29
 
22
30
  # @api private
23
31
  # @return [Boolean]
24
32
  def does_not_match?(actual)
25
- actual = actual.to_hash if convert_to_hash?(actual)
26
- perform_match(actual) { |v| !v }
33
+ check_actual?(actual) &&
34
+ if check_expected_count?
35
+ !expected_count_matches?(count_inclusions)
36
+ else
37
+ perform_match { |v| !v }
38
+ end
27
39
  end
28
40
 
29
41
  # @api private
30
42
  # @return [String]
31
43
  def description
32
- improve_hash_formatting("include#{readable_list_of(expecteds)}")
44
+ improve_hash_formatting("include#{readable_list_of(expecteds)}#{count_expectation_description}")
33
45
  end
34
46
 
35
47
  # @api private
@@ -62,12 +74,33 @@ module RSpec
62
74
 
63
75
  private
64
76
 
65
- def format_failure_message(preposition)
66
- if actual.respond_to?(:include?)
67
- improve_hash_formatting("expected #{description_of @actual} #{preposition} include#{readable_list_of @divergent_items}")
68
- else
69
- improve_hash_formatting(yield) + ", but it does not respond to `include?`"
77
+ def check_actual?(actual)
78
+ actual = actual.to_hash if convert_to_hash?(actual)
79
+ @actual = actual
80
+ @actual.respond_to?(:include?)
81
+ end
82
+
83
+ def check_expected_count?
84
+ case
85
+ when !has_expected_count?
86
+ return false
87
+ when expecteds.size != 1
88
+ raise NotImplementedError, 'Count constraint supported only when testing for a single value being included'
89
+ when actual.is_a?(Hash)
90
+ raise NotImplementedError, 'Count constraint on hash keys not implemented'
70
91
  end
92
+ true
93
+ end
94
+
95
+ def format_failure_message(preposition)
96
+ msg = if actual.respond_to?(:include?)
97
+ "expected #{description_of @actual} #{preposition}" \
98
+ " include#{readable_list_of @divergent_items}" \
99
+ "#{count_failure_reason('it is included') if has_expected_count?}"
100
+ else
101
+ "#{yield}, but it does not respond to `include?`"
102
+ end
103
+ improve_hash_formatting(msg)
71
104
  end
72
105
 
73
106
  def readable_list_of(items)
@@ -79,10 +112,9 @@ module RSpec
79
112
  end
80
113
  end
81
114
 
82
- def perform_match(actual, &block)
83
- @actual = actual
115
+ def perform_match(&block)
84
116
  @divergent_items = excluded_from_actual(&block)
85
- actual.respond_to?(:include?) && @divergent_items.empty?
117
+ @divergent_items.empty?
86
118
  end
87
119
 
88
120
  def excluded_from_actual
@@ -107,7 +139,10 @@ module RSpec
107
139
  end
108
140
 
109
141
  def actual_hash_includes?(expected_key, expected_value)
110
- actual_value = actual.fetch(expected_key) { return false }
142
+ actual_value =
143
+ actual.fetch(expected_key) do
144
+ actual.find(Proc.new { return false }) { |actual_key, _| values_match?(expected_key, actual_key) }[1]
145
+ end
111
146
  values_match?(expected_value, actual_value)
112
147
  end
113
148
 
@@ -131,6 +166,28 @@ module RSpec
131
166
  actual.any? { |value| values_match?(expected_item, value) }
132
167
  end
133
168
 
169
+ if RUBY_VERSION < '1.9'
170
+ def count_enumerable(expected_item)
171
+ actual.select { |value| values_match?(expected_item, value) }.size
172
+ end
173
+ else
174
+ def count_enumerable(expected_item)
175
+ actual.count { |value| values_match?(expected_item, value) }
176
+ end
177
+ end
178
+
179
+ def count_inclusions
180
+ @divergent_items = expected
181
+ case actual
182
+ when String
183
+ actual.scan(expected.first).length
184
+ when Enumerable
185
+ count_enumerable(expected.first)
186
+ else
187
+ raise NotImplementedError, 'Count constraints are implemented for Enumerable and String values only'
188
+ end
189
+ end
190
+
134
191
  def diff_would_wrongly_highlight_matched_item?
135
192
  return false unless actual.is_a?(String) && expected.is_a?(Array)
136
193
 
@@ -9,13 +9,20 @@ module RSpec
9
9
  class RaiseError
10
10
  include Composable
11
11
 
12
- def initialize(expected_error_or_message=nil, 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)
13
19
  @block = block
14
20
  @actual_error = nil
15
- @warn_about_bare_error = expected_error_or_message.nil?
21
+ @warn_about_bare_error = UndefinedValue === expected_error_or_message
22
+ @warn_about_nil_error = expected_error_or_message.nil?
16
23
 
17
24
  case expected_error_or_message
18
- when nil
25
+ when nil, UndefinedValue
19
26
  @expected_error = Exception
20
27
  @expected_message = expected_message
21
28
  when String
@@ -58,8 +65,11 @@ module RSpec
58
65
  end
59
66
  end
60
67
 
61
- warn_about_bare_error if warning_about_bare_error && !negative_expectation
62
- eval_block if !negative_expectation && ready_to_eval_block?
68
+ unless negative_expectation
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?
72
+ end
63
73
 
64
74
  expectation_matched?
65
75
  end
@@ -67,7 +77,7 @@ module RSpec
67
77
 
68
78
  # @private
69
79
  def does_not_match?(given_proc)
70
- warn_for_false_positives
80
+ warn_for_negative_false_positives!
71
81
  !matches?(given_proc, :negative_expectation) && Proc === given_proc
72
82
  end
73
83
 
@@ -131,29 +141,35 @@ module RSpec
131
141
  values_match?(@expected_message, @actual_error.message.to_s)
132
142
  end
133
143
 
134
- def warn_for_false_positives
144
+ def warn_for_negative_false_positives!
135
145
  expression = if expecting_specific_exception? && @expected_message
136
146
  "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
137
147
  elsif expecting_specific_exception?
138
148
  "`expect { }.not_to raise_error(SpecificErrorClass)`"
139
149
  elsif @expected_message
140
150
  "`expect { }.not_to raise_error(message)`"
151
+ elsif @warn_about_nil_error
152
+ "`expect { }.not_to raise_error(nil)`"
141
153
  end
142
154
 
143
155
  return unless expression
144
156
 
145
- warn_about_negative_false_positive expression
157
+ warn_about_negative_false_positive! expression
146
158
  end
147
159
 
148
160
  def handle_warning(message)
149
161
  RSpec::Expectations.configuration.false_positives_handler.call(message)
150
162
  end
151
163
 
152
- def warning_about_bare_error
164
+ def warn_about_bare_error?
153
165
  @warn_about_bare_error && @block.nil?
154
166
  end
155
167
 
156
- def warn_about_bare_error
168
+ def warn_about_nil_error?
169
+ @warn_about_nil_error
170
+ end
171
+
172
+ def warn_about_bare_error!
157
173
  handle_warning("Using the `raise_error` matcher without providing a specific " \
158
174
  "error or message risks false positives, since `raise_error` " \
159
175
  "will match when Ruby raises a `NoMethodError`, `NameError` or " \
@@ -166,11 +182,24 @@ module RSpec
166
182
  "_positives = :nothing`")
167
183
  end
168
184
 
169
- def warn_about_negative_false_positive(expression)
185
+ def warn_about_nil_error!
186
+ handle_warning("Using the `raise_error` matcher with a `nil` error is probably " \
187
+ "unintentional, it risks false positives, since `raise_error` " \
188
+ "will match when Ruby raises a `NoMethodError`, `NameError` or " \
189
+ "`ArgumentError`, potentially allowing the expectation to pass " \
190
+ "without even executing the method you are intending to call. " \
191
+ "#{warning}"\
192
+ "Instead consider providing a specific error class or message. " \
193
+ "This message can be suppressed by setting: " \
194
+ "`RSpec::Expectations.configuration.on_potential_false" \
195
+ "_positives = :nothing`")
196
+ end
197
+
198
+ def warn_about_negative_false_positive!(expression)
170
199
  handle_warning("Using #{expression} risks false positives, since literally " \
171
200
  "any other error would cause the expectation to pass, " \
172
- "including those raised by Ruby (e.g. NoMethodError, NameError " \
173
- "and ArgumentError), meaning the code you are intending to test " \
201
+ "including those raised by Ruby (e.g. `NoMethodError`, `NameError` " \
202
+ "and `ArgumentError`), meaning the code you are intending to test " \
174
203
  "may not even get reached. Instead consider using " \
175
204
  "`expect { }.not_to raise_error` or `expect { }.to raise_error" \
176
205
  "(DifferentSpecificErrorClass)`. This message can be suppressed by " \
@@ -1,7 +1,5 @@
1
1
  RSpec::Support.require_rspec_support "method_signature_verifier"
2
2
 
3
- # TODO: Refactor this file to be under our class length
4
- # rubocop:disable ClassLength
5
3
  module RSpec
6
4
  module Matchers
7
5
  module BuiltIn
@@ -118,49 +116,15 @@ module RSpec
118
116
  end
119
117
  end
120
118
 
121
- def setup_method_signature_expectation
122
- expectation = Support::MethodSignatureExpectation.new
123
-
124
- if @expected_arity.is_a?(Range)
125
- expectation.min_count = @expected_arity.min
126
- expectation.max_count = @expected_arity.max
127
- else
128
- expectation.min_count = @expected_arity
129
- end
130
-
131
- expectation.keywords = @expected_keywords
132
- expectation.expect_unlimited_arguments = @unlimited_arguments
133
- expectation.expect_arbitrary_keywords = @arbitrary_keywords
134
-
135
- expectation
136
- end
137
-
138
119
  def matches_arity?(actual, name)
139
- expectation = setup_method_signature_expectation
140
-
141
- return true if expectation.empty?
142
-
143
- begin
144
- Support::StrictSignatureVerifier.new(method_signature_for(actual, name)).
145
- with_expectation(expectation).valid?
146
- rescue NameError
147
- return true if @ignoring_method_signature_failure
148
- raise ArgumentError, "The #{matcher_name} matcher requires that " \
149
- "the actual object define the method(s) in " \
150
- "order to check arity, but the method " \
151
- "`#{name}` is not defined. Remove the arity " \
152
- "check or define the method to continue."
153
- end
154
- end
155
-
156
- def method_signature_for(actual, name)
157
- method_handle = Support.method_handle_for(actual, name)
158
-
159
- if name == :new && method_handle.owner === ::Class && ::Class === actual
160
- Support::MethodSignature.new(actual.instance_method(:initialize))
161
- else
162
- Support::MethodSignature.new(method_handle)
163
- end
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."
164
128
  end
165
129
 
166
130
  def with_arity
@@ -192,8 +156,45 @@ module RSpec
192
156
  def pp_names
193
157
  @names.length == 1 ? "##{@names.first}" : description_of(@names)
194
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
196
+ end
195
197
  end
196
198
  end
197
199
  end
198
200
  end
199
- # rubocop:enable ClassLength
@@ -1,3 +1,5 @@
1
+ require 'rspec/matchers/built_in/count_expectation'
2
+
1
3
  RSpec::Support.require_rspec_support 'method_signature_verifier'
2
4
 
3
5
  module RSpec
@@ -97,64 +99,12 @@ module RSpec
97
99
  # Provides the implementation for `yield_control`.
98
100
  # Not intended to be instantiated directly.
99
101
  class YieldControl < BaseMatcher
100
- def initialize
101
- at_least(:once)
102
- end
103
-
104
- # @api public
105
- # Specifies that the method is expected to yield once.
106
- def once
107
- exactly(1)
108
- self
109
- end
110
-
111
- # @api public
112
- # Specifies that the method is expected to yield twice.
113
- def twice
114
- exactly(2)
115
- self
116
- end
117
-
118
- # @api public
119
- # Specifies that the method is expected to yield thrice.
120
- def thrice
121
- exactly(3)
122
- self
123
- end
124
-
125
- # @api public
126
- # Specifies that the method is expected to yield the given number of times.
127
- def exactly(number)
128
- set_expected_yields_count(:==, number)
129
- self
130
- end
131
-
132
- # @api public
133
- # Specifies the maximum number of times the method is expected to yield
134
- def at_most(number)
135
- set_expected_yields_count(:<=, number)
136
- self
137
- end
138
-
139
- # @api public
140
- # Specifies the minimum number of times the method is expected to yield
141
- def at_least(number)
142
- set_expected_yields_count(:>=, number)
143
- self
144
- end
145
-
146
- # @api public
147
- # No-op. Provides syntactic sugar.
148
- def times
149
- self
150
- end
151
-
102
+ include CountExpectation
152
103
  # @private
153
104
  def matches?(block)
154
105
  @probe = YieldProbe.probe(block)
155
106
  return false unless @probe.has_block?
156
-
157
- @probe.num_yields.__send__(@expectation_type, @expected_yields_count)
107
+ expected_count_matches?(@probe.num_yields)
158
108
  end
159
109
 
160
110
  # @private
@@ -181,37 +131,10 @@ module RSpec
181
131
 
182
132
  private
183
133
 
184
- def set_expected_yields_count(relativity, n)
185
- @expectation_type = relativity
186
- @expected_yields_count = case n
187
- when Numeric then n
188
- when :once then 1
189
- when :twice then 2
190
- when :thrice then 3
191
- end
192
- end
193
-
194
134
  def failure_reason
195
135
  return ' but was not a block' unless @probe.has_block?
196
- return '' unless @expected_yields_count
197
- " #{human_readable_expectation_type}#{human_readable_count(@expected_yields_count)}" \
198
- " but yielded #{human_readable_count(@probe.num_yields)}"
199
- end
200
-
201
- def human_readable_expectation_type
202
- case @expectation_type
203
- when :<= then 'at most '
204
- when :>= then 'at least '
205
- else ''
206
- end
207
- end
208
-
209
- def human_readable_count(count)
210
- case count
211
- when 1 then 'once'
212
- when 2 then 'twice'
213
- else "#{count} times"
214
- end
136
+ return "#{count_expectation_description} but did not yield" if @probe.num_yields == 0
137
+ count_failure_reason('yielded')
215
138
  end
216
139
  end
217
140