muack 1.3.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 44cee556b89d8eb6f0b0cf73e9082a41c24f0c07
4
- data.tar.gz: 3843d7a99fd975384d3faccce6b4235ce3d273f3
2
+ SHA256:
3
+ metadata.gz: 012f658496e2711ec98bf468ffbb58ddfe339eafe03c4618e3cc9b91025b0b03
4
+ data.tar.gz: 505dbe6462efb20502468cf75f23459afa4589a8cd9d5f5ebc24eaf9a4d004ea
5
5
  SHA512:
6
- metadata.gz: 5a50371ce353f2063dcbf323e3089b51a8c3e45bb1dd0b1b14882dd0849b4d499e5c61a6dc64ac7a4db9ecd84a11085db9ad95507633fcdaac029fd9b5551611
7
- data.tar.gz: b21dbd44169cea461871d4d67ddc9cdb7fecf0b41378b118046b82093e5690165da8c69f907f20d602da8d935c7e50b06896c92ae4149faeb9f641ba6e4000a5
6
+ metadata.gz: 9b538db391549b7da213ab0be7af6f58f71682277e897aae341285daecb47aad5bedfee581e0602908df4a5be55e5faa471d4bf8127a5cd68190058e22dc422c
7
+ data.tar.gz: 2d3ca26974fa4b73f2dd394ccf0323ebb720a40f3f6c30c72736680f4399f714a4a141998abf799dc846c4f1e0d2d4e3716fb719a256cfd2dd3618a57572be27
@@ -1,11 +1,22 @@
1
-
1
+ sudo: false
2
2
  language: ruby
3
- rvm:
4
- - 2.0
5
- - 2.1
6
- - 2.2
7
- - rbx-2
8
- - jruby
9
3
 
10
- install: 'bundle install --retry=3'
11
- script: 'ruby -r bundler/setup -S rake test'
4
+ install: 'gem update --system; gem install bundler; bundle install --retry=3'
5
+ before_script: unset CI
6
+ script: 'ruby -vwr bundler/setup -S rake test'
7
+
8
+ matrix:
9
+ include:
10
+ - rvm: 2.3
11
+ - rvm: 2.4
12
+ env: RUBYOPT=--enable-frozen-string-literal
13
+ - rvm: 2.5
14
+ env: RUBYOPT=--enable-frozen-string-literal
15
+ - rvm: 2.6
16
+ env: RUBYOPT=--enable-frozen-string-literal
17
+ - rvm: 2.7
18
+ env: RUBYOPT=--enable-frozen-string-literal
19
+ - rvm: ruby-head
20
+ env: RUBYOPT=--enable-frozen-string-literal
21
+ - rvm: jruby-9.2
22
+ env: RUBYOPT=--enable-frozen-string-literal
data/CHANGES.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # CHANGES
2
2
 
3
+ ## Muack 1.6.0 -- 2020-12-06
4
+
5
+ ### Enhancement
6
+
7
+ * Fix a few cases for mocking against modules/classes with prepended modules,
8
+ especially when `any_instance_of` is also used in combination. Previously,
9
+ it's either not mocking correctly or it may affect modules/classes which
10
+ also use the same prepended modules. For mocking prepended methods, an
11
+ internal `MuackPrepended` module will be prepended into the modules/classes
12
+ to properly override the prepended methods. This module cannot be removed
13
+ between tests because there's no way to do that with current Rubies.
14
+ * Performance could be potentially slightly improved with Ruby 2.6+
15
+
16
+ ## Muack 1.5.1 -- 2020-12-06
17
+
18
+ ### Bugs fixed
19
+
20
+ * Eliminated potential keyword arguments warnings for `initialize` when
21
+ mocking against `new`.
22
+
23
+ ## Muack 1.5.0 -- 2020-11-28
24
+
25
+ ### Bugs fixed
26
+
27
+ * Properly handle prepended objects
28
+ * Properly restore method visibilities for singleton methods
29
+
30
+ ### Enhancement
31
+
32
+ * Eliminated any potential keyword arguments warnings to be future proof
33
+ * Some major internal restructure
34
+
35
+ ## Muack 1.4.0 -- 2015-11-21
36
+
37
+ ### Incompatible changes / Enhancement
38
+
39
+ * Spies would now do pattern matching like stubs for matching
40
+ method arguments. This could be an incompatible change if you're
41
+ relying on spies checking the order for the same method calls.
42
+ This change would make spies and stubs more similar. If you are
43
+ really into a specific order, use mocks instead.
44
+
45
+ ## Muack 1.3.2 -- 2015-06-11
46
+
47
+ * Fixed a bug for `where`, `having`, and `allowing` which should distinguish
48
+ between `nil` and `undefined` (which does not have a key in a hash)
49
+
3
50
  ## Muack 1.3.1 -- 2015-05-27
