mocha 1.12.0 → 2.1.0

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -13
  3. data/.yardopts +0 -1
  4. data/CONTRIBUTING.md +1 -1
  5. data/COPYING.md +2 -2
  6. data/Gemfile +27 -0
  7. data/MIT-LICENSE.md +1 -1
  8. data/README.md +12 -8
  9. data/RELEASE.md +169 -0
  10. data/Rakefile +22 -29
  11. data/gemfiles/Gemfile.minitest.latest +1 -0
  12. data/gemfiles/Gemfile.test-unit.latest +2 -5
  13. data/lib/mocha/any_instance_method.rb +0 -5
  14. data/lib/mocha/api.rb +29 -78
  15. data/lib/mocha/backtrace_filter.rb +2 -2
  16. data/lib/mocha/class_methods.rb +2 -2
  17. data/lib/mocha/configuration.rb +36 -114
  18. data/lib/mocha/debug.rb +2 -5
  19. data/lib/mocha/expectation.rb +63 -8
  20. data/lib/mocha/inspect.rb +6 -4
  21. data/lib/mocha/instance_method.rb +0 -4
  22. data/lib/mocha/integration/mini_test/adapter.rb +1 -1
  23. data/lib/mocha/integration/mini_test.rb +10 -38
  24. data/lib/mocha/integration/test_unit/adapter.rb +4 -4
  25. data/lib/mocha/integration/test_unit.rb +10 -33
  26. data/lib/mocha/invocation.rb +12 -16
  27. data/lib/mocha/minitest.rb +2 -4
  28. data/lib/mocha/mock.rb +32 -32
  29. data/lib/mocha/mockery.rb +1 -3
  30. data/lib/mocha/names.rb +1 -1
  31. data/lib/mocha/parameter_matchers/base.rb +1 -1
  32. data/lib/mocha/parameter_matchers/equivalent_uri.rb +1 -2
  33. data/lib/mocha/parameter_matchers/has_entry.rb +22 -13
  34. data/lib/mocha/parameter_matchers/has_keys.rb +53 -0
  35. data/lib/mocha/parameter_matchers/instance_methods.rb +10 -1
  36. data/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb +64 -0
  37. data/lib/mocha/parameter_matchers.rb +1 -0
  38. data/lib/mocha/parameters_matcher.rb +3 -3
  39. data/lib/mocha/ruby_version.rb +1 -2
  40. data/lib/mocha/stubbed_method.rb +5 -42
  41. data/lib/mocha/test_unit.rb +2 -4
  42. data/lib/mocha/version.rb +1 -1
  43. data/lib/mocha.rb +0 -8
  44. data/mocha.gemspec +4 -29
  45. metadata +10 -87
  46. data/bin/build-matrix +0 -82
  47. data/gemfiles/Gemfile.minitest.1.3.0 +0 -7
  48. data/gemfiles/Gemfile.minitest.1.4.0 +0 -7
  49. data/gemfiles/Gemfile.minitest.1.4.1 +0 -7
  50. data/gemfiles/Gemfile.minitest.1.4.2 +0 -7
  51. data/gemfiles/Gemfile.minitest.2.0.0 +0 -7
  52. data/gemfiles/Gemfile.minitest.2.0.1 +0 -7
  53. data/gemfiles/Gemfile.minitest.2.11.0 +0 -7
  54. data/gemfiles/Gemfile.minitest.2.11.2 +0 -7
  55. data/gemfiles/Gemfile.minitest.2.3.0 +0 -7
  56. data/gemfiles/Gemfile.minitest.5.11.3 +0 -7
  57. data/gemfiles/Gemfile.test-unit.2.0.0 +0 -7
  58. data/gemfiles/Gemfile.test-unit.2.0.1 +0 -7
  59. data/gemfiles/Gemfile.test-unit.2.0.3 +0 -7
  60. data/init.rb +0 -1
  61. data/lib/mocha/integration/mini_test/nothing.rb +0 -19
  62. data/lib/mocha/integration/mini_test/version_13.rb +0 -54
  63. data/lib/mocha/integration/mini_test/version_140.rb +0 -54
  64. data/lib/mocha/integration/mini_test/version_141.rb +0 -65
  65. data/lib/mocha/integration/mini_test/version_142_to_172.rb +0 -65
  66. data/lib/mocha/integration/mini_test/version_200.rb +0 -66
  67. data/lib/mocha/integration/mini_test/version_201_to_222.rb +0 -66
  68. data/lib/mocha/integration/mini_test/version_2110_to_2111.rb +0 -70
  69. data/lib/mocha/integration/mini_test/version_2112_to_320.rb +0 -73
  70. data/lib/mocha/integration/mini_test/version_230_to_2101.rb +0 -68
  71. data/lib/mocha/integration/test_unit/gem_version_200.rb +0 -62
  72. data/lib/mocha/integration/test_unit/gem_version_201_to_202.rb +0 -62
  73. data/lib/mocha/integration/test_unit/gem_version_203_to_220.rb +0 -62
  74. data/lib/mocha/integration/test_unit/gem_version_230_to_250.rb +0 -68
  75. data/lib/mocha/integration/test_unit/nothing.rb +0 -19
  76. data/lib/mocha/integration/test_unit/ruby_version_185_and_below.rb +0 -61
  77. data/lib/mocha/integration/test_unit/ruby_version_186_and_above.rb +0 -63
  78. data/lib/mocha/integration.rb +0 -11
  79. data/lib/mocha/setup.rb +0 -14
  80. data/lib/mocha/singleton_class.rb +0 -9
  81. data/yard-templates/default/layout/html/google_analytics.erb +0 -8
  82. data/yard-templates/default/layout/html/setup.rb +0 -5
