appmap 0.48.0 → 0.51.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.
data/lib/appmap/config.rb CHANGED
@@ -167,8 +167,10 @@ module AppMap
167
167
  ),
168
168
  package_hooks('actionpack',
169
169
  [
170
- method_hook('ActionDispatch::Request::Session', %i[destroy [] dig values []= clear update delete fetch merge], %w[http.session]),
171
- method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session]),
170
+ method_hook('ActionDispatch::Request::Session', %i[[] dig values fetch], %w[http.session.read]),
171
+ method_hook('ActionDispatch::Request::Session', %i[destroy[]= clear update delete merge], %w[http.session.write]),
172
+ method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.read]),
173
+ method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session.write]),
172
174
  method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
173
175
  ],
174
176
  package_name: 'action_dispatch'
@@ -213,15 +215,22 @@ module AppMap
213
215
  # This is happening: Method send_command not found on Net::IMAP
214
216
  # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
215
217
  # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
216
- 'Psych' => TargetMethods.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
217
- 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
218
- 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
218
+ 'Psych' => [
219
+ TargetMethods.new(%i[load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.parse])),
220
+ TargetMethods.new(%i[dump dump_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml.generate])),
221
+ ],
222
+ 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.parse])),
223
+ 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json.generate])),
219
224
  }.freeze
220
225
 
221
- attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
226
+ attr_reader :name, :appmap_dir, :packages, :exclude, :hooked_methods, :builtin_hooks
222
227
 
223
- def initialize(name, packages, exclude: [], functions: [])
228
+ def initialize(name,
229
+ packages: [],
230
+ exclude: [],
231
+ functions: [])
224
232
  @name = name
233
+ @appmap_dir = AppMap::DEFAULT_APPMAP_DIR
225
234
  @packages = packages
226
235
  @hook_paths = Set.new(packages.map(&:path))
227
236
  @exclude = exclude
@@ -248,38 +257,119 @@ module AppMap
248
257
  class << self
249
258
  # Loads configuration data from a file, specified by the file name.
250
259
  def load_from_file(config_file_name)