4
51
 
5
52
  * Fixed a bug for `where`, `having`, and `allowing` which would raise an
data/Gemfile CHANGED
@@ -8,7 +8,3 @@ gem 'pork'
8
8
 
9
9
  gem 'simplecov', :require => false if ENV['COV']
10
10
  gem 'coveralls', :require => false if ENV['CI']
11
-
12
- platform :rbx do
13
- gem 'rubysl-singleton' # used in rake
14
- end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Muack [![Build Status](https://secure.travis-ci.org/godfat/muack.png?branch=master)](http://travis-ci.org/godfat/muack) [![Coverage Status](https://coveralls.io/repos/godfat/muack/badge.png?branch=master)](https://coveralls.io/r/godfat/muack?branch=master)
1
+ # Muack [![Build Status](https://secure.travis-ci.org/godfat/muack.png?branch=master)](http://travis-ci.org/godfat/muack) [![Coverage Status](https://coveralls.io/repos/github/godfat/muack/badge.png)](https://coveralls.io/github/godfat/muack) [![Join the chat at https://gitter.im/godfat/muack](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/godfat/muack)
2
2
 
3
3
  by Lin Jen-Shin ([godfat](http://godfat.org))
4
4
 
@@ -7,6 +7,7 @@ by Lin Jen-Shin ([godfat](http://godfat.org))
7
7
  * [github](https://github.com/godfat/muack)
8
8
  * [rubygems](https://rubygems.org/gems/muack)
9
9
  * [rdoc](http://rdoc.info/github/godfat/muack)
10
+ * [issues](https://github.com/godfat/muack/issues) (feel free to ask for support)
10
11
 
11
12
  ## DESCRIPTION:
12
13
 
@@ -25,7 +26,7 @@ Muack is much simpler and thus much faster and much more consistent.
25
26
 
26
27
  ## REQUIREMENTS:
27
28
 
28
- * Tested with MRI (official CRuby), Rubinius and JRuby.
29
+ * Tested with MRI (official CRuby) and JRuby.
29
30
 
30
31
  ## INSTALLATION:
31
32
 
@@ -39,14 +40,14 @@ Here's a quick example using [Pork][].
39
40
  require 'pork/auto'
40
41
  require 'muack'
41
42
 
42
- include Muack::API
43
-
44
43
  describe 'Hello' do
44
+ include Muack::API
45
+
45
46
  before{ Muack.reset }
46
47
  after { Muack.verify }
47
48
 
48
49
  would 'say world!' do
49
- str = 'Hello'
50
+ str = 'Hello'.dup
50
51
  mock(str).say('!'){ |arg| "World#{arg}" }
51
52
  str.say('!').should.eq 'World!'
52
53
  end
@@ -98,7 +99,7 @@ p obj.name # 'obj'
98
99
  p Muack.verify # true
99
100
  ```
100
101
 
101
- Which is roughly semantically equivalent to using a stub with a spy:
102
+ Which is similar to using a stub with a spy:
102
103
 
103
104
  ``` ruby
104
105
  obj = Object.new
@@ -143,6 +144,11 @@ p Muack.verify # true
143
144
  However you should not mix mocks and stubs with the same method, or you
144
145
  might encounter some unexpected result. Jump to _Caveat_ for more detail.
145
146
 
147
+ The other differences for stubs and spies, please check
148
+ [Pattern Matching for stubs and spies][pattern-matching].
149
+ In short, stubs and spies would do some kind of pattern matching,
150
+ making the order of the same method irrelevant.
151
+
146
152
  On the other hand, stubs aren't limited to testing. If we want to monkey
147
153
  patching something, stubs could be useful as we don't care how many times
148
154
  the injected methods are called. Jump to _Muack as a mocky patching library_
@@ -167,7 +173,7 @@ affecting the others. This is helpful in the cases of mocking some very
167
173
  basic objects like Time, without causing too much side effect.
168
174
 
169
175
  ``` ruby
170
- name = 'str'
176
+ name = 'str'.dup
171
177
  stub(name).to_s{ 'hi' }
172
178
  stub(Time).new { Time.at(0) }
173
179
  mock(Time).now { Time.new }
@@ -269,7 +275,7 @@ into proxy mode we simply do not provide any block to the injected method,
269
275
  but just name it. Here's an example:
270
276
 
271
277
  ``` ruby
272
- str = 'str'
278
+ str = 'str'.dup
273
279
  mock(str).reverse
274
280
  p str.reverse # 'rts'
275
281
  p Muack.verify # true
@@ -279,7 +285,7 @@ Note that if reverse was not called exactly once, the mock would complain.
279
285
  We could also use stub + spy to do the same thing as well:
280
286
 
281
287
  ``` ruby
282
- str = 'str'
288
+ str = 'str'.dup
283
289
  stub(str).reverse
284
290
  p str.reverse # 'rts'
285
291
  spy(str).reverse
@@ -326,7 +332,7 @@ If the order is reversed, then it would always return the original value,
326
332
  because the proxy would always match, and Muack would stop searching the
327
333
  next stub.
328
334
 
329
- [pattern-matching]: #arguments-verifiers-satisfy
335
+ [pattern-matching]: #pattern-matching-for-stubs-and-spies
330
336
 
331
337
  #### any_instance_of mode
332
338
 
@@ -446,7 +452,7 @@ p Muack.verify # true
446
452
 
447
453
  Note that it does not make sense to specify `times` for stubs, because
448
454
  stubs don't care about times. Spies do, though. So this is also
449
- semantically equivalent to below:
455
+ similar to below:
450
456
 
451
457
  ``` ruby
452
458
  obj = Object.new
@@ -618,7 +624,7 @@ our own behaviour, then we already have full control of the arguments.
618
624
  There's no points to use both. This also applies to `peek_return`.
619
625
 
620
626
  ``` ruby
621
- str = 'ff'
627
+ str = 'ff'.dup
622
628
  mock(str).to_i.with_any_args.peek_args{ |radix| radix * 2 }
623
629
  p str.to_i(8) # 255
624
630
  p Muack.verify # true
@@ -649,7 +655,7 @@ might just want to take a look at the return? Here's an example using
649
655
  `peek_return` to modify the original return value.
650
656
 
651
657
  ``` ruby
652
- str = 'ff'
658
+ str = 'ff'.dup
653
659
  mock(str).to_i.with_any_args.peek_return{ |int| int * 2 }
654
660
  p str.to_i(16) # 510
655
661
  p Muack.verify # true
@@ -708,6 +714,8 @@ obj.say{ |msg| p msg } # 'Hi'
708
714
  p Muack.verify # true
709
715
  ```
710
716
 
717
+ #### Pattern Matching for stubs and spies
718
+
711
719
  Moreover, we could also have stubs on the same method for different
712
720
  arguments. We could think of this as a sort of pattern matching, and Muack
713
721
  would try to find the best matched stub for us.
@@ -722,7 +730,8 @@ p Muack.verify # true
722
730
  ```
723
731
 
724
732
  If `obj.find(2)` is called and Muack cannot find a matched stub, it would
725
- raise a `Muack::Unexpected` and list the candidates for us.
733
+ raise a `Muack::Unexpected` and list the candidates for us. This also
734
+ applies to spies.
726
735
 
727
736
  However, What if we don't want to be so exact? Then we should use verifiers.
728
737
  We'll introduce each of them in next section. Note that verifiers
@@ -824,7 +833,7 @@ p Muack.verify # true
824
833
 
825
834
  ``` ruby
826
835
  obj = Object.new
827
- mock(obj).say(where(:a => is_a(Fixnum))){ |arg| arg }
836
+ mock(obj).say(where(:a => is_a(Integer))){ |arg| arg }
828
837
  p obj.say(:a => 0) # {:a => 0}
829
838
  p Muack.verify # true
830
839
  ```
@@ -833,7 +842,7 @@ Note that this could be recursive.
833
842
 
834
843
  ``` ruby
835
844
  obj = Object.new
836
- mock(obj).say(where(:a => {:b => [is_a(Fixnum)]})){ |arg| arg[:a] }
845
+ mock(obj).say(where(:a => {:b => [is_a(Integer)]})){ |arg| arg[:a] }
837
846
  p obj.say(:a => {:b => [0]}) # {:b => [0]}
838
847
  p Muack.verify # true
839
848
  ```
@@ -854,7 +863,7 @@ Note that this could be recursive.
854
863
 
855
864
  ``` ruby
856
865
  obj = Object.new
857
- mock(obj).say(having(:a => {:b => [is_a(Fixnum)]})){ |arg| arg[:c] }
866
+ mock(obj).say(having(:a => {:b => [is_a(Integer)]})){ |arg| arg[:c] }
858
867
  p obj.say(:a => {:b => [1]}, :c => 2) # 2
859
868
  p Muack.verify # true
860
869
  ```
@@ -875,7 +884,7 @@ Note that this could be recursive.
875
884
 
876
885
  ``` ruby
877
886
  obj = Object.new
878
- mock(obj).say(allowing(:a => {:b => is_a(Fixnum), :c => 1})){ |arg| arg[:a] }
887
+ mock(obj).say(allowing(:a => {:b => is_a(Integer), :c => 1})){ |arg| arg[:a] }
879
888
  p obj.say(:a => {:b => 2}) # {:b => 2}
880
889
  p Muack.verify # true
881
890
  ```
@@ -1114,7 +1123,7 @@ Consider we have two classes:
1114
1123
 
1115
1124
  ``` ruby
1116
1125
  Food = Class.new
1117
- User = Class.new{ attr_accessor :food }
1126
+ User = Class.new(Struct.new(:food))
1118
1127
  ```
1119
1128
 
1120
1129
  And we could make sure User#food is always a kind of `Food` by putting this
@@ -1141,7 +1150,7 @@ verifiers. For example, we could do this to check if the food is frozen:
1141
1150
 
1142
1151
  ``` ruby
1143
1152
  Food = Class.new
1144
- User = Class.new{ attr_accessor :food }
1153
+ User = Class.new(Struct.new(:food))
1145
1154
 
1146
1155
  FoodFrozen = Class.new(Muack::Satisfying) do
1147
1156
  def match actual_arg
@@ -1201,8 +1210,8 @@ Or, I am happy to break legacy.
1201
1210
  ## USERS:
1202
1211
 
1203
1212
  * [Rib][]
1204
- * [rest-core](https://github.com/cardinalblue/rest-core)
1205
- * [rest-more](https://github.com/cardinalblue/rest-more)
1213
+ * [rest-core](https://github.com/godfat/rest-core)
1214
+ * [rest-more](https://github.com/godfat/rest-more)
1206
1215
 
1207
1216
  ## CONTRIBUTORS:
1208
1217
 
@@ -1210,9 +1219,9 @@ Or, I am happy to break legacy.
1210
1219
 
1211
1220
  ## LICENSE:
1212
1221
 
1213
- Apache License 2.0
1222
+ Apache License 2.0 (Apache-2.0)
1214
1223
 
1215
- Copyright (c) 2013-2015, Lin Jen-Shin (godfat)
1224
+ Copyright (c) 2013-2020, Lin Jen-Shin (godfat)
1216
1225
 
1217
1226
  Licensed under the Apache License, Version 2.0 (the "License");
1218
1227
  you may not use this file except in compliance with the License.
data/Rakefile CHANGED
@@ -1,14 +1,13 @@
1
1
 
2
2
  begin
3
- require "#{dir = File.dirname(__FILE__)}/task/gemgem"
3
+ require "#{__dir__}/task/gemgem"
4
4
  rescue LoadError
5
- sh 'git submodule update --init'
5
+ sh 'git submodule update --init --recursive'
6
6
  exec Gem.ruby, '-S', $PROGRAM_NAME, *ARGV
7
7
  end
8
8
 
9
- Gemgem.init(dir) do |s|
9
+ Gemgem.init(__dir__) do |s|
10
10
  require 'muack/version'
11
11
  s.name = 'muack'
12
12
  s.version = Muack::VERSION
13
- %w[].each{ |g| s.add_runtime_dependency(g) }
14
13
  end
@@ -1,17 +1,6 @@
1
1
 
2
- module Muack
3
- class Block
4
- attr_accessor :block, :context
5
- def initialize block, context=nil
6
- self.block, self.context = block, context
7
- end
8
-
9
- def call *args, &actual_block
10
- if context # ruby: no way to pass actual_block to instance_exec
11
- context.instance_exec(*args, &block)
12
- else
13
- block.call(*args, &actual_block)
14
- end
15
- end
16
- end
2
+ if RUBY_VERSION < '2.7'
3
+ require 'muack/block_26'
4
+ else
5
+ require 'muack/block_27'
17
6
  end
@@ -0,0 +1,16 @@
1
+
2
+ module Muack
3
+ class Block < Struct.new(:block, :context)
4
+ def initialize block, context=nil
5
+ super
6
+ end
7
+
8
+ def call(*args, &block)
9
+ if context
10
+ context.instance_exec(*args, &block)
11
+ else
12
+ block.call(*args, &block)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+
2
+ module Muack
3
+ class Block < Struct.new(:block, :context)
4
+ def initialize block, context=nil
5
+ super
6
+ end
7
+
8
+ def call(...)
9
+ if context
10
+ context.instance_exec(...)
11
+ else
12
+ block.call(...)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -4,7 +4,7 @@ require 'muack/mock'
4
4
  module Muack
5
5
  class Coat < Mock
6
6
  # used for mocked object to dispatch mocked method
7
- def __mock_dispatch msg, actual_args
7
+ def __mock_dispatch actual_call
8
8
  defi = super
9
9
  if __mock_defis[defi.msg].empty?
10
10
  __mock_reset_method(defi)
@@ -1,7 +1,8 @@
1
1
 
2
2
  module Muack
3
- Definition = Class.new(Struct.new(:msg, :args, :returns,
4
- :peek_args, :peek_return,
5
- :original_method))
3
+ Definition = Struct.new(:msg, :args, :returns,
4
+ :peek_args, :peek_return,
5
+ :target, :original_method, :visibility)
6
+ ActualCall = Struct.new(:msg, :args, :block)
6
7
  WithAnyArgs = Object.new
7
8
  end
@@ -12,11 +12,12 @@ module Muack
12
12
 
13
13
  class Unexpected < Failure
14
14
  attr_reader :was
15
- def initialize obj, expected_defis, msg, args
16
- @was = "#{obj.inspect}.#{msg}(" \
17
- "#{args.map(&:inspect).join(', ')})"
15
+ def initialize obj, expected_defis, actual_call
16
+ args = actual_call.args.map(&:inspect)
17
+ @was = "#{obj.inspect}.#{actual_call.msg}(#{args.join(', ')})"
18
+
18
19
  if expected_defis.empty?
19
- super("\nUnexpected call: #{@was}")
20
+ super("\nUnexpected call: #{was}")
20
21
  else
21
22
  build_expected(obj, expected_defis)
22
23
  super("\nExpected: #{expected}\n but was: #{was}")
@@ -6,8 +6,6 @@ require 'muack/block'
6
6
  require 'muack/error'
7
7
 
8
8
  module Muack
9
- EmptyBlock = proc{}
10
-
11
9
  class Mock < BasicObject
12
10
  attr_reader :object
13
11
  def initialize object
@@ -24,8 +22,8 @@ module Muack
24
22
  end
25
23
 
26
24
  # Public API: Define mocked method
27
- def method_missing msg, *args, &block
28
- defi = Definition.new(msg, args, block)
25
+ def method_missing msg, *args, &returns
26
+ defi = Definition.new(msg, args, returns)
29
27
  if injected = __mock_injected[defi.msg]
30
28
  defi.original_method = injected.original_method
31
29
  else
@@ -51,49 +49,42 @@ module Muack
51
49
  end
52
50
 
53
51
  # used for mocked object to dispatch mocked method
54
- def __mock_dispatch msg, actual_args
55
- if defi = __mock_defis[msg].shift
52
+ def __mock_dispatch actual_call
53
+ if defi = __mock_defis[actual_call.msg].shift
56
54
  __mock_disps_push(defi)
57
- if __mock_check_args(defi.args, actual_args)
55
+ if __mock_check_args(defi, actual_call)
58
56
  defi
59
57
  else
60
58
  Mock.__send__(:raise, # Wrong argument
61
- Unexpected.new(object, [defi], msg, actual_args))
59
+ Unexpected.new(object, [defi], actual_call))
62
60
  end
63
61
  else
64
- defis = __mock_disps[msg]
65
- if expected = defis.find{ |d| __mock_check_args(d.args, actual_args) }
66
- Mock.__send__(:raise, # Too many times
67
- Expected.new(object, expected, defis.size, defis.size+1))
68
- else
69
- Mock.__send__(:raise, # Wrong argument
70
- Unexpected.new(object, defis, msg, actual_args))
71
- end
62
+ __mock_failed(actual_call)
72
63
  end
73
64
  end
74
65
 
75
66
  # used for mocked object to dispatch mocked method
76
- def __mock_dispatch_call context, disp, actual_args, actual_block, &_yield
77
- args = if disp.peek_args
78
- __mock_block_call(context, disp.peek_args,
79
- actual_args, actual_block, true)
80
- else
81
- actual_args
82
- end
83
-
84
- ret = if disp.returns
85
- __mock_block_call(context, disp.returns,
86
- args, actual_block, true)
87
- elsif disp.original_method # proxies for singleton methods
88
- context.__send__(disp.original_method, *args, &actual_block)
89
- else # proxies for instance methods
90
- # need the original context for calling `super`
91
- # ruby: can't pass a block to yield, so we name it _yield
92
- _yield.call(args, &actual_block)
93
- end
67
+ def __mock_dispatch_call context, disp, actual_call, &proxy_super
68
+ # resolving arguments
69
+ call =
70
+ if disp.peek_args
71
+ args = __mock_block_call(context, disp.peek_args, actual_call)
72
+ ActualCall.new(actual_call.msg, args, actual_call.block)
73
+ else
74
+ actual_call
75
+ end
94
76
 
77
+ # retrieve actual return
78
+ ret =
79
+ if disp.returns
80
+ __mock_block_call(context, disp.returns, call)
81
+ else
82
+ __mock_proxy_call(context, disp, call, proxy_super)
83
+ end
84
+
85
+ # resolving return
95
86
  if disp.peek_return
96
- __mock_block_call(context, disp.peek_return, ret, EmptyBlock, false)
87
+ __mock_block_call(context, disp.peek_return, ret, true)
97
88
  else
98
89
  ret
99
90
  end
@@ -102,9 +93,9 @@ module Muack
102
93
  # used for Muack::Session#verify
103
94
  def __mock_verify
104
95
  __mock_defis.values.all?(&:empty?) || begin
105
- msg, defis_with_same_msg = __mock_defis.find{ |_, v| v.size > 0 }
96
+ msg, defis_with_same_msg = __mock_defis.find{ |_, v| v.any? }
106
97
  args, defis = defis_with_same_msg.group_by(&:args).first
107
- dsize = __mock_disps[msg].select{ |d| d.args == args }.size
98
+ dsize = __mock_disps[msg].count{ |d| d.args == args }
108
99
  Mock.__send__(:raise, # Too little times
109
100
  Expected.new(object, defis.first, defis.size + dsize, dsize))
110
101
  end
@@ -121,80 +112,207 @@ module Muack
121
112
  private
122
113
  def __mock_inject_method defi
123
114
  __mock_injected[defi.msg] = defi
124
- target = object.singleton_class # would be the class in AnyInstanceOf
125
- privilege = Mock.store_original_method(target, defi)
126
- __mock_inject_mock_method(target, defi, privilege)
115
+ target = Mock.prepare_target(object.singleton_class, defi.msg)
116
+ defi.target = target
117
+ Mock.store_original_method(target, defi)
118
+ __mock_inject_mock_method(target, defi)
127
119
  end
128
120
 
129
121
  def __mock_reset_method defi
130
- object.singleton_class.module_eval do
122
+ defi.target.module_eval do
131
123
  remove_method(defi.msg)
132
124
  # restore original method
133
- if instance_methods(false).include?(defi.original_method) ||
134
- private_instance_methods(false).include?(defi.original_method)
125
+ if defi.original_method &&
126
+ Mock.direct_method_defined?(self, defi.original_method)
135
127
  alias_method(defi.msg, defi.original_method)
128
+ __send__(defi.visibility, defi.msg)
136
129
  remove_method(defi.original_method)
137
130
  end
138
131
  end
139
132
  end
140
133
 
141
- def self.store_original_method klass, defi
142
- privilege = if klass.instance_methods(false).include?(defi.msg)
143
- :public # TODO: forget about protected methods?
144
- elsif klass.private_instance_methods(false).include?(defi.msg)
145
- :private
134
+ if ::Class.instance_method(:method_defined?).arity == 1 # Ruby 2.5-
135
+ def self.direct_method_defined? mod, msg
136
+ mod.public_instance_methods(false).include?(msg) ||
137
+ mod.protected_instance_methods(false).include?(msg) ||
138
+ mod.private_instance_methods(false).include?(msg)
139
+ end
140
+
141
+ def self.method_visibility mod, msg
142
+ if mod.public_instance_methods(false).include?(msg)
143
+ :public
144
+ elsif mod.protected_instance_methods(false).include?(msg)
145
+ :protected
146
+ elsif mod.private_instance_methods(false).include?(msg)
147
+ :private
148
+ end
149
+ end
150
+ else # Ruby 2.6+
151
+ def self.direct_method_defined? mod, msg
152
+ mod.method_defined?(msg, false) || # this doesn't cover private method
153
+ mod.private_method_defined?(msg, false)
154
+ end
155
+
156
+ def self.method_visibility mod, msg
157
+ if mod.public_method_defined?(msg, false)
158
+ :public
159
+ elsif mod.protected_method_defined?(msg, false)
160
+ :protected
161
+ elsif mod.private_method_defined?(msg, false)
162
+ :private
163
+ end
164
+ end
165
+ end
166
+
167
+ def self.prepare_target singleton_class, msg
168
+ if singleton_class == singleton_class.ancestors.first # no prepended mod
169
+ singleton_class
170
+ else # check if we need to prepend an internal module to override
171
+ index = singleton_class.ancestors.index(singleton_class)
172
+ prepended_modules = singleton_class.ancestors[0...index]
173
+
174
+ if prepended_modules.find{ |m| direct_method_defined?(m, msg) }
175
+ # prepend an internal module to override the prepended module(s)
176
+ name = :MuackPrepended
177
+ if singleton_class.const_defined?(name)
178
+ singleton_class.const_get(name)
179
+ else
180
+ prepended = ::Module.new
181
+ singleton_class.const_set(name, prepended)
182
+ singleton_class.private_constant(name)
183
+ singleton_class.prepend(prepended)
184
+ prepended
185
+ end
186
+ else # we don't need to override so singleton class is enough
187
+ singleton_class
188
+ end
146
189
  end
190
+ end
147
191
 
148
- return :public unless privilege
149
- # store original method
150
- original_method = find_new_name(klass, defi.msg)
151
- klass.__send__(:alias_method, original_method, defi.msg)
152
- defi.original_method = original_method
153
- privilege
192
+ def self.store_original_method mod, defi
193
+ if visibility = method_visibility(mod, defi.msg)
194
+ original_method = find_new_name(mod, defi.msg)
195
+ mod.__send__(:alias_method, original_method, defi.msg)
196
+ defi.original_method = original_method
197
+ defi.visibility = visibility
198
+ else
199
+ defi.visibility = :public
200
+ end
154
201
  end
155
202
 
156
- def self.find_new_name klass, message, level=0
203
+ def self.find_new_name mod, message, level=0
157
204
  if level >= (::ENV['MUACK_RECURSION_LEVEL'] || 9).to_i
158
205
  raise CannotFindInjectionName.new(level+1, message)
159
206
  end
160
207
 
161
208
  new_name = "__muack_#{name}_#{level}_#{message}".to_sym
162
- if klass.instance_methods(false).include?(new_name)
163
- find_new_name(klass, message, level+1)
209
+ if direct_method_defined?(mod, new_name)
210
+ find_new_name(mod, message, level+1)
164
211
  else
165
212
  new_name
166
213
  end
167
214
  end
168
215
 
169
- def __mock_inject_mock_method target, defi, privilege=:public
216
+ def __mock_inject_mock_method target, defi
170
217
  mock = self # remember the context
171
218
  target.__send__(:define_method, defi.msg){ |*actual_args, &actual_block|
172
- disp = mock.__mock_dispatch(defi.msg, actual_args)
173
- mock.__mock_dispatch_call(self, disp, actual_args,
174
- actual_block) do |args, &block|
175
- super(*args, &block)
219
+ actual_call = ActualCall.new(defi.msg, actual_args, actual_block)
220
+ disp = mock.__mock_dispatch(actual_call)
221
+ mock.__mock_dispatch_call(self, disp, actual_call) do |call, has_kargs|
222
+ # need the original context for calling `super`
223
+ if has_kargs && kargs = call.args.last
224
+ super(*call.args[0...-1], **kargs, &call.block)
225
+ else
226
+ super(*call.args, &call.block)
227
+ end
176
228
  end
177
229
  }
178
- target.__send__(privilege, defi.msg)
230
+ target.__send__(defi.visibility, defi.msg)
231
+ end
232
+
233
+ # used for __mock_dispatch
234
+ def __mock_failed actual_call, disps=__mock_disps[actual_call.msg]
235
+ if expected = __mock_find_checked_difi(disps, actual_call)
236
+ Mock.__send__(:raise, # Too many times
237
+ Expected.new(object, expected, disps.size, disps.size+1))
238
+ else
239
+ Mock.__send__(:raise, # Wrong argument
240
+ Unexpected.new(object, disps, actual_call))
241
+ end
179
242
  end
180
243
 
181
244
  # used for __mock_dispatch_call
182
- def __mock_block_call context, block, actual_args, actual_block, splat
183
- return unless block
245
+ def __mock_block_call context, block, actual_call, peek_return=false
184
246
  # for AnyInstanceOf, we don't have the actual context at the time
185
247
  # we're defining it, so we update it here
186
- block.context = context if block.kind_of?(Block)
187
- if splat
188
- block.call(*actual_args, &actual_block)
189
- else # peek_return doesn't need splat
190
- block.call(actual_args, &actual_block)
248
+ if block.kind_of?(Block)
249
+ block.context = context
250
+ instance_exec_block = block.block
251
+ end
252
+
253
+ if peek_return # actual_call is the actual return in this case
254
+ block.call(actual_call, &instance_exec_block)
255
+ else
256
+ actual_block = actual_call.block || instance_exec_block
257
+ if __mock_block_with_kargs?(instance_exec_block || block) &&
258
+ kargs = actual_call.args.last
259
+ block.call(*actual_call.args[0...-1], **kargs, &actual_block)
260
+ else
261
+ block.call(*actual_call.args, &actual_block)
262
+ end
263
+ end
264
+ end
265
+
266
+ # used for __mock_dispatch_call
267
+ def __mock_proxy_call context, disp, call, proxy_super
268
+ if disp.original_method # proxies for singleton methods with __send__
269
+ if __mock_method_with_kargs?(context, disp.original_method) &&
270
+ kargs = call.args.last
271
+ context.__send__(
272
+ disp.original_method, *call.args[0...-1], **kargs, &call.block)
273
+ else
274
+ context.__send__(disp.original_method, *call.args, &call.block)
275
+ end
276
+ else # proxies for instance methods with super
277
+ proxy_super.call(call, __mock_super_with_kargs?(context, call.msg))
191
278
  end
192
279
  end
193
280
 
194
- def __mock_check_args expected_args, actual_args
195
- if expected_args == [WithAnyArgs]
196
- true
197
- elsif expected_args.none?{ |arg| arg.kind_of?(Satisfying) }
281
+ def __mock_find_checked_difi defis, actual_call, meth=:find
282
+ defis.public_send(meth){ |d| __mock_check_args(d, actual_call) }
283
+ end
284
+
285
+ def __mock_method_with_kargs? object, method_name
286
+ __mock_block_with_kargs?(
287
+ ::Kernel.instance_method(:method).bind(object).call(method_name))
288
+ end
289
+
290
+ def __mock_super_with_kargs? object, method_name
291
+ super_method =
292
+ ::Kernel.instance_method(:method).bind(object).call(method_name).
293
+ super_method
294
+
295
+ if super_method.owner == ::Class && super_method.name == :new
296
+ initialize_method = ::Class.instance_method(:instance_method).
297
+ bind(object).call(:initialize)
298
+ __mock_block_with_kargs?(initialize_method)
299
+ else
300
+ super_method && __mock_block_with_kargs?(super_method)
301
+ end
302
+ end
303
+
304
+ def __mock_block_with_kargs? block
305
+ # there's no Symbol#start_with? in older Ruby
306
+ block.parameters.dig(-1, 0).to_s.start_with?('key')
307
+ end
308
+
309
+ def __mock_check_args defi, actual_call
310
+ return true if defi.args.size == 1 && defi.args.first == WithAnyArgs
311
+
312
+ expected_args = defi.args
313
+ actual_args = actual_call.args
314
+
315
+ if expected_args.none?{ |arg| arg.kind_of?(Satisfying) }
198
316
  expected_args == actual_args
199
317
 
200
318
  elsif expected_args.size == actual_args.size