@@ -12,7 +12,7 @@ module Mocha
12
12
  include Mocha::API
13
13
 
14
14
  # @private
15
- def self.applicable_to?(test_unit_version, _ruby_version = nil)
15
+ def self.applicable_to?(test_unit_version)
16
16
  Gem::Requirement.new('>= 2.5.1').satisfied_by?(test_unit_version)
17
17
  end
18
18
 
@@ -23,16 +23,16 @@ module Mocha
23
23
 
24
24
  # @private
25
25
  def self.included(mod)
26
- mod.setup :mocha_setup, :before => :prepend
26
+ mod.setup :mocha_setup, before: :prepend
27
27
 
28
28
  mod.exception_handler(:handle_mocha_expectation_error)
29
29
 
30
- mod.cleanup :after => :append do
30
+ mod.cleanup after: :append do
31
31
  assertion_counter = Integration::AssertionCounter.new(self)
32
32
  mocha_verify(assertion_counter)
33
33
  end
34
34
 
35
- mod.teardown :mocha_teardown, :after => :append
35
+ mod.teardown :mocha_teardown, after: :append
36
36
  end
37
37
 
38
38
  private
@@ -1,49 +1,26 @@
1
1
  require 'mocha/debug'
2
-
3
2
  require 'mocha/detection/test_unit'
4
-
5
- require 'mocha/integration/test_unit/nothing'
6
- require 'mocha/integration/test_unit/ruby_version_185_and_below'
7
- require 'mocha/integration/test_unit/ruby_version_186_and_above'
8
- require 'mocha/integration/test_unit/gem_version_200'
9
- require 'mocha/integration/test_unit/gem_version_201_to_202'
10
- require 'mocha/integration/test_unit/gem_version_203_to_220'
11
- require 'mocha/integration/test_unit/gem_version_230_to_250'
12
3
  require 'mocha/integration/test_unit/adapter'
13
4
 
14
- require 'mocha/deprecation'
15
-
16
5
  module Mocha
17
6
  module Integration
18
7
  module TestUnit
19
8
  def self.activate
20
- return false unless Detection::TestUnit.testcase
21
- test_unit_version = Gem::Version.new(Detection::TestUnit.version)
22
- ruby_version = Gem::Version.new(RUBY_VERSION.dup)
9
+ target = Detection::TestUnit.testcase
10
+ return false unless target
23
11
 
