mrubyc-test 0.1.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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/mrubyc-test +6 -0
- data/lib/mrubyc-ext/hash.rb +10 -0
- data/lib/mrubyc-ext/mock.rb +8 -0
- data/lib/mrubyc-ext/mrubyc_test_case.rb +72 -0
- data/lib/mrubyc-ext/object.rb +54 -0
- data/lib/mrubyc-test.rb +81 -0
- data/lib/mrubyc/test/config.rb +29 -0
- data/lib/mrubyc/test/generator/attribute.rb +49 -0
- data/lib/mrubyc/test/generator/double.rb +34 -0
- data/lib/mrubyc/test/generator/script.rb +21 -0
- data/lib/mrubyc/test/generator/test_case.rb +58 -0
- data/lib/mrubyc/test/init.rb +56 -0
- data/lib/mrubyc/test/version.rb +5 -0
- data/lib/mrubyc_test_case/mrubyc_test_case.rb +79 -0
- data/lib/templates/main.c.erb +39 -0
- data/lib/templates/sample_test.rb.erb +55 -0
- data/lib/templates/test.rb.erb +111 -0
- data/mrubyc-test.gemspec +33 -0
- metadata +158 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class Object
|
|
2
|
+
def to_ss
|
|
3
|
+
if self.class == NilClass
|
|
4
|
+
'nil [NilClass]'
|
|
5
|
+
elsif self == ''
|
|
6
|
+
'[NULL String]'
|
|
7
|
+
else
|
|
8
|
+
self.to_s + ' [' + self.class_name + ']'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
def class_name
|
|
12
|
+
case self.class
|
|
13
|
+
when String
|
|
14
|
+
'String'
|
|
15
|
+
when Array
|
|
16
|
+
'Array'
|
|
17
|
+
when FalseClass
|
|
18
|
+
'FalseClass'
|
|
19
|
+
when Fixnum
|
|
20
|
+
'Fixnum'
|
|
21
|
+
when Float
|
|
22
|
+
'Float'
|
|
23
|
+
when Hash
|
|
24
|
+
'Hash'
|
|
25
|
+
when Math
|
|
26
|
+
'Math'
|
|
27
|
+
when Mutex
|
|
28
|
+
'Mutex'
|
|
29
|
+
when Numeric
|
|
30
|
+
'Numeric'
|
|
31
|
+
when Object
|
|
32
|
+
case self
|
|
33
|
+
when false
|
|
34
|
+
'FalseClass'
|
|
35
|
+
when true
|
|
36
|
+
'TrueClass'
|
|
37
|
+
else
|
|
38
|
+
'Object'
|
|
39
|
+
end
|
|
40
|
+
when Proc
|
|
41
|
+
'Proc'
|
|
42
|
+
when Range
|
|
43
|
+
'Range'
|
|
44
|
+
when String
|
|
45
|
+
'String'
|
|
46
|
+
when Symbol
|
|
47
|
+
'Symbol'
|
|
48
|
+
when VM
|
|
49
|
+
'VM'
|
|
50
|
+
else
|
|
51
|
+
'[User Defined Class]'
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/mrubyc-test.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mrubyc_test_case/mrubyc_test_case"
|
|
4
|
+
require "mrubyc/test/version"
|
|
5
|
+
require "mrubyc/test/config"
|
|
6
|
+
require "mrubyc/test/init"
|
|
7
|
+
require "mrubyc/test/generator/attribute"
|
|
8
|
+
require "mrubyc/test/generator/test_case"
|
|
9
|
+
require "mrubyc/test/generator/script"
|
|
10
|
+
require "mrubyc/test/generator/double"
|
|
11
|
+
require "thor"
|
|
12
|
+
|
|
13
|
+
module Mrubyc::Test
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
class Tool < Thor
|
|
17
|
+
desc 'init', 'Initialize requirements for unit test of mruby/c. Some directories and files will be created. Note that existing objects may be overridden'
|
|
18
|
+
def init
|
|
19
|
+
Mrubyc::Test::Init.run
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'prepare', 'Create a ruby script that has all the requirements of your test'
|
|
23
|
+
def prepare
|
|
24
|
+
config = Mrubyc::Test::Config.read
|
|
25
|
+
model_files = Dir.glob(File.join(Dir.pwd, config['mruby_lib_dir'], 'models', '*.rb'))
|
|
26
|
+
test_path = File.join(Dir.pwd, config['test_dir'], '*.rb')
|
|
27
|
+
test_files = Dir.glob(test_path)
|
|
28
|
+
if test_files.size == 0
|
|
29
|
+
puts 'Test not found'
|
|
30
|
+
puts 'search path: ' + test_path
|
|
31
|
+
exit(1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# gather attributes from your implementations and tests
|
|
35
|
+
attributes = Mrubyc::Test::Generator::Attribute.run(model_files: model_files, test_files: test_files)
|
|
36
|
+
|
|
37
|
+
# convert attributes into tast_cases
|
|
38
|
+
test_cases = Mrubyc::Test::Generator::TestCase.run(attributes)
|
|
39
|
+
|
|
40
|
+
# generate a ruby script that will be compiled by mrbc and executed in mruby/c VM
|
|
41
|
+
Mrubyc::Test::Generator::Script.run(model_files: model_files, test_files: test_files, test_cases: test_cases)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc 'make', 'compile test script into executable && run it'
|
|
45
|
+
def make
|
|
46
|
+
config = Mrubyc::Test::Config.read
|
|
47
|
+
tmp_dir = File.join(Dir.pwd, config['test_tmp_dir'])
|
|
48
|
+
puts "cd #{tmp_dir}"
|
|
49
|
+
puts
|
|
50
|
+
exit_code = 0
|
|
51
|
+
pwd = Dir.pwd
|
|
52
|
+
FileUtils.mv "#{pwd}/#{config['mrubyc_src_dir']}/hal", "#{pwd}/#{config['mrubyc_src_dir']}/~hal"
|
|
53
|
+
begin
|
|
54
|
+
FileUtils.ln_s "#{pwd}/#{config['test_tmp_dir']}/hal", "#{pwd}/#{config['mrubyc_src_dir']}/hal"
|
|
55
|
+
Dir.chdir(tmp_dir) do
|
|
56
|
+
['~/.rbenv/versions/mruby-1.4.1/bin/mrbc -E -B test test.rb',
|
|
57
|
+
"cc -I #{pwd}/#{config['mrubyc_src_dir']} -DMRBC_DEBUG -o test main.c #{pwd}/#{config['mrubyc_src_dir']}/*.c #{pwd}/#{config['mrubyc_src_dir']}/hal/*.c",
|
|
58
|
+
'./test'].each do |cmd|
|
|
59
|
+
puts cmd
|
|
60
|
+
puts
|
|
61
|
+
exit_code = system(cmd) ? 0 : 1
|
|
62
|
+
end
|
|
63
|
+
puts
|
|
64
|
+
puts "cd -"
|
|
65
|
+
puts
|
|
66
|
+
end
|
|
67
|
+
ensure
|
|
68
|
+
FileUtils.rm "#{pwd}/#{config['mrubyc_src_dir']}/hal"
|
|
69
|
+
FileUtils.mv "#{pwd}/#{config['mrubyc_src_dir']}/~hal", "#{pwd}/#{config['mrubyc_src_dir']}/hal"
|
|
70
|
+
end
|
|
71
|
+
exit(exit_code)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc 'test', 'shortcut for `prepare` && `make`'
|
|
75
|
+
def test
|
|
76
|
+
prepare
|
|
77
|
+
make
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Mrubyc
|
|
7
|
+
module Test
|
|
8
|
+
class Config
|
|
9
|
+
class << self
|
|
10
|
+
def read(check: true)
|
|
11
|
+
FileUtils.touch('.mrubycconfig')
|
|
12
|
+
config = YAML.load_file('.mrubycconfig')
|
|
13
|
+
if check
|
|
14
|
+
if !config || config == [] || !config['test_dir']
|
|
15
|
+
raise 'Check if `.mrubycconfig` exists.'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
config || {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(config)
|
|
22
|
+
File.open('.mrubycconfig', 'r+') do |file|
|
|
23
|
+
file.write(config.to_yaml)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
|
4
|
+
|
|
5
|
+
module Mrubyc
|
|
6
|
+
module Test
|
|
7
|
+
module Generator
|
|
8
|
+
class Attribute
|
|
9
|
+
class << self
|
|
10
|
+
def run(model_files: [], test_files:)
|
|
11
|
+
# get information from model files(application code)
|
|
12
|
+
model_files.each do |model_file|
|
|
13
|
+
load model_file
|
|
14
|
+
model_class = Module.const_get(File.basename(model_file, '.rb').camelize)
|
|
15
|
+
model_class.class_eval do
|
|
16
|
+
def method_missing(_method_name, *_args)
|
|
17
|
+
# do nothing
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
# get information from test files
|
|
22
|
+
method_locations = {}
|
|
23
|
+
description_locations = {}
|
|
24
|
+
double_method_locations = {}
|
|
25
|
+
test_files.each do |test_file|
|
|
26
|
+
load test_file
|
|
27
|
+
test_class = Module.const_get(File.basename(test_file, '.rb').camelize)
|
|
28
|
+
method_locations.merge!(test_class.class_variable_get(:@@method_locations))
|
|
29
|
+
description_locations.merge!(test_class.class_variable_get(:@@description_locations))
|
|
30
|
+
my_test = test_class.new
|
|
31
|
+
double_method_locations[test_class] = []
|
|
32
|
+
test_class.class_variable_get(:@@added_method_names)[test_class].each do |method_name, _v|
|
|
33
|
+
Mrubyc::Test::Generator::Double.init_double_method_locations
|
|
34
|
+
begin
|
|
35
|
+
my_test.send(method_name)
|
|
36
|
+
rescue NoMethodError => e
|
|
37
|
+
end
|
|
38
|
+
double_method_locations[test_class] << { method_name => Mrubyc::Test::Generator::Double.class_variable_get(:@@double_method_locations) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
{ method_locations: method_locations,
|
|
42
|
+
description_locations: description_locations,
|
|
43
|
+
double_method_locations: double_method_locations }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mrubyc
|
|
4
|
+
module Test
|
|
5
|
+
module Generator
|
|
6
|
+
class Double
|
|
7
|
+
class << self
|
|
8
|
+
def init_double_method_locations
|
|
9
|
+
@@double_method_locations = []
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(type, object, location)
|
|
14
|
+
@type = type
|
|
15
|
+
@klass = object.class
|
|
16
|
+
@location = location
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def method_missing(method_name, *args)
|
|
20
|
+
@@double_method_locations << {
|
|
21
|
+
type: @type,
|
|
22
|
+
class: @klass,
|
|
23
|
+
method_name: method_name,
|
|
24
|
+
args: args.to_s,
|
|
25
|
+
block: (block_given? ? yield : nil),
|
|
26
|
+
label: @location.label,
|
|
27
|
+
path: @location.absolute_path || @location.path,
|
|
28
|
+
line: @location.lineno
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rufo'
|
|
4
|
+
require 'erb'
|
|
5
|
+
|
|
6
|
+
module Mrubyc
|
|
7
|
+
module Test
|
|
8
|
+
module Generator
|
|
9
|
+
class Script
|
|
10
|
+
class << self
|
|
11
|
+
def run(model_files: [], test_files:, test_cases:)
|
|
12
|
+
config = Mrubyc::Test::Config.read
|
|
13
|
+
erb = ERB.new(File.read(File.expand_path('../../../../templates/test.rb.erb', __FILE__)), nil, '-')
|
|
14
|
+
mrubyc_class_dir = File.expand_path('../../../../mrubyc-ext/', __FILE__)
|
|
15
|
+
File.write(File.join(config['test_tmp_dir'], 'test.rb'), Rufo.format(erb.result(binding)))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mrubyc
|
|
4
|
+
module Test
|
|
5
|
+
module Generator
|
|
6
|
+
class TestCase
|
|
7
|
+
class << self
|
|
8
|
+
def run(attributes)
|
|
9
|
+
test_cases = []
|
|
10
|
+
attributes[:method_locations].each do |klass, methods|
|
|
11
|
+
methods.each do |method|
|
|
12
|
+
if attributes[:description_locations].size > 0
|
|
13
|
+
found_desc = attributes[:description_locations][klass].find do |hash|
|
|
14
|
+
hash[:line] == method[:line] -1
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
description = found_desc ? found_desc[:text] : ''
|
|
18
|
+
information = {
|
|
19
|
+
test_class_name: klass.to_s,
|
|
20
|
+
method_name: method[:method_name],
|
|
21
|
+
path: method[:path],
|
|
22
|
+
line: method[:line],
|
|
23
|
+
description: description
|
|
24
|
+
}
|
|
25
|
+
stubs = []
|
|
26
|
+
mocks = []
|
|
27
|
+
attributes[:double_method_locations][klass].each do |hash|
|
|
28
|
+
if hash.keys[0] == method[:method_name]
|
|
29
|
+
hash[method[:method_name]].each do |double|
|
|
30
|
+
stub_or_mock = { class_name: double[:class].to_s,
|
|
31
|
+
instance_variables: nil, # TODO
|
|
32
|
+
method_name: double[:method_name].to_s,
|
|
33
|
+
args: double[:args],
|
|
34
|
+
return_value: double[:block],
|
|
35
|
+
line: double[:line]
|
|
36
|
+
}
|
|
37
|
+
if double[:type] == :stub
|
|
38
|
+
stubs << stub_or_mock
|
|
39
|
+
else
|
|
40
|
+
mocks << stub_or_mock
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
test_cases << {
|
|
46
|
+
information: information,
|
|
47
|
+
stubs: stubs,
|
|
48
|
+
mocks: mocks
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
test_cases
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Mrubyc
|
|
7
|
+
module Test
|
|
8
|
+
class Init
|
|
9
|
+
class << self
|
|
10
|
+
def run
|
|
11
|
+
puts 'Initializing...'
|
|
12
|
+
puts
|
|
13
|
+
|
|
14
|
+
puts ' touch .mrubycconfig'
|
|
15
|
+
config = Mrubyc::Test::Config.read(check: false)
|
|
16
|
+
config['test_dir'] = 'test'
|
|
17
|
+
config['test_tmp_dir'] = 'test/tmp'
|
|
18
|
+
config['mruby_lib_dir'] = 'mrblib'
|
|
19
|
+
puts ' add config to .mrubycconfig'
|
|
20
|
+
Mrubyc::Test::Config.write(config)
|
|
21
|
+
|
|
22
|
+
hal_dir = "#{config['test_tmp_dir']}/hal"
|
|
23
|
+
puts " mikdir -p #{hal_dir}"
|
|
24
|
+
FileUtils.mkdir_p(hal_dir)
|
|
25
|
+
|
|
26
|
+
Dir.chdir(hal_dir) do
|
|
27
|
+
puts " download from https://raw.githubusercontent.com/mrubyc/mrubyc/master/src/hal_posix/hal.h"
|
|
28
|
+
system 'wget https://raw.githubusercontent.com/mrubyc/mrubyc/master/src/hal_posix/hal.h'
|
|
29
|
+
puts " download from https://raw.githubusercontent.com/mrubyc/mrubyc/master/src/hal_posix/hal.c"
|
|
30
|
+
system 'wget https://raw.githubusercontent.com/mrubyc/mrubyc/master/src/hal_posix/hal.c'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
puts " mikdir -p #{config['mruby_lib_dir']}/models"
|
|
35
|
+
FileUtils.mkdir_p('test/tmp')
|
|
36
|
+
|
|
37
|
+
puts ' cp test/sample_test.rb'
|
|
38
|
+
erb = ERB.new(File.read(File.expand_path('../../../templates/sample_test.rb.erb', __FILE__)), nil, '-')
|
|
39
|
+
File.write(File.join(config['test_dir'], 'sample_test.rb'), erb.result(binding))
|
|
40
|
+
|
|
41
|
+
puts ' cp test/tmp/main.c'
|
|
42
|
+
erb = ERB.new(File.read(File.expand_path('../../../templates/main.c.erb', __FILE__)), nil, '-')
|
|
43
|
+
File.write(File.join(config['test_tmp_dir'], 'main.c'), erb.result(binding))
|
|
44
|
+
|
|
45
|
+
puts
|
|
46
|
+
puts "\e[32mWelcome to mrubyc-test, the world\'s first TDD tool for mruby/c microcontroller development.\e[37m"
|
|
47
|
+
puts "\e[33m"
|
|
48
|
+
puts 'Caution:'
|
|
49
|
+
puts 'For the time being, mrubyc-test assumes you installed mruby-1.4.1 by rbenv. So you should have `~/.rbenv/versions/mruby-1.4.1/bin.mrbc`'
|
|
50
|
+
puts 'Sorry for the inconvenience. It will be fixed soon!'
|
|
51
|
+
puts "\e[37m"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mrubyc/test/generator/double'
|
|
4
|
+
|
|
5
|
+
class MrubycTestCase
|
|
6
|
+
class << self
|
|
7
|
+
@@description_locations = {}
|
|
8
|
+
def description(text)
|
|
9
|
+
location = caller_locations(1, 1)[0]
|
|
10
|
+
path = location.absolute_path || location.path
|
|
11
|
+
line = location.lineno
|
|
12
|
+
location = {
|
|
13
|
+
text: text,
|
|
14
|
+
path: File.expand_path(path),
|
|
15
|
+
line: line,
|
|
16
|
+
}
|
|
17
|
+
add_description_location(location)
|
|
18
|
+
end
|
|
19
|
+
alias :desc :description
|
|
20
|
+
|
|
21
|
+
@@added_method_names = {}
|
|
22
|
+
@@method_locations = {}
|
|
23
|
+
def method_added(name)
|
|
24
|
+
return false if %i(method_missing setup teardown).include?(name)
|
|
25
|
+
# puts "method '#{self}' '#{name}' '#{name.class}' was added"
|
|
26
|
+
added_method_names = (@@added_method_names[self] ||= {})
|
|
27
|
+
stringified_name = name.to_s
|
|
28
|
+
location = caller_locations(1, 1)[0]
|
|
29
|
+
path = location.absolute_path || location.path
|
|
30
|
+
line = location.lineno
|
|
31
|
+
location = {
|
|
32
|
+
method_name: stringified_name,
|
|
33
|
+
path: File.expand_path(path),
|
|
34
|
+
line: line,
|
|
35
|
+
}
|
|
36
|
+
add_method_location(location)
|
|
37
|
+
added_method_names[stringified_name] = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def added_method_names
|
|
41
|
+
(@@added_method_names[self] ||= {}).keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# for RSpec
|
|
47
|
+
def init_class_variables
|
|
48
|
+
@@description_locations = {}
|
|
49
|
+
@@method_locations = {}
|
|
50
|
+
@@added_method_names = {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def method_locations
|
|
54
|
+
@@method_locations[self] ||= []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_method_location(location)
|
|
58
|
+
method_locations << location
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def description_locations
|
|
62
|
+
@@description_locations[self] ||= []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_description_location(location)
|
|
66
|
+
description_locations << location
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def method_missing(method_name, *args)
|
|
72
|
+
case method_name
|
|
73
|
+
when :stub, :mock
|
|
74
|
+
location = caller_locations(1, 1)[0]
|
|
75
|
+
Mrubyc::Test::Generator::Double.new(method_name, args[0], location)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|