muack 1.5.1 → 1.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77dfffce2bb5df1930472ff39a8280bd9a1a69f6e81d1f0173d02099b5cdeaf9
4
- data.tar.gz: aabb83e37e3ae995c02ad1ba1e938866cd3e036fa8a3f7682273a80255c60f90
3
+ metadata.gz: 012f658496e2711ec98bf468ffbb58ddfe339eafe03c4618e3cc9b91025b0b03
4
+ data.tar.gz: 505dbe6462efb20502468cf75f23459afa4589a8cd9d5f5ebc24eaf9a4d004ea
5
5
  SHA512:
6
- metadata.gz: 01e57727b0e41455e69a5c4f0f6552b14d1efc192b08435d4b154abe68d86bf62e058cc54196ca05fb1519a301ae7c4fd3bc9924e25a2036c44d1b5a3c3a7843
7
- data.tar.gz: 2324f6b087b7d79c46c5727078fce53a2f178ff8474bbed48af3750e447ad6e16168b788d148ca3c3a347a6a11ac578b923aa5effeb81013a9ab5e159d5508e3
6
+ metadata.gz: 9b538db391549b7da213ab0be7af6f58f71682277e897aae341285daecb47aad5bedfee581e0602908df4a5be55e5faa471d4bf8127a5cd68190058e22dc422c
7
+ data.tar.gz: 2d3ca26974fa4b73f2dd394ccf0323ebb720a40f3f6c30c72736680f4399f714a4a141998abf799dc846c4f1e0d2d4e3716fb719a256cfd2dd3618a57572be27
@@ -7,6 +7,7 @@ script: 'ruby -vwr bundler/setup -S rake test'
7
7
 
8
8
  matrix:
9
9
  include:
10
+ - rvm: 2.3
10
11
  - rvm: 2.4
11
12
  env: RUBYOPT=--enable-frozen-string-literal
12
13
  - rvm: 2.5
@@ -17,5 +18,5 @@ matrix:
17
18
  env: RUBYOPT=--enable-frozen-string-literal
18
19
  - rvm: ruby-head
19
20
  env: RUBYOPT=--enable-frozen-string-literal
20
- - rvm: jruby
21
+ - rvm: jruby-9.2
21
22
  env: RUBYOPT=--enable-frozen-string-literal
data/CHANGES.md CHANGED
@@ -1,5 +1,18 @@
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
+
3
16
  ## Muack 1.5.1 -- 2020-12-06
4
17
 
5
18
  ### Bugs fixed
@@ -2,7 +2,7 @@
2
2
  module Muack
3
3
  Definition = Struct.new(:msg, :args, :returns,
4
4
  :peek_args, :peek_return,
5
- :original_method, :visibility)
5
+ :target, :original_method, :visibility)
6
6
  ActualCall = Struct.new(:msg, :args, :block)
7
7
  WithAnyArgs = Object.new
8
8
  end
@@ -112,22 +112,18 @@ module Muack
112
112
  private
113
113
  def __mock_inject_method defi
114
114
  __mock_injected[defi.msg] = defi
115
- # a) ancestors.first is the first module in the method chain.
116
- # it's just the singleton_class when nothing was prepended,
117
- # otherwise the last prepended module.
118
- # b) would be the class in AnyInstanceOf.
119
- target = object.singleton_class.ancestors.first
115
+ target = Mock.prepare_target(object.singleton_class, defi.msg)
116
+ defi.target = target
120
117
  Mock.store_original_method(target, defi)
121
118
  __mock_inject_mock_method(target, defi)
122
119
  end
123
120
 
124
121
  def __mock_reset_method defi
125
- object.singleton_class.ancestors.first.module_eval do
122
+ defi.target.module_eval do
126
123
  remove_method(defi.msg)
127
124
  # restore original method
128
- if public_instance_methods(false).include?(defi.original_method) ||
129
- protected_instance_methods(false).include?(defi.original_method) ||
130
- private_instance_methods(false).include?(defi.original_method)
125
+ if defi.original_method &&
126
+ Mock.direct_method_defined?(self, defi.original_method)
131
127
  alias_method(defi.msg, defi.original_method)
