rcapture 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/License +26 -0
- data/README +46 -0
- data/Rakefile +38 -0
- data/example/class_methods.rb +14 -0
- data/example/filter_method_calls.rb +24 -0
- data/example/hello_world.rb +35 -0
- data/example/inheritance.rb +36 -0
- data/example/modify_arguments.rb +39 -0
- data/example/multithreaded.rb +32 -0
- data/example/new_with_block.rb +30 -0
- data/example/no_endless_recursion.rb +16 -0
- data/rcapture.rb +19 -0
- data/rcapture/capture.rb +60 -0
- data/rcapture/capture_18x.rb +78 -0
- data/rcapture/capture_19x.rb +65 -0
- data/rcapture/capture_status.rb +53 -0
- data/rcapture/captured_info.rb +62 -0
- data/rcapture/interceptable.rb +93 -0
- data/rcapture/module_doc.rb +61 -0
- data/rcapture/singleton_class.rb +12 -0
- data/rcapture/symbol.rb +7 -0
- data/test/acceptance/acc_array.rb +36 -0
- data/test/acceptance/acc_inheritance.rb +61 -0
- data/test/acceptance/acc_modify_arguments.rb +50 -0
- data/test/acceptance/acc_new_with_block.rb +52 -0
- data/test/acceptance/acc_policies.rb +37 -0
- data/test/benchmark/benchmark_capture.rb +60 -0
- data/test/unit/test_capture_status.rb +66 -0
- data/test/unit/test_captured_info.rb +93 -0
- data/test/unit/test_method_with_blocks.rb +32 -0
- data/test/unit/test_modify_arguments.rb +99 -0
- data/test/unit/test_return_value.rb +38 -0
- data/test/unit/test_self_capture.rb +40 -0
- data/test/unit/test_singleton_vs_class.rb +37 -0
- data/test/unit/test_thread.rb +69 -0
- data/test/unit/test_visibility.rb +29 -0
- data/test/unit/testee.rb +27 -0
- metadata +108 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
module RCapture
|
3
|
+
|
4
|
+
module Internal
|
5
|
+
|
6
|
+
# Pre-capture single method.
|
7
|
+
# This version is used on Ruby 1.9.x where define_method
|
8
|
+
# allows for block arguments
|
9
|
+
def Internal.capture_single_pre(klass, method, additional_args, &callback)
|
10
|
+
prev = klass.instance_method(method)
|
11
|
+
# http://eigenclass.org/hiki/Changes+in+Ruby+1.9#l32
|
12
|
+
is_private = klass.private_instance_methods.include?(method)
|
13
|
+
|
14
|
+
klass.class_eval do
|
15
|
+
# http://eigenclass.org/hiki/Changes+in+Ruby+1.9#l11
|
16
|
+
define_method "#{method}" do |*args, &b|
|
17
|
+
status = CaptureStatus.current
|
18
|
+
call_prev = true; alt_ret = nil
|
19
|
+
if status.on?
|
20
|
+
status.use(false) do
|
21
|
+
ci = CapturedInfo.current
|
22
|
+
ci.fill(args, self, method, b)
|
23
|
+
callback.call(ci)
|
24
|
+
alt_ret = ci.return
|
25
|
+
call_prev = ci.predicate
|
26
|
+
end
|
27
|
+
end
|
28
|
+
if call_prev
|
29
|
+
prev.bind(self).call(*args, &b)
|
30
|
+
else
|
31
|
+
alt_ret
|
32
|
+
end
|
33
|
+
end
|
34
|
+
private "#{method}" if is_private
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Post-capture single method.
|
39
|
+
# This version is used on Ruby 1.9.x where define_method
|
40
|
+
# allows for block arguments
|
41
|
+
def Internal.capture_single_post(klass, method, additional_args, &callback)
|
42
|
+
prev = klass.instance_method(method)
|
43
|
+
# http://eigenclass.org/hiki/Changes+in+Ruby+1.9#l32
|
44
|
+
is_private = klass.private_instance_methods.include?(method)
|
45
|
+
|
46
|
+
klass.class_eval do
|
47
|
+
define_method "#{method}" do |*args, &b|
|
48
|
+
ret = prev.bind(self).call(*args, &b)
|
49
|
+
status = CaptureStatus.current
|
50
|
+
if status.on?
|
51
|
+
status.use(false) do
|
52
|
+
ci = CapturedInfo.current
|
53
|
+
ci.fill(args, self, method, b, ret)
|
54
|
+
callback.call(ci)
|
55
|
+
ret = ci.return
|
56
|
+
end
|
57
|
+
end
|
58
|
+
ret
|
59
|
+
end
|
60
|
+
private "#{method}" if is_private
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module RCapture
|
4
|
+
|
5
|
+
# Stack-based boolean enabled/disabled status.
|
6
|
+
# CaptureStatus is used to disable capturing from
|
7
|
+
# within callbacks which could otherwise lead to
|
8
|
+
# infinite recursion or undesired results.
|
9
|
+
class CaptureStatus
|
10
|
+
|
11
|
+
def initialize(value = true)
|
12
|
+
@status = value
|
13
|
+
@stack = []
|
14
|
+
end
|
15
|
+
|
16
|
+
# Access thread-local capture status variable
|
17
|
+
def CaptureStatus.current
|
18
|
+
Thread.current[:capture_status] ||= CaptureStatus.new(true)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Status is set to +value+ while operating on the block.
|
22
|
+
def use(value)
|
23
|
+
begin
|
24
|
+
self.set(value)
|
25
|
+
yield
|
26
|
+
ensure
|
27
|
+
self.restore
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set/push new status
|
32
|
+
def set(value)
|
33
|
+
@stack.unshift @status
|
34
|
+
@status = value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Query status
|
38
|
+
def on?
|
39
|
+
@status
|
40
|
+
end
|
41
|
+
|
42
|
+
# Query status
|
43
|
+
def off?
|
44
|
+
!@status
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pop status from stack
|
48
|
+
def restore
|
49
|
+
@status = @stack.shift if @stack.size > 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module RCapture
|
4
|
+
|
5
|
+
# Holds captured method information and acts as a communicator
|
6
|
+
# between callback and RCapture.
|
7
|
+
#
|
8
|
+
# === Post Capture
|
9
|
+
# When called from a method previously captured by +capture_post+,
|
10
|
+
# the following meaning holds true:
|
11
|
+
# args:: Array of input arguments passed to the method.
|
12
|
+
# sender:: Instance the method was called on.
|
13
|
+
# method:: Symbol of method called.
|
14
|
+
# return:: Value returned by method. When modified, the modified value is
|
15
|
+
# returned from the method.
|
16
|
+
# block:: Block passed to method or nil.
|
17
|
+
# predicate:: Unused.
|
18
|
+
#
|
19
|
+
# === Pre Capture
|
20
|
+
# When called from a method that was previously captured by +capture_pre+,
|
21
|
+
# the following meaning holds true:
|
22
|
+
# args:: Array of input arguments passed to the method. Modifyable.
|
23
|
+
# sender:: Instance the method was called on.
|
24
|
+
# method:: Symbol of method called.
|
25
|
+
# block:: Block passed to method or nil.
|
26
|
+
# predicate:: Initially true. If set to false the captured method will not be invoked.
|
27
|
+
# return:: Initially nil. If +predicate+ is false, then the value of +return+ is returned
|
28
|
+
# from the method.
|
29
|
+
#
|
30
|
+
class CapturedInfo
|
31
|
+
attr_accessor :args
|
32
|
+
attr_accessor :sender
|
33
|
+
attr_accessor :method
|
34
|
+
attr_accessor :return
|
35
|
+
attr_accessor :predicate
|
36
|
+
attr_accessor :block
|
37
|
+
|
38
|
+
# Access thread-local CapturedInfo variable
|
39
|
+
def CapturedInfo.current
|
40
|
+
Thread.current[:captured_info] ||= CapturedInfo.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# Fill with values
|
44
|
+
def fill(args, sender, method, block = nil, ret = nil)
|
45
|
+
@args = args
|
46
|
+
@sender = sender
|
47
|
+
@method = method
|
48
|
+
@return = ret
|
49
|
+
@block = block
|
50
|
+
@predicate = true
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :a, :args
|
54
|
+
alias_method :s, :sender
|
55
|
+
alias_method :m, :method
|
56
|
+
alias_method :r, :return
|
57
|
+
alias_method :r=, :return=
|
58
|
+
alias_method :b, :block
|
59
|
+
alias_method :p, :predicate
|
60
|
+
alias_method :p=, :predicate=
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
module RCapture
|
3
|
+
|
4
|
+
# = Introduction
|
5
|
+
# Interceptable is a module mixin to enrich the receiver with capturing capatibilities.
|
6
|
+
#
|
7
|
+
# It can be used at a class scope:
|
8
|
+
#
|
9
|
+
# class Array
|
10
|
+
# include RCapture::Interceptable
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# This will provide the methods +capture+, +capture_pre+, and +capture_post+.
|
14
|
+
# at class and instance level:
|
15
|
+
#
|
16
|
+
# Array.capture :methods => :push do
|
17
|
+
# puts 'pushed'
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# will affect all instances of Array. On the other hand
|
21
|
+
#
|
22
|
+
# a = []
|
23
|
+
# a.capture :methods => :push do
|
24
|
+
# puts 'pushed'
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# will affect only the instance +a+.
|
28
|
+
#
|
29
|
+
# Instances can be extended by Interceptable to provide capturing capatibilities
|
30
|
+
# for the selected instance only:
|
31
|
+
#
|
32
|
+
# a = []
|
33
|
+
# a.extend(RCapture::Interceptable)
|
34
|
+
# a.capture :methods => :push do
|
35
|
+
# puts 'pushed'
|
36
|
+
# end
|
37
|
+
module Interceptable
|
38
|
+
|
39
|
+
module ClassMethods # :nodoc:
|
40
|
+
def capture(args, &callback)
|
41
|
+
RCapture::Internal.capture_post(self, args, &callback)
|
42
|
+
end
|
43
|
+
alias_method :capture_post, :capture
|
44
|
+
|
45
|
+
def capture_pre(args, &callback)
|
46
|
+
RCapture::Internal.capture_pre(self, args, &callback)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.included(klass) # :nodoc:
|
51
|
+
klass.extend(Interceptable::ClassMethods)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Perform a post-capture on the given class/instance.
|
55
|
+
#
|
56
|
+
# === Arguments
|
57
|
+
# args:: A hash of options. The following are recognized
|
58
|
+
# [:methods] Instance methods to capture. Either a single method name/symbol or
|
59
|
+
# and array of names/symbols.
|
60
|
+
# [:class_methods] Class methods to capture. Either a single method name/symbol or
|
61
|
+
# and array of names/symbols.
|
62
|
+
# callback:: The block to be invoked after captured method was invoked. The callback will be given
|
63
|
+
# an instance of CapturedInfo.
|
64
|
+
def capture_post(args, &callback)
|
65
|
+
RCapture::Internal.capture_post(
|
66
|
+
RCapture::Internal.singleton_class(self),
|
67
|
+
args,
|
68
|
+
&callback
|
69
|
+
)
|
70
|
+
end
|
71
|
+
alias_method :capture, :capture_post
|
72
|
+
|
73
|
+
# Perform a pre-capture on the given class/instance.
|
74
|
+
#
|
75
|
+
# === Arguments
|
76
|
+
# args:: A hash of options. The following are recognized
|
77
|
+
# [:methods] Instance methods to capture. Either a single method name/symbol or
|
78
|
+
# and array of names/symbols.
|
79
|
+
# [:class_methods] Class methods to capture. Either a single method name/symbol or
|
80
|
+
# and array of names/symbols.
|
81
|
+
# callback:: The block to be invoked before the captured method is invoked. The callback will be given
|
82
|
+
# an instance of CapturedInfo.
|
83
|
+
def capture_pre(args, &callback)
|
84
|
+
RCapture::Internal.capture_pre(
|
85
|
+
RCapture::Internal.singleton_class(self),
|
86
|
+
args,
|
87
|
+
&callback
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
# = Introduction
|
3
|
+
# The module RCapture is your single entry point when it comes to
|
4
|
+
# method capturing/hooking.
|
5
|
+
#
|
6
|
+
# To hook/capture methods you need to
|
7
|
+
# - enrich target(s) with capturing capatibilities. This is provided by the module mixin Interceptable.
|
8
|
+
# - capture the methods by invoking methods from Interceptable.
|
9
|
+
#
|
10
|
+
# = Examples
|
11
|
+
# Here is a set of examples that should help you getting started. Those examples are part of
|
12
|
+
# the RCapture distribution
|
13
|
+
#
|
14
|
+
# == Hello World
|
15
|
+
# The following example illustrates basic class and instance level hooking and
|
16
|
+
# makes use of the passed CapturedInfo instance to query details.
|
17
|
+
# :include:example/hello_world.rb
|
18
|
+
#
|
19
|
+
# == Class Methods
|
20
|
+
# In this example quickly shows how to hook class methods.
|
21
|
+
# :include:example/class_methods.rb
|
22
|
+
#
|
23
|
+
# == Modifying Arguments and Return Values
|
24
|
+
# The following example illustrates capturing methods in order to modify input and return arguments.
|
25
|
+
# Further, it illustrates how capture methods of a single instance only.
|
26
|
+
# :include:example/modify_arguments.rb
|
27
|
+
#
|
28
|
+
# == Filtering Method Calls
|
29
|
+
# Additionally to modifying arguments you can prevent captured method from being called using a Interceptable#capture_pre hook.
|
30
|
+
# :include:example/filter_method_calls.rb
|
31
|
+
#
|
32
|
+
# == Callbacks Are Capture-Free
|
33
|
+
# During processing of a callback, capturing is disabled. Otherwise, this could lead to infinite recursion as the following
|
34
|
+
# examples demonstrates.
|
35
|
+
# :include:example/no_endless_recursion.rb
|
36
|
+
#
|
37
|
+
# == Inheritance
|
38
|
+
# Captured methods propagate through derivates as long as they are not overridden by a class.
|
39
|
+
# :include:example/inheritance.rb
|
40
|
+
#
|
41
|
+
# == Multithreading
|
42
|
+
# RCapture is a lock-free implementation that supports multithreading. There is no restriction on the number of threads
|
43
|
+
# that call a shared callback. Captures, however, should be called only from a single thread or guaranteed to be synchronized
|
44
|
+
# by the user. Since callbacks can be invoked from multiple threads in parallel, shared resources from within the callbacks
|
45
|
+
# need to be synchronized by the user. The passed CapturedInfo is a thread-local variable and needs not to be synchronized.
|
46
|
+
# The following example illustrates this.
|
47
|
+
# :include:example/multithreaded.rb
|
48
|
+
#
|
49
|
+
# == New with Block
|
50
|
+
# Here is one final example that stimulate your imagination of what can be done with RCapture: given any class, it is often
|
51
|
+
# desireable to work with newly created instance by passing a block to new. If that class does not support this you can use
|
52
|
+
# RCapture to enable this behaviour.
|
53
|
+
# :include:example/new_with_block.rb
|
54
|
+
#
|
55
|
+
# = Benchmark
|
56
|
+
# RCapture works on Ruby 1.8.x and 1.9.x. Due to API changes between 1.8 and 1.9, RCapture selects an appropriate
|
57
|
+
# implementation of its core methods. Here is a benchmark that illustrates the overhead on 1.8 and 1.9 when
|
58
|
+
# a captured method is called.
|
59
|
+
# :include:test/benchmark/benchmark_capture.rb
|
60
|
+
module RCapture
|
61
|
+
end
|
data/rcapture/symbol.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# (c) Christoph Heindl, 2010
|
3
|
+
# http://cheind.wordpress.com
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
require 'rcapture'
|
9
|
+
|
10
|
+
class Array
|
11
|
+
include RCapture::Interceptable
|
12
|
+
end
|
13
|
+
|
14
|
+
class TestAcceptanceArrray < Test::Unit::TestCase
|
15
|
+
|
16
|
+
def test_array
|
17
|
+
op_count = 0
|
18
|
+
a = Array.new
|
19
|
+
|
20
|
+
a.capture :methods => [:push, :<<] do |ci|
|
21
|
+
puts "intercepted"
|
22
|
+
end
|
23
|
+
|
24
|
+
Array.capture_pre :class_methods => :new do |ci|
|
25
|
+
p "new called"
|
26
|
+
end
|
27
|
+
|
28
|
+
r = Array.new(3)
|
29
|
+
|
30
|
+
a << 1 << 2
|
31
|
+
[] << 1
|
32
|
+
p a
|
33
|
+
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
#
|
2
|
+
# (c) Christoph Heindl, 2010
|
3
|
+
# http://cheind.wordpress.com
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
require 'rcapture'
|
9
|
+
include RCapture
|
10
|
+
|
11
|
+
# Closed polygon dummy
|
12
|
+
class Polygon
|
13
|
+
include Interceptable
|
14
|
+
def vertices
|
15
|
+
self.edges
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Triangle < Polygon
|
20
|
+
def edges
|
21
|
+
3
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Degenerate Polygon
|
26
|
+
class Digon < Polygon
|
27
|
+
def edges
|
28
|
+
1
|
29
|
+
end
|
30
|
+
|
31
|
+
def vertices
|
32
|
+
2
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Quadliteral < Polygon
|
37
|
+
def edges
|
38
|
+
4
|
39
|
+
end
|
40
|
+
|
41
|
+
def vertices
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class AcceptanceInheritance < Test::Unit::TestCase
|
47
|
+
def test_inheritance
|
48
|
+
called = 0
|
49
|
+
Polygon.capture :methods => :vertices do |ci|
|
50
|
+
called += 1
|
51
|
+
end
|
52
|
+
|
53
|
+
assert_equal(3, Triangle.new.vertices)
|
54
|
+
assert_equal(1, called)
|
55
|
+
|
56
|
+
assert_equal(2, Digon.new.vertices)
|
57
|
+
assert_equal(1, called)
|
58
|
+
assert_equal(4, Quadliteral.new.vertices)
|
59
|
+
assert_equal(2, called)
|
60
|
+
end
|
61
|
+
end
|