24
- Debug.puts "Detected Ruby version: #{ruby_version}"
12
+ test_unit_version = Gem::Version.new(Detection::TestUnit.version)
25
13
  Debug.puts "Detected Test::Unit version: #{test_unit_version}"
26
14
 
27
- integration_module = [
28
- TestUnit::Adapter,
29
- TestUnit::GemVersion230To250,
30
- TestUnit::GemVersion203To220,
31
- TestUnit::GemVersion201To202,
32
- TestUnit::GemVersion200,
33
- TestUnit::RubyVersion186AndAbove,
34
- TestUnit::RubyVersion185AndBelow,
35
- TestUnit::Nothing
36
- ].detect { |m| m.applicable_to?(test_unit_version, ruby_version) }
15
+ unless TestUnit::Adapter.applicable_to?(test_unit_version)
16
+ raise 'Versions of test-unit earlier than v2.5.1 are not supported.'
17
+ end
37
18
 
38
- unless ::Test::Unit::TestCase < integration_module
39
- unless integration_module == TestUnit::Adapter
40
- Deprecation.warning(
41
- 'Versions of test-unit earlier than v2.5.1 will not be supported in future versions of Mocha.'
42
- )
43
- end
44
- Debug.puts "Applying #{integration_module.description}"
45
- ::Test::Unit::TestCase.send(:include, integration_module)
19
+ unless target < TestUnit::Adapter
20
+ Debug.puts "Applying #{TestUnit::Adapter.description}"
21
+ target.send(:include, TestUnit::Adapter)
46
22
  end
23
+
47
24
  true
48
25
  end
49
26
  end
@@ -3,14 +3,12 @@ require 'mocha/raised_exception'
3
3
  require 'mocha/return_values'
4
4
  require 'mocha/thrown_object'
5
5
  require 'mocha/yield_parameters'
6
- require 'mocha/configuration'
7
- require 'mocha/deprecation'
8
6
 
9
7
  module Mocha
10
8
  class Invocation
11
9
  attr_reader :method_name, :block
12
10
 
13
- def initialize(mock, method_name, *arguments, &block)
11
+ def initialize(mock, method_name, arguments = [], block = nil)
14
12
  @mock = mock
15
13
  @method_name = method_name
16
14
  @arguments = arguments
@@ -22,18 +20,8 @@ module Mocha
22
20
  def call(yield_parameters = YieldParameters.new, return_values = ReturnValues.new)
23
21
  yield_parameters.next_invocation.each do |yield_args|
24
22
  @yields << ParametersMatcher.new(yield_args)
25
- if @block
26
- @block.call(*yield_args)
27
- else
28
- raise LocalJumpError unless Mocha.configuration.reinstate_undocumented_behaviour_from_v1_9?
29
- yield_args_description = ParametersMatcher.new(yield_args).mocha_inspect
30
- Deprecation.warning(
31
- "Stubbed method was instructed to yield #{yield_args_description}, but no block was given by invocation: #{call_description}.",
32
- ' This will raise a LocalJumpError in the future.',
33
- ' Use Expectation#with_block_given to constrain this expectation to match invocations supplying a block.',
34
- ' And, if necessary, add another expectation to match invocations not supplying a block.'
35
- )
36
- end
23
+ raise LocalJumpError unless @block
24
+ @block.call(*yield_args)
37
25
  end
38
26
  return_values.next(self)
39
27
  end
@@ -55,7 +43,7 @@ module Mocha
55
43
  end
56
44
 
57
45
  def call_description
58
- description = "#{@mock.mocha_inspect}.#{@method_name}#{ParametersMatcher.new(@arguments).mocha_inspect}"
46
+ description = "#{@mock.mocha_inspect}.#{@method_name}#{argument_description}"
59
47
  description << ' { ... }' unless @block.nil?
60
48
  description
61
49
  end
@@ -73,5 +61,13 @@ module Mocha
73
61
  def full_description
74
62
  "\n - #{call_description} #{result_description}"
75
63
  end
