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
data/License
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
# = License
|
3
|
+
#
|
4
|
+
# Copyright (c) 2010, Christoph Heindl
|
5
|
+
# All rights reserved.
|
6
|
+
#
|
7
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
8
|
+
# are permitted provided that the following conditions are met:
|
9
|
+
# - Redistributions of source code must retain the above copyright notice, this list
|
10
|
+
# of conditions and the following disclaimer.
|
11
|
+
# - Redistributions in binary form must reproduce the above copyright notice, this list
|
12
|
+
# of conditions and the following disclaimer in the documentation and/or other materials
|
13
|
+
# provided with the distribution.
|
14
|
+
# - Neither the name of the Christoph Heindl nor the names of its contributors may be
|
15
|
+
# used to endorse or promote products derived from this software without specific prior
|
16
|
+
# written permission.
|
17
|
+
#
|
18
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
19
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
20
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
21
|
+
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
22
|
+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
23
|
+
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
24
|
+
# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
25
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
26
|
+
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
= RCapture
|
3
|
+
This package contains the module RCapture, a collection of intuitive methods to capture method invocations.
|
4
|
+
|
5
|
+
RCapture has the following features
|
6
|
+
- Capturing of instance and class methods of individual objects or entire population of objects.
|
7
|
+
- Capturing pre or post method invocation.
|
8
|
+
- Multiple capturings per method.
|
9
|
+
- Modify method arguments and return values.
|
10
|
+
- Filter method calls.
|
11
|
+
- Developed with multithreaded environments in mind.
|
12
|
+
and many more.
|
13
|
+
|
14
|
+
= Simple Example
|
15
|
+
The example below will capture insertion methods of arrays and output statistics upon
|
16
|
+
invocation. For more examples see RCapture module documentation.
|
17
|
+
|
18
|
+
require 'rcapture'
|
19
|
+
|
20
|
+
class Array
|
21
|
+
include RCapture::Interceptable
|
22
|
+
end
|
23
|
+
|
24
|
+
Array.capture_post :methods => [:<<, :push] do |cs|
|
25
|
+
puts "#{cs.args.first} was inserted to array #{cs.sender}"
|
26
|
+
end
|
27
|
+
|
28
|
+
[] << 1 << 2
|
29
|
+
[].push 3
|
30
|
+
|
31
|
+
#=> 1 was inserted to array [1]
|
32
|
+
#=> 2 was inserted to array [1, 2]
|
33
|
+
#=> 3 was inserted to array [3]
|
34
|
+
|
35
|
+
= Requirements
|
36
|
+
Non except Ruby. This distribution was tested on Ruby 1.8.6 and Ruby 1.9.1.
|
37
|
+
|
38
|
+
= License
|
39
|
+
RCapture is Copyright (c) 2010 Christoph Heindl. It is free software, and may be redistributed under the terms
|
40
|
+
specified in the {License}[link:files/License.html] file.
|
41
|
+
|
42
|
+
= Support
|
43
|
+
The RCapture homepage is http://code.google.com/p/cheind-blog-files. There you will find links report
|
44
|
+
{issues}[http://code.google.com/p/cheind-blog-files/issues/list] (use tag component-rcapture) and latest source code.
|
45
|
+
You might find additional help on the author's homepage http://cheind.wordpress.com. For general questions contact the
|
46
|
+
author via email at {christoph.heindl@gmail.com}[mailto:christoph.heindl@gmail.com]
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#
|
2
|
+
# (c) Christoph Heindl, 2010
|
3
|
+
# http://cheind.wordpress.com
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'rake'
|
7
|
+
require 'rake/testtask'
|
8
|
+
require 'rake/rdoctask'
|
9
|
+
|
10
|
+
task :default => [:test_units]
|
11
|
+
|
12
|
+
desc "Run unit tests"
|
13
|
+
Rake::TestTask.new("test") { |t|
|
14
|
+
t.pattern = FileList['test/unit/test_*.rb']
|
15
|
+
t.verbose = true
|
16
|
+
t.warning = false
|
17
|
+
}
|
18
|
+
|
19
|
+
desc "Run acceptance tests"
|
20
|
+
Rake::TestTask.new("acceptance") { |t|
|
21
|
+
t.pattern = FileList['test/acceptance/acc_*.rb']
|
22
|
+
t.verbose = false
|
23
|
+
t.warning = false
|
24
|
+
}
|
25
|
+
|
26
|
+
desc "Run benchmarks"
|
27
|
+
Rake::TestTask.new("benchmark") { |t|
|
28
|
+
t.pattern = FileList['test/benchmark/benchmark_*.rb']
|
29
|
+
t.verbose = false
|
30
|
+
t.warning = false
|
31
|
+
}
|
32
|
+
|
33
|
+
desc "Generate rdoc documentation"
|
34
|
+
Rake::RDocTask.new do |rd|
|
35
|
+
rd.main = 'README'
|
36
|
+
rd.rdoc_dir = "doc"
|
37
|
+
rd.rdoc_files.include('README', 'License', 'rcapture/**/*.rb')
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
|
3
|
+
# This array will only accept even numbers
|
4
|
+
x = []
|
5
|
+
x.extend(RCapture::Interceptable)
|
6
|
+
|
7
|
+
even_filter = Proc.new do |ci|
|
8
|
+
# Define the predicate that must evaluate to true
|
9
|
+
# in order to call the captured method
|
10
|
+
ci.predicate = (ci.args.first % 2 == 0)
|
11
|
+
|
12
|
+
# In case the predicate evaluates to false you
|
13
|
+
# can use the return property to control
|
14
|
+
# what is returned from the captured method instead
|
15
|
+
# Insertion to array returns the array itself:
|
16
|
+
ci.return = ci.sender
|
17
|
+
end
|
18
|
+
|
19
|
+
x.capture_pre :methods => [:<<, :push], &even_filter
|
20
|
+
|
21
|
+
x << 2 << 3 << 4 << 5 << 6
|
22
|
+
x.push(7).push(8)
|
23
|
+
p x
|
24
|
+
#=> [2,4,6,8]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
|
3
|
+
# Enrich Array class.
|
4
|
+
class Array
|
5
|
+
include RCapture::Interceptable
|
6
|
+
end
|
7
|
+
|
8
|
+
# Install a hook at insertion methods of Array.
|
9
|
+
# Calling capture methods at class level will capture
|
10
|
+
# insertions from all instances of Array.
|
11
|
+
Array.capture_pre :methods => [:<<, :push] do |cs|
|
12
|
+
puts "'#{cs.method}' will be called with #{cs.args.first}"
|
13
|
+
end
|
14
|
+
|
15
|
+
[] << 1 << 2
|
16
|
+
[].push :hello
|
17
|
+
|
18
|
+
# Installing a hook on instance level will
|
19
|
+
# affect only that single instance
|
20
|
+
a = []
|
21
|
+
a.capture_post :methods => :length do |cs|
|
22
|
+
puts "Length of 'a' is #{cs.return}"
|
23
|
+
end
|
24
|
+
|
25
|
+
a << 1
|
26
|
+
a.length
|
27
|
+
|
28
|
+
# Any other array will not be affected
|
29
|
+
[].length
|
30
|
+
|
31
|
+
#=> '<<' was called with 1!
|
32
|
+
#=> '<<' was called with 2!
|
33
|
+
#=> 'push' was called with hello!
|
34
|
+
#=> '<<' was called with 1!
|
35
|
+
#=> Length of 'a' is 1
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
include RCapture
|
3
|
+
|
4
|
+
# Closed polygon
|
5
|
+
class Polygon
|
6
|
+
include Interceptable
|
7
|
+
def vertices; self.edges; end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Triangle
|
11
|
+
class Triangle < Polygon
|
12
|
+
def edges; 3; end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Degenerate polygon
|
16
|
+
class Digon < Polygon
|
17
|
+
def edges; 1; end
|
18
|
+
def vertices; 2; end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Four sided polygon
|
22
|
+
class Quadliteral < Polygon
|
23
|
+
def edges; 4; end
|
24
|
+
def vertices; super; end
|
25
|
+
end
|
26
|
+
|
27
|
+
called = 0
|
28
|
+
Polygon.capture :methods => :vertices do
|
29
|
+
called += 1
|
30
|
+
end
|
31
|
+
|
32
|
+
Triangle.new.vertices # +1
|
33
|
+
Digon.new.vertices # -
|
34
|
+
Quadliteral.new.vertices # +1
|
35
|
+
|
36
|
+
p called #=> 2
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
|
3
|
+
# Enrich single instance
|
4
|
+
x = []
|
5
|
+
x.extend(RCapture::Interceptable)
|
6
|
+
|
7
|
+
# Define procs that will modify the input arguments
|
8
|
+
inc = Proc.new { |ci| ci.args[0] += 1 }
|
9
|
+
dec = Proc.new { |ci| ci.args[0] -= 1 }
|
10
|
+
mul = Proc.new { |ci| ci.args[0] *= 2 }
|
11
|
+
|
12
|
+
# Capture ':<<' multiple times.
|
13
|
+
x.capture_pre :methods => :<<, &inc
|
14
|
+
x.capture_pre :methods => :<<, &mul
|
15
|
+
x.capture_pre :methods => :<<, &inc
|
16
|
+
x.capture_pre :methods => :<<, &dec
|
17
|
+
x.capture_pre :methods => :<<, &dec
|
18
|
+
|
19
|
+
x << 2 << 4
|
20
|
+
p x
|
21
|
+
#=> [3,7]
|
22
|
+
|
23
|
+
# Similarily, you can modify return values
|
24
|
+
y = []
|
25
|
+
y.extend(RCapture::Interceptable)
|
26
|
+
|
27
|
+
inc = Proc.new { |ci| ci.return += 1 }
|
28
|
+
dec = Proc.new { |ci| ci.return -= 1 }
|
29
|
+
mul = Proc.new { |ci| ci.return *= 2 }
|
30
|
+
|
31
|
+
y.capture_post :methods => :[], &inc
|
32
|
+
y.capture_post :methods => :[], &mul
|
33
|
+
y.capture_post :methods => :[], &inc
|
34
|
+
y.capture_post :methods => :[], &dec
|
35
|
+
y.capture_post :methods => :[], &dec
|
36
|
+
|
37
|
+
y << 1 << 4
|
38
|
+
p y[0] #=> 3
|
39
|
+
p y[1] #=> 9
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
# Will keep track of Enumberable#inject results
|
5
|
+
inject_results = []
|
6
|
+
m = Mutex.new
|
7
|
+
|
8
|
+
module Enumerable
|
9
|
+
include RCapture::Interceptable
|
10
|
+
end
|
11
|
+
|
12
|
+
Enumerable.capture :methods => :inject do |cs|
|
13
|
+
# Callbacks can be invoked from multiple threads.
|
14
|
+
# So we need to synchronize access to shared resources.
|
15
|
+
m.synchronize do
|
16
|
+
inject_results << cs.return
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
array = []
|
21
|
+
10000.times {array << rand(100)}
|
22
|
+
|
23
|
+
threads = []
|
24
|
+
5.times do
|
25
|
+
t = Thread.new do
|
26
|
+
sum = array.inject(0) {|memo, e| memo + e}
|
27
|
+
end
|
28
|
+
threads << t
|
29
|
+
end
|
30
|
+
threads.each {|t| t.join }
|
31
|
+
|
32
|
+
p inject_results #=> [493311, 493311, 493311, 493311, 493311] or similar
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
include RCapture
|
3
|
+
|
4
|
+
class X
|
5
|
+
include Interceptable
|
6
|
+
def initialize(name); @name = name; end
|
7
|
+
def say_hello; puts "Hello, #{@name}!"; end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Y < X
|
11
|
+
def initialize; super("Y"); end
|
12
|
+
end
|
13
|
+
|
14
|
+
X.capture :class_methods => :new do |ci|
|
15
|
+
ci.block.call(ci.return) if ci.block
|
16
|
+
end
|
17
|
+
|
18
|
+
# Now you can use X and Y as if it supports
|
19
|
+
# blocks
|
20
|
+
x = X.new("Christoph") do |x|
|
21
|
+
x.say_hello #=> "Hello, Christoph!"
|
22
|
+
end
|
23
|
+
|
24
|
+
y = Y.new do |y|
|
25
|
+
y.say_hello #=> "Hello, Y!"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Or leaf the block argument away
|
29
|
+
x = X.new("Christoph")
|
30
|
+
y = Y.new
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rcapture'
|
2
|
+
|
3
|
+
class Fixnum
|
4
|
+
include RCapture::Interceptable
|
5
|
+
end
|
6
|
+
|
7
|
+
op_count = 0
|
8
|
+
Fixnum.capture :methods => :+ do |cs|
|
9
|
+
# Here we use Fixnum#+ inside the callback.
|
10
|
+
# RCapture takes special care that anything that
|
11
|
+
# happens inside a callback remains capture-free.
|
12
|
+
op_count = op_count + 1
|
13
|
+
end
|
14
|
+
|
15
|
+
x = 1 + 1 + 2 + 3
|
16
|
+
p op_count #=> 3
|
data/rcapture.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# (c) Christoph Heindl, 2010
|
3
|
+
# http://cheind.wordpress.com
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'rcapture/symbol'
|
7
|
+
require 'rcapture/capture_status'
|
8
|
+
require 'rcapture/captured_info'
|
9
|
+
require 'rcapture/singleton_class'
|
10
|
+
require 'rcapture/interceptable'
|
11
|
+
require 'rcapture/capture'
|
12
|
+
if RUBY_VERSION >= "1.9" # will fail when ruby reaches 1.10
|
13
|
+
require 'rcapture/capture_19x'
|
14
|
+
else
|
15
|
+
require 'rcapture/capture_18x'
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
data/rcapture/capture.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
|
2
|
+
module RCapture
|
3
|
+
|
4
|
+
# RCapture internal methods
|
5
|
+
module Internal # :nodoc:
|
6
|
+
|
7
|
+
# Perform a pre-capture on the given class.
|
8
|
+
def Internal.capture_pre(klass, args, &callback)
|
9
|
+
CaptureStatus.current.use(false) do
|
10
|
+
RCapture::Internal.capture(klass, { :type => :pre }.merge(args), &callback)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Perform a post-capture on the given class.
|
15
|
+
def Internal.capture_post(klass, args, &callback)
|
16
|
+
CaptureStatus.current.use(false) do
|
17
|
+
RCapture::Internal.capture(klass, { :type => :post }.merge(args), &callback)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Dispatches to internal methods by arguments provided.
|
22
|
+
def Internal.capture(klass, args, &callback)
|
23
|
+
args = { :methods => [], :class_methods => [] }.merge(args)
|
24
|
+
|
25
|
+
RCapture::Internal.capture_methods(
|
26
|
+
klass,
|
27
|
+
args[:methods].to_a,
|
28
|
+
args,
|
29
|
+
&callback
|
30
|
+
)
|
31
|
+
|
32
|
+
RCapture::Internal.capture_methods(
|
33
|
+
RCapture::Internal.singleton_class(klass),
|
34
|
+
args[:class_methods].to_a,
|
35
|
+
args,
|
36
|
+
&callback
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Iterate over each method and install capturing code
|
41
|
+
def Internal.capture_methods(klass, methods, additional_args, &callback)
|
42
|
+
type = additional_args[:type]
|
43
|
+
methods.each do |m|
|
44
|
+
RCapture::Internal.capture_single_method(klass, m.to_sym, type, additional_args, &callback)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Install capturing code at single method.
|
49
|
+
# Dispatches based on the type of capturing (pre/post)
|
50
|
+
def Internal.capture_single_method(klass, method, type, additional_args, &callback)
|
51
|
+
case type
|
52
|
+
when :post
|
53
|
+
RCapture::Internal.capture_single_post(klass, method, additional_args, &callback)
|
54
|
+
when :pre
|
55
|
+
RCapture::Internal.capture_single_pre(klass, method, additional_args, &callback)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
|
2
|
+
module RCapture
|
3
|
+
|
4
|
+
module Internal
|
5
|
+
|
6
|
+
# Pre-capture single method.
|
7
|
+
# This version is used on Ruby 1.8.x where define_method
|
8
|
+
# does not allow for block arguments. See http://tinyurl.com/yczowjx
|
9
|
+
def Internal.capture_single_pre(klass, method, additional_args, &callback)
|
10
|
+
prev = klass.instance_method(method)
|
11
|
+
id = prev.object_id
|
12
|
+
is_private = klass.private_instance_methods.include?(method.to_s)
|
13
|
+
|
14
|
+
klass.class_eval do
|
15
|
+
define_method "#{method}_#{id}_" do |b, *args|
|
16
|
+
status = CaptureStatus.current
|
17
|
+
call_prev = true; alt_ret = nil
|
18
|
+
if status.on?
|
19
|
+
status.use(false) do
|
20
|
+
ci = CapturedInfo.current
|
21
|
+
ci.fill(args, self, method, b)
|
22
|
+
callback.call(ci)
|
23
|
+
alt_ret = ci.return
|
24
|
+
call_prev = ci.predicate
|
25
|
+
end
|
26
|
+
end
|
27
|
+
if call_prev
|
28
|
+
prev.bind(self).call(*args, &b)
|
29
|
+
else
|
30
|
+
alt_ret
|
31
|
+
end
|
32
|
+
end
|
33
|
+
private "#{method}_#{id}_"
|
34
|
+
end
|
35
|
+
|
36
|
+
klass.class_eval <<-EOF
|
37
|
+
def #{method} *args, &block
|
38
|
+
self.send "#{method}_#{id}_", block, *args
|
39
|
+
end
|
40
|
+
private "#{method}" if #{is_private}
|
41
|
+
EOF
|
42
|
+
end
|
43
|
+
|
44
|
+
# Post-capture single method.
|
45
|
+
# This version is used on Ruby 1.8.x where define_method
|
46
|
+
# does not allow for block arguments. See http://tinyurl.com/yczowjx
|
47
|
+
def Internal.capture_single_post(klass, method, additional_args, &callback)
|
48
|
+
prev = klass.instance_method(method)
|
49
|
+
id = prev.object_id
|
50
|
+
is_private = klass.private_instance_methods.include?(method.to_s)
|
51
|
+
|
52
|
+
klass.class_eval do
|
53
|
+
define_method "#{method}_#{id}_" do |b, *args|
|
54
|
+
ret = prev.bind(self).call(*args, &b)
|
55
|
+
status = CaptureStatus.current
|
56
|
+
if status.on?
|
57
|
+
status.use(false) do
|
58
|
+
ci = CapturedInfo.current
|
59
|
+
ci.fill(args, self, method, b, ret)
|
60
|
+
callback.call(ci)
|
61
|
+
ret = ci.return
|
62
|
+
end
|
63
|
+
end
|
64
|
+
ret
|
65
|
+
end
|
66
|
+
private "#{method}_#{id}_"
|
67
|
+
end
|
68
|
+
|
69
|
+
klass.class_eval <<-EOF
|
70
|
+
def #{method} *args, &block
|
71
|
+
self.send "#{method}_#{id}_", block, *args
|
72
|
+
end
|
73
|
+
private "#{method}" if #{is_private}
|
74
|
+
EOF
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|