rspec-expectations 3.8.6 → 3.12.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/Changelog.md +147 -1
  4. data/README.md +35 -20
  5. data/lib/rspec/expectations/configuration.rb +15 -0
  6. data/lib/rspec/expectations/expectation_target.rb +42 -6
  7. data/lib/rspec/expectations/failure_aggregator.rb +41 -6
  8. data/lib/rspec/expectations/handler.rb +20 -8
  9. data/lib/rspec/expectations/version.rb +1 -1
  10. data/lib/rspec/matchers/aliased_matcher.rb +3 -3
  11. data/lib/rspec/matchers/built_in/base_matcher.rb +5 -0
  12. data/lib/rspec/matchers/built_in/be.rb +10 -107
  13. data/lib/rspec/matchers/built_in/be_within.rb +2 -2
  14. data/lib/rspec/matchers/built_in/change.rb +22 -0
  15. data/lib/rspec/matchers/built_in/compound.rb +20 -1
  16. data/lib/rspec/matchers/built_in/contain_exactly.rb +10 -2
  17. data/lib/rspec/matchers/built_in/count_expectation.rb +169 -0
  18. data/lib/rspec/matchers/built_in/exist.rb +1 -1
  19. data/lib/rspec/matchers/built_in/has.rb +88 -24
  20. data/lib/rspec/matchers/built_in/have_attributes.rb +1 -1
  21. data/lib/rspec/matchers/built_in/include.rb +72 -15
  22. data/lib/rspec/matchers/built_in/output.rb +7 -0
  23. data/lib/rspec/matchers/built_in/raise_error.rb +63 -22
  24. data/lib/rspec/matchers/built_in/respond_to.rb +53 -18
  25. data/lib/rspec/matchers/built_in/throw_symbol.rb +10 -4
  26. data/lib/rspec/matchers/built_in/yield.rb +26 -83
  27. data/lib/rspec/matchers/built_in.rb +2 -1
  28. data/lib/rspec/matchers/composable.rb +1 -1
  29. data/lib/rspec/matchers/dsl.rb +29 -11
  30. data/lib/rspec/matchers/english_phrasing.rb +1 -1
  31. data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +16 -7
  32. data/lib/rspec/matchers/matcher_protocol.rb +6 -0
  33. data/lib/rspec/matchers.rb +75 -65
  34. data.tar.gz.sig +0 -0
  35. metadata +27 -12
  36. metadata.gz.sig +0 -0
@@ -143,8 +143,13 @@ module RSpec
143
143
  end
144
144
 
145
145
  def matches?(actual)
146
- @actual = actual
147
- @actual.__send__ @operator, @expected
146
+ perform_match(actual)
147
+ rescue ArgumentError, NoMethodError
148
+ false
149
+ end
150
+
151
+ def does_not_match?(actual)
152
+ !perform_match(actual)
148
153
  rescue ArgumentError, NoMethodError
149
154
  false
150
155
  end
@@ -173,114 +178,12 @@ module RSpec
173
178
  def description
174
179
  "be #{@operator} #{expected_to_sentence}#{args_to_sentence}"
175
180
  end
176
- end
177
-
178
- # @api private
179
- # Provides the implementation of `be_<predicate>`.
180
- # Not intended to be instantiated directly.
181
- class BePredicate < BaseMatcher
182
- include BeHelpers
183
-
184
- def initialize(*args, &block)
185
- @expected = parse_expected(args.shift)
186
- @args = args
187
- @block = block
188
- end
189
-
190
- def matches?(actual, &block)
191
- @actual = actual
192
- @block ||= block
193
- predicate_accessible? && predicate_matches?
194
- end
195
-
196
- def does_not_match?(actual, &block)
197
- @actual = actual
198
- @block ||= block
199
- predicate_accessible? && !predicate_matches?
200
- end
201
-
202
- # @api private
203
- # @return [String]
204
- def failure_message
205
- failure_message_expecting(true)
206
- end
207
-
208
- # @api private
209
- # @return [String]
210
- def failure_message_when_negated
211
- failure_message_expecting(false)
212
- end
213
-
214
- # @api private
215
- # @return [String]
216
- def description
217
- "#{prefix_to_sentence}#{expected_to_sentence}#{args_to_sentence}"
218
- end
219
181
 