64
+
65
+ private
66
+
67
+ def argument_description
68
+ signature = arguments.mocha_inspect
69
+ signature = signature.gsub(/^\[|\]$/, '')
70
+ "(#{signature})"
71
+ end
76
72
  end
77
73
  end
@@ -1,8 +1,6 @@
1
+ require 'mocha/ruby_version'
1
2
  require 'mocha/integration/mini_test'
2
- require 'mocha/deprecation'
3
3
 
4
4
  unless Mocha::Integration::MiniTest.activate
5
- Mocha::Deprecation.warning(
6
- "MiniTest must be loaded *before* `require 'mocha/minitest'`."
7
- )
5
+ raise "MiniTest must be loaded *before* `require 'mocha/minitest'`."
8
6
  end
data/lib/mocha/mock.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'mocha/singleton_class'
1
+ require 'ruby2_keywords'
2
2
  require 'mocha/expectation'
3
3
  require 'mocha/expectation_list'
4
4
  require 'mocha/invocation'
@@ -8,7 +8,6 @@ require 'mocha/method_matcher'
8
8
  require 'mocha/parameters_matcher'
9
9
  require 'mocha/argument_iterator'
10
10
  require 'mocha/expectation_error_factory'
11
- require 'mocha/ruby_version'
12
11
 
13
12
  module Mocha
14
13
  # Traditional mock object.
@@ -109,6 +108,7 @@ module Mocha
109
108
  # object.expects(:expected_method_one).returns(:result_one)
110
109
  # object.expects(:expected_method_two).returns(:result_two)
111
110
  def expects(method_name_or_hash, backtrace = nil)
111
+ expectation = nil
112
112
  iterator = ArgumentIterator.new(method_name_or_hash)
113
113
  iterator.each do |*args|
114
114
  method_name = args.shift
@@ -117,6 +117,7 @@ module Mocha
117
117
  expectation.returns(args.shift) unless args.empty?
118
118
  @expectations.add(expectation)
119
119
  end
120
+ expectation
120
121
  end
121
122
 
122
123
  # Adds an expectation that the specified method may be called any number of times with any parameters.
@@ -145,6 +146,7 @@ module Mocha
145
146
  # object.stubs(:stubbed_method_one).returns(:result_one)
146
147
  # object.stubs(:stubbed_method_two).returns(:result_two)
147
148
  def stubs(method_name_or_hash, backtrace = nil)
149
+ expectation = nil
148
150
  iterator = ArgumentIterator.new(method_name_or_hash)
149
151
  iterator.each do |*args|
150
152
  method_name = args.shift
@@ -154,6 +156,7 @@ module Mocha
154
156
  expectation.returns(args.shift) unless args.empty?
155
157
  @expectations.add(expectation)
156
158
  end
159
+ expectation
157
160
  end
158
161
 
159
162
  # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them.
@@ -161,7 +164,7 @@ module Mocha
161
164
  # @param [Array<Symbol>] method_names names of methods to unstub.
162
165
  #
163
166
  # @example Invoking an unstubbed method causes error to be raised
164
- # object = mock('mock') do
167
+ # object = mock('mock')
165
168
  # object.stubs(:stubbed_method).returns(:result_one)
166
169
  # object.stubbed_method # => :result_one
167
170
  # object.unstub(:stubbed_method)
@@ -180,11 +183,11 @@ module Mocha
180
183
  end
181
184
  end
182
185
 
183
- # Constrains the {Mock} instance so that it can only expect or stub methods to which +responder+ responds. The constraint is only applied at method invocation time.
186
+ # Constrains the {Mock} instance so that it can only expect or stub methods to which +responder+ responds publicly. The constraint is only applied at method invocation time.
184
187
  #
185
- # A +NoMethodError+ will be raised if the +responder+ does not +#respond_to?+ a method invocation (even if the method has been expected or stubbed).
188
+ # A +NoMethodError+ will be raised if the +responder+ does not publicly +#respond_to?+ the invoked method (even if the method has been expected or stubbed).
186
189
  #