251
- require 'yaml'
252
- load YAML.safe_load(::File.read(config_file_name))
260
+ logo = lambda do
261
+ Util.color(<<~LOGO, :magenta)
262
+ ___ __ ___
263
+ / _ | ___ ___ / |/ /__ ____
264
+ / __ |/ _ \\/ _ \\/ /|_/ / _ `/ _ \\
265
+ /_/ |_/ .__/ .__/_/ /_/\\_,_/ .__/
266
+ /_/ /_/ /_/
267
+ LOGO
268
+ end
269
+
270
+ config_present = true if File.exists?(config_file_name)
271
+
272
+ config_data = if config_present
273
+ require 'yaml'
274
+ YAML.safe_load(::File.read(config_file_name))
275
+ else
276
+ warn logo.()
277
+ warn ''
278
+ warn Util.color(%Q|NOTICE: The AppMap config file #{config_file_name} was not found!|, :magenta, bold: true)
279
+ warn ''
280
+ warn Util.color(<<~MISSING_FILE_MSG, :magenta)
281
+ AppMap uses this file to customize its behavior. For example, you can use
282
+ the 'packages' setting to indicate which local file paths and dependency
283
+ gems you want to include in the AppMap. Since you haven't provided specific
284
+ settings, the appmap gem will try and guess some reasonable defaults.
285
+ To suppress this message, create the file:
286
+
287
+ #{Pathname.new(config_file_name).expand_path}.
288
+
289
+ Here are the default settings that will be used in the meantime. You can
290
+ copy and paste this example to start your appmap.yml.
291
+ MISSING_FILE_MSG
292
+ {}
293
+ end
294
+ load(config_data).tap do |config|
295
+ config_yaml = {
296
+ 'name' => config.name,
297
+ 'packages' => config.packages.select{|p| p.path}.map do |pkg|
298
+ { 'path' => pkg.path }
299
+ end,
300
+ 'exclude' => []
301
+ }.compact
302
+ unless config_present
303
+ warn Util.color(YAML.dump(config_yaml), :magenta)
304
+ warn logo.()
305
+ end
306
+ end
253
307
  end
254
308
 
255
309
  # Loads configuration from a Hash.
256
310
  def load(config_data)
257
- functions = (config_data['functions'] || []).map do |function_data|
258
- package = function_data['package']
259
- cls = function_data['class']
260
- functions = function_data['function'] || function_data['functions']
261
- raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
262
- functions = Array(functions).map(&:to_sym)
263
- labels = function_data['label'] || function_data['labels']
264
- labels = Array(labels).map(&:to_s) if labels
265
- Function.new(package, cls, labels, functions)
311
+ name = config_data['name'] || guess_name
312
+ config_params = {
313
+ exclude: config_data['exclude']
314
+ }.compact
315
+
316
+ if config_data['functions']
317
+ config_params[:functions] = config_data['functions'].map do |function_data|
318
+ package = function_data['package']
319
+ cls = function_data['class']
320
+ functions = function_data['function'] || function_data['functions']
321
+ raise %q(AppMap config 'function' element should specify 'package', 'class' and 'function' or 'functions') unless package && cls && functions
322
+
323
+ functions = Array(functions).map(&:to_sym)
324
+ labels = function_data['label'] || function_data['labels']
325
+ labels = Array(labels).map(&:to_s) if labels
326
+ Function.new(package, cls, labels, functions)
327
+ end
266
328
  end
267
- packages = (config_data['packages'] || []).map do |package|
268
- gem = package['gem']
269
- path = package['path']
270
- raise 'AppMap package configuration should specify gem or path, not both' if gem && path
271
-
272
- if gem
273
- shallow = package['shallow']
274
- # shallow is true by default for gems
275
- shallow = true if shallow.nil?
276
- Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
329
+
330
+ config_params[:packages] = \
331
+ if config_data['packages']
332
+ config_data['packages'].map do |package|
333
+ gem = package['gem']
334
+ path = package['path']
335
+ raise %q(AppMap config 'package' element should specify 'gem' or 'path', not both) if gem && path
336
+
337
+ if gem
338
+ shallow = package['shallow']
339
+ # shallow is true by default for gems
340
+ shallow = true if shallow.nil?
341
+ Package.build_from_gem(gem, exclude: package['exclude'] || [], shallow: shallow)
342
+ else
343
+ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
344
+ end
345
+ end.compact
277
346
  else
278
- Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
347
+ Array(guess_paths).map do |path|
348
+ Package.build_from_path(path)
349
+ end
279
350
  end
280
- end.compact
281
- exclude = config_data['exclude'] || []
282
- Config.new config_data['name'], packages, exclude: exclude, functions: functions
351
+
352
+ Config.new name, config_params
353
+ end
354
+
355
+ def guess_name
356
+ reponame = lambda do
357
+ next unless File.directory?('.git')
358
+
359
+ repo_name = `git config --get remote.origin.url`.strip
360
+ repo_name.split('/').last.split('.').first unless repo_name == ''
361
+ end
362
+ dirname = -> { Dir.pwd.split('/').last }
363
+
364
+ reponame.() || dirname.()
365
+ end
366
+
367
+ def guess_paths
368
+ if defined?(::Rails)
369
+ %w[app/controllers app/models]
370
+ elsif File.directory?('lib')
371
+ %w[lib]
372
+ end
283
373
  end
284
374
  end
285
375
 
@@ -289,7 +379,7 @@ module AppMap
289
379
  packages: packages.map(&:to_h),
290
380
  functions: @functions.map(&:to_h),
291
381
  exclude: exclude
292
- }
382
+ }.compact
293
383
  end
294
384
 
295
385
  # Determines if methods defined in a file path should possibly be hooked.
data/lib/appmap/event.rb CHANGED
@@ -213,7 +213,7 @@ module AppMap
213
213
  exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
214
214
  exceptions << {
215
215
  class: best_class_name(next_exception),
216
- message: next_exception.message,
216
+ message: display_string(next_exception.message),
217
217
  object_id: next_exception.__id__,
218
218
  path: exception_backtrace&.path,
219
219
  lineno: exception_backtrace&.lineno
data/lib/appmap/hook.rb CHANGED
@@ -36,7 +36,7 @@ module AppMap
36
36
 
37
37
  def initialize(config)
38
38
  @config = config
39
- @trace_locations = []
39
+ @trace_enabled = []
40
40
  # Paths that are known to be non-tracing
41
41
  @notrace_paths = Set.new
42
42
  end
@@ -47,10 +47,8 @@ module AppMap
47
47
 
48
48
  hook_builtins
49
49
 
50
- @trace_begin = TracePoint.new(:class, &method(:trace_class))
51
50
  @trace_end = TracePoint.new(:end, &method(:trace_end))
52
-
53
- @trace_begin.enable(&block)
51
+ @trace_end.enable(&block)
54
52
  end
55
53
 
56
54
  # hook_builtins builds hooks for code that is built in to the Ruby standard library.
@@ -96,29 +94,22 @@ module AppMap
96
94
 
97
95
  protected
98
96
 
99
- def trace_class(trace_point)
100
- path = trace_point.path
101
-
102
- return if @notrace_paths.member?(path)
103
-
104
- if config.path_enabled?(path)
105
- location = trace_location(trace_point)
106
- warn "Entering hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
107
- @trace_locations << location
108
- unless @trace_end.enabled?
109
- warn "Enabling hooking" if Hook::LOG || Hook::LOG_HOOK
110
- @trace_end.enable
111
- end
112
- else
113
- @notrace_paths << path
114
- end
115
- end
116
-
117
97
  def trace_location(trace_point)
118
98
  [ trace_point.path, trace_point.lineno ].join(':')
119
99
  end
120
100
 
121
101
  def trace_end(trace_point)
102
+ location = trace_location(trace_point)
103
+ warn "Class or module ends at location #{trace_location(trace_point)}" if Hook::LOG || Hook::LOG_HOOK
104
+
105
+ path = trace_point.path
106
+ enabled = !@notrace_paths.member?(path) && config.path_enabled?(path)
107
+ if !enabled
108
+ warn "Not hooking - path is not enabled" if Hook::LOG || Hook::LOG_HOOK
109
+ @notrace_paths << path
110
+ return
111
+ end
112
+
122
113
  cls = trace_point.self
123
114
 
124
115
  instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
@@ -151,7 +142,8 @@ module AppMap
151
142
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
152
143
 
153
144
  disasm = RubyVM::InstructionSequence.disasm(method)
154
- # Skip methods that have no instruction sequence, as they are obviously trivial.
145
+ # Skip methods that have no instruction sequence, as they are either have no body or they are or native.
146
+ # TODO: Figure out how to tell the difference?
155
147
  next unless disasm
156
148
 
157
149
  package = config.lookup_package(hook_cls, method)
@@ -170,13 +162,6 @@ module AppMap
170
162
  # uninitialized constant Faraday::Connection
171
163
  warn "NameError in #{__FILE__}: #{$!.message}"
172
164
  end
173
-
174
- location = @trace_locations.pop
175
- warn "Leaving hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
176
- if @trace_locations.empty?
177
- warn "Disabling hooking" if Hook::LOG || Hook::LOG_HOOK
178
- @trace_end.disable
179
- end
180
165
  end
181
166
  end
182
167
  end
@@ -54,15 +54,21 @@ module AppMap
54
54
 
55
55
  @recordings_by_test = {}
56
56
  @event_methods = Set.new
57
+ @recording_count = 0
57
58
 
58
59
  class << self
59
60
  def init
60
- warn 'Configuring AppMap recorder for Minitest'
61
-
62
61
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
63
62
  end
64
63
 
64
+ def first_recording?
65
+ @recording_count == 0
66
+ end
67
+
65
68
  def begin_test(test, name)
69
+ AppMap.info 'Configuring AppMap recorder for Minitest' if first_recording?
70
+ @recording_count += 1
71
+
66
72
  @recordings_by_test[test.object_id] = Recording.new(test, name)
67
73
  end
68
74
 
@@ -98,7 +104,7 @@ module AppMap
98
104
  if exception
99
105
  m[:exception] = {
100
106
  class: exception.class.name,
101
- message: exception.to_s
107
+ message: AppMap::Event::MethodEvent.display_string(exception.to_s)
102
108
  }
103
109
  end
104
110
  end
@@ -3,6 +3,13 @@
3
3
  module AppMap
4
4
  # Railtie connects the AppMap recorder to Rails-specific features.
5
5
  class Railtie < ::Rails::Railtie
6
+ initializer 'appmap.remote_recording' do
7
+ require 'appmap/middleware/remote_recording'
8
+ Rails.application.config.middleware.insert_after \
9
+ Rails::Rack::Logger,
10
+ AppMap::Middleware::RemoteRecording
11
+ end
12
+
6
13
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
7
14
  # AppMap events.
8
15
  initializer 'appmap.subscribe' do |_| # params: app
data/lib/appmap/rspec.rb CHANGED
@@ -139,15 +139,21 @@ module AppMap
139
139
 
140
140
  @recordings_by_example = {}
141
141
  @event_methods = Set.new
142
+ @recording_count = 0
142
143
 
143
144
  class << self
144
145
  def init
145
- warn 'Configuring AppMap recorder for RSpec'
146
-
147
146
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
148
147
  end
149
148
 
149
+ def first_recording?
150
+ @recording_count == 0
151
+ end
152
+
150
153
  def begin_spec(example)
154
+ AppMap.info 'Configuring AppMap recorder for RSpec' if first_recording?
155
+ @recording_count += 1
156
+
151
157
  @recordings_by_example[example.object_id] = Recording.new(example)
152
158
  end
153
159
 
@@ -183,7 +189,7 @@ module AppMap
183
189
  if exception
184
190
  m[:exception] = {
185
191
  class: exception.class.name,
186
- message: exception.to_s
192
+ message: AppMap::Event::MethodEvent.display_string(exception.to_s)
187
193
  }
188
194
  end
189
195
  end
data/lib/appmap/util.rb CHANGED
@@ -1,7 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bundler'
4
+
3
5
  module AppMap
4
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
+
5
22
  class << self
6
23
  # scenario_filename builds a suitable file name from a scenario name.
7
24
  # Special characters are removed, and the file name is truncated to fit within
@@ -94,7 +111,7 @@ module AppMap
94
111
  end
95
112
 
96
113
  def normalize_path(path)
97
- if path.index(Dir.pwd) == 0
114
+ if path.index(Dir.pwd) == 0 && !path.index(Bundler.bundle_path.to_s)
98
115
  path[Dir.pwd.length + 1..-1]
99
116
  else
100
117
  path
@@ -126,6 +143,12 @@ module AppMap
126
143
  FileUtils.mv tempfile.path, filename
127
144
  end
128
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
129
152
  end
130
153
  end
131
154
  end
@@ -3,7 +3,9 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.48.0'
6
+ VERSION = '0.51.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
+
10
+ DEFAULT_APPMAP_DIR = 'tmp/appmap'.freeze
9
11
  end
@@ -1,9 +1,30 @@
1
1
  require 'rails_spec_helper'
2
2
 
3
3
  describe 'Rails' do
4
+ shared_context 'rails integration test setup' do
5
+ def tmpdir
6
+ 'tmp/spec/AbstractControllerBase'
7
+ end
8
+
9
+ unless use_existing_data?
10
+ before(:all) do
11
+ FileUtils.rm_rf tmpdir
12
+ FileUtils.mkdir_p tmpdir
13
+ run_spec 'spec/controllers/users_controller_spec.rb'
14
+ run_spec 'spec/controllers/users_controller_api_spec.rb'
15
+ end
16
+ end
17
+
18
+ let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
19
+ let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
20
+ let(:appmap) { JSON.parse File.read(appmap_json_path) }
21
+ let(:events) { appmap['events'] }
22
+ end
23
+
4
24
  %w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
5
25
  context "#{rails_major_version}" do
6
26
  include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app" unless use_existing_data?
27
+ include_context 'rails integration test setup'
7
28
 
8
29
  def run_spec(spec_name)
9
30
  cmd = <<~CMD.gsub "\n", ' '
@@ -13,24 +34,6 @@ describe 'Rails' do
13
34
  run_cmd cmd, chdir: fixture_dir
14
35
  end
15
36
 
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
37
  describe 'an API route' do
35
38
  describe 'creating an object' do
36
39
  let(:appmap_json_file) do
@@ -253,4 +256,40 @@ describe 'Rails' do
253
256
  end
254
257
  end
255
258
  end
259
+
260
+ describe 'with default appmap.yml' do
261
+ include_context 'Rails app pg database', "spec/fixtures/rails5_users_app" unless use_existing_data?
262
+ include_context 'rails integration test setup'
263
+
264
+ def run_spec(spec_name)
265
+ cmd = <<~CMD.gsub "\n", ' '
266
+ docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true -e APPMAP_CONFIG_FILE=no/such/file
267
+ -v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
268
+ CMD
269
+ run_cmd cmd, chdir: fixture_dir
270
+ end
271
+
272
+ let(:appmap_json_file) do
273
+ 'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
274
+ end
275
+
276
+ it 'http_server_request is recorded' do
277
+ expect(events).to include(
278
+ hash_including(
279
+ 'http_server_request' => hash_including(
280
+ 'request_method' => 'POST',
281
+ 'path_info' => '/api/users'
282
+ )
283
+ )
284
+ )
285
+ end
286
+
287
+ it 'controller method is recorded' do
288
+ expect(events).to include hash_including(
289
+ 'defined_class' => 'Api::UsersController',
290
+ 'method_id' => 'build_user',
291
+ 'path' => 'app/controllers/api/users_controller.rb',
292
+ )
293
+ end
294
+ end
256
295
  end