132
128
  __send__(defi.visibility, defi.msg)
133
129
  remove_method(defi.original_method)
@@ -135,18 +131,68 @@ module Muack
135
131
  end
136
132
  end
137
133
 
138
- def self.store_original_method klass, defi
139
- visibility = if klass.public_instance_methods(false).include?(defi.msg)
140
- :public
141
- elsif klass.protected_instance_methods(false).include?(defi.msg)
142
- :protected
143
- elsif klass.private_instance_methods(false).include?(defi.msg)
144
- :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)
145
139
  end
146
140
 
147
- if visibility # store original method
148
- original_method = find_new_name(klass, defi.msg)
149
- klass.__send__(:alias_method, original_method, defi.msg)
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
189
+ end
190
+ end
191
+
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)
150
196
  defi.original_method = original_method
151
197
  defi.visibility = visibility
152
198
  else
@@ -154,14 +200,14 @@ module Muack
154
200
  end
155
201
  end
156
202
 
157
- def self.find_new_name klass, message, level=0
203
+ def self.find_new_name mod, message, level=0
158
204
  if level >= (::ENV['MUACK_RECURSION_LEVEL'] || 9).to_i
159
205
  raise CannotFindInjectionName.new(level+1, message)
160
206
  end
161
207
 
162
208
  new_name = "__muack_#{name}_#{level}_#{message}".to_sym
163
- if klass.instance_methods(false).include?(new_name)
164
- find_new_name(klass, message, level+1)
209
+ if direct_method_defined?(mod, new_name)
210
+ find_new_name(mod, message, level+1)
165
211
  else
166
212
  new_name
167
213
  end
@@ -1,4 +1,4 @@
1
1
 
2
2
  module Muack
3
- VERSION = '1.5.1'
3
+ VERSION = '1.6.0'
4
4
  end
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: muack 1.5.1 ruby lib
2
+ # stub: muack 1.6.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "muack".freeze
6
- s.version = "1.5.1"
6
+ s.version = "1.6.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -104,10 +104,73 @@ describe Muack::Mock do
104
104
  end
105
105
 
106
106
  describe 'not affect the original module' do
107
- would 'with include' do
108
- m = Module.new{ def f; :f; end }
109
- c0 = Class.new{ include m }.new
110
- c1 = Class.new{ include m }.new
107
+ def mod
108
+ @mod ||= Module.new{ def f; :f; end }
109
+ end
110
+
111
+ def setup type
112
+ m = mod
113
+ Array.new(2).map{ Class.new{ public_send(type, m) }.new }
114
+ end
115
+
116
+ would 'with include and mock' do
117
+ c0, c1 = setup(:include)
118
+
119
+ mock(c0).f{:g}
120
+
121
+ expect(c0.f).eq :g
122
+ expect(c1.f).eq :f
123
+ end
124
+
125
+ would 'with prepend and mock' do
126
+ c0, c1 = setup(:prepend)
127
+
128
+ mock(c0).f{:g}
129
+
130
+ expect(c0.f).eq :g
131
+ expect(c1.f).eq :f
132
+ end
133
+
134
+ would 'with include and mock any_instance_of C0' do
135
+ c0, c1 = setup(:include)
136
+
137
+ mock(any_instance_of(c0.class)).f{:g}
138
+
139
+ expect(c0.f).eq :g
140
+ expect(c1.f).eq :f
141
+ end
142
+
143
+ would 'with prepend and mock any_instance_of C0' do
144
+ c0, c1 = setup(:prepend)
145
+
146
+ mock(any_instance_of(c0.class)).f{:g}
147
+
148
+ expect(c0.f).eq :g
149
+ expect(c1.f).eq :f
150
+ end
151
+
152
+ would 'with include and mock any_instance_of M' do
153
+ c0, c1 = setup(:include)
154
+
155
+ mock(any_instance_of(mod)).f{:g}.times(2)
156
+
157
+ expect(c0.f).eq :g
158
+ expect(c1.f).eq :g
159
+ end
160
+
161
+ would 'with prepend and mock any_instance_of M' do
162
+ c0, c1 = setup(:prepend)
163
+
164
+ mock(any_instance_of(mod)).f{:g}.times(2)
165
+
166
+ expect(c0.f).eq :g
167
+ expect(c1.f).eq :g
168
+ end
169
+
170
+ would 'with extend on the instance' do
171
+ m = mod
172
+ c0 = Class.new.new.extend(m)
173
+ c1 = Class.new{ prepend m }.new
111
174
 
