rspec-mocks 3.0.4 → 3.12.6
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 +512 -2
- data/{License.txt → LICENSE.md} +5 -4
- data/README.md +113 -30
- data/lib/rspec/mocks/any_instance/chain.rb +5 -3
- data/lib/rspec/mocks/any_instance/error_generator.rb +31 -0
- data/lib/rspec/mocks/any_instance/expect_chain_chain.rb +1 -5
- data/lib/rspec/mocks/any_instance/expectation_chain.rb +9 -8
- data/lib/rspec/mocks/any_instance/message_chains.rb +7 -8
- data/lib/rspec/mocks/any_instance/proxy.rb +14 -5
- data/lib/rspec/mocks/any_instance/recorder.rb +61 -31
- data/lib/rspec/mocks/any_instance/stub_chain.rb +15 -11
- data/lib/rspec/mocks/any_instance/stub_chain_chain.rb +1 -5
- data/lib/rspec/mocks/any_instance.rb +1 -0
- data/lib/rspec/mocks/argument_list_matcher.rb +55 -10
- data/lib/rspec/mocks/argument_matchers.rb +88 -30
- data/lib/rspec/mocks/configuration.rb +61 -13
- data/lib/rspec/mocks/error_generator.rb +250 -107
- data/lib/rspec/mocks/example_methods.rb +151 -28
- data/lib/rspec/mocks/instance_method_stasher.rb +17 -6
- data/lib/rspec/mocks/matchers/have_received.rb +50 -20
- data/lib/rspec/mocks/matchers/receive.rb +39 -11
- data/lib/rspec/mocks/matchers/receive_message_chain.rb +22 -7
- data/lib/rspec/mocks/matchers/receive_messages.rb +12 -7
- data/lib/rspec/mocks/message_chain.rb +3 -7
- data/lib/rspec/mocks/message_expectation.rb +466 -307
- data/lib/rspec/mocks/method_double.rb +88 -29
- data/lib/rspec/mocks/method_reference.rb +85 -25
- data/lib/rspec/mocks/minitest_integration.rb +68 -0
- data/lib/rspec/mocks/mutate_const.rb +50 -109
- data/lib/rspec/mocks/object_reference.rb +89 -32
- data/lib/rspec/mocks/order_group.rb +4 -5
- data/lib/rspec/mocks/proxy.rb +156 -60
- data/lib/rspec/mocks/space.rb +52 -35
- data/lib/rspec/mocks/standalone.rb +1 -1
- data/lib/rspec/mocks/syntax.rb +26 -30
- data/lib/rspec/mocks/targets.rb +55 -28
- data/lib/rspec/mocks/test_double.rb +43 -7
- data/lib/rspec/mocks/verifying_double.rb +27 -33
- data/lib/rspec/mocks/{verifying_message_expecation.rb → verifying_message_expectation.rb} +11 -16
- data/lib/rspec/mocks/verifying_proxy.rb +77 -26
- data/lib/rspec/mocks/version.rb +1 -1
- data/lib/rspec/mocks.rb +8 -1
- data.tar.gz.sig +0 -0
- metadata +80 -43
- metadata.gz.sig +0 -0
@@ -1,3 +1,5 @@
|
|
1
|
+
RSpec::Support.require_rspec_support "object_formatter"
|
2
|
+
|
1
3
|
module RSpec
|
2
4
|
module Mocks
|
3
5
|
# Raised when a message expectation is not satisfied.
|
@@ -10,6 +12,19 @@ module RSpec
|
|
10
12
|
# Raised when doubles or partial doubles are used outside of the per-test lifecycle.
|
11
13
|
OutsideOfExampleError = Class.new(StandardError)
|
12
14
|
|
15
|
+
# Raised when an expectation customization method (e.g. `with`,
|
16
|
+
# `and_return`) is called on a message expectation which has already been
|
17
|
+
# invoked.
|
18
|
+
MockExpectationAlreadyInvokedError = Class.new(Exception)
|
19
|
+
|
20
|
+
# Raised for situations that RSpec cannot support due to mutations made
|
21
|
+
# externally on arguments that RSpec is holding onto to use for later
|
22
|
+
# comparisons.
|
23
|
+
#
|
24
|
+
# @deprecated We no longer raise this error but the constant remains until
|
25
|
+
# RSpec 4 for SemVer reasons.
|
26
|
+
CannotSupportArgMutationsError = Class.new(StandardError)
|
27
|
+
|
13
28
|
# @private
|
14
29
|
UnsupportedMatcherError = Class.new(StandardError)
|
15
30
|
# @private
|
@@ -21,9 +36,8 @@ module RSpec
|
|
21
36
|
class ErrorGenerator
|
22
37
|
attr_writer :opts
|
23
38
|
|
24
|
-
def initialize(target
|
39
|
+
def initialize(target=nil)
|
25
40
|
@target = target
|
26
|
-
@name = name
|
27
41
|
end
|
28
42
|
|
29
43
|
# @private
|
@@ -32,44 +46,65 @@ module RSpec
|
|
32
46
|
end
|
33
47
|
|
34
48
|
# @private
|
35
|
-
def raise_unexpected_message_error(message,
|
36
|
-
__raise "#{intro} received unexpected message :#{message}#{
|
49
|
+
def raise_unexpected_message_error(message, args)
|
50
|
+
__raise "#{intro} received unexpected message :#{message} with #{format_args(args)}"
|
37
51
|
end
|
38
52
|
|
39
53
|
# @private
|
40
|
-
def raise_unexpected_message_args_error(expectation,
|
41
|
-
|
42
|
-
actual_args = format_received_args(*args)
|
43
|
-
__raise "#{intro} received #{expectation.message.inspect} with unexpected arguments\n expected: #{expected_args}\n got: #{actual_args}"
|
54
|
+
def raise_unexpected_message_args_error(expectation, args_for_multiple_calls, source_id=nil)
|
55
|
+
__raise error_message(expectation, args_for_multiple_calls), nil, source_id
|
44
56
|
end
|
45
57
|
|
46
58
|
# @private
|
47
|
-
def raise_missing_default_stub_error(expectation,
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
def raise_missing_default_stub_error(expectation, args_for_multiple_calls)
|
60
|
+
__raise(
|
61
|
+
error_message(expectation, args_for_multiple_calls) +
|
62
|
+
"\n Please stub a default value first if message might be received with other args as well. \n"
|
63
|
+
)
|
51
64
|
end
|
52
65
|
|
53
66
|
# @private
|
54
|
-
def raise_similar_message_args_error(expectation,
|
55
|
-
|
56
|
-
actual_args = args_for_multiple_calls.collect {|a| format_received_args(*a)}.join(", ")
|
57
|
-
__raise "#{intro} received #{expectation.message.inspect} with unexpected arguments\n expected: #{expected_args}\n got: #{actual_args}"
|
67
|
+
def raise_similar_message_args_error(expectation, args_for_multiple_calls, backtrace_line=nil)
|
68
|
+
__raise error_message(expectation, args_for_multiple_calls), backtrace_line
|
58
69
|
end
|
59
70
|
|
71
|
+
def default_error_message(expectation, expected_args, actual_args)
|
72
|
+
"#{intro} received #{expectation.message.inspect} #{unexpected_arguments_message(expected_args, actual_args)}".dup
|
73
|
+
end
|
74
|
+
|
75
|
+
# rubocop:disable Metrics/ParameterLists
|
60
76
|
# @private
|
61
|
-
def raise_expectation_error(message, expected_received_count, argument_list_matcher,
|
77
|
+
def raise_expectation_error(message, expected_received_count, argument_list_matcher,
|
78
|
+
actual_received_count, expectation_count_type, args,
|
79
|
+
backtrace_line=nil, source_id=nil)
|
62
80
|
expected_part = expected_part_of_expectation_error(expected_received_count, expectation_count_type, argument_list_matcher)
|
63
|
-
received_part = received_part_of_expectation_error(actual_received_count,
|
64
|
-
__raise "(#{intro}).#{message}#{format_args(
|
81
|
+
received_part = received_part_of_expectation_error(actual_received_count, args)
|
82
|
+
__raise "(#{intro(:unwrapped)}).#{message}#{format_args(args)}\n #{expected_part}\n #{received_part}", backtrace_line, source_id
|
65
83
|
end
|
84
|
+
# rubocop:enable Metrics/ParameterLists
|
66
85
|
|
67
86
|
# @private
|
68
|
-
def raise_unimplemented_error(doubled_module, method_name)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
87
|
+
def raise_unimplemented_error(doubled_module, method_name, object)
|
88
|
+
message = case object
|
89
|
+
when InstanceVerifyingDouble
|
90
|
+
"the %s class does not implement the instance method: %s".dup <<
|
91
|
+
if ObjectMethodReference.for(doubled_module, method_name).implemented?
|
92
|
+
". Perhaps you meant to use `class_double` instead?"
|
93
|
+
else
|
94
|
+
""
|
95
|
+
end
|
96
|
+
when ClassVerifyingDouble
|
97
|
+
"the %s class does not implement the class method: %s".dup <<
|
98
|
+
if InstanceMethodReference.for(doubled_module, method_name).implemented?
|
99
|
+
". Perhaps you meant to use `instance_double` instead?"
|
100
|
+
else
|
101
|
+
""
|
102
|
+
end
|
103
|
+
else
|
104
|
+
"%s does not implement: %s"
|
105
|
+
end
|
106
|
+
|
107
|
+
__raise message % [doubled_module.description, method_name]
|
73
108
|
end
|
74
109
|
|
75
110
|
# @private
|
@@ -87,161 +122,269 @@ module RSpec
|
|
87
122
|
# @private
|
88
123
|
def raise_expired_test_double_error
|
89
124
|
raise ExpiredTestDoubleError,
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
125
|
+
"#{intro} was originally created in one example but has leaked into " \
|
126
|
+
"another example and can no longer be used. rspec-mocks' doubles are " \
|
127
|
+
"designed to only last for one example, and you need to create a new " \
|
128
|
+
"one in each example you wish to use it for."
|
94
129
|
end
|
95
130
|
|
96
131
|
# @private
|
97
|
-
def
|
98
|
-
"
|
99
|
-
actual_method_call_args_description(actual_received_count, args)
|
132
|
+
def describe_expectation(verb, message, expected_received_count, _actual_received_count, args)
|
133
|
+
"#{verb} #{message}#{format_args(args)} #{count_message(expected_received_count)}"
|
100
134
|
end
|
101
135
|
|
102
136
|
# @private
|
103
|
-
def
|
104
|
-
"
|
105
|
-
expected_method_call_args_description(argument_list_matcher.expected_args)
|
137
|
+
def raise_out_of_order_error(message)
|
138
|
+
__raise "#{intro} received :#{message} out of order"
|
106
139
|
end
|
107
140
|
|
108
141
|
# @private
|
109
|
-
def
|
110
|
-
|
111
|
-
if count > 0 && args.length > 0
|
112
|
-
" with arguments: #{args.inspect.gsub(/\A\[(.+)\]\z/, '(\1)')}"
|
113
|
-
else
|
114
|
-
""
|
115
|
-
end
|
142
|
+
def raise_missing_block_error(args_to_yield)
|
143
|
+
__raise "#{intro} asked to yield |#{arg_list(args_to_yield)}| but no block was passed"
|
116
144
|
end
|
117
145
|
|
118
146
|
# @private
|
119
|
-
def
|
120
|
-
|
121
|
-
if args.length > 0
|
122
|
-
" with arguments: #{format_args(*args)}"
|
123
|
-
else
|
124
|
-
""
|
125
|
-
end
|
147
|
+
def raise_wrong_arity_error(args_to_yield, signature)
|
148
|
+
__raise "#{intro} yielded |#{arg_list(args_to_yield)}| to block with #{signature.description}"
|
126
149
|
end
|
127
150
|
|
128
151
|
# @private
|
129
|
-
def
|
130
|
-
|
131
|
-
|
132
|
-
return " with any arguments"
|
133
|
-
when ArgumentMatchers::NoArgsMatcher
|
134
|
-
return " with no arguments"
|
135
|
-
end
|
152
|
+
def raise_only_valid_on_a_partial_double(method)
|
153
|
+
__raise "#{intro} is a pure test double. `#{method}` is only " \
|
154
|
+
"available on a partial double."
|
136
155
|
end
|
137
156
|
|
138
157
|
# @private
|
139
|
-
def
|
140
|
-
"have received #{
|
158
|
+
def raise_expectation_on_unstubbed_method(method)
|
159
|
+
__raise "#{intro} expected to have received #{method}, but that " \
|
160
|
+
"object is not a spy or method has not been stubbed."
|
141
161
|
end
|
142
162
|
|
143
163
|
# @private
|
144
|
-
def
|
145
|
-
__raise "#{intro} received
|
164
|
+
def raise_expectation_on_mocked_method(method)
|
165
|
+
__raise "#{intro} expected to have received #{method}, but that " \
|
166
|
+
"method has been mocked instead of stubbed or spied."
|
146
167
|
end
|
147
168
|
|
148
169
|
# @private
|
149
|
-
def
|
150
|
-
__raise "
|
170
|
+
def raise_double_negation_error(wrapped_expression)
|
171
|
+
__raise "Isn't life confusing enough? You've already set a " \
|
172
|
+
"negative message expectation and now you are trying to " \
|
173
|
+
"negate it again with `never`. What does an expression like " \
|
174
|
+
"`#{wrapped_expression}.not_to receive(:msg).never` even mean?"
|
151
175
|
end
|
152
176
|
|
153
177
|
# @private
|
154
|
-
def
|
155
|
-
|
178
|
+
def raise_verifying_double_not_defined_error(ref)
|
179
|
+
notify(VerifyingDoubleNotDefinedError.new(
|
180
|
+
"#{ref.description.inspect} is not a defined constant. " \
|
181
|
+
"Perhaps you misspelt it? " \
|
182
|
+
"Disable check with `verify_doubled_constant_names` configuration option."
|
183
|
+
))
|
156
184
|
end
|
157
185
|
|
158
186
|
# @private
|
159
|
-
def
|
160
|
-
__raise "#{
|
187
|
+
def raise_have_received_disallowed(type, reason)
|
188
|
+
__raise "Using #{type}(...) with the `have_received` " \
|
189
|
+
"matcher is not supported#{reason}."
|
161
190
|
end
|
162
191
|
|
163
192
|
# @private
|
164
|
-
def
|
165
|
-
__raise "
|
166
|
-
"available on a partial double."
|
193
|
+
def raise_cant_constrain_count_for_negated_have_received_error(count_constraint)
|
194
|
+
__raise "can't use #{count_constraint} when negative"
|
167
195
|
end
|
168
196
|
|
169
197
|
# @private
|
170
|
-
def
|
171
|
-
__raise "
|
172
|
-
"method has not been stubbed."
|
198
|
+
def raise_method_not_stubbed_error(method_name)
|
199
|
+
__raise "The method `#{method_name}` was not stubbed or was already unstubbed"
|
173
200
|
end
|
174
201
|
|
175
202
|
# @private
|
176
|
-
def
|
177
|
-
|
178
|
-
|
203
|
+
def raise_already_invoked_error(message, calling_customization)
|
204
|
+
error_message = "The message expectation for #{intro}.#{message} has already been invoked " \
|
205
|
+
"and cannot be modified further (e.g. using `#{calling_customization}`). All message expectation " \
|
206
|
+
"customizations must be applied before it is used for the first time."
|
207
|
+
|
208
|
+
notify MockExpectationAlreadyInvokedError.new(error_message)
|
179
209
|
end
|
180
210
|
|
181
|
-
def
|
182
|
-
|
183
|
-
"negative message expectation and now you are trying to " +
|
184
|
-
"negate it again with `never`. What does an expression like " +
|
185
|
-
"`#{wrapped_expression}.not_to receive(:msg).never` even mean?"
|
211
|
+
def raise_expectation_on_nil_error(method_name)
|
212
|
+
__raise expectation_on_nil_message(method_name)
|
186
213
|
end
|
187
214
|
|
188
|
-
|
215
|
+
def expectation_on_nil_message(method_name)
|
216
|
+
"An expectation of `:#{method_name}` was set on `nil`. " \
|
217
|
+
"To allow expectations on `nil` and suppress this message, set `RSpec::Mocks.configuration.allow_message_expectations_on_nil` to `true`. " \
|
218
|
+
"To disallow expectations on `nil`, set `RSpec::Mocks.configuration.allow_message_expectations_on_nil` to `false`"
|
219
|
+
end
|
220
|
+
|
221
|
+
# @private
|
222
|
+
def intro(unwrapped=false)
|
223
|
+
case @target
|
224
|
+
when TestDouble then TestDoubleFormatter.format(@target, unwrapped)
|
225
|
+
when Class then
|
226
|
+
formatted = "#{@target.inspect} (class)"
|
227
|
+
return formatted if unwrapped
|
228
|
+
"#<#{formatted}>"
|
229
|
+
when NilClass then "nil"
|
230
|
+
else @target.inspect
|
231
|
+
end
|
232
|
+
end
|
189
233
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
elsif Class === @target
|
196
|
-
"<#{@target.inspect} (class)>"
|
197
|
-
elsif @target
|
198
|
-
@target
|
234
|
+
# @private
|
235
|
+
def method_call_args_description(args, generic_prefix=" with arguments: ", matcher_prefix=" with ")
|
236
|
+
case args.first
|
237
|
+
when ArgumentMatchers::AnyArgsMatcher then "#{matcher_prefix}any arguments"
|
238
|
+
when ArgumentMatchers::NoArgsMatcher then "#{matcher_prefix}no arguments"
|
199
239
|
else
|
200
|
-
|
240
|
+
if yield
|
241
|
+
"#{generic_prefix}#{format_args(args)}"
|
242
|
+
else
|
243
|
+
""
|
244
|
+
end
|
201
245
|
end
|
202
246
|
end
|
203
247
|
|
204
|
-
|
205
|
-
|
206
|
-
|
248
|
+
private
|
249
|
+
|
250
|
+
def received_part_of_expectation_error(actual_received_count, args)
|
251
|
+
"received: #{count_message(actual_received_count)}" +
|
252
|
+
method_call_args_description(args) do
|
253
|
+
actual_received_count > 0 && args.length > 0
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def expected_part_of_expectation_error(expected_received_count, expectation_count_type, argument_list_matcher)
|
258
|
+
"expected: #{count_message(expected_received_count, expectation_count_type)}" +
|
259
|
+
method_call_args_description(argument_list_matcher.expected_args) do
|
260
|
+
argument_list_matcher.expected_args.length > 0
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def unexpected_arguments_message(expected_args_string, actual_args_string)
|
265
|
+
"with unexpected arguments\n expected: #{expected_args_string}\n got: #{actual_args_string}"
|
266
|
+
end
|
267
|
+
|
268
|
+
def error_message(expectation, args_for_multiple_calls)
|
269
|
+
expected_args = format_args(expectation.expected_args)
|
270
|
+
actual_args = format_received_args(args_for_multiple_calls)
|
271
|
+
|
272
|
+
if RSpec::Support::RubyFeatures.distincts_kw_args_from_positional_hash?
|
273
|
+
expected_hash = expectation.expected_args.last
|
274
|
+
actual_hash = args_for_multiple_calls.last.last
|
275
|
+
if Hash === expected_hash && Hash === actual_hash &&
|
276
|
+
(Hash.ruby2_keywords_hash?(expected_hash) != Hash.ruby2_keywords_hash?(actual_hash))
|
277
|
+
|
278
|
+
actual_description = Hash.ruby2_keywords_hash?(actual_hash) ? " (keyword arguments)" : " (options hash)"
|
279
|
+
expected_description = Hash.ruby2_keywords_hash?(expected_hash) ? " (keyword arguments)" : " (options hash)"
|
280
|
+
|
281
|
+
if actual_description != expected_description
|
282
|
+
actual_args += actual_description
|
283
|
+
expected_args += expected_description
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
message = default_error_message(expectation, expected_args, actual_args)
|
289
|
+
|
290
|
+
if args_for_multiple_calls.one?
|
291
|
+
diff = diff_message(expectation.expected_args, args_for_multiple_calls.first)
|
292
|
+
if RSpec::Mocks.configuration.color?
|
293
|
+
message << "\nDiff:#{diff}" unless diff.gsub(/\e\[\d+m/, '').strip.empty?
|
294
|
+
else
|
295
|
+
message << "\nDiff:#{diff}" unless diff.strip.empty?
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
message
|
300
|
+
end
|
301
|
+
|
302
|
+
def diff_message(expected_args, actual_args)
|
303
|
+
formatted_expected_args = expected_args.map do |x|
|
304
|
+
RSpec::Support.rspec_description_for_object(x)
|
305
|
+
end
|
306
|
+
|
307
|
+
formatted_expected_args, actual_args = unpack_string_args(formatted_expected_args, actual_args)
|
308
|
+
|
309
|
+
differ.diff(actual_args, formatted_expected_args)
|
310
|
+
end
|
311
|
+
|
312
|
+
def unpack_string_args(formatted_expected_args, actual_args)
|
313
|
+
if [formatted_expected_args, actual_args].all? { |x| list_of_exactly_one_string?(x) }
|
314
|
+
[formatted_expected_args.first, actual_args.first]
|
315
|
+
else
|
316
|
+
[formatted_expected_args, actual_args]
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def list_of_exactly_one_string?(args)
|
321
|
+
Array === args && args.count == 1 && String === args.first
|
207
322
|
end
|
208
323
|
|
209
|
-
def
|
210
|
-
|
324
|
+
def differ
|
325
|
+
RSpec::Support::Differ.new(:color => RSpec::Mocks.configuration.color?)
|
211
326
|
end
|
212
327
|
|
213
|
-
def
|
214
|
-
|
328
|
+
def __raise(message, backtrace_line=nil, source_id=nil)
|
329
|
+
message = opts[:message] unless opts[:message].nil?
|
330
|
+
exception = RSpec::Mocks::MockExpectationError.new(message)
|
331
|
+
prepend_to_backtrace(exception, backtrace_line) if backtrace_line
|
332
|
+
notify exception, :source_id => source_id
|
215
333
|
end
|
216
334
|
|
217
|
-
|
218
|
-
|
335
|
+
if RSpec::Support::Ruby.jruby?
|
336
|
+
def prepend_to_backtrace(exception, line)
|
337
|
+
raise exception
|
338
|
+
rescue RSpec::Mocks::MockExpectationError => with_backtrace
|
339
|
+
with_backtrace.backtrace.unshift(line)
|
340
|
+
end
|
341
|
+
else
|
342
|
+
def prepend_to_backtrace(exception, line)
|
343
|
+
exception.set_backtrace(caller.unshift line)
|
344
|
+
end
|
219
345
|
end
|
220
346
|
|
221
|
-
def
|
222
|
-
|
347
|
+
def notify(*args)
|
348
|
+
RSpec::Support.notify_failure(*args)
|
349
|
+
end
|
223
350
|
|
224
|
-
|
351
|
+
def format_args(args)
|
352
|
+
return "(no args)" if args.empty?
|
353
|
+
"(#{arg_list(args)})"
|
225
354
|
end
|
226
355
|
|
227
|
-
def
|
228
|
-
args.
|
356
|
+
def arg_list(args)
|
357
|
+
args.map { |arg| RSpec::Support::ObjectFormatter.format(arg) }.join(", ")
|
229
358
|
end
|
230
359
|
|
231
|
-
def
|
232
|
-
|
360
|
+
def format_received_args(args_for_multiple_calls)
|
361
|
+
grouped_args(args_for_multiple_calls).map do |args_for_one_call, index|
|
362
|
+
"#{format_args(args_for_one_call)}#{group_count(index, args_for_multiple_calls)}"
|
363
|
+
end.join("\n ")
|
233
364
|
end
|
234
365
|
|
235
366
|
def count_message(count, expectation_count_type=nil)
|
236
367
|
return "at least #{times(count.abs)}" if count < 0 || expectation_count_type == :at_least
|
237
368
|
return "at most #{times(count)}" if expectation_count_type == :at_most
|
238
|
-
|
369
|
+
times(count)
|
239
370
|
end
|
240
371
|
|
241
372
|
def times(count)
|
242
373
|
"#{count} time#{count == 1 ? '' : 's'}"
|
243
374
|
end
|
244
375
|
|
376
|
+
def grouped_args(args)
|
377
|
+
Hash[args.group_by { |x| x }.map { |k, v| [k, v.count] }]
|
378
|
+
end
|
379
|
+
|
380
|
+
def group_count(index, args)
|
381
|
+
" (#{times(index)})" if args.size > 1 || index > 1
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# @private
|
386
|
+
def self.error_generator
|
387
|
+
@error_generator ||= ErrorGenerator.new
|
245
388
|
end
|
246
389
|
end
|
247
390
|
end
|