187
- # The {Mock} instance will delegate its +#respond_to?+ method to the +responder+.
190
+ # The {Mock} instance will delegate its +#respond_to?+ method to the +responder+. However, the +include_all+ parameter is not passed through, so only public methods on the +responder+ will be considered.
188
191
  #
189
192
  # Note that the methods on +responder+ are never actually invoked.
190
193
  #
@@ -234,11 +237,11 @@ module Mocha
234
237
  self
235
238
  end
236
239
 
237
- # Constrains the {Mock} instance so that it can only expect or stub methods to which an instance of the +responder_class+ responds. The constraint is only applied at method invocation time. Note that the responder instance is instantiated using +Class#allocate+.
240
+ # Constrains the {Mock} instance so that it can only expect or stub methods to which an instance of the +responder_class+ responds publicly. The constraint is only applied at method invocation time. Note that the responder instance is instantiated using +Class#allocate+.
238
241
  #
239
- # A +NoMethodError+ will be raised if the responder instance does not +#respond_to?+ a method invocation (even if the method has been expected or stubbed).
242
+ # A +NoMethodError+ will be raised if the responder instance does not publicly +#respond_to?+ the invoked method (even if the method has been expected or stubbed).
240
243
  #
241
- # The {Mock} instance will delegate its +#respond_to?+ method to the responder instance.
244
+ # The {Mock} instance will delegate its +#respond_to?+ method to the responder instance. However, the +include_all+ parameter is not passed through, so only public methods on the +responder+ will be considered.
242
245
  #
243
246
  # Note that the methods on the responder instance are never actually invoked.
244
247
  #
@@ -306,10 +309,18 @@ module Mocha
306
309
  end
307
310
 
308
311
  # @private
309
- def method_missing(symbol, *arguments, &block) # rubocop:disable Style/MethodMissingSuper
312
+ # rubocop:disable Style/MethodMissingSuper
313
+ def method_missing(symbol, *arguments, &block)
314
+ handle_method_call(symbol, arguments, block)
315
+ end
316
+ ruby2_keywords(:method_missing)
317
+ # rubocop:enable Style/MethodMissingSuper
318
+
319
+ # @private
320
+ def handle_method_call(symbol, arguments, block)
310
321
  check_expiry
311
322
  check_responder_responds_to(symbol)
312
- invocation = Invocation.new(self, symbol, *arguments, &block)
323
+ invocation = Invocation.new(self, symbol, arguments, block)
313
324
  if (matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(invocation))
314
325
  matching_expectation_allowing_invocation.invoke(invocation)
315
326
  elsif (matching_expectation = all_expectations.match(invocation)) || (!matching_expectation && !@everything_stubbed)
@@ -318,25 +329,14 @@ module Mocha
318
329
  end
319
330
 
320
331
  # @private
321
- def respond_to_missing?(symbol, include_private = false)
332
+ def respond_to_missing?(symbol, _include_all)
322
333
  if @responder
323
- if @responder.method(:respond_to?).arity > 1
324
- @responder.respond_to?(symbol, include_private)
325
- else
326
- @responder.respond_to?(symbol)
327
- end
334
+ @responder.respond_to?(symbol)
328
335
  else
329
336
  @everything_stubbed || all_expectations.matches_method?(symbol)
330
337
  end
331
338
  end
332
339
 
333
- if PRE_RUBY_V19
334
- # @private
335
- def respond_to?(symbol, include_private = false)
336
- respond_to_missing?(symbol, include_private)
337
- end
338
- end
339
-
340
340
  # @private
341
341
  def __verified__?(assertion_counter = nil)
342
342
  @expectations.verified?(assertion_counter)
@@ -387,14 +387,14 @@ module Mocha
387
387
  end
388
388
 
389
389
  def check_expiry
