cheap_advice 1.0.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.
@@ -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
+