muack 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+
2
+ require "#{dir = File.dirname(__FILE__)}/task/gemgem"
3
+ Gemgem.dir = dir
4
+
5
+ ($LOAD_PATH << File.expand_path("#{Gemgem.dir}/lib")).uniq!
6
+
7
+ desc 'Generate gemspec'
8
+ task 'gem:spec' do
9
+ Gemgem.spec = Gemgem.create do |s|
10
+ require 'muack/version'
11
+ s.name = 'muack'
12
+ s.version = Muack::VERSION
13
+
14
+ %w[].each{ |g| s.add_runtime_dependency(g) }
15
+ end
16
+
17
+ Gemgem.write
18
+ end
data/lib/muack.rb ADDED
@@ -0,0 +1,77 @@
1
+
2
+ require 'muack/mock'
3
+ require 'muack/stub'
4
+ require 'muack/proxy'
5
+
6
+ require 'muack/satisfy'
7
+ require 'muack/session'
8
+
9
+ require 'muack/any_instance_of'
10
+
11
+ module Muack
12
+ def self.verify
13
+ session.verify
14
+ ensure
15
+ reset
16
+ end
17
+
18
+ def self.session
19
+ @session ||= Muack::Session.new
20
+ end
21
+
22
+ def self.reset
23
+ @session && @session.reset
24
+ @session = nil
25
+ end
26
+
27
+ module API
28
+ module_function
29
+ def mock obj=Object.new
30
+ ret = Muack.session["mk #{obj.__id__}"] ||= Muack::Mock.new(obj)
31
+ if block_given? then yield(ret) else ret end
32
+ end
33
+
34
+ def stub obj=Object.new
35
+ ret = Muack.session["sb #{obj.__id__}"] ||= Muack::Stub.new(obj)
36
+ if block_given? then yield(ret) else ret end
37
+ end
38
+
39
+ def mock_proxy obj=Object.new
40
+ ret = Muack.session["mp #{obj.__id__}"] ||= Muack::MockProxy.new(obj)
41
+ if block_given? then yield(ret) else ret end
42
+ end
43
+
44
+ def stub_proxy obj=Object.new
45
+ ret = Muack.session["sp #{obj.__id__}"] ||= Muack::StubProxy.new(obj)
46
+ if block_given? then yield(ret) else ret end
47
+ end
48
+
49
+ def any_instance_of klass
50
+ yield Muack::AnyInstanceOf.new(klass)
51
+ end
52
+
53
+ def is_a klass
54
+ Muack::IsA.new(klass)
55
+ end
56
+
57
+ def anything
58
+ Muack::Anything.new
59
+ end
60
+
61
+ def match regexp
62
+ Muack::Match.new(regexp)
63
+ end
64
+
65
+ def hash_including hash
66
+ Muack::HashIncluding.new(hash)
67
+ end
68
+
69
+ def within range_or_array
70
+ Muack::Within.new(range_or_array)
71
+ end
72
+
73
+ def satisfy &block
74
+ Muack::Satisfy.new(block)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module Muack
3
+ AnyInstanceOf = Class.new(Struct.new(:singleton_class))
4
+ end
@@ -0,0 +1,5 @@
1
+
2
+ module Muack
3
+ Definition = Class.new(Struct.new(:msg, :args, :block, :original_method))
4
+ WithAnyArgs = Object.new
5
+ end
@@ -0,0 +1,38 @@
1
+
2
+ module Muack
3
+ class Failure < Exception
4
+ attr_reader :expected
5
+ def build_expected obj, expected_defis
6
+ @expected = expected_defis.uniq{ |d| [d.msg, d.args] }.map{ |defi|
7
+ "#{obj.inspect}.#{defi.msg}(" \
8
+ "#{defi.args.map(&:inspect).join(', ')})"
9
+ }.join("\n or: ")
10
+ end
11
+ end
12
+
13
+ class Unexpected < Failure
14
+ attr_reader :was
15
+ def initialize obj, expected_defis, msg, args
16
+ @was = "#{obj.inspect}.#{msg}(" \
17
+ "#{args.map(&:inspect).join(', ')})"
18
+ if expected_defis.empty?
19
+ super("\nExpected: #{@was}\n was not called.")
20
+ else
21
+ build_expected(obj, expected_defis)
22
+ super("\nExpected: #{expected}\n but was: #{was}")
23
+ end
24
+ end
25
+ end
26
+
27
+ class Expected < Failure
28
+ attr_reader :expected_times, :actual_times
29
+ def initialize obj, defi, expected_times, actual_times
30
+ build_expected(obj, [defi])
31
+ @expected_times = expected_times
32
+ @actual_times = actual_times
33
+
34
+ super("\nExpected: #{expected}\n called #{expected_times} times\n" \
35
+ " but was #{actual_times} times.")
36
+ end
37
+ end
38
+ end
data/lib/muack/mock.rb ADDED
@@ -0,0 +1,167 @@
1
+
2
+ require 'muack/definition'
3
+ require 'muack/modifier'
4
+ require 'muack/failure'
5
+
6
+ module Muack
7
+ class Mock < BasicObject
8
+ attr_reader :object
9
+ def initialize object
10
+ @object = object
11
+ @__mock_ignore = []
12
+ [:__mock_defis=, :__mock_disps=].each do |m|
13
+ __send__(m, ::Hash.new{ |h, k| h[k] = [] })
14
+ end
15
+ end
16
+
17
+ # Public API: Bacon needs this, or we often ended up with stack overflow
18
+ def inspect
19
+ "#<#{class << self; self; end.superclass} object=#{object.inspect}>"
20
+ end
21
+
22
+ # Public API: Define mocked method
23
+ def method_missing msg, *args, &block
24
+ defi = Definition.new(msg, args, block)
25
+ __mock_inject_method(defi) if __mock_pure?(defi)
26
+ __mock_defis_push(defi)
27
+ Modifier.new(self, defi)
28
+ end
29
+
30
+ # used for Muack::Modifier#times
31
+ def __mock_defis_push defi
32
+ __mock_defis[defi.msg] << defi
33
+ end
34
+
35
+ # used for Muack::Modifier#times
36
+ def __mock_defis_pop defi
37
+ __mock_defis[defi.msg].pop
38
+ end
39
+
40
+ # used for Muack::Modifier#times
41
+ def __mock_ignore defi
42
+ @__mock_ignore << defi
43
+ end
44
+
45
+ # used for mocked object to dispatch mocked method
46
+ def __mock_dispatch msg, actual_args, actual_block
47
+ if defi = __mock_defis[msg].shift
48
+ __mock_disps_push(defi)
49
+ if __mock_check_args(defi.args, actual_args)
50
+ __mock_block_call(defi, actual_args, actual_block)
51
+ else
52
+ Mock.__send__(:raise, # Wrong argument
53
+ Unexpected.new(object, [defi], msg, actual_args))
54
+ end
55
+ else
56
+ defis = __mock_disps[msg]
57
+ if expected_defi = defis.find{ |d| d.args == actual_args }
58
+ Mock.__send__(:raise, # Too many times
59
+ Expected.new(object, expected_defi, defis.size, defis.size+1))
60
+ else
61
+ Mock.__send__(:raise, # Wrong argument
62
+ Unexpected.new(object, defis, msg, actual_args))
63
+ end
64
+ end
65
+ end
66
+
67
+ # used for Muack::Session#verify
68
+ def __mock_verify
69
+ __mock_defis.values.all?(&:empty?) || begin
70
+ msg, defis_with_same_msg = __mock_defis.find{ |_, v| v.size > 0 }
71
+ args, defis = defis_with_same_msg.group_by(&:args).first
72
+ dsize = __mock_disps[msg].select{ |d| d.args == args }.size
73
+ Mock.__send__(:raise, # Too little times
74
+ Expected.new(object, defis.first, defis.size + dsize, dsize))
75
+ end
76
+ end
77
+
78
+ # used for Muack::Session#reset
79
+ def __mock_reset
80
+ [__mock_defis.values, __mock_disps.values, @__mock_ignore].
81
+ flatten.compact.each do |defi|
82
+ object.singleton_class.module_eval do
83
+ methods = instance_methods(false)
84
+ if methods.include?(defi.msg) # removed mocked method
85
+ remove_method(defi.msg) # could be removed by other defi
86
+ end
87
+ if methods.include?(defi.original_method) # restore original method
88
+ alias_method defi.msg, defi.original_method
89
+ remove_method defi.original_method
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ protected # get warnings for private attributes
96
+ attr_accessor :__mock_defis, :__mock_disps
97
+
98
+ private
99
+ def __mock_pure? defi
100
+ __mock_defis[defi.msg].empty? && __mock_disps[defi.msg].empty?
101
+ end
102
+
103
+ def __mock_inject_method defi
104
+ target = object.singleton_class
105
+ Mock.store_original_method(target, defi)
106
+ __mock_inject_mock_method(target, defi)
107
+ end
108
+
109
+ def self.store_original_method klass, defi
110
+ return unless klass.instance_methods(false).include?(defi.msg)
111
+ # store original method
112
+ original_method = find_new_name(klass, defi.msg)
113
+ klass.__send__(:alias_method, original_method, defi.msg)
114
+ defi.original_method = original_method
115
+ end
116
+
117
+ def self.find_new_name klass, message, level=0
118
+ raise "Cannot find a suitable method name, tried #{level+1} times." if
119
+ level >= 9
120
+
121
+ new_name = "__muack_mock_#{level}_#{message}".to_sym
122
+ if klass.instance_methods(false).include?(new_name)
123
+ find_new_name(klass, message, level+1)
124
+ else
125
+ new_name
126
+ end
127
+ end
128
+
129
+ def __mock_inject_mock_method target, defi
130
+ mock = self # remember the context
131
+ target.__send__(:define_method, defi.msg){|*actual_args, &actual_block|
132
+ mock.__mock_dispatch(defi.msg, actual_args, actual_block)
133
+ }
134
+ end
135
+
136
+ def __mock_block_call defi, actual_args, actual_block
137
+ if block = defi.block
138
+ arity = block.arity
139
+ if arity < 0
140
+ block.call(*actual_args , &actual_block)
141
+ else
142
+ block.call(*actual_args.first(arity), &actual_block)
143
+ end
144
+ end
145
+ end
146
+
147
+ def __mock_check_args expected_args, actual_args
148
+ if expected_args == [WithAnyArgs]
149
+ true
150
+ elsif expected_args.none?{ |arg| arg.kind_of?(Satisfy) }
151
+ expected_args == actual_args
152
+
153
+ elsif expected_args.size == actual_args.size
154
+ expected_args.zip(actual_args).all?{ |(e, a)|
155
+ if e.kind_of?(Satisfy) then e.match(a) else e == a end
156
+ }
157
+ else
158
+ false
159
+ end
160
+ end
161
+
162
+ # used for Muack::Mock#__mock_dispatch
163
+ def __mock_disps_push defi
164
+ __mock_disps[defi.msg] << defi
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,29 @@
1
+
2
+ require 'muack/definition'
3
+
4
+ module Muack
5
+ class Modifier < Struct.new(:mock, :defi)
6
+ # Public API
7
+ def with_any_args
8
+ defi.args = [WithAnyArgs]
9
+ self
10
+ end
11
+
12
+ # Public API
13
+ def times number
14
+ if number >= 1
15
+ (number - 1).times{ mock.__mock_defis_push(defi) }
16
+ elsif number == 0
17
+ mock.__mock_ignore(mock.__mock_defis_pop(defi))
18
+ else
19
+ raise "What would you expect from calling a method #{number} times?"
20
+ end
21
+ self
22
+ end
23
+
24
+ # Public API
25
+ def object
26
+ mock.object
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+
2
+ require 'muack/mock'
3
+ require 'muack/stub'
4
+
5
+ module Muack
6
+ module Proxy
7
+ def __mock_block_call defi, actual_args, actual_block
8
+ # handle block call in injected method, since we need to call origin
9
+ defi # but we still want to know which defi gets dispatched!
10
+ end
11
+
12
+ def __mock_inject_mock_method target, defi
13
+ mock = self # remember the context
14
+ target.__send__(:define_method, defi.msg){|*actual_args, &actual_block|
15
+ d = mock.__mock_dispatch(defi.msg, actual_args, actual_block)
16
+
17
+ ret = if d.original_method
18
+ __send__(d.original_method, *actual_args, &actual_block)
19
+ else
20
+ super(*actual_args, &actual_block)
21
+ end
22
+
23
+ if d.block
24
+ d.block.call(ret)
25
+ else
26
+ ret
27
+ end
28
+ }
29
+ end
30
+ end
31
+
32
+ class MockProxy < Mock
33
+ include Proxy
34
+ end
35
+
36
+ class StubProxy < Stub
37
+ include Proxy
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+
2
+ module Muack
3
+ class Satisfy < Struct.new(:block, :api_args)
4
+ def match actual_arg
5
+ !!block.call(actual_arg)
6
+ end
7
+
8
+ def to_s
9
+ "Muack::API.#{api_name}(#{api_args.map(&:inspect).join(', ')})"
10
+ end
11
+ alias_method :inspect, :to_s
12
+
13
+ def api_name
14
+ self.class.name[/::(\w+)$/, 1].
15
+ gsub(/([A-Z][a-z]*)+?(?=[A-Z][a-z]*)/, '\\1_').downcase
16
+ end
17
+
18
+ def api_args
19
+ super || [block]
20
+ end
21
+ end
22
+
23
+ class IsA < Satisfy
24
+ def initialize klass
25
+ super lambda{ |actual_arg| actual_arg.kind_of?(klass) }, [klass]
26
+ end
27
+ end
28
+
29
+ class Anything < Satisfy
30
+ def initialize
31
+ super lambda{ |_| true }, []
32
+ end
33
+ end
34
+
35
+ class Match < Satisfy
36
+ def initialize regexp
37
+ super lambda{ |actual_arg| regexp.match(actual_arg) }, [regexp]
38
+ end
39
+ end
40
+
41
+ class HashIncluding < Satisfy
42
+ def initialize hash
43
+ super lambda{ |actual_arg|
44
+ actual_arg.values_at(*hash.keys) == hash.values }, [hash]
45
+ end
46
+ end
47
+
48
+ class Within < Satisfy
49
+ def initialize range_or_array
50
+ super lambda{ |actual_arg| range_or_array.include?(actual_arg) },
51
+ [range_or_array]
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+
2
+ module Muack
3
+ class Session < Hash
4
+ def verify
5
+ each_value.all?(&:__mock_verify)
6
+ end
7
+
8
+ def reset
9
+ each_value(&:__mock_reset)
10
+ end
11
+ end
12
+ end