390
- if @expired # rubocop:disable Style/GuardClause
391
- Deprecation.warning(
392
- "#{mocha_inspect} was instantiated in one test but it is receiving invocations within another test.",
393
- ' This can lead to unintended interactions between tests and hence unexpected test failures.',
394
- ' Ensure that every test correctly cleans up any state that it introduces.',
395
- ' A Mocha::StubbingError will be raised in this scenario in the future.'
396
- )
397
- end
390
+ return unless @expired
391
+
392
+ sentences = [
393
+ "#{mocha_inspect} was instantiated in one test but it is receiving invocations within another test.",
394
+ 'This can lead to unintended interactions between tests and hence unexpected test failures.',
395
+ 'Ensure that every test correctly cleans up any state that it introduces.'
396
+ ]
397
+ raise StubbingError.new(sentences.join(' '), caller)
398
398
  end
399
399
  end
400
400
  end
data/lib/mocha/mockery.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'mocha/ruby_version'
2
1
  require 'mocha/central'
3
2
  require 'mocha/mock'
4
3
  require 'mocha/names'
@@ -119,10 +118,9 @@ module Mocha
119
118
  end
120
119
 
121
120
  def on_stubbing(object, method)
122
- method = PRE_RUBY_V19 ? method.to_s : method.to_sym
123
121
  signature_proc = lambda { "#{object.mocha_inspect}.#{method}" }
124
122
  check(:stubbing_non_existent_method, 'non-existent method', signature_proc) do
125
- !(object.stubba_class.__method_exists__?(method, true) || object.respond_to?(method.to_sym))
123
+ !(object.stubba_class.__method_exists__?(method, true) || object.respond_to?(method))
126
124
  end
127
125
  check(:stubbing_non_public_method, 'non-public method', signature_proc) do
128
126
  object.stubba_class.__method_exists__?(method, false)
data/lib/mocha/names.rb CHANGED
@@ -37,7 +37,7 @@ module Mocha
37
37
  def mocha_inspect
38
38
  address = @mock.__id__ * 2
39
39
  address += 0x100000000 if address < 0
40
- "#<Mock:0x#{format('%x', address)}>"
40
+ "#<Mock:0x#{format('%<address>x', address: address)}>"
41
41
  end
42
42
  end
43
43
  end
@@ -3,7 +3,7 @@ module Mocha
3
3
  # @abstract Subclass and implement +#matches?+ and +#mocha_inspect+ to define a custom matcher. Also add a suitably named instance method to {ParameterMatchers} to build an instance of the new matcher c.f. {#equals}.
4
4
  class Base
5
5
  # @private
6
- def to_matcher
6
+ def to_matcher(_expectation = nil)
7
7
  self
8
8
  end
9
9
 
@@ -1,4 +1,3 @@
1
- require 'mocha/deprecation'
2
1
  require 'mocha/parameter_matchers/base'
3
2
  require 'uri'
4
3
  require 'cgi'
@@ -51,7 +50,7 @@ module Mocha
51
50
  # @private
52
51
  def explode(uri)
53
52
  query_hash = CGI.parse(uri.query || '')
54
- URI::Generic::COMPONENT.inject({}) { |h, k| h.merge(k => uri.__send__(k)) }.merge(:query => query_hash)
53
+ URI::Generic::COMPONENT.inject({}) { |h, k| h.merge(k => uri.__send__(k)) }.merge(query: query_hash)
55
54
  end
56
55
  end
57
56
  end
@@ -42,20 +42,10 @@ module Mocha
42
42
  #
43
43
  def has_entry(*options) # rubocop:disable Naming/PredicateName
44
44
  case options.length
45
+ when 0
46
+ raise ArgumentError, 'No arguments. Expecting at least one.'
45
47
  when 1
46
- case options[0]
47
- when Hash
48
- case options[0].length
49
- when 0
50
- raise ArgumentError, 'Argument has no entries.'
51
- when 1
52
- key, value = options[0].first
53
- else
54
- raise ArgumentError, 'Argument has multiple entries. Use Mocha::ParameterMatchers#has_entries instead.'
55
- end
56
- else
57
- raise ArgumentError, 'Argument is not a Hash.'
58
- end
48
+ key, value = parse_option(options[0])
59
49
  when 2
60
50
  key, value = options
