appmap 0.48.0 → 0.51.0

Sign up to get free protection for your applications and to get access to all the features.
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