cheap_advice 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,217 @@
1
+ require 'cheap_advice'
2
+
3
+ class CheapAdvice
4
+ #
5
+ class Configuration
6
+ class Error < ::CheapAdvice::Error; end
7
+
8
+ # Configuration input hash.
9
+ attr_accessor :config
10
+
11
+ # Hash mapping advice names to CheapAdvice objects.
12
+ attr_accessor :advice
13
+
14
+ # Array of CheapAdvice::Advised methods.
15
+ attr_accessor :advised
16
+
17
+ # Hash of file names that were explicity required before applying advice.
18
+ attr_accessor :required
19
+
20
+ # Flag
21
+ attr_accessor :config_changed
22
+ alias :config_changed? :config_changed
23
+
24
+ attr_accessor :verbose
25
+
26
+ def initialize opts = nil
27
+ opts ||= EMPTY_Hash
28
+ @verbose = false
29
+ opts.each do | k, v |
30
+ send(:"#{k}=", v)
31
+ end
32
+ @advice ||= { }
33
+ @targets = [ ]
34
+ @advised = [ ]
35
+ @required = { }
36
+ end
37
+
38
+ def config_changed!
39
+ @config_changed = true
40
+ self
41
+ end
42
+
43
+ def configure_if_changed!
44
+ if config_changed?
45
+ configure!
46
+ @config_changed = false
47
+ end
48
+ self
49
+ end
50
+
51
+ def configure!
52
+ disable!
53
+
54
+ # First pass: parse target and defaults.
55
+ c = [ ]
56
+ d = { }
57
+ get_config.each do | target_name, target_config |
58
+ annotate_error "target=#{target_name}" do
59
+ t = parse_target(target_name)
60
+ # _log { "#{target_name.inspect} => #{t.inspect}" }
61
+ case target_config
62
+ when true, false
63
+ target_config = { :enabled => target_config }
64
+ end
65
+ t.update(target_config) if target_config
66
+ [ :advice, :require ].each do | k |
67
+ t[k] = as_array(t[k]) if t.key?(k)
68
+ end
69
+ case
70
+ when t[:meth].nil? && t[:mod].nil? # global default.
71
+ d[nil] = t
72
+ when t[:meth].nil? # module default.
73
+ d[t[:mod]] = t
74
+ else
75
+ c << t # real target
76
+ end
77
+ end
78
+ end
79
+ d[nil] ||= { }
80
+
81
+ # Second pass: merge defaults with target.
82
+ @targets = [ ]
83
+ c.each do | t |
84
+ x = merge!(d[nil].dup, d[t[:mod]] || EMPTY_Hash)
85
+ t = merge!(x, t)
86
+ # _log { "target = #{t.inspect}" }
87
+ next if t[:enabled] == false
88
+ @targets << t
89
+ end
90
+
91
+ enable!
92
+
93
+ self
94
+ end
95
+
96
+ def disable!
97
+ @targets.each do | t |
98
+ (t[:advice] || EMPTY_Array).each do | advice_name |
99
+ advice_name = advice_name.to_sym
100
+ if advised = (t[:advised] || EMPTY_Hash)[advice_name]
101
+ advised.disable!
102
+ end
103
+ end
104
+ end
105
+ @advised.clear
106
+ self
107
+ end
108
+
109
+ def enable!
110
+ @advised.clear
111
+ @targets.each do | t |
112
+ t_str = target_as_string(t)
113
+ annotate_error "target=#{t_str.inspect}" do
114
+ (t[:require] || EMPTY_Array).each do | r |
115
+ _log { "#{t_str}: require #{r}" }
116
+ unless @required[r]
117
+ require r
118
+ @required[r] = true
119
+ end
120
+ end
121
+ end
122
+
123
+ (t[:advice] || EMPTY_Array).each do | advice_name |
124
+ advice_name = advice_name.to_sym
125
+ annotate_error "target=#{target_as_string(t)} advice=#{advice_name.inspect}" do
126
+ unless advice = @advice[advice_name]
127
+ raise Error, "no advice by that name"
128
+ end
129
+ options = t[:options][nil]
130
+ options = merge!(options, t[:options][advice_name])
131
+ # _log { "#{t.inspect} options => #{options.inspect}" }
132
+
133
+ advised = advice.advise!(t[:mod], t[:meth], t[:kind], options)
134
+
135
+ (t[:advised] ||= { })[advice_name] = advised
136
+
137
+ @advised << advised
138
+ end
139
+ end
140
+ end
141
+ self
142
+ end
143
+
144
+ def _log msg = nil
145
+ return self unless @verbose
146
+ msg ||= yield if block_given?
147
+ $stderr.puts "#{self.class}: #{msg}"
148
+ self
149
+ end
150
+
151
+ def annotate_error x
152
+ yield
153
+ rescue Exception => err
154
+ msg = "in #{x.inspect}: #{err.inspect}"
155
+ _log { "ERROR: #{msg}\n #{err.backtrace * "\n "}" }
156
+ raise Error, msg, err.backtrace
157
+ end
158
+
159
+ def get_config
160
+ case @config
161
+ when Hash
162
+ @config
163
+ when Proc
164
+ @config.call(self)
165
+ when nil
166
+ raise Error, "no config"
167
+ end
168
+ end
169
+
170
+ def as_array x
171
+ x = EMPTY_Array if x == nil
172
+ x = x.split(/\s+|\s*,\s*/) if String === x
173
+ raise "Unexpected Hash" if Hash === x
174
+ x
175
+ end
176
+
177
+ def parse_target x
178
+ case x
179
+ when nil
180
+ { }
181
+ when Hash
182
+ x
183
+ when String, Symbol
184
+ if x.to_s =~ /\A([a-z0-9_:]+)(?:([#\.])([a-z0-9_]+[=\!\?]?))?\Z/i
185
+ { :mod => $1,
186
+ :kind => $2 && ($2 == '.' ? :module : :instance),
187
+ :meth => $3,
188
+ }
189
+ else
190
+ raise Error, "cannot parse #{x.inspect}"
191
+ end
192
+ end
193
+ end
194
+ def target_as_string t
195
+ "#{t[:mod]}#{t[:kind] == :instance ? '#' : '.'}#{t[:meth]}"
196
+ end
197
+
198
+ def merge! dst, src
199
+ case dst
200
+ when nil, Hash
201
+ case src
202
+ when Hash
203
+ dst = dst ? dst.dup : { }
204
+ src.each do | k, v |
205
+ dst[k] = merge!(dst[k], v)
206
+ end
207
+ else
208
+ dst = src
209
+ end
210
+ else
211
+ dst = src
212
+ end
213
+ dst
214
+ end
215
+
216
+ end
217
+ end
@@ -0,0 +1,228 @@
1
+ require 'cheap_advice'
2
+
3
+ require 'time' # Time#iso8601
4
+
5
+ class CheapAdvice
6
+ # Sample Tracing Advice factory.
7
+ module Trace
8
+ def self.new opts = nil
9
+ opts ||= { }
10
+ trace = CheapAdvice.new(:around, opts) do | ar, body |
11
+ a = ar.advice
12
+ ad = ar.advised
13
+ logger = ad.logger[:name] || ad.logger_default[:name]
14
+ logger = a.logger[logger] || a.logger_default[logger]
15
+
16
+ formatter = nil
17
+ if ad[:log_before] != false
18
+ a.log(logger) do
19
+ formatter = a.new_formatter(logger)
20
+ ar[:time_before] = Time.now
21
+ formatter.record(ar, :before)
22
+ end
23
+ end
24
+
25
+ body.call
26
+
27
+ if ad[:log_after] != false
28
+ a.log(logger) do
29
+ formatter ||= a.new_formatter(logger)
30
+ ar[:time_after] = Time.now
31
+ if ar.error
32
+ ar[:error] = ar.error if ad[:log_error] != false
33
+ else
34
+ ar[:result] = ar.result if ad[:log_result] != false
35
+ end
36
+ formatter.record(ar, :after)
37
+ end
38
+ end
39
+ end
40
+ trace.extend(Behavior)
41
+ trace.advised_extend = Behavior
42
+ trace
43
+ end
44
+
45
+ module Behavior
46
+ def logger
47
+ # $stderr.puts " #{self.class} @options = #{@options.inspect}"
48
+ @options[:logger] ||= { }
49
+ end
50
+ def logger_default
51
+ logger[nil] ||= { }
52
+ end
53
+
54
+ def new_formatter logger
55
+ formatter(logger).new(logger, *formatter_options(logger))
56
+ end
57
+
58
+ def formatter logger
59
+ logger[:formatter] ||=
60
+ logger_default[:formatter] ||=
61
+ DefaultFormatter
62
+ end
63
+
64
+ def formatter_options logger
65
+ logger[:formatter_options] ||=
66
+ logger_default[:formatter_options] ||=
67
+ [ ]
68
+ end
69
+
70
+ def log_prefix logger, ar
71
+ pre =
72
+ logger[:log_prefix] ||=
73
+ logger_default[:log_prefix] ||=
74
+ EMPTY_String
75
+ case pre
76
+ when Proc
77
+ pre.call(ar)
78
+ else
79
+ pre
80
+ end
81
+ end
82
+
83
+ def log logger, msg = nil
84
+ return msg unless logger
85
+ msg ||= yield if block_given?
86
+ return msg if msg.nil?
87
+ dst = logger[:target]
88
+ case dst
89
+ when nil
90
+ nil
91
+ when IO
92
+ dst.seek(0, IO::SEEK_END)
93
+ dst.puts msg.to_s
94
+ dst.flush
95
+ when Proc
96
+ dst.call(msg)
97
+ else
98
+ dst.send(logger[:method] || :debug, msg)
99
+ end
100
+ msg
101
+ end
102
+
103
+ def log_all msg = nil
104
+ logger.values.each do | dst |
105
+ log(dst) { msg ||= yield if block_given?; msg }
106
+ end
107
+ msg
108
+ end
109
+
110
+ end
111
+
112
+ class BaseFormatter
113
+ attr_reader :logger
114
+
115
+ def initialize logger, *args
116
+ @logger = logger
117
+ end
118
+
119
+ def format obj, mode
120
+ case mode
121
+ when :rcvr
122
+ obj && obj.to_s
123
+ when :module
124
+ obj && obj.name
125
+ when :time
126
+ obj && obj.iso8601(6)
127
+ when :error
128
+ obj.inspect
129
+ when :result
130
+ obj.inspect
131
+ when :method
132
+ ad = ar.meth_to_s
133
+ else
134
+ nil
135
+ end
136
+ end
137
+ end
138
+
139
+ class DefaultFormatter < BaseFormatter
140
+ def format obj, mode
141
+ case mode
142
+ when :error
143
+ return "ERROR #{obj.inspect}"
144
+ when :result
145
+ return "=> #{obj.inspect}"
146
+ when :time
147
+ return super
148
+ else
149
+ obj = super || obj
150
+ end
151
+
152
+ obj = obj.inspect
153
+ if mode == :args
154
+ obj = obj.to_s.gsub(/\A\[|\]\Z/, '')
155
+ end
156
+ obj
157
+ end
158
+
159
+ # Formats the ActivationRecord for the logger.
160
+ def record ar, mode
161
+ ad = ar.advised
162
+ msg = nil
163
+
164
+ case mode
165
+ when :before, :after
166
+ msg = ad.log_prefix(logger, ar).to_s
167
+ msg = msg.dup if msg.frozen?
168
+ ar[:args] ||= format(ar.args, :args) if ad[:log_args] != false
169
+ ar[:meth] ||= "#{ad.meth_to_s} #{ar.rcvr.class}"
170
+ msg << "#{format(ar[:"time_#{mode}"], :time)} #{ar[:meth]}"
171
+ msg << " #{format(ar.rcvr, :rcvr)}" if ad[:log_rcvr]
172
+ msg << " ( #{ar[:args]} )" if ar[:args]
173
+ end
174
+
175
+ case mode
176
+ when :before
177
+ msg << " {"
178
+ when :after
179
+ msg << " }"
180
+ if ar.error
181
+ msg << " #{format(ar[:error], :error )}" if ad[:log_error] != false
182
+ else
183
+ msg << " #{format(ar[:result], :result)}" if ad[:log_result] != false
184
+ end
185
+ end
186
+
187
+ msg
188
+ end
189
+ end # class
190
+
191
+ class YamlFormatter < BaseFormatter
192
+ def to_hash ar, mode
193
+ ad = ar.advised
194
+
195
+ data = (ar.advice[:log_data] || EMPTY_Hash).dup
196
+ data.update(ar.advised[:log_data] || EMPTY_Hash)
197
+ # pp [ :'ar.data=', ar.data ]
198
+ data.update(ar.data)
199
+ # pp [ :'data=', data ]
200
+ if x = ad.log_prefix(logger, ar)
201
+ data[:log_prefix] = x
202
+ end
203
+ data[:meth] = ar.meth
204
+ data[:mod] = Module === (x = ar.mod) ? x.name : x
205
+ data[:kind] = ar.kind
206
+ data[:signature] = ar.meth_to_s
207
+ data[:rcvr] = format(ar.rcvr, :rcvr) if ad[:log_rcvr]
208
+ data[:rcvr_class] = ar.rcvr.class.name
209
+ if x = data[:time_after] &&
210
+ data[:time_before] &&
211
+ (data[:time_after].to_f - data[:time_before].to_f)
212
+ data[:time_elapsed] = x
213
+ end
214
+ data
215
+ end
216
+
217
+ def record ar, mode
218
+ case mode
219
+ when :after
220
+ data = to_hash(ar, mode)
221
+ YAML.dump(data)
222
+ else
223
+ nil
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,342 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class CheapAdvice
4
+ module Test
5
+
6
+ module M
7
+ attr_accessor :_m
8
+ def m(arg)
9
+ @_m = 1 + arg
10
+ end
11
+ def self.mm(arg)
12
+ 3 + arg
13
+ end
14
+ end
15
+
16
+ class Foo
17
+ include M
18
+ attr_accessor :foo, :bar
19
+ attr_reader :_baz, :_bar
20
+ (class << self; self; end).instance_eval do
21
+ attr_accessor :_baz
22
+ end
23
+
24
+ def self.baz(arg)
25
+ self._baz = 3 + arg
26
+ end
27
+
28
+ def baz(arg)
29
+ @_baz = 5 + arg
30
+ end
31
+
32
+ def do_it(arg)
33
+ yield(arg + 7) + 2
34
+ end
35
+ end
36
+
37
+
38
+ class Bar
39
+ include M
40
+ attr_accessor :bar
41
+ attr_reader :_baz
42
+
43
+ def baz(arg)
44
+ @_baz = 7 + arg
45
+ end
46
+
47
+ def calls_private_method(arg)
48
+ private_method(arg)
49
+ end
50
+
51
+ private
52
+ def private_method(arg)
53
+ arg
54
+ end
55
+
56
+ protected
57
+ def protected_method(arg)
58
+ arg
59
+ end
60
+ end
61
+
62
+ class Baz < Bar
63
+ def calls_protected_method(arg)
64
+ protected_method(arg)
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+
71
+
72
+ describe "CheapAdvice" do
73
+ attr_reader :tracing_advice
74
+ attr_reader :f, :b
75
+
76
+ before(:each) do
77
+ @tracing_advice = CheapAdvice.new(:around) do | ar, body |
78
+ ar.advice.log " TRACE: before #{ar.rcvr.class}\##{ar.meth}(#{ar.args.join(", ")})"
79
+ ar.advice.log " foo = #{@foo.inspect}"
80
+ ar.advice.log " bar = #{@bar.inspect}"
81
+ result = body.call
82
+ ar.advice.log " TRACE: after #{ar.rcvr.class}\##{ar.meth}(#{ar.args.join(", ")}) => #{result.inspect}"
83
+ ar.result = "yo!"
84
+ ar.advice.log " TRACE: return #{ar.result.inspect}"
85
+ "oy!" # Not relevant.
86
+ end
87
+ @tracing_advice.instance_eval do
88
+ def log msg = nil
89
+ return @log unless msg
90
+ (@log ||= [ ]) << msg.dup
91
+ end
92
+ end
93
+ end
94
+
95
+ it "handles simple tracing_advice example." do
96
+ @f = CheapAdvice::Test::Foo.new
97
+ @b = CheapAdvice::Test::Bar.new
98
+
99
+ assert_without_advice
100
+ tracing_advice.log.should == nil
101
+
102
+ tracing_advice.advise!( [CheapAdvice::Test::Foo, CheapAdvice::Test::Bar], [ :bar, :bar=, :baz ])
103
+ f.foo = 10
104
+ f.foo.should == 10
105
+ f.baz(10).should == "yo!"
106
+ b.bar = 101
107
+ b.bar.should == "yo!"
108
+ b.baz(10).should == "yo!"
109
+ tracing_advice.log.should_not == nil
110
+ tracing_advice.log.size.should == 20
111
+
112
+ tracing_advice.unadvise!
113
+ assert_without_advice
114
+ end
115
+
116
+ def assert_without_advice
117
+ f.foo = 10
118
+ f.foo.should == 10
119
+ f.baz(10).should == 15
120
+ f._baz.should == 15
121
+
122
+ b.bar = 101
123
+ b.bar.should == 101
124
+ b.baz(10).should == 17
125
+ b._baz.should == 17
126
+ end
127
+
128
+ it 'handles methods with blocks.' do
129
+ ars = [ ]
130
+ basic_advice = CheapAdvice.new(:before) do | ar |
131
+ ars << ar
132
+ end
133
+ basic_advice.advised.size.should == 0
134
+
135
+ @f = CheapAdvice::Test::Foo.new
136
+
137
+ assert_do_it f
138
+ ars.size.should == 0
139
+
140
+ basic_advice.advise! CheapAdvice::Test::Foo, 'do_it'
141
+
142
+ basic_advice.advised.size.should == 1
143
+ advised = basic_advice.advised.first
144
+ advised.mod.should == CheapAdvice::Test::Foo
145
+ advised.meth.should == :do_it
146
+ advised.enabled.should == true
147
+
148
+ assert_do_it f
149
+ ars.size.should == 1
150
+
151
+ advised.unadvise!
152
+ advised.enabled.should == false
153
+
154
+ assert_do_it f
155
+ ars.size.should == 1
156
+ end
157
+
158
+ def assert_do_it f
159
+ arg = nil
160
+ result = f.do_it(10) do | _arg |
161
+ _arg.should == 17
162
+ arg = _arg
163
+ end
164
+ arg.should == 17
165
+ result.should == 19
166
+ end
167
+
168
+ it 'handles applying the same advice only once.' do
169
+ null_advice = CheapAdvice.new(:before) do | ar |
170
+ end
171
+ null_advice.advised.size.should == 0
172
+
173
+ advised = null_advice.advise! CheapAdvice::Test::Foo, :do_it
174
+ null_advice.advised.size.should == 1
175
+
176
+ advised_again = null_advice.advise! CheapAdvice::Test::Foo, :do_it
177
+ advised_again.object_id.should == advised.object_id
178
+
179
+ null_advice.advised.size.should == 1
180
+
181
+ advised = null_advice.advise! CheapAdvice::Test::Foo, :baz, :class
182
+ null_advice.advised.size.should == 2
183
+
184
+ advised_again = null_advice.advise! CheapAdvice::Test::Foo, :baz, :class
185
+ advised_again.object_id.should == advised.object_id
186
+
187
+ null_advice.advised.size.should == 2
188
+
189
+ advised.unadvise!
190
+ end
191
+
192
+ it 'handles String for class and method names.' do
193
+ advice_called = 0
194
+ null_advice = CheapAdvice.new(:before) do | ar |
195
+ advice_called += 1
196
+ end
197
+ null_advice.advised.size.should == 0
198
+
199
+
200
+ @f = CheapAdvice::Test::Foo.new
201
+
202
+ advice_called.should == 0
203
+
204
+ advised = null_advice.advise!('CheapAdvice::Test::Foo', 'baz')
205
+ null_advice.advised.size.should == 1
206
+
207
+ @f.baz(5).should == 10
208
+ @f._baz.should == 10
209
+ advice_called.should == 1
210
+
211
+ advised.unadvise!
212
+ end
213
+
214
+ it 'handles Module instance method advice.' do
215
+ advice_called = 0
216
+ null_advice = CheapAdvice.new(:before) do | ar |
217
+ advice_called += 1
218
+ end
219
+ null_advice.advised.size.should == 0
220
+
221
+
222
+ @f = CheapAdvice::Test::Foo.new
223
+ @b = CheapAdvice::Test::Bar.new
224
+
225
+ advice_called.should == 0
226
+
227
+ advised = null_advice.advise!('CheapAdvice::Test::M', 'm')
228
+ null_advice.advised.size.should == 1
229
+
230
+ @f.m(5).should == 6
231
+ @f._m.should == 6
232
+
233
+ @b.m(5).should == 6
234
+ @b._m.should == 6
235
+
236
+ advice_called.should == 2
237
+
238
+ advised.unadvise!
239
+ end
240
+
241
+ it 'handles Module singleton method advice.' do
242
+ advice_called = 0
243
+ null_advice = CheapAdvice.new(:before) do | ar |
244
+ advice_called += 1
245
+ end
246
+ null_advice.advised.size.should == 0
247
+
248
+ advice_called.should == 0
249
+
250
+ advised = null_advice.advise!(CheapAdvice::Test::M, :mm, :module)
251
+ null_advice.advised.size.should == 1
252
+
253
+ CheapAdvice::Test::M.mm(5).should == 8
254
+
255
+ advice_called.should == 1
256
+
257
+ advised.unadvise!
258
+ end
259
+
260
+ it 'handles Class method advice.' do
261
+ advice_called = 0
262
+ null_advice = CheapAdvice.new(:before) do | ar |
263
+ advice_called += 1
264
+ end
265
+ null_advice.advised.size.should == 0
266
+
267
+
268
+ @f = CheapAdvice::Test::Foo.new
269
+
270
+ advice_called.should == 0
271
+ CheapAdvice::Test::Foo._baz.should == nil
272
+
273
+ advised = null_advice.advise!(CheapAdvice::Test::Foo, :baz, :class)
274
+ null_advice.advised.size.should == 1
275
+
276
+ CheapAdvice::Test::Foo.baz(5).should == 8
277
+ CheapAdvice::Test::Foo._baz.should == 8
278
+ @f.baz(5).should == 10
279
+ @f._baz.should == 10
280
+ advice_called.should == 1
281
+
282
+ advised.unadvise!
283
+
284
+ CheapAdvice::Test::Foo.baz(7).should == 10
285
+ CheapAdvice::Test::Foo._baz.should == 10
286
+ @f.baz(5).should == 10
287
+ @f._baz.should == 10
288
+ advice_called.should == 1
289
+ end
290
+
291
+ it 'handles private method advice.' do
292
+ advice_called = 0
293
+ null_advice = CheapAdvice.new(:before) do | ar |
294
+ advice_called += 1
295
+ end
296
+ null_advice.advised.size.should == 0
297
+
298
+ advice_called.should == 0
299
+
300
+ advised = null_advice.advise!(CheapAdvice::Test::Bar, :private_method)
301
+ advised.scope.should == :private
302
+ null_advice.advised.size.should == 1
303
+
304
+ @b = CheapAdvice::Test::Bar.new
305
+
306
+ @b.calls_private_method(5).should == 5
307
+ advice_called.should == 1
308
+
309
+ advised.unadvise!
310
+
311
+ @b.calls_private_method(5).should == 5
312
+ advice_called.should == 1
313
+ end
314
+
315
+
316
+ it 'handles protected method advice.' do
317
+ advice_called = 0
318
+ null_advice = CheapAdvice.new(:before) do | ar |
319
+ advice_called += 1
320
+ end
321
+ null_advice.advised.size.should == 0
322
+
323
+ advice_called.should == 0
324
+
325
+ advised = null_advice.advise!(CheapAdvice::Test::Bar, :protected_method)
326
+ advised.scope.should == :protected
327
+ null_advice.advised.size.should == 1
328
+
329
+ @b = CheapAdvice::Test::Baz.new
330
+
331
+ @b.calls_protected_method(5).should == 5
332
+ advice_called.should == 1
333
+
334
+ advised.unadvise!
335
+
336
+ @b.calls_protected_method(5).should == 5
337
+ advice_called.should == 1
338
+ end
339
+
340
+ end
341
+
342
+