61
51
  else
@@ -85,5 +75,24 @@ module Mocha
85
75
  "has_entry(#{@key.mocha_inspect} => #{@value.mocha_inspect})"
86
76
  end
87
77
  end
78
+
79
+ private
80
+
81
+ # @private
82
+ def parse_option(option)
83
+ case option
84
+ when Hash
85
+ case option.length
86
+ when 0
87
+ raise ArgumentError, 'Argument has no entries.'
88
+ when 1
89
+ option.first
90
+ else
91
+ raise ArgumentError, 'Argument has multiple entries. Use Mocha::ParameterMatchers#has_entries instead.'
92
+ end
93
+ else
94
+ raise ArgumentError, 'Argument is not a Hash.'
95
+ end
96
+ end
88
97
  end
89
98
  end
@@ -0,0 +1,53 @@
1
+ require 'mocha/parameter_matchers/base'
2
+
3
+ module Mocha
4
+ module ParameterMatchers
5
+ # Matches +Hash+ containing +keys+.
6
+ #
7
+ # @param [*Array<Object>] keys expected keys.
8
+ # @return [HasKeys] parameter matcher.
9
+ #
10
+ # @see Expectation#with
11
+ #
12
+ # @example Actual parameter contains entry with expected keys.
13
+ # object = mock()
14
+ # object.expects(:method_1).with(has_keys(:key_1, :key_2))
15
+ # object.method_1(:key_1 => 1, :key_2 => 2, :key_3 => 3)
16
+ # # no error raised
17
+ #
18
+ # @example Actual parameter does not contain all expected keys.
19
+ # object = mock()
20
+ # object.expects(:method_1).with(has_keys(:key_1, :key_2))
21
+ # object.method_1(:key_2 => 2)
22
+ # # error raised, because method_1 was not called with Hash containing key: :key_1
23
+ #
24
+ def has_keys(*keys) # rubocop:disable Naming/PredicateName
25
+ HasKeys.new(*keys)
26
+ end
27
+
28
+ # Parameter matcher which matches when actual parameter contains +Hash+ with all expected keys.
29
+ class HasKeys < Base
30
+ # @private
31
+ def initialize(*keys)
32
+ raise ArgumentError, 'No arguments. Expecting at least one.' if keys.empty?
33
+
34
+ @keys = keys
35
+ end
36
+
37
+ # @private
38
+ def matches?(available_parameters)
39
+ parameter = available_parameters.shift
40
+ return false unless parameter.respond_to?(:keys)
41
+
42
+ @keys.map(&:to_matcher).all? do |matcher|
43
+ parameter.keys.any? { |key| matcher.matches?([key]) }
44
+ end
45
+ end
46
+
47
+ # @private
48
+ def mocha_inspect
49
+ "has_keys(#{@keys.mocha_inspect(false)})"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,11 +1,12 @@
1
1
  require 'mocha/parameter_matchers/equals'
2
+ require 'mocha/parameter_matchers/positional_or_keyword_hash'
2
3
 
3
4
  module Mocha
4
5
  module ParameterMatchers
5
6
  # @private
6
7
  module InstanceMethods
7
8
  # @private
8
- def to_matcher
9
+ def to_matcher(_expectation = nil)
9
10
  Mocha::ParameterMatchers::Equals.new(self)
10
11
  end
11
12
  end
@@ -16,3 +17,11 @@ end
16
17
  class Object
17
18
  include Mocha::ParameterMatchers::InstanceMethods
18
19
  end