220
182
  private
221
183
 
222
- def predicate_accessible?
223
- actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
224
- end
225
-
226
- # support 1.8.7, evaluate once at load time for performance
227
- if String === methods.first
228
- # :nocov:
229
- def private_predicate?
230
- @actual.private_methods.include? predicate.to_s
231
- end
232
- # :nocov:
233
- else
234
- def private_predicate?
235
- @actual.private_methods.include? predicate
236
- end
237
- end
238
-
239
- def predicate_matches?
240
- method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
241
- @predicate_matches = actual.__send__(method_name, *@args, &@block)
242
- end
243
-
244
- def predicate
245
- :"#{@expected}?"
246
- end
247
-
248
- def present_tense_predicate
249
- :"#{@expected}s?"
250
- end
251
-
252
- def parse_expected(expected)
253
- @prefix, expected = prefix_and_expected(expected)
254
- expected
255
- end
256
-
257
- def prefix_and_expected(symbol)
258
- Matchers::BE_PREDICATE_REGEX.match(symbol.to_s).captures.compact
259
- end
260
-
261
- def prefix_to_sentence
262
- EnglishPhrasing.split_words(@prefix)
263
- end
264
-
265
- def failure_message_expecting(value)
266
- validity_message ||
267
- "expected `#{actual_formatted}.#{predicate}#{args_to_s}` to return #{value}, got #{description_of @predicate_matches}"
268
- end
269
-
270
- def validity_message
271
- return nil if predicate_accessible?
272
-
273
- msg = "expected #{actual_formatted} to respond to `#{predicate}`".dup
274
-
275
- if private_predicate?
276
- msg << " but `#{predicate}` is a private method"
277
- elsif predicate == :true?
278
- msg << " or perhaps you meant `be true` or `be_truthy`"
279
- elsif predicate == :false?
280
- msg << " or perhaps you meant `be false` or `be_falsey`"
281
- end
282
-
283
- msg
184
+ def perform_match(actual)
185
+ @actual = actual
186
+ @actual.__send__ @operator, @expected
284
187
  end
285
188
  end
286
189
  end
@@ -23,7 +23,7 @@ module RSpec
23
23
  # a percent comparison.
24
24
  def percent_of(expected)
25
25
  @expected = expected
26
- @tolerance = @delta * @expected.abs / 100.0
26
+ @tolerance = @expected.abs * @delta / 100.0
27
27
  @unit = '%'
28
28
  self
29
29
  end
@@ -50,7 +50,7 @@ module RSpec
50
50
  # @api private
51
51
  # @return [String]
52
52
  def description
53
- "be within #{@delta}#{@unit} of #{@expected}"
53
+ "be within #{@delta}#{@unit} of #{expected_formatted}"
54
54
  end
55
55
 
56
56
  private
@@ -77,6 +77,11 @@ module RSpec
77
77
  true
78
78
  end
79
79
 
80
+ # @private
81
+ def supports_value_expectations?
82
+ false
83
+ end
84
+
80
85
  private
81
86
 
82
87
  def initialize(receiver=nil, message=nil, &block)
@@ -158,6 +163,11 @@ module RSpec
158
163
  true
159
164
  end
160
165
 
166
+ # @private
167
+ def supports_value_expectations?
168
+ false
169
+ end
170
+
161
171
  private
162
172
 
163
173
  def failure_reason
@@ -201,6 +211,11 @@ module RSpec
201
211
  true
202
212
  end
203
213
 
214
+ # @private
215
+ def supports_value_expectations?
216
+ false
217
+ end
218
+
204
219
  private
205
220
 
206
221
  def perform_change(event_proc)
@@ -337,6 +352,8 @@ module RSpec
337
352
  class ChangeDetails
