muack 1.5.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
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)