112
175
  mock(c0).f{:g}
113
176
 
@@ -115,9 +178,10 @@ describe Muack::Mock do
115
178
  expect(c1.f).eq :f
116
179
  end
117
180
 
118
- would 'with prepend' do
119
- m = Module.new{ def f; :f; end }
120
- c0 = Class.new{ prepend m }.new
181
+ would 'with prepend on the singleton class of instance' do
182
+ m = mod
183
+ c0 = Class.new.new
184
+ c0.singleton_class.prepend m
121
185
  c1 = Class.new{ prepend m }.new
122
186
 
123
187
  mock(c0).f{:g}
@@ -125,6 +189,83 @@ describe Muack::Mock do
125
189
  expect(c0.f).eq :g
126
190
  expect(c1.f).eq :f
127
191
  end
192
+
193
+ describe 'with prepend on any_instance_of' do
194
+ describe 'on prepended method' do
195
+ would 'have a muack prepended module' do
196
+ m = mod
197
+ c = Class.new{ prepend m }
198
+ ancestors_size = c.ancestors.size
199
+
200
+ mock(any_instance_of(c)).f{:g}
201
+
202
+ expect(c.new.f).eq :g
203
+ expect(c).const_defined?(:MuackPrepended)
204
+ expect(c.ancestors.size).eq ancestors_size + 1
205
+
206
+ expect(Muack.verify).eq true
207
+
208
+ # Make sure there's only one MuackPrepended
209
+ mock(any_instance_of(c)).f{:h}
210
+ expect(c.ancestors.size).eq ancestors_size + 1
211
+ expect(c.new.f).eq :h
212
+ end
213
+ end
214
+
215
+ describe 'on non-prepended method' do
216
+ would 'not have a muack prepended module' do
217
+ m = mod
218
+ c = Class.new{ def g; :g; end; prepend m }
219
+ ancestors_size = c.ancestors.size
220
+
221
+ mock(any_instance_of(c)).g{:m}
222
+
223
+ expect(c.new.f).eq :f
224
+ expect(c.new.g).eq :m
225
+ expect(c).not.const_defined?(:MuackPrepended)
226
+ expect(c.ancestors.size).eq ancestors_size
227
+ end
228
+ end
229
+ end
230
+
231
+ describe 'any_instance_of on a module which has a prepended module' do
232
+ before do
233
+ @m0 = m0 = Module.new{ def f; :m0; end }
234
+ @m1 = m1 = Module.new{ def f; :m1; end; prepend m0 }
235
+ @c0 = Class.new{ prepend m0 }.new
236
+ @c1 = Class.new{ prepend m1 }.new
237
+ end
238
+
239
+ would 'any_instance_of m0' do
240
+ mock(any_instance_of(@m0)).f{:g}.times(2)
241
+
242
+ expect(@c0.f).eq :g
243
+ expect(@c1.f).eq :g
244
+ end
245
+
246
+ would 'any_instance_of m1' do
247
+ skip
248
+
249
+ mock(any_instance_of(@m1)).f{:g}
250
+
251
+ expect(@c0.f).eq :m0
252
+ expect(@c1.f).eq :g # m1 does not know c1 thus no way to pass this
253
+ end
254
+
255
+ would 'any_instance_of c0.class' do
256
+ mock(any_instance_of(@c0.class)).f{:g}
257
+
258
+ expect(@c0.f).eq :g
259
+ expect(@c1.f).eq :m0
260
+ end
261
+
262
+ would 'any_instance_of c1.class' do
263
+ mock(any_instance_of(@c1.class)).f{:g}
264
+
265
+ expect(@c0.f).eq :m0
266
+ expect(@c1.f).eq :g
267
+ end
268
+ end
128
269
  end
129
270
  end
130
271
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: muack
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lin Jen-Shin (godfat)