appmap 0.50.0 → 0.52.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -1
- data/CHANGELOG.md +38 -0
- data/Rakefile +21 -5
- data/appmap.gemspec +11 -6
- data/exe/appmap-agent-setup +47 -0
- data/exe/appmap-inspect +7 -0
- data/lib/appmap.rb +56 -17
- data/lib/appmap/command/agent_setup/init.rb +44 -0
- data/lib/appmap/command/inspect.rb +27 -0
- data/lib/appmap/command_error.rb +13 -0
- data/lib/appmap/config.rb +96 -29
- data/lib/appmap/handler/rails/template.rb +19 -5
- data/lib/appmap/node_cli.rb +59 -0
- data/lib/appmap/service/guesser.rb +26 -0
- data/lib/appmap/trace.rb +4 -2
- data/lib/appmap/util.rb +52 -2
- data/lib/appmap/version.rb +4 -1
- data/package.json +6 -7
- data/spec/config_spec.rb +21 -0
- data/spec/fixtures/rails5_users_app/docker-compose.yml +1 -1
- data/spec/fixtures/rails6_users_app/Dockerfile +9 -0
- data/spec/fixtures/rails6_users_app/docker-compose.yml +1 -1
- data/spec/hook_spec.rb +2 -2
- data/spec/{abstract_controller_base_spec.rb → rails_recording_spec.rb} +39 -19
- data/spec/rails_spec_helper.rb +22 -0
- data/spec/record_net_http_spec.rb +1 -1
- data/test/agent_setup_cli_test.rb +37 -0
- data/test/fixtures/gem_test/Gemfile +1 -0
- data/test/inspect_cli_test.rb +12 -0
- data/yarn.lock +477 -0
- metadata +23 -47
- data/lib/appmap/algorithm/prune_class_map.rb +0 -67
- data/lib/appmap/algorithm/stats.rb +0 -91
- data/lib/appmap/command/record.rb +0 -38
- data/lib/appmap/command/stats.rb +0 -14
- data/lore/pages/2019-05-21-install-and-record/index.pug +0 -51
- data/lore/pages/2019-05-21-install-and-record/install_example_appmap.png +0 -0
- data/lore/pages/2019-05-21-install-and-record/metadata.yml +0 -5
- data/lore/pages/layout.pug +0 -66
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css +0 -1912
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css.map +0 -1
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css +0 -7
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css.map +0 -1
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css +0 -331
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css.map +0 -1
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css +0 -8
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css.map +0 -1
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css +0 -9030
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css.map +0 -1
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css +0 -7
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css.map +0 -1
- data/lore/public/stylesheets/style.css +0 -8
- data/package-lock.json +0 -1064
@@ -14,16 +14,30 @@ module AppMap
|
|
14
14
|
# The class name is generated from the template path. The package name is
|
15
15
|
# 'app/views', and the method name is 'render'. The source location of the method
|
16
16
|
# is, of course, the path to the view template.
|
17
|
-
TemplateMethod
|
18
|
-
private_instance_methods :path
|
17
|
+
class TemplateMethod
|
19
18
|
attr_reader :class_name
|
20
|
-
|
19
|
+
|
20
|
+
attr_reader :path
|
21
|
+
private_instance_methods :path
|
22
|
+
|
21
23
|
def initialize(path)
|
22
|
-
|
24
|
+
@path = path
|
23
25
|
|
24
26
|
@class_name = path.parameterize.underscore
|
25
27
|
end
|
26
|
-
|
28
|
+
|
29
|
+
def id
|
30
|
+
[ package, path, name ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
id.hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def eql?(other)
|
38
|
+
other.is_a?(TemplateMethod) && id.eql?(other.id)
|
39
|
+
end
|
40
|
+
|
27
41
|
def package
|
28
42
|
'app/views'
|
29
43
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'appmap/util'
|
4
|
+
require 'appmap/command_error'
|
5
|
+
|
6
|
+
module AppMap
|
7
|
+
# Utilities for invoking the +@appland/appmap+ CLI.
|
8
|
+
class NodeCLI
|
9
|
+
APPMAP_JS = Pathname.new(__dir__).join('../../node_modules/@appland/cli/src/cli.js').expand_path.to_s
|
10
|
+
|
11
|
+
attr_reader :verbose
|
12
|
+
# Directory to scan for AppMaps.
|
13
|
+
attr_accessor :appmap_dir
|
14
|
+
|
15
|
+
def initialize(verbose: false, appmap_dir: AppMap::DEFAULT_APPMAP_DIR)
|
16
|
+
@verbose = verbose
|
17
|
+
@appmap_dir = appmap_dir
|
18
|
+
|
19
|
+
detect_nodejs
|
20
|
+
end
|
21
|
+
|
22
|
+
def detect_nodejs
|
23
|
+
do_fail('node', 'please install NodeJS') unless system('node --version 2>&1 > /dev/null')
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def index_appmaps
|
28
|
+
command [ 'index', '--appmap-dir', appmap_dir ]
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def command(command, options = {})
|
33
|
+
command.unshift << '--verbose' if verbose
|
34
|
+
command.unshift APPMAP_JS
|
35
|
+
command.unshift 'node'
|
36
|
+
|
37
|
+
warn command.join(' ') if verbose
|
38
|
+
stdout, stderr, status = Open3.capture3({ 'NODE_OPTIONS' => '--trace-warnings' }, *command.map(&:shellescape), options)
|
39
|
+
stdout_msg = stdout.split("\n").map {|line| "stdout: #{line}"}.join("\n") unless Util.blank?(stdout)
|
40
|
+
stderr_msg = stderr.split("\n").map {|line| "stderr: #{line}"}.join("\n") unless Util.blank?(stderr)
|
41
|
+
if verbose
|
42
|
+
warn stdout_msg if stdout_msg
|
43
|
+
warn stderr_msg if stderr_msg
|
44
|
+
end
|
45
|
+
unless status.exitstatus == 0
|
46
|
+
raise CommandError.new(command, [ stdout_msg, stderr_msg ].compact.join("\n"))
|
47
|
+
end
|
48
|
+
[ stdout, stderr ]
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def do_fail(command, msg)
|
54
|
+
command = command.join(' ') if command.is_a?(Array)
|
55
|
+
warn [ command, msg ].join('; ') if verbose
|
56
|
+
raise CommandError.new(command, msg)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppMap
|
4
|
+
module Service
|
5
|
+
class Guesser
|
6
|
+
POSSIBLE_PATHS = %w[app/controllers app/models lib]
|
7
|
+
class << self
|
8
|
+
def guess_name
|
9
|
+
reponame = lambda do
|
10
|
+
next unless File.directory?('.git')
|
11
|
+
|
12
|
+
repo_name = `git config --get remote.origin.url`.strip
|
13
|
+
repo_name.split('/').last.split('.').first unless repo_name == ''
|
14
|
+
end
|
15
|
+
dirname = -> { Dir.pwd.split('/').last }
|
16
|
+
|
17
|
+
reponame.() || dirname.()
|
18
|
+
end
|
19
|
+
|
20
|
+
def guess_paths
|
21
|
+
POSSIBLE_PATHS.select { |path| File.directory?(path) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/appmap/trace.rb
CHANGED
@@ -2,10 +2,12 @@
|
|
2
2
|
|
3
3
|
module AppMap
|
4
4
|
module Trace
|
5
|
-
class RubyMethod
|
5
|
+
class RubyMethod < SimpleDelegator
|
6
6
|
attr_reader :class_name, :static
|
7
7
|
|
8
8
|
def initialize(package, class_name, method, static)
|
9
|
+
super(method)
|
10
|
+
|
9
11
|
@package = package
|
10
12
|
@class_name = class_name
|
11
13
|
@method = method
|
@@ -111,7 +113,7 @@ module AppMap
|
|
111
113
|
@last_package_for_thread[Thread.current.object_id] = package if package
|
112
114
|
@events << event
|
113
115
|
static = event.static if event.respond_to?(:static)
|
114
|
-
|
116
|
+
record_method Trace::RubyMethod.new(package, defined_class, method, static) \
|
115
117
|
if package && defined_class && method && (event.event == :call)
|
116
118
|
end
|
117
119
|
|
data/lib/appmap/util.rb
CHANGED
@@ -4,6 +4,21 @@ require 'bundler'
|
|
4
4
|
|
5
5
|
module AppMap
|
6
6
|
module Util
|
7
|
+
# https://wynnnetherland.com/journal/a-stylesheet-author-s-guide-to-terminal-colors/
|
8
|
+
# Embed in a String to clear all previous ANSI sequences.
|
9
|
+
CLEAR = "\e[0m"
|
10
|
+
BOLD = "\e[1m"
|
11
|
+
|
12
|
+
# Colors
|
13
|
+
BLACK = "\e[30m"
|
14
|
+
RED = "\e[31m"
|
15
|
+
GREEN = "\e[32m"
|
16
|
+
YELLOW = "\e[33m"
|
17
|
+
BLUE = "\e[34m"
|
18
|
+
MAGENTA = "\e[35m"
|
19
|
+
CYAN = "\e[36m"
|
20
|
+
WHITE = "\e[37m"
|
21
|
+
|
7
22
|
class << self
|
8
23
|
# scenario_filename builds a suitable file name from a scenario name.
|
9
24
|
# Special characters are removed, and the file name is truncated to fit within
|
@@ -86,13 +101,13 @@ module AppMap
|
|
86
101
|
# Rack prepends HTTP_ to all client-sent headers.
|
87
102
|
matching_headers = env
|
88
103
|
.select { |k,v| k.start_with? 'HTTP_'}
|
89
|
-
.reject { |k,v|
|
104
|
+
.reject { |k,v| blank?(v) }
|
90
105
|
.each_with_object({}) do |kv, memo|
|
91
106
|
key = kv[0].sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
92
107
|
value = kv[1]
|
93
108
|
memo[key] = value
|
94
109
|
end
|
95
|
-
|
110
|
+
blank?(matching_headers) ? nil : matching_headers
|
96
111
|
end
|
97
112
|
|
98
113
|
def normalize_path(path)
|
@@ -128,6 +143,41 @@ module AppMap
|
|
128
143
|
FileUtils.mv tempfile.path, filename
|
129
144
|
end
|
130
145
|
end
|
146
|
+
|
147
|
+
def color(text, color, bold: false)
|
148
|
+
color = Util.const_get(color.to_s.upcase) if color.is_a?(Symbol)
|
149
|
+
bold = bold ? BOLD : ""
|
150
|
+
"#{bold}#{color}#{text}#{CLEAR}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def classify(word)
|
154
|
+
word.split(/[\-_]/).map(&:capitalize).join
|
155
|
+
end
|
156
|
+
|
157
|
+
def deep_dup(hash)
|
158
|
+
# This is a simple way to avoid the need for deep_dup from activesupport.
|
159
|
+
Marshal.load(Marshal.dump(hash))
|
160
|
+
end
|
161
|
+
|
162
|
+
def blank?(obj)
|
163
|
+
return true if obj.nil?
|
164
|
+
|
165
|
+
return true if obj.is_a?(String) && obj == ''
|
166
|
+
|
167
|
+
return true if obj.respond_to?(:length) && obj.length == 0
|
168
|
+
|
169
|
+
return true if obj.respond_to?(:size) && obj.size == 0
|
170
|
+
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
def startup_message(msg)
|
175
|
+
if defined?(::Rails) && defined?(::Rails.logger) && ::Rails.logger
|
176
|
+
::Rails.logger.debug msg
|
177
|
+
elsif ENV['DEBUG'] == 'true'
|
178
|
+
warn msg
|
179
|
+
end
|
180
|
+
end
|
131
181
|
end
|
132
182
|
end
|
133
183
|
end
|
data/lib/appmap/version.rb
CHANGED
data/package.json
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
{
|
2
|
-
"name": "appmap-ruby
|
2
|
+
"name": "appmap-ruby",
|
3
3
|
"version": "1.0.0",
|
4
|
-
"description": "
|
5
|
-
"scripts": {
|
6
|
-
"start": "./node_modules/applore/bin/lore serve"
|
7
|
-
},
|
4
|
+
"description": "AppMap client agent for Ruby",
|
8
5
|
"directories": {
|
9
|
-
"example": "examples",
|
10
6
|
"lib": "lib",
|
11
7
|
"test": "test"
|
12
8
|
},
|
@@ -14,11 +10,14 @@
|
|
14
10
|
"type": "git",
|
15
11
|
"url": "git+https://github.com/applandinc/appmap-ruby.git"
|
16
12
|
},
|
13
|
+
"author": "kgilpin@gmail.com",
|
14
|
+
"license": "MIT",
|
17
15
|
"bugs": {
|
18
16
|
"url": "https://github.com/applandinc/appmap-ruby/issues"
|
19
17
|
},
|
20
18
|
"homepage": "https://github.com/applandinc/appmap-ruby#readme",
|
21
19
|
"dependencies": {
|
22
|
-
"
|
20
|
+
"@appland/cli": "^1.1.0"
|
23
21
|
}
|
24
22
|
}
|
23
|
+
|
data/spec/config_spec.rb
CHANGED
@@ -55,4 +55,25 @@ describe AppMap::Config, docker: false do
|
|
55
55
|
|
56
56
|
expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
|
57
57
|
end
|
58
|
+
|
59
|
+
context do
|
60
|
+
let(:warnings) { @warnings ||= [] }
|
61
|
+
let(:warning) { warnings.join }
|
62
|
+
before do
|
63
|
+
expect(AppMap::Config).to receive(:warn).at_least(1) { |msg| warnings << msg }
|
64
|
+
end
|
65
|
+
it 'prints a warning and uses a default config' do
|
66
|
+
config = AppMap::Config.load_from_file 'no/such/file'
|
67
|
+
expect(config.to_h).to eq(YAML.load(<<~CONFIG))
|
68
|
+
:name: appmap-ruby
|
69
|
+
:packages:
|
70
|
+
- :path: lib
|
71
|
+
:handler_class: AppMap::Handler::Function
|
72
|
+
:shallow: false
|
73
|
+
:functions: []
|
74
|
+
:exclude: []
|
75
|
+
CONFIG
|
76
|
+
expect(warning).to include('NOTICE: The AppMap config file no/such/file was not found!')
|
77
|
+
end
|
78
|
+
end
|
58
79
|
end
|
@@ -4,9 +4,18 @@ ARG RUBY_VERSION
|
|
4
4
|
FROM appmap:${GEM_VERSION} as appmap
|
5
5
|
|
6
6
|
FROM ruby:${RUBY_VERSION}
|
7
|
+
|
8
|
+
SHELL ["/bin/bash", "-c"]
|
9
|
+
|
7
10
|
RUN apt-get update && apt-get install -y vim less
|
8
11
|
RUN apt-get install -y postgresql-client
|
9
12
|
|
13
|
+
RUN curl -fsSL https://fnm.vercel.app/install | bash \
|
14
|
+
&& source /root/.bashrc \
|
15
|
+
&& fnm install --lts \
|
16
|
+
&& echo 'fnm default $(fnm current)' >> ~/.bashrc \
|
17
|
+
&& ln -s $(which node) /usr/local/bin/node
|
18
|
+
|
10
19
|
RUN mkdir /app
|
11
20
|
WORKDIR /app
|
12
21
|
|
data/spec/hook_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe 'AppMap class Hooking', docker: false do
|
|
21
21
|
def invoke_test_file(file, setup: nil, &block)
|
22
22
|
AppMap.configuration = nil
|
23
23
|
package = AppMap::Config::Package.build_from_path(file)
|
24
|
-
config = AppMap::Config.new('hook_spec', [ package ])
|
24
|
+
config = AppMap::Config.new('hook_spec', packages: [ package ])
|
25
25
|
AppMap.configuration = config
|
26
26
|
tracer = nil
|
27
27
|
AppMap::Hook.new(config).enable do
|
@@ -57,7 +57,7 @@ describe 'AppMap class Hooking', docker: false do
|
|
57
57
|
it 'excludes named classes and methods' do
|
58
58
|
load 'spec/fixtures/hook/exclude.rb'
|
59
59
|
package = AppMap::Config::Package.build_from_path('spec/fixtures/hook/exclude.rb')
|
60
|
-
config = AppMap::Config.new('hook_spec', [ package ], exclude: %w[ExcludeTest])
|
60
|
+
config = AppMap::Config.new('hook_spec', packages: [ package ], exclude: %w[ExcludeTest])
|
61
61
|
AppMap.configuration = config
|
62
62
|
|
63
63
|
expect(config.never_hook?(ExcludeTest, ExcludeTest.new.method(:instance_method))).to be_truthy
|
@@ -4,6 +4,7 @@ describe 'Rails' do
|
|
4
4
|
%w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
|
5
5
|
context "#{rails_major_version}" do
|
6
6
|
include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app" unless use_existing_data?
|
7
|
+
include_context 'rails integration test setup'
|
7
8
|
|
8
9
|
def run_spec(spec_name)
|
9
10
|
cmd = <<~CMD.gsub "\n", ' '
|
@@ -13,24 +14,6 @@ describe 'Rails' do
|
|
13
14
|
run_cmd cmd, chdir: fixture_dir
|
14
15
|
end
|
15
16
|
|
16
|
-
def tmpdir
|
17
|
-
'tmp/spec/AbstractControllerBase'
|
18
|
-
end
|
19
|
-
|
20
|
-
unless use_existing_data?
|
21
|
-
before(:all) do
|
22
|
-
FileUtils.rm_rf tmpdir
|
23
|
-
FileUtils.mkdir_p tmpdir
|
24
|
-
run_spec 'spec/controllers/users_controller_spec.rb'
|
25
|
-
run_spec 'spec/controllers/users_controller_api_spec.rb'
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
|
30
|
-
let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
|
31
|
-
let(:appmap) { JSON.parse File.read(appmap_json_path) }
|
32
|
-
let(:events) { appmap['events'] }
|
33
|
-
|
34
17
|
describe 'an API route' do
|
35
18
|
describe 'creating an object' do
|
36
19
|
let(:appmap_json_file) do
|
@@ -240,7 +223,8 @@ describe 'Rails' do
|
|
240
223
|
'children' => include(hash_including(
|
241
224
|
'name' => 'ActionView',
|
242
225
|
'children' => include(hash_including(
|
243
|
-
|
226
|
+
# Rails 6/5 difference
|
227
|
+
'name' => /^(Template)?Renderer$/,
|
244
228
|
'children' => include(hash_including(
|
245
229
|
'name' => 'render',
|
246
230
|
'labels' => ['mvc.view']
|
@@ -253,4 +237,40 @@ describe 'Rails' do
|
|
253
237
|
end
|
254
238
|
end
|
255
239
|
end
|
240
|
+
|
241
|
+
describe 'with default appmap.yml' do
|
242
|
+
include_context 'Rails app pg database', "spec/fixtures/rails5_users_app" unless use_existing_data?
|
243
|
+
include_context 'rails integration test setup'
|
244
|
+
|
245
|
+
def run_spec(spec_name)
|
246
|
+
cmd = <<~CMD.gsub "\n", ' '
|
247
|
+
docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true -e APPMAP_CONFIG_FILE=no/such/file
|
248
|
+
-v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
|
249
|
+
CMD
|
250
|
+
run_cmd cmd, chdir: fixture_dir
|
251
|
+
end
|
252
|
+
|
253
|
+
let(:appmap_json_file) do
|
254
|
+
'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'http_server_request is recorded' do
|
258
|
+
expect(events).to include(
|
259
|
+
hash_including(
|
260
|
+
'http_server_request' => hash_including(
|
261
|
+
'request_method' => 'POST',
|
262
|
+
'path_info' => '/api/users'
|
263
|
+
)
|
264
|
+
)
|
265
|
+
)
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'controller method is recorded' do
|
269
|
+
expect(events).to include hash_including(
|
270
|
+
'defined_class' => 'Api::UsersController',
|
271
|
+
'method_id' => 'build_user',
|
272
|
+
'path' => 'app/controllers/api/users_controller.rb',
|
273
|
+
)
|
274
|
+
end
|
275
|
+
end
|
256
276
|
end
|
data/spec/rails_spec_helper.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/core_ext'
|
4
6
|
require 'open3'
|
5
7
|
|
6
8
|
def wait_for_container(app_name)
|
@@ -58,3 +60,23 @@ shared_context 'Rails app pg database' do |fixture_dir|
|
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
63
|
+
|
64
|
+
shared_context 'rails integration test setup' do
|
65
|
+
def tmpdir
|
66
|
+
'tmp/spec/AbstractControllerBase'
|
67
|
+
end
|
68
|
+
|
69
|
+
unless use_existing_data?
|
70
|
+
before(:all) do
|
71
|
+
FileUtils.rm_rf tmpdir
|
72
|
+
FileUtils.mkdir_p tmpdir
|
73
|
+
run_spec 'spec/controllers/users_controller_spec.rb'
|
74
|
+
run_spec 'spec/controllers/users_controller_api_spec.rb'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
|
79
|
+
let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
|
80
|
+
let(:appmap) { JSON.parse File.read(appmap_json_path) }
|
81
|
+
let(:events) { appmap['events'] }
|
82
|
+
end
|