rcapture 1.0.4
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/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
|