20
+
21
+ # @private
22
+ class Hash
23
+ # @private
24
+ def to_matcher(expectation = nil)
25
+ Mocha::ParameterMatchers::PositionalOrKeywordHash.new(self, expectation)
26
+ end
27
+ end
@@ -0,0 +1,64 @@
1
+ require 'mocha/configuration'
2
+ require 'mocha/deprecation'
3
+ require 'mocha/parameter_matchers/base'
4
+
5
+ module Mocha
6
+ module ParameterMatchers
7
+ # @private
8
+ class PositionalOrKeywordHash < Base
9
+ def initialize(value, expectation)
10
+ @value = value
11
+ @expectation = expectation
12
+ end
13
+
14
+ def matches?(available_parameters)
15
+ parameter, is_last_parameter = extract_parameter(available_parameters)
16
+ return false unless parameter == @value
17
+
18
+ if is_last_parameter && !same_type_of_hash?(parameter, @value)
19
+ return false if Mocha.configuration.strict_keyword_argument_matching?
20
+
21
+ deprecation_warning(parameter, @value) if Mocha::RUBY_V27_PLUS
22
+ end
23
+
24
+ true
25
+ end
26
+
27
+ def mocha_inspect
28
+ @value.mocha_inspect
29
+ end
30
+
31
+ private
32
+
33
+ def extract_parameter(available_parameters)
34
+ [available_parameters.shift, available_parameters.empty?]
35
+ end
36
+
37
+ def same_type_of_hash?(actual, expected)
38
+ ruby2_keywords_hash?(actual) == ruby2_keywords_hash?(expected)
39
+ end
40
+
41
+ def deprecation_warning(actual, expected)
42
+ details1 = "Expectation #{expectation_definition} expected #{hash_type(expected)} (#{expected.mocha_inspect}),".squeeze(' ')
43
+ details2 = "but received #{hash_type(actual)} (#{actual.mocha_inspect})."
44
+ sentence1 = 'These will stop matching when strict keyword argument matching is enabled.'
45
+ sentence2 = 'See the documentation for Mocha::Configuration#strict_keyword_argument_matching=.'
46
+ Deprecation.warning([details1, details2, sentence1, sentence2].join(' '))
47
+ end
48
+
49
+ def hash_type(hash)
50
+ ruby2_keywords_hash?(hash) ? 'keyword arguments' : 'positional hash'
51
+ end
52
+
53
+ def ruby2_keywords_hash?(hash)
54
+ hash.is_a?(Hash) && ::Hash.ruby2_keywords_hash?(hash)
55
+ end
56
+
57
+ def expectation_definition
58
+ return nil unless @expectation
59
+
60
+ "defined at #{@expectation.definition_location}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -13,6 +13,7 @@ require 'mocha/parameter_matchers/equals'
13
13
  require 'mocha/parameter_matchers/has_entry'
14
14
  require 'mocha/parameter_matchers/has_entries'
15
15
  require 'mocha/parameter_matchers/has_key'
16
+ require 'mocha/parameter_matchers/has_keys'
16
17
  require 'mocha/parameter_matchers/has_value'
17
18
  require 'mocha/parameter_matchers/includes'
18
19
  require 'mocha/parameter_matchers/instance_of'
@@ -3,8 +3,9 @@ require 'mocha/parameter_matchers'
3
3
 
4
4
  module Mocha
5
5
  class ParametersMatcher
6
- def initialize(expected_parameters = [ParameterMatchers::AnyParameters.new], &matching_block)
6
+ def initialize(expected_parameters = [ParameterMatchers::AnyParameters.new], expectation = nil, &matching_block)
7
7
  @expected_parameters = expected_parameters
8
+ @expectation = expectation
8
9
  @matching_block = matching_block
9
10
  end
10
11
 
@@ -23,12 +24,11 @@ module Mocha
23
24
  def mocha_inspect
24
25
  signature = matchers.mocha_inspect
25
26
  signature = signature.gsub(/^\[|\]$/, '')
26
- signature = signature.gsub(/^\{|\}$/, '') if matchers.length == 1
27
27
  "(#{signature})"
28
28
  end
29
29
 
30
30
  def matchers
31
- @expected_parameters.map(&:to_matcher)
31
+ @expected_parameters.map { |p| p.to_matcher(@expectation) }
32
32
  end
33
33
  end
34
34
  end
@@ -1,4 +1,3 @@
1
1
  module Mocha
2
- PRE_RUBY_V19 = Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('1.9')
3
- RUBY_V2_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2')
2
+ RUBY_V27_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.7')
4
3
  end