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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/.document +1 -1
- data/.yardopts +1 -1
- data/Changelog.md +530 -5
- data/{License.txt → LICENSE.md} +5 -4
- data/README.md +73 -31
- data/lib/rspec/expectations/block_snippet_extractor.rb +253 -0
- data/lib/rspec/expectations/configuration.rb +96 -1
- data/lib/rspec/expectations/expectation_target.rb +82 -38
- data/lib/rspec/expectations/fail_with.rb +11 -6
- data/lib/rspec/expectations/failure_aggregator.rb +229 -0
- data/lib/rspec/expectations/handler.rb +36 -15
- data/lib/rspec/expectations/minitest_integration.rb +43 -2
- data/lib/rspec/expectations/syntax.rb +5 -5
- data/lib/rspec/expectations/version.rb +1 -1
- data/lib/rspec/expectations.rb +15 -1
- data/lib/rspec/matchers/aliased_matcher.rb +79 -4
- data/lib/rspec/matchers/built_in/all.rb +11 -0
- data/lib/rspec/matchers/built_in/base_matcher.rb +111 -28
- data/lib/rspec/matchers/built_in/be.rb +28 -114
- data/lib/rspec/matchers/built_in/be_between.rb +1 -1
- data/lib/rspec/matchers/built_in/be_instance_of.rb +5 -1
- data/lib/rspec/matchers/built_in/be_kind_of.rb +5 -1
- data/lib/rspec/matchers/built_in/be_within.rb +5 -12
- data/lib/rspec/matchers/built_in/change.rb +171 -63
- data/lib/rspec/matchers/built_in/compound.rb +201 -30
- data/lib/rspec/matchers/built_in/contain_exactly.rb +73 -12
- data/lib/rspec/matchers/built_in/count_expectation.rb +169 -0
- data/lib/rspec/matchers/built_in/eq.rb +3 -38
- data/lib/rspec/matchers/built_in/eql.rb +2 -2
- data/lib/rspec/matchers/built_in/equal.rb +3 -3
- data/lib/rspec/matchers/built_in/exist.rb +7 -3
- data/lib/rspec/matchers/built_in/has.rb +93 -30
- data/lib/rspec/matchers/built_in/have_attributes.rb +114 -0
- data/lib/rspec/matchers/built_in/include.rb +133 -25
- data/lib/rspec/matchers/built_in/match.rb +79 -2
- data/lib/rspec/matchers/built_in/operators.rb +14 -5
- data/lib/rspec/matchers/built_in/output.rb +59 -2
- data/lib/rspec/matchers/built_in/raise_error.rb +130 -27
- data/lib/rspec/matchers/built_in/respond_to.rb +117 -15
- data/lib/rspec/matchers/built_in/satisfy.rb +28 -14
- data/lib/rspec/matchers/built_in/{start_and_end_with.rb → start_or_end_with.rb} +20 -8
- data/lib/rspec/matchers/built_in/throw_symbol.rb +15 -5
- data/lib/rspec/matchers/built_in/yield.rb +129 -156
- data/lib/rspec/matchers/built_in.rb +5 -3
- data/lib/rspec/matchers/composable.rb +24 -36
- data/lib/rspec/matchers/dsl.rb +203 -37
- data/lib/rspec/matchers/english_phrasing.rb +58 -0
- data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +82 -0
- data/lib/rspec/matchers/fail_matchers.rb +42 -0
- data/lib/rspec/matchers/generated_descriptions.rb +1 -2
- data/lib/rspec/matchers/matcher_delegator.rb +3 -4
- data/lib/rspec/matchers/matcher_protocol.rb +105 -0
- data/lib/rspec/matchers.rb +267 -144
- data.tar.gz.sig +0 -0
- metadata +71 -49
- metadata.gz.sig +0 -0
- data/lib/rspec/matchers/pretty.rb +0 -77
@@ -5,7 +5,7 @@ module RSpec
|
|
5
5
|
# Base class for `and` and `or` compound matchers.
|
6
6
|
class Compound < BaseMatcher
|
7
7
|
# @private
|
8
|
-
attr_reader :matcher_1, :matcher_2
|
8
|
+
attr_reader :matcher_1, :matcher_2, :evaluator
|
9
9
|
|
10
10
|
def initialize(matcher_1, matcher_2)
|
11
11
|
@matcher_1 = matcher_1
|
@@ -14,14 +14,56 @@ module RSpec
|
|
14
14
|
|
15
15
|
# @private
|
16
16
|
def does_not_match?(_actual)
|
17
|
-
raise NotImplementedError, "`expect(...).not_to " \
|
18
|
-
"
|
17
|
+
raise NotImplementedError, "`expect(...).not_to matcher.#{conjunction} matcher` " \
|
18
|
+
"is not supported, since it creates a bit of an ambiguity. Instead, define negated versions " \
|
19
|
+
"of whatever matchers you wish to negate with `RSpec::Matchers.define_negated_matcher` and " \
|
20
|
+
"use `expect(...).to matcher.#{conjunction} matcher`."
|
19
21
|
end
|
20
22
|
|
21
23
|
# @api private
|
22
24
|
# @return [String]
|
23
25
|
def description
|
24
|
-
|
26
|
+
"#{matcher_1.description} #{conjunction} #{matcher_2.description}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def supports_block_expectations?
|
31
|
+
matcher_supports_block_expectations?(matcher_1) &&
|
32
|
+
matcher_supports_block_expectations?(matcher_2)
|
33
|
+
end
|
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
|
42
|
+
def expects_call_stack_jump?
|
43
|
+
NestedEvaluator.matcher_expects_call_stack_jump?(matcher_1) ||
|
44
|
+
NestedEvaluator.matcher_expects_call_stack_jump?(matcher_2)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
# @return [Boolean]
|
49
|
+
def diffable?
|
50
|
+
matcher_is_diffable?(matcher_1) || matcher_is_diffable?(matcher_2)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
# @return [RSpec::Matchers::ExpectedsForMultipleDiffs]
|
55
|
+
def expected
|
56
|
+
return nil unless evaluator
|
57
|
+
::RSpec::Matchers::ExpectedsForMultipleDiffs.for_many_matchers(diffable_matcher_list)
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def diffable_matcher_list
|
63
|
+
list = []
|
64
|
+
list.concat(diffable_matcher_list_for(matcher_1)) unless matcher_1_matches?
|
65
|
+
list.concat(diffable_matcher_list_for(matcher_2)) unless matcher_2_matches?
|
66
|
+
list
|
25
67
|
end
|
26
68
|
|
27
69
|
private
|
@@ -32,6 +74,16 @@ module RSpec
|
|
32
74
|
super
|
33
75
|
end
|
34
76
|
|
77
|
+
def match(_expected, actual)
|
78
|
+
evaluator_klass = if supports_block_expectations? && Proc === actual
|
79
|
+
NestedEvaluator
|
80
|
+
else
|
81
|
+
SequentialEvaluator
|
82
|
+
end
|
83
|
+
|
84
|
+
@evaluator = evaluator_klass.new(actual, matcher_1, matcher_2)
|
85
|
+
end
|
86
|
+
|
35
87
|
def indent_multiline_message(message)
|
36
88
|
message.lines.map do |line|
|
37
89
|
line =~ /\S/ ? ' ' + line : line
|
@@ -39,30 +91,150 @@ module RSpec
|
|
39
91
|
end
|
40
92
|
|
41
93
|
def compound_failure_message
|
42
|
-
|
43
|
-
|
94
|
+
"#{indent_multiline_message(matcher_1.failure_message.sub(/\n+\z/, ''))}" \
|
95
|
+
"\n\n...#{conjunction}:" \
|
96
|
+
"\n\n#{indent_multiline_message(matcher_2.failure_message.sub(/\A\n+/, ''))}"
|
97
|
+
end
|
44
98
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
99
|
+
def matcher_1_matches?
|
100
|
+
evaluator.matcher_matches?(matcher_1)
|
101
|
+
end
|
102
|
+
|
103
|
+
def matcher_2_matches?
|
104
|
+
evaluator.matcher_matches?(matcher_2)
|
105
|
+
end
|
106
|
+
|
107
|
+
def matcher_supports_block_expectations?(matcher)
|
108
|
+
matcher.supports_block_expectations?
|
109
|
+
rescue NoMethodError
|
110
|
+
false
|
50
111
|
end
|
51
112
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
indent_multiline_message(message_2.sub(/\A\n+/, ''))
|
57
|
-
].join("\n\n")
|
113
|
+
def matcher_supports_value_expectations?(matcher)
|
114
|
+
matcher.supports_value_expectations?
|
115
|
+
rescue NoMethodError
|
116
|
+
true
|
58
117
|
end
|
59
118
|
|
60
|
-
def
|
61
|
-
|
119
|
+
def matcher_is_diffable?(matcher)
|
120
|
+
matcher.diffable?
|
121
|
+
rescue NoMethodError
|
122
|
+
false
|
62
123
|
end
|
63
124
|
|
64
|
-
def
|
65
|
-
[
|
125
|
+
def diffable_matcher_list_for(matcher)
|
126
|
+
return [] unless matcher_is_diffable?(matcher)
|
127
|
+
return matcher.diffable_matcher_list if Compound === matcher
|
128
|
+
[matcher]
|
129
|
+
end
|
130
|
+
|
131
|
+
# For value expectations, we can evaluate the matchers sequentially.
|
132
|
+
class SequentialEvaluator
|
133
|
+
def initialize(actual, *)
|
134
|
+
@actual = actual
|
135
|
+
end
|
136
|
+
|
137
|
+
def matcher_matches?(matcher)
|
138
|
+
matcher.matches?(@actual)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Normally, we evaluate the matching sequentially. For an expression like
|
143
|
+
# `expect(x).to foo.and bar`, this becomes:
|
144
|
+
#
|
145
|
+
# expect(x).to foo
|
146
|
+
# expect(x).to bar
|
147
|
+
#
|
148
|
+
# For block expectations, we need to nest them instead, so that
|
149
|
+
# `expect { x }.to foo.and bar` becomes:
|
150
|
+
#
|
151
|
+
# expect {
|
152
|
+
# expect { x }.to foo
|
153
|
+
# }.to bar
|
154
|
+
#
|
155
|
+
# This is necessary so that the `expect` block is only executed once.
|
156
|
+
class NestedEvaluator
|
157
|
+
def initialize(actual, matcher_1, matcher_2)
|
158
|
+
@actual = actual
|
159
|
+
@matcher_1 = matcher_1
|
160
|
+
@matcher_2 = matcher_2
|
161
|
+
@match_results = {}
|
162
|
+
|
163
|
+
inner, outer = order_block_matchers
|
164
|
+
|
165
|
+
@match_results[outer] = outer.matches?(Proc.new do |*args|
|
166
|
+
@match_results[inner] = inner.matches?(inner_matcher_block(args))
|
167
|
+
end)
|
168
|
+
end
|
169
|
+
|
170
|
+
def matcher_matches?(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
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
# Some block matchers (such as `yield_xyz`) pass args to the `expect` block.
|
182
|
+
# When such a matcher is used as the outer matcher, we need to forward the
|
183
|
+
# the args on to the `expect` block.
|
184
|
+
def inner_matcher_block(outer_args)
|
185
|
+
return @actual if outer_args.empty?
|
186
|
+
|
187
|
+
Proc.new do |*inner_args|
|
188
|
+
unless inner_args.empty?
|
189
|
+
raise ArgumentError, "(#{@matcher_1.description}) and " \
|
190
|
+
"(#{@matcher_2.description}) cannot be combined in a compound expectation " \
|
191
|
+
"since both matchers pass arguments to the block."
|
192
|
+
end
|
193
|
+
|
194
|
+
@actual.call(*outer_args)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# For a matcher like `raise_error` or `throw_symbol`, where the block will jump
|
199
|
+
# up the call stack, we need to order things so that it is the inner matcher.
|
200
|
+
# For example, we need it to be this:
|
201
|
+
#
|
202
|
+
# expect {
|
203
|
+
# expect {
|
204
|
+
# x += 1
|
205
|
+
# raise "boom"
|
206
|
+
# }.to raise_error("boom")
|
207
|
+
# }.to change { x }.by(1)
|
208
|
+
#
|
209
|
+
# ...rather than:
|
210
|
+
#
|
211
|
+
# expect {
|
212
|
+
# expect {
|
213
|
+
# x += 1
|
214
|
+
# raise "boom"
|
215
|
+
# }.to change { x }.by(1)
|
216
|
+
# }.to raise_error("boom")
|
217
|
+
#
|
218
|
+
# In the latter case, the after-block logic in the `change` matcher would never
|
219
|
+
# get executed because the `raise "boom"` line would jump to the `rescue` in the
|
220
|
+
# `raise_error` logic, so only the former case will work properly.
|
221
|
+
#
|
222
|
+
# This method figures out which matcher should be the inner matcher and which
|
223
|
+
# should be the outer matcher.
|
224
|
+
def order_block_matchers
|
225
|
+
return @matcher_1, @matcher_2 unless self.class.matcher_expects_call_stack_jump?(@matcher_2)
|
226
|
+
return @matcher_2, @matcher_1 unless self.class.matcher_expects_call_stack_jump?(@matcher_1)
|
227
|
+
|
228
|
+
raise ArgumentError, "(#{@matcher_1.description}) and " \
|
229
|
+
"(#{@matcher_2.description}) cannot be combined in a compound expectation " \
|
230
|
+
"because they both expect a call stack jump."
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.matcher_expects_call_stack_jump?(matcher)
|
234
|
+
matcher.expects_call_stack_jump?
|
235
|
+
rescue NoMethodError
|
236
|
+
false
|
237
|
+
end
|
66
238
|
end
|
67
239
|
|
68
240
|
# @api public
|
@@ -71,9 +243,9 @@ module RSpec
|
|
71
243
|
# @api private
|
72
244
|
# @return [String]
|
73
245
|
def failure_message
|
74
|
-
if
|
246
|
+
if matcher_1_matches?
|
75
247
|
matcher_2.failure_message
|
76
|
-
elsif
|
248
|
+
elsif matcher_2_matches?
|
77
249
|
matcher_1.failure_message
|
78
250
|
else
|
79
251
|
compound_failure_message
|
@@ -82,11 +254,9 @@ module RSpec
|
|
82
254
|
|
83
255
|
private
|
84
256
|
|
85
|
-
def match(
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
@matcher_1_matches && @matcher_2_matches
|
257
|
+
def match(*)
|
258
|
+
super
|
259
|
+
matcher_1_matches? && matcher_2_matches?
|
90
260
|
end
|
91
261
|
|
92
262
|
def conjunction
|
@@ -105,8 +275,9 @@ module RSpec
|
|
105
275
|
|
106
276
|
private
|
107
277
|
|
108
|
-
def match(
|
109
|
-
|
278
|
+
def match(*)
|
279
|
+
super
|
280
|
+
matcher_1_matches? || matcher_2_matches?
|
110
281
|
end
|
111
282
|
|
112
283
|
def conjunction
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module RSpec
|
2
2
|
module Matchers
|
3
3
|
module BuiltIn
|
4
|
+
# rubocop:disable Metrics/ClassLength
|
4
5
|
# @api private
|
5
6
|
# Provides the implementation for `contain_exactly` and `match_array`.
|
6
7
|
# Not intended to be instantiated directly.
|
@@ -9,31 +10,74 @@ module RSpec
|
|
9
10
|
# @return [String]
|
10
11
|
def failure_message
|
11
12
|
if Array === actual
|
12
|
-
|
13
|
-
message += "actual collection contained: #{safe_sort(actual).inspect}\n"
|
14
|
-
message += "the missing elements were: #{safe_sort(surface_descriptions_in missing_items).inspect}\n" unless missing_items.empty?
|
15
|
-
message += "the extra elements were: #{safe_sort(extra_items).inspect}\n" unless extra_items.empty?
|
16
|
-
message
|
13
|
+
generate_failure_message
|
17
14
|
else
|
18
15
|
"expected a collection that can be converted to an array with " \
|
19
|
-
"`#to_ary` or `#to_a`, but got #{
|
16
|
+
"`#to_ary` or `#to_a`, but got #{actual_formatted}"
|
20
17
|
end
|
21
18
|
end
|
22
19
|
|
23
20
|
# @api private
|
24
21
|
# @return [String]
|
25
22
|
def failure_message_when_negated
|
26
|
-
|
23
|
+
list = EnglishPhrasing.list(surface_descriptions_in(expected))
|
24
|
+
"expected #{actual_formatted} not to contain exactly#{list}"
|
27
25
|
end
|
28
26
|
|
29
27
|
# @api private
|
30
28
|
# @return [String]
|
31
29
|
def description
|
32
|
-
|
30
|
+
list = EnglishPhrasing.list(surface_descriptions_in(expected))
|
31
|
+
"contain exactly#{list}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def matches?(actual)
|
35
|
+
@pairings_maximizer = nil
|
36
|
+
@best_solution = nil
|
37
|
+
@extra_items = nil
|
38
|
+
@missing_items = nil
|
39
|
+
super(actual)
|
33
40
|
end
|
34
41
|
|
35
42
|
private
|
36
43
|
|
44
|
+
def generate_failure_message
|
45
|
+
message = expected_collection_line
|
46
|
+
message += actual_collection_line
|
47
|
+
message += missing_elements_line unless missing_items.empty?
|
48
|
+
message += extra_elements_line unless extra_items.empty?
|
49
|
+
message
|
50
|
+
end
|
51
|
+
|
52
|
+
def expected_collection_line
|
53
|
+
message_line('expected collection contained', expected, true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def actual_collection_line
|
57
|
+
message_line('actual collection contained', actual)
|
58
|
+
end
|
59
|
+
|
60
|
+
def missing_elements_line
|
61
|
+
message_line('the missing elements were', missing_items, true)
|
62
|
+
end
|
63
|
+
|
64
|
+
def extra_elements_line
|
65
|
+
message_line('the extra elements were', extra_items)
|
66
|
+
end
|
67
|
+
|
68
|
+
def describe_collection(collection, surface_descriptions=false)
|
69
|
+
if surface_descriptions
|
70
|
+
"#{description_of(safe_sort(surface_descriptions_in collection))}\n"
|
71
|
+
else
|
72
|
+
"#{description_of(safe_sort(collection))}\n"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def message_line(prefix, collection, surface_descriptions=false)
|
77
|
+
"%-32s%s" % [prefix + ':',
|
78
|
+
describe_collection(collection, surface_descriptions)]
|
79
|
+
end
|
80
|
+
|
37
81
|
def match(_expected, _actual)
|
38
82
|
return false unless convert_actual_to_an_array
|
39
83
|
match_when_sorted? || (extra_items.empty? && missing_items.empty?)
|
@@ -50,15 +94,30 @@ module RSpec
|
|
50
94
|
def convert_actual_to_an_array
|
51
95
|
if actual.respond_to?(:to_ary)
|
52
96
|
@actual = actual.to_ary
|
53
|
-
elsif
|
97
|
+
elsif actual.respond_to?(:to_a) && !to_a_disallowed?(actual)
|
54
98
|
@actual = actual.to_a
|
55
99
|
else
|
56
|
-
|
100
|
+
false
|
57
101
|
end
|
58
102
|
end
|
59
103
|
|
60
104
|
def safe_sort(array)
|
61
|
-
array.sort
|
105
|
+
array.sort
|
106
|
+
rescue Support::AllExceptionsExceptOnesWeMustNotRescue
|
107
|
+
array
|
108
|
+
end
|
109
|
+
|
110
|
+
if RUBY_VERSION == "1.8.7"
|
111
|
+
def to_a_disallowed?(object)
|
112
|
+
case object
|
113
|
+
when NilClass, String then true
|
114
|
+
else Kernel == RSpec::Support.method_handle_for(object, :to_a).owner
|
115
|
+
end
|
116
|
+
end
|
117
|
+
else
|
118
|
+
def to_a_disallowed?(object)
|
119
|
+
NilClass === object
|
120
|
+
end
|
62
121
|
end
|
63
122
|
|
64
123
|
def missing_items
|
@@ -125,6 +184,7 @@ module RSpec
|
|
125
184
|
#
|
126
185
|
# @private
|
127
186
|
class PairingsMaximizer
|
187
|
+
# @private
|
128
188
|
Solution = Struct.new(:unmatched_expected_indexes, :unmatched_actual_indexes,
|
129
189
|
:indeterminate_expected_indexes, :indeterminate_actual_indexes) do
|
130
190
|
def worse_than?(other)
|
@@ -227,7 +287,7 @@ module RSpec
|
|
227
287
|
|
228
288
|
modified_expecteds.delete(expected_index)
|
229
289
|
|
230
|
-
modified_actuals
|
290
|
+
modified_actuals = apply_pairing_to(
|
231
291
|
solution.indeterminate_actual_indexes,
|
232
292
|
actual_to_expected_matched_indexes, expected_index)
|
233
293
|
|
@@ -244,6 +304,7 @@ module RSpec
|
|
244
304
|
end
|
245
305
|
end
|
246
306
|
end
|
307
|
+
# rubocop:enable Metrics/ClassLength
|
247
308
|
end
|
248
309
|
end
|
249
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
|
@@ -8,19 +8,19 @@ module RSpec
|
|
8
8
|
# @api private
|
9
9
|
# @return [String]
|
10
10
|
def failure_message
|
11
|
-
"\nexpected: #{
|
11
|
+
"\nexpected: #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using ==)\n"
|
12
12
|
end
|
13
13
|
|
14
14
|
# @api private
|
15
15
|
# @return [String]
|
16
16
|
def failure_message_when_negated
|
17
|
-
"\nexpected: value != #{
|
17
|
+
"\nexpected: value != #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using ==)\n"
|
18
18
|
end
|
19
19
|
|
20
20
|
# @api private
|
21
21
|
# @return [String]
|
22
22
|
def description
|
23
|
-
"
|
23
|
+
"eq #{expected_formatted}"
|
24
24
|
end
|
25
25
|
|
26
26
|
# @api private
|
@@ -34,41 +34,6 @@ module RSpec
|
|
34
34
|
def match(expected, actual)
|
35
35
|
actual == expected
|
36
36
|
end
|
37
|
-
|
38
|
-
def format_object(object)
|
39
|
-
if Time === object
|
40
|
-
format_time(object)
|
41
|
-
elsif defined?(DateTime) && DateTime === object
|
42
|
-
format_date_time(object)
|
43
|
-
elsif defined?(BigDecimal) && BigDecimal === object
|
44
|
-
"#{object.to_s 'F'} (#{object.inspect})"
|
45
|
-
else
|
46
|
-
object.inspect
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
51
|
-
|
52
|
-
if Time.method_defined?(:nsec)
|
53
|
-
def format_time(time)
|
54
|
-
time.strftime("#{TIME_FORMAT}.#{"%09d" % time.nsec} %z")
|
55
|
-
end
|
56
|
-
else # for 1.8.7
|
57
|
-
def format_time(time)
|
58
|
-
time.strftime("#{TIME_FORMAT}.#{"%06d" % time.usec} %z")
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
DATE_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S.%N %z"
|
63
|
-
# ActiveSupport sometimes overrides inspect. If `ActiveSupport` is
|
64
|
-
# defined use a custom format string that includes more time precision.
|
65
|
-
def format_date_time(date_time)
|
66
|
-
if defined?(ActiveSupport)
|
67
|
-
date_time.strftime(DATE_TIME_FORMAT)
|
68
|
-
else
|
69
|
-
date_time.inspect
|
70
|
-
end
|
71
|
-
end
|
72
37
|
end
|
73
38
|
end
|
74
39
|
end
|
@@ -8,13 +8,13 @@ module RSpec
|
|
8
8
|
# @api private
|
9
9
|
# @return [String]
|
10
10
|
def failure_message
|
11
|
-
"\nexpected: #{
|
11
|
+
"\nexpected: #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using eql?)\n"
|
12
12
|
end
|
13
13
|
|
14
14
|
# @api private
|
15
15
|
# @return [String]
|
16
16
|
def failure_message_when_negated
|
17
|
-
"\nexpected: value != #{
|
17
|
+
"\nexpected: value != #{expected_formatted}\n got: #{actual_formatted}\n\n(compared using eql?)\n"
|
18
18
|
end
|
19
19
|
|
20
20
|
# @api private
|