rgot 1.1.0 → 1.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +5 -5
- data/.gitignore +1 -1
- data/Gemfile +4 -0
- data/Gemfile.lock +73 -0
- data/README.md +72 -2
- data/Rakefile +3 -0
- data/Steepfile +4 -0
- data/bin/rgot +1 -1
- data/lib/rgot/b.rb +17 -14
- data/lib/rgot/benchmark_result.rb +6 -0
- data/lib/rgot/cli.rb +99 -72
- data/lib/rgot/common.rb +8 -4
- data/lib/rgot/example_parser.rb +6 -1
- data/lib/rgot/f.rb +230 -0
- data/lib/rgot/m.rb +121 -22
- data/lib/rgot/pb.rb +2 -2
- data/lib/rgot/t.rb +8 -4
- data/lib/rgot/version.rb +3 -1
- data/lib/rgot.rb +12 -8
- data/rbs_collection.lock.yaml +49 -0
- data/rbs_collection.yaml +53 -0
- data/rgot.gemspec +1 -1
- data/sig/patch.rbs +3 -0
- data/sig/rgot.rbs +232 -0
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2f0075689755b0a44db0674a5069769d1ee4c2567774d7bb7422ba5b27c8960
|
4
|
+
data.tar.gz: 9ddd64091ca0daeff256fcc5c305aad378a92472f4522b875512fb35915d74aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e24662ccc2f6f4644eec95f9764975b86d2ea40f49c3c8ae569f428809ca08df79e6517d2bbcf0ec7d08e610867972133a1a9ab5df245cf36896afb8ace7e63
|
7
|
+
data.tar.gz: ac5c041deb124812d911d0af697a06a398ced2c79bc54f6058ec4cb2499af9dd814007f3e0c8951c50448892ba0fbc0a6846dfd879c3497601b4aebf5d00116e
|
data/.github/workflows/main.yml
CHANGED
@@ -14,13 +14,13 @@ jobs:
|
|
14
14
|
strategy:
|
15
15
|
matrix:
|
16
16
|
ruby:
|
17
|
-
- '2.
|
18
|
-
- '
|
19
|
-
- '3.
|
20
|
-
- '3.
|
17
|
+
- '2.7.7'
|
18
|
+
- '3.0.5'
|
19
|
+
- '3.1.3'
|
20
|
+
- '3.2.0'
|
21
21
|
|
22
22
|
steps:
|
23
|
-
- uses: actions/checkout@
|
23
|
+
- uses: actions/checkout@v3
|
24
24
|
- name: Set up Ruby
|
25
25
|
uses: ruby/setup-ruby@v1
|
26
26
|
with:
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rgot (1.3.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activesupport (7.0.4)
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
+
i18n (>= 1.6, < 2)
|
12
|
+
minitest (>= 5.1)
|
13
|
+
tzinfo (~> 2.0)
|
14
|
+
ast (2.4.2)
|
15
|
+
concurrent-ruby (1.1.10)
|
16
|
+
csv (3.2.6)
|
17
|
+
debug (1.7.1)
|
18
|
+
ffi (1.15.5)
|
19
|
+
fileutils (1.7.0)
|
20
|
+
i18n (1.12.0)
|
21
|
+
concurrent-ruby (~> 1.0)
|
22
|
+
json (2.6.3)
|
23
|
+
language_server-protocol (3.17.0.2)
|
24
|
+
listen (3.7.1)
|
25
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
26
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
27
|
+
logger (1.5.3)
|
28
|
+
minitest (5.17.0)
|
29
|
+
parallel (1.22.1)
|
30
|
+
parser (3.2.0.0)
|
31
|
+
ast (~> 2.4.1)
|
32
|
+
rainbow (3.1.1)
|
33
|
+
rake (13.0.6)
|
34
|
+
rb-fsevent (0.11.2)
|
35
|
+
rb-inotify (0.10.1)
|
36
|
+
ffi (~> 1.0)
|
37
|
+
rbs (2.8.2)
|
38
|
+
securerandom (0.2.2)
|
39
|
+
steep (1.3.0)
|
40
|
+
activesupport (>= 5.1)
|
41
|
+
csv (>= 3.0.9)
|
42
|
+
fileutils (>= 1.1.0)
|
43
|
+
json (>= 2.1.0)
|
44
|
+
language_server-protocol (>= 3.15, < 4.0)
|
45
|
+
listen (~> 3.0)
|
46
|
+
logger (>= 1.3.0)
|
47
|
+
parallel (>= 1.0.0)
|
48
|
+
parser (>= 3.1)
|
49
|
+
rainbow (>= 2.2.2, < 4.0)
|
50
|
+
rbs (>= 2.8.0)
|
51
|
+
securerandom (>= 0.1)
|
52
|
+
strscan (>= 1.0.0)
|
53
|
+
terminal-table (>= 2, < 4)
|
54
|
+
strscan (3.0.5)
|
55
|
+
terminal-table (3.0.2)
|
56
|
+
unicode-display_width (>= 1.1.1, < 3)
|
57
|
+
tzinfo (2.0.5)
|
58
|
+
concurrent-ruby (~> 1.0)
|
59
|
+
unicode-display_width (2.4.2)
|
60
|
+
|
61
|
+
PLATFORMS
|
62
|
+
ruby
|
63
|
+
|
64
|
+
DEPENDENCIES
|
65
|
+
bundler
|
66
|
+
debug
|
67
|
+
rake
|
68
|
+
rbs
|
69
|
+
rgot!
|
70
|
+
steep
|
71
|
+
|
72
|
+
BUNDLED WITH
|
73
|
+
2.4.2
|
data/README.md
CHANGED
@@ -93,6 +93,33 @@ ok FooTest 2.782s
|
|
93
93
|
|
94
94
|
`b.n` is automatically adjusted.
|
95
95
|
|
96
|
+
## Fuzzing
|
97
|
+
|
98
|
+
```
|
99
|
+
$ rgot target_file_test.rb --fuzz . --fuzztime 1
|
100
|
+
```
|
101
|
+
|
102
|
+
Fuzzing tests are also supported.
|
103
|
+
Please refer to the gloang documentation for details.
|
104
|
+
|
105
|
+
https://go.dev/security/fuzz/
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
module FooTest
|
109
|
+
# To enable fuzzing, the method name
|
110
|
+
# should be prefixed with `fuzz`.
|
111
|
+
def fuzz_any_func(f)
|
112
|
+
f.add(5, "hello")
|
113
|
+
f.fuzz do |t, i, s|
|
114
|
+
out, err = foo(i, s)
|
115
|
+
if err != nil && out != ""
|
116
|
+
t.errorf("%s, %s", out, err)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
96
123
|
## Example
|
97
124
|
|
98
125
|
Rgot's example feature is the best and if you want to write the sample code of your library.
|
@@ -189,6 +216,8 @@ Method name should be set `test_*` for testing.
|
|
189
216
|
|
190
217
|
And benchmark method should be set `benchmark_*`.
|
191
218
|
|
219
|
+
And fuzz method should be set `fuzz_*`.
|
220
|
+
|
192
221
|
And example method should be set `example_*`.
|
193
222
|
|
194
223
|
```ruby
|
@@ -199,6 +228,9 @@ module XxxTest
|
|
199
228
|
def benchmark_any_name(b)
|
200
229
|
end
|
201
230
|
|
231
|
+
def fuzz_any_name(f)
|
232
|
+
end
|
233
|
+
|
202
234
|
def example_any_name
|
203
235
|
end
|
204
236
|
end
|
@@ -218,6 +250,8 @@ Usage: rgot [options]
|
|
218
250
|
--thread [count,...] set thread counts of comma split
|
219
251
|
--require [path] load some code before running
|
220
252
|
--load-path [path] Specify $LOAD_PATH directory
|
253
|
+
--fuzz [regexp] run the fuzz test matching `regexp`
|
254
|
+
--fuzztime [sec] time to spend fuzzing; default is to run indefinitely
|
221
255
|
```
|
222
256
|
|
223
257
|
## Basic
|
@@ -341,7 +375,7 @@ And this is default virtual main code.
|
|
341
375
|
```ruby
|
342
376
|
module TestSomeCode
|
343
377
|
def test_main(m)
|
344
|
-
|
378
|
+
m.run
|
345
379
|
end
|
346
380
|
end
|
347
381
|
```
|
@@ -362,7 +396,7 @@ module TestSomeCode
|
|
362
396
|
the_before_running_some_code
|
363
397
|
code = m.run
|
364
398
|
the_after_running_some_code
|
365
|
-
|
399
|
+
code
|
366
400
|
end
|
367
401
|
end
|
368
402
|
```
|
@@ -515,3 +549,39 @@ def benchmark_foo(b)
|
|
515
549
|
end
|
516
550
|
end
|
517
551
|
```
|
552
|
+
|
553
|
+
## Rgot::F (Fuzzing)
|
554
|
+
|
555
|
+
### Rgot::F#add
|
556
|
+
|
557
|
+
Set the sample value with `#add`. This value is also used as a test. It guesses the type from the value and generates a random value.
|
558
|
+
|
559
|
+
### Rgot::F#fuzz
|
560
|
+
|
561
|
+
Generate the random value generated by `#fuzz` and execute the code.
|
562
|
+
The `t` becomes an instance of `Rgot::T` and the test can be run as usual.
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
def fuzz_foo(f)
|
566
|
+
f.add(100, "hello")
|
567
|
+
f.fuzz do |t, i, s|
|
568
|
+
i #=> 100, 84, 17, 9, 66, ...
|
569
|
+
s #=> "hello", "Y\xD5\xAB\xBA\x8E", "r\x95D\xA5\xF7", "\xCEj=\x9C\xBD", ...
|
570
|
+
if !foo(i, s)
|
571
|
+
t.error("fail with i=#{i}, s=#{s}")
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
```
|
576
|
+
|
577
|
+
# TODO
|
578
|
+
|
579
|
+
- [ ] Support to save and load fuzzing data
|
580
|
+
|
581
|
+
## v2
|
582
|
+
|
583
|
+
- [ ] Support sub testing
|
584
|
+
- [ ] Fix duration argument unit
|
585
|
+
- [ ] Refactoring
|
586
|
+
- [ ] Fix M#initialize argument
|
587
|
+
- [ ] Fix internal class API
|
data/Rakefile
CHANGED
data/Steepfile
ADDED
data/bin/rgot
CHANGED
data/lib/rgot/b.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rgot
|
2
4
|
class B < Common
|
3
|
-
Options = Struct.new(
|
5
|
+
Options = _ = Struct.new(
|
4
6
|
:procs,
|
5
7
|
:threads,
|
6
8
|
:benchtime,
|
7
9
|
)
|
8
10
|
|
11
|
+
# @dynamic n, n=
|
9
12
|
attr_accessor :n
|
13
|
+
|
10
14
|
def initialize(benchmark_module, name, opts = Options.new)
|
11
15
|
super()
|
12
16
|
@n = 1
|
@@ -14,7 +18,7 @@ module Rgot
|
|
14
18
|
@name = name
|
15
19
|
@opts = opts
|
16
20
|
@timer_on = false
|
17
|
-
@duration = 0
|
21
|
+
@duration = 0.0
|
18
22
|
@module.extend @module if @module
|
19
23
|
end
|
20
24
|
|
@@ -36,34 +40,34 @@ module Rgot
|
|
36
40
|
if @timer_on
|
37
41
|
@start = Rgot.now
|
38
42
|
end
|
39
|
-
@duration = 0
|
43
|
+
@duration = 0.0
|
40
44
|
end
|
41
45
|
|
42
46
|
def run(&block)
|
43
47
|
n = 1
|
44
48
|
benchtime = (@opts.benchtime || 1).to_f
|
45
49
|
catch(:skip) do
|
46
|
-
run_n(n
|
47
|
-
while !failed? && @duration < benchtime &&
|
50
|
+
run_n(n, block)
|
51
|
+
while !failed? && @duration < benchtime && n < 1e9
|
48
52
|
if @duration < (benchtime / 100.0)
|
49
|
-
|
53
|
+
n *= 100
|
50
54
|
elsif @duration < (benchtime / 10.0)
|
51
|
-
|
55
|
+
n *= 10
|
52
56
|
elsif @duration < (benchtime / 5.0)
|
53
|
-
|
57
|
+
n *= 5
|
54
58
|
elsif @duration < (benchtime / 2.0)
|
55
|
-
|
59
|
+
n *= 2
|
56
60
|
else
|
57
|
-
if
|
61
|
+
if n == 1
|
58
62
|
break
|
59
63
|
end
|
60
|
-
|
64
|
+
n = [(n * 1.2).to_i, n + 1].max || raise
|
61
65
|
end
|
62
|
-
run_n(
|
66
|
+
run_n(n, block)
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
66
|
-
BenchmarkResult.new(n:
|
70
|
+
BenchmarkResult.new(n: n, t: @duration)
|
67
71
|
end
|
68
72
|
|
69
73
|
def run_parallel
|
@@ -81,7 +85,6 @@ module Rgot
|
|
81
85
|
}.each(&:join)
|
82
86
|
end
|
83
87
|
end
|
84
|
-
@n *= procs * threads
|
85
88
|
Process.waitall
|
86
89
|
end
|
87
90
|
|
data/lib/rgot/cli.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
|
3
5
|
require_relative '../rgot'
|
@@ -11,7 +13,7 @@ module Rgot
|
|
11
13
|
def run
|
12
14
|
opts = Rgot::M::Options.new
|
13
15
|
parse_option(opts)
|
14
|
-
|
16
|
+
main_process(opts)
|
15
17
|
end
|
16
18
|
|
17
19
|
private
|
@@ -50,6 +52,15 @@ module Rgot
|
|
50
52
|
o.on '--load-path [path]', "Specify $LOAD_PATH directory" do |arg|
|
51
53
|
$LOAD_PATH.unshift(arg)
|
52
54
|
end
|
55
|
+
o.on '--fuzz [regexp]', "run the fuzz test matching `regexp`" do |arg|
|
56
|
+
unless arg
|
57
|
+
raise Rgot::OptionError, "missing argument for flag --fuzz"
|
58
|
+
end
|
59
|
+
opts.fuzz = arg
|
60
|
+
end
|
61
|
+
o.on '--fuzztime [sec]', "time to spend fuzzing; default is to run indefinitely" do |arg|
|
62
|
+
opts.fuzztime = arg
|
63
|
+
end
|
53
64
|
end
|
54
65
|
parser.parse!(@argv)
|
55
66
|
|
@@ -74,81 +85,97 @@ module Rgot
|
|
74
85
|
end
|
75
86
|
end
|
76
87
|
|
77
|
-
def
|
88
|
+
def main_process(opts)
|
78
89
|
code = 0
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
duration = Rgot.now
|
114
|
-
at_exit do
|
115
|
-
template = "%s\t%s\t%.3fs"
|
116
|
-
|
117
|
-
case $!
|
118
|
-
when SystemExit
|
119
|
-
if $!.success?
|
120
|
-
# exit 0
|
121
|
-
puts sprintf(template, "ok ", test_module, Rgot.now - duration)
|
122
|
-
else
|
123
|
-
# exit 1
|
124
|
-
puts "exit status #{$!.status}"
|
125
|
-
puts sprintf(template, "FAIL", test_module, Rgot.now - duration)
|
126
|
-
end
|
127
|
-
when NilClass
|
128
|
-
# not raise, not exit
|
129
|
-
else
|
130
|
-
# any exception
|
131
|
-
puts sprintf(template, "FAIL", test_module, Rgot.now - duration)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
m = Rgot::M.new(tests: tests, benchmarks: benchmarks, examples: examples, opts: opts)
|
135
|
-
if main
|
136
|
-
main.module.extend main.module
|
137
|
-
main.module.instance_method(main.name).bind(main.module).call(m)
|
138
|
-
else
|
139
|
-
exit m.run
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
ensure
|
144
|
-
_, status = Process.waitpid2(pid)
|
145
|
-
unless status.success?
|
146
|
-
code = 1
|
147
|
-
end
|
90
|
+
|
91
|
+
testing_files.each do |testing_file|
|
92
|
+
result = child_process(opts, testing_file)
|
93
|
+
unless result == 0
|
94
|
+
code = 1
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
code
|
99
|
+
end
|
100
|
+
|
101
|
+
def child_process(opts, testing_file)
|
102
|
+
node = RubyVM::AbstractSyntaxTree.parse_file(testing_file).children[2]
|
103
|
+
test_module_name = find_toplevel_name(node)
|
104
|
+
|
105
|
+
if opts.fuzz
|
106
|
+
# fuzzing observes changes in coverage.
|
107
|
+
require 'coverage'
|
108
|
+
Coverage.start(oneshot_lines: true)
|
109
|
+
end
|
110
|
+
load testing_file
|
111
|
+
|
112
|
+
test_module = Object.const_get(test_module_name)
|
113
|
+
tests = []
|
114
|
+
benchmarks = []
|
115
|
+
examples = []
|
116
|
+
fuzz_targets = []
|
117
|
+
main = nil
|
118
|
+
methods = test_module.public_instance_methods
|
119
|
+
methods.grep(/\Atest_/).each do |m|
|
120
|
+
if m == :test_main && main.nil?
|
121
|
+
main = Rgot::InternalTest.new(test_module, m)
|
122
|
+
else
|
123
|
+
tests << Rgot::InternalTest.new(test_module, m)
|
148
124
|
end
|
149
125
|
end
|
150
126
|
|
151
|
-
|
127
|
+
methods.grep(/\Abenchmark_/).each do |m|
|
128
|
+
benchmarks << Rgot::InternalBenchmark.new(test_module, m)
|
129
|
+
end
|
130
|
+
|
131
|
+
methods.grep(/\Aexample_?/).each do |m|
|
132
|
+
examples << Rgot::InternalExample.new(test_module, m)
|
133
|
+
end
|
134
|
+
|
135
|
+
methods.grep(/\Afuzz_/).each do |m|
|
136
|
+
fuzz_targets << Rgot::InternalFuzzTarget.new(test_module, m)
|
137
|
+
end
|
138
|
+
|
139
|
+
m = Rgot::M.new(
|
140
|
+
test_module: test_module,
|
141
|
+
tests: tests,
|
142
|
+
benchmarks: benchmarks,
|
143
|
+
examples: examples,
|
144
|
+
fuzz_targets: fuzz_targets,
|
145
|
+
opts: opts
|
146
|
+
)
|
147
|
+
if main
|
148
|
+
main.module.extend main.module
|
149
|
+
main.module.instance_method(:test_main).bind(main.module).call(m)
|
150
|
+
else
|
151
|
+
m.run
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def find_toplevel_name(node)
|
156
|
+
case node.type
|
157
|
+
when :MODULE
|
158
|
+
find_toplevel_name(node.children.first)
|
159
|
+
when :CONST, :COLON3
|
160
|
+
node.children.first
|
161
|
+
when :COLON2
|
162
|
+
case node.children
|
163
|
+
in [nil, sym]
|
164
|
+
# module Foo
|
165
|
+
sym
|
166
|
+
in [namespace, sym]
|
167
|
+
# module Foo::Bar
|
168
|
+
find_toplevel_name(namespace)
|
169
|
+
end
|
170
|
+
when :BLOCK
|
171
|
+
module_node = node.children.find { |c| c.type == :MODULE }
|
172
|
+
unless module_node
|
173
|
+
raise "no module found"
|
174
|
+
end
|
175
|
+
find_toplevel_name(module_node)
|
176
|
+
else
|
177
|
+
raise node.type.to_s
|
178
|
+
end
|
152
179
|
end
|
153
180
|
end
|
154
181
|
end
|
data/lib/rgot/common.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'thread'
|
2
4
|
require 'pathname'
|
3
5
|
|
4
6
|
module Rgot
|
5
7
|
class Common
|
8
|
+
# @dynamic output, output=
|
6
9
|
attr_accessor :output
|
7
10
|
|
8
11
|
def initialize
|
9
|
-
@output = ""
|
12
|
+
@output = "".dup
|
10
13
|
@failed = false
|
11
14
|
@skipped = false
|
12
15
|
@finished = false
|
13
16
|
@start = Rgot.now
|
14
|
-
@mutex = Mutex.new
|
17
|
+
@mutex = Thread::Mutex.new
|
15
18
|
end
|
16
19
|
|
17
20
|
def failed?
|
@@ -98,9 +101,10 @@ module Rgot
|
|
98
101
|
# internal_log -> synchronize -> internal_log -> other log -> running method
|
99
102
|
c = caller[4]
|
100
103
|
path = c.sub(/:.*/, '')
|
101
|
-
line = c.match(/:(\d+?):/)[1
|
104
|
+
line = c.match(/:(\d+?):/)&.[](1)
|
102
105
|
relative_path = Pathname.new(path).relative_path_from(Pathname.new(Dir.pwd)).to_s
|
103
|
-
|
106
|
+
# Every line is indented at least 4 spaces.
|
107
|
+
" #{relative_path}:#{line}: #{str}\n"
|
104
108
|
end
|
105
109
|
|
106
110
|
def internal_log(msg)
|
data/lib/rgot/example_parser.rb
CHANGED
@@ -1,14 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'ripper'
|
2
4
|
|
3
5
|
module Rgot
|
4
6
|
class ExampleParser < Ripper
|
7
|
+
|
8
|
+
# @dynamic examples, examples=
|
5
9
|
attr_accessor :examples
|
10
|
+
|
6
11
|
def initialize(code)
|
7
12
|
super
|
8
13
|
@examples = []
|
9
14
|
@in_def = false
|
10
15
|
@has_output = false
|
11
|
-
@output = ""
|
16
|
+
@output = "".dup
|
12
17
|
end
|
13
18
|
|
14
19
|
def on_def(method, args, body)
|