338
353
  attr_reader :actual_after
339
354
 
355
+ UNDEFINED = Module.new.freeze
356
+
340
357
  def initialize(matcher_name, receiver=nil, message=nil, &block)
341
358
  if receiver && !message
342
359
  raise(
@@ -351,6 +368,11 @@ module RSpec
351
368
  @receiver = receiver
352
369
  @message = message
353
370
  @value_proc = block
371
+ # TODO: temporary measure to mute warning of access to an initialized
372
+ # instance variable when a deprecated implicit block expectation
373
+ # syntax is used. This may be removed once `fail` is used, and the
374
+ # matcher never issues this warning.
375
+ @actual_after = UNDEFINED
354
376
  end
355
377
 
356
378
  def value_representation
@@ -26,11 +26,19 @@ module RSpec
26
26
  "#{matcher_1.description} #{conjunction} #{matcher_2.description}"
27
27
  end
28
28
 
29
+ # @api private
29
30
  def supports_block_expectations?
30
31
  matcher_supports_block_expectations?(matcher_1) &&
31
32
  matcher_supports_block_expectations?(matcher_2)
32
33
  end
33
34
 
35
+ # @api private
36
+ def supports_value_expectations?
37
+ matcher_supports_value_expectations?(matcher_1) &&
38
+ matcher_supports_value_expectations?(matcher_2)
39
+ end
40
+
41
+ # @api private
34
42
  def expects_call_stack_jump?
35
43
  NestedEvaluator.matcher_expects_call_stack_jump?(matcher_1) ||
36
44
  NestedEvaluator.matcher_expects_call_stack_jump?(matcher_2)
@@ -102,6 +110,12 @@ module RSpec
102
110
  false
103
111
  end
104
112
 
113
+ def matcher_supports_value_expectations?(matcher)
114
+ matcher.supports_value_expectations?
115
+ rescue NoMethodError
116
+ true
117
+ end
118
+
105
119
  def matcher_is_diffable?(matcher)
106
120
  matcher.diffable?
107
121
  rescue NoMethodError
@@ -154,7 +168,12 @@ module RSpec
154
168
  end
155
169
 
156
170
  def matcher_matches?(matcher)
157
- @match_results.fetch(matcher)
171
+ @match_results.fetch(matcher) do
172
+ raise ArgumentError, "Your #{matcher.description} has no match " \
173
+ "results, this can occur when an unexpected call stack or " \
174
+ "local jump occurs. Perhaps one of your matchers needs to " \
175
+ "declare `expects_call_stack_jump?` as `true`?"
176
+ end
158
177
  end
159
178
 
160
179
  private
@@ -1,7 +1,7 @@
1
1
  module RSpec
2
2
  module Matchers
3
3
  module BuiltIn
4
- # rubocop:disable ClassLength
4
+ # rubocop:disable Metrics/ClassLength
5
5
  # @api private
6
6
  # Provides the implementation for `contain_exactly` and `match_array`.
7
7
  # Not intended to be instantiated directly.
@@ -31,6 +31,14 @@ module RSpec
31
31
  "contain exactly#{list}"
32
32
  end
33
33
 
34
+ def matches?(actual)
35
+ @pairings_maximizer = nil
36
+ @best_solution = nil
37
+ @extra_items = nil
38
+ @missing_items = nil
39
+ super(actual)
40
+ end
41
+
34
42
  private
35
43
 
36
44
  def generate_failure_message
@@ -296,7 +304,7 @@ module RSpec
296
304
  end
297
305
  end
298
306
  end
299
- # rubocop:enable ClassLength
307
+ # rubocop:enable Metrics/ClassLength
300
308
  end
301
309
  end
302
310
  end
@@ -0,0 +1,169 @@
1
+ module RSpec
2
+ module Matchers
3
+ module BuiltIn
4
+ # @api private
5
+ # Abstract class to implement `once`, `at_least` and other
6
+ # count constraints.
7
+ module CountExpectation
8
+ # @api public
9
+ # Specifies that the method is expected to match once.
10
+ def once
11
+ exactly(1)
12
+ end
13
+
14
+ # @api public
15
+ # Specifies that the method is expected to match twice.
16
+ def twice
17
+ exactly(2)
18
+ end
19
+
20
+ # @api public
21
+ # Specifies that the method is expected to match thrice.
22
+ def thrice
23
+ exactly(3)
24
+ end
25
+
26
+ # @api public
27
+ # Specifies that the method is expected to match the given number of times.
28
+ def exactly(number)
29
+ set_expected_count(:==, number)
30
+ self
31
+ end
32
+
33
+ # @api public
34
+ # Specifies the maximum number of times the method is expected to match
35
+ def at_most(number)
36
+ set_expected_count(:<=, number)
37
+ self
38
+ end
39
+
40
+ # @api public
41
+ # Specifies the minimum number of times the method is expected to match
42
+ def at_least(number)
43
+ set_expected_count(:>=, number)
44
+ self
45
+ end
46
+
47
+ # @api public
48
+ # No-op. Provides syntactic sugar.
49
+ def times
50
+ self
51
+ end
52
+
53
+ protected
54
+ # @api private
55
+ attr_reader :count_expectation_type, :expected_count
56
+
57
+ private
58
+
59
+ if RUBY_VERSION.to_f > 1.8
60
+ def cover?(count, number)
61
+ count.cover?(number)
62
+ end
63
+ else
64
+ def cover?(count, number)
65
+ number >= count.first && number <= count.last
66
+ end
67
+ end
68
+
69
+ def expected_count_matches?(actual_count)
70
+ @actual_count = actual_count
71
+ return @actual_count > 0 unless count_expectation_type
72
+ return cover?(expected_count, actual_count) if count_expectation_type == :<=>
73
+
74
+ @actual_count.__send__(count_expectation_type, expected_count)
75
+ end
76
+
77
+ def has_expected_count?
78
+ !!count_expectation_type
79
+ end
80
+
81
+ def set_expected_count(relativity, n)
82
+ raise_unsupported_count_expectation if unsupported_count_expectation?(relativity)
83
+
84
+ count = count_constraint_to_number(n)
85
+
86
+ if count_expectation_type == :<= && relativity == :>=
87
+ raise_impossible_count_expectation(count) if count > expected_count
88
+ @count_expectation_type = :<=>
89
+ @expected_count = count..expected_count
90
+ elsif count_expectation_type == :>= && relativity == :<=
91
+ raise_impossible_count_expectation(count) if count < expected_count
92
+ @count_expectation_type = :<=>
93
+ @expected_count = expected_count..count
94
+ else
95
+ @count_expectation_type = relativity
96
+ @expected_count = count
97
+ end
98
+ end
99
+
100
+ def raise_impossible_count_expectation(count)
101
+ text =
102
+ case count_expectation_type
103
+ when :<= then "at_least(#{count}).at_most(#{expected_count})"
104
+ when :>= then "at_least(#{expected_count}).at_most(#{count})"
105
+ end
106
+ raise ArgumentError, "The constraint #{text} is not possible"
107
+ end
108
+
109
+ def raise_unsupported_count_expectation
110
+ text =
111
+ case count_expectation_type
112
+ when :<= then "at_least"
113
+ when :>= then "at_most"
114
+ when :<=> then "at_least/at_most combination"
115
+ else "count"
116
+ end
117
+ raise ArgumentError, "Multiple #{text} constraints are not supported"
118
+ end
119
+
120
+ def count_constraint_to_number(n)
121
+ case n
122
+ when Numeric then n
123
+ when :once then 1
124
+ when :twice then 2
125
+ when :thrice then 3
126
+ else
127
+ raise ArgumentError, "Expected a number, :once, :twice or :thrice," \
128
+ " but got #{n}"
129
+ end
130
+ end
131
+
132
+ def unsupported_count_expectation?(relativity)
133
+ return true if count_expectation_type == :==
134
+ return true if count_expectation_type == :<=>
135
+ (count_expectation_type == :<= && relativity == :<=) ||
136
+ (count_expectation_type == :>= && relativity == :>=)
137
+ end
138
+
139
+ def count_expectation_description
140
+ "#{human_readable_expectation_type}#{human_readable_count(expected_count)}"
141
+ end
142
+
143
+ def count_failure_reason(action)
144
+ "#{count_expectation_description}" \
145
+ " but #{action}#{human_readable_count(@actual_count)}"
146
+ end
147
+
148
+ def human_readable_expectation_type
149
+ case count_expectation_type
150
+ when :<= then ' at most'
151
+ when :>= then ' at least'
152
+ when :<=> then ' between'
153
+ else ''
154
+ end
155
+ end
156
+
157
+ def human_readable_count(count)
158
+ case count
159
+ when Range then " #{count.first} and #{count.last} times"
160
+ when nil then ''
161
+ when 1 then ' once'
162
+ when 2 then ' twice'
163
+ else " #{count} times"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -81,7 +81,7 @@ module RSpec
81
81
  end
82
82
 
83
83
  def deprecated(predicate, actual)
84
- predicate == :exists? && File == actual
84
+ predicate == :exists? && (File == actual || FileTest == actual || Dir == actual)
85
85
  end
86
86
  end
87
87
  end
@@ -2,12 +2,15 @@ module RSpec
2
2
  module Matchers
3
3
  module BuiltIn
4
4
  # @api private
5
- # Provides the implementation for `has_<predicate>`.
6
- # Not intended to be instantiated directly.
7
- class Has < BaseMatcher
5
+ # Provides the implementation for dynamic predicate matchers.
6
+ # Not intended to be inherited directly.
7
+ class DynamicPredicate < BaseMatcher
8
+ include BeHelpers
9
+
8
10
  def initialize(method_name, *args, &block)
9
11
  @method_name, @args, @block = method_name, args, block
10
12
  end
13
+ ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
11
14
 
12
15
  # @private
13
16
  def matches?(actual, &block)
@@ -20,31 +23,31 @@ module RSpec
20
23
  def does_not_match?(actual, &block)
21
24
  @actual = actual
22
25
  @block ||= block
23
- predicate_accessible? && !predicate_matches?
26
+ predicate_accessible? && predicate_matches?(false)
24
27
  end
25
28
 
26
29
  # @api private
27
30
  # @return [String]
28
31
  def failure_message
29
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return true, got false"
32
+ failure_message_expecting(true)
30
33
  end
31
34
 
32
35
  # @api private
33
36
  # @return [String]
34
37
  def failure_message_when_negated
35
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return false, got true"
38
+ failure_message_expecting(false)
36
39
  end
37
40
 
38
41
  # @api private
39
42
  # @return [String]
40
43
  def description
41
- [method_description, args_description].compact.join(' ')
44
+ "#{method_description}#{args_to_sentence}"
42
45
  end
43
46
 
44
47
  private
45
48
 
46
49
  def predicate_accessible?
47
- !private_predicate? && predicate_exists?
50
+ @actual.respond_to? predicate
48
51
  end
49
52
 
50
53
  # support 1.8.7, evaluate once at load time for performance
@@ -60,44 +63,105 @@ module RSpec
60
63
  end
61
64
  end
62
65
 
63
- def predicate_exists?
64
- @actual.respond_to? predicate
66
+ def predicate_result
67
+ @predicate_result = actual.__send__(predicate_method_name, *@args, &@block)
65
68
  end
66
69
 
67
- def predicate_matches?
68
- @actual.__send__(predicate, *@args, &@block)
70
+ def predicate_method_name
71
+ predicate
69
72
  end
70
73
 
71
- def predicate
74
+ def predicate_matches?(value=true)
75
+ if RSpec::Expectations.configuration.strict_predicate_matchers?
76
+ value == predicate_result
77
+ else
78
+ value == !!predicate_result
79
+ end
80
+ end
81
+
82
+ def root
72
83
  # On 1.9, there appears to be a bug where String#match can return `false`
73
84
  # rather than the match data object. Changing to Regex#match appears to
74
85
  # work around this bug. For an example of this bug, see:
75
86
  # https://travis-ci.org/rspec/rspec-expectations/jobs/27549635
76
- @predicate ||= :"has_#{Matchers::HAS_REGEX.match(@method_name.to_s).captures.first}?"
87
+ self.class::REGEX.match(@method_name.to_s).captures.first
77
88
  end
78
89
 
79
90
  def method_description
80
- @method_name.to_s.tr('_', ' ')
91
+ EnglishPhrasing.split_words(@method_name)
81
92
  end
82
93
 
83
- def args_description
84
- return nil if @args.empty?
85
- @args.map { |arg| RSpec::Support::ObjectFormatter.format(arg) }.join(', ')
94
+ def failure_message_expecting(value)
95
+ validity_message ||
96
+ "expected `#{actual_formatted}.#{predicate}#{args_to_s}` to #{expectation_of value}, got #{description_of @predicate_result}"
86
97
  end
87
98
 
88
- def failure_message_args_description
89
- desc = args_description
90
- "(#{desc})" if desc
99
+ def expectation_of(value)
100
+ if RSpec::Expectations.configuration.strict_predicate_matchers?
101
+ "return #{value}"
102
+ elsif value
103
+ "be truthy"
104
+ else
105
+ "be falsey"
106
+ end
91
107
  end
92
108
 
93
109
  def validity_message
110
+ return nil if predicate_accessible?
111
+
112
+ "expected #{actual_formatted} to respond to `#{predicate}`#{failure_to_respond_explanation}"
113
+ end
114
+
115
+ def failure_to_respond_explanation
94
116
  if private_predicate?
95
- "expected #{@actual} to respond to `#{predicate}` but `#{predicate}` is a private method"
96
- elsif !predicate_exists?
97
- "expected #{@actual} to respond to `#{predicate}`"
117
+ " but `#{predicate}` is a private method"
98
118
  end
99
119
  end
100
120
  end
121
+
122
+ # @api private
123
+ # Provides the implementation for `has_<predicate>`.
124
+ # Not intended to be instantiated directly.
125
+ class Has < DynamicPredicate
126
+ # :nodoc:
127
+ REGEX = Matchers::HAS_REGEX
128
+ private
129
+ def predicate
130
+ @predicate ||= :"has_#{root}?"
131
+ end
132
+ end
133
+
134
+ # @api private
135
+ # Provides the implementation of `be_<predicate>`.
136
+ # Not intended to be instantiated directly.
137
+ class BePredicate < DynamicPredicate
138
+ # :nodoc:
139
+ REGEX = Matchers::BE_PREDICATE_REGEX
140
+ private
141
+ def predicate
142
+ @predicate ||= :"#{root}?"
143
+ end
144
+
145
+ def predicate_method_name
146
+ actual.respond_to?(predicate) ? predicate : present_tense_predicate
147
+ end
148
+
149
+ def failure_to_respond_explanation
150
+ super || if predicate == :true?
151
+ " or perhaps you meant `be true` or `be_truthy`"
152
+ elsif predicate == :false?
153
+ " or perhaps you meant `be false` or `be_falsey`"
154
+ end
155
+ end
156
+
157
+ def predicate_accessible?
158
+ super || actual.respond_to?(present_tense_predicate)
159
+ end
160
+
161
+ def present_tense_predicate
162
+ :"#{root}s?"
163
+ end
164
+ end
101
165
  end
102
166
  end
103
167
  end
@@ -93,7 +93,7 @@ module RSpec
93
93
  end
94
94
 
95
95
  def respond_to_matcher
96
- @respond_to_matcher ||= RespondTo.new(*expected.keys).with(0).arguments
96
+ @respond_to_matcher ||= RespondTo.new(*expected.keys).with(0).arguments.tap { |m| m.ignoring_method_signature_failure! }
97
97
  end
98
98
 
99
99
  def respond_to_failure_message_or