muack 0.5.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.
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