appmap 0.34.4 → 0.36.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.
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppMap
2
4
  class Hook
3
5
  class Method
4
- attr_reader :hook_class, :hook_method
6
+ attr_reader :hook_package, :hook_class, :hook_method
5
7
 
6
8
  # +method_display_name+ may be nil if name resolution gets
7
9
  # deferred until runtime (e.g. for a singleton method on an
@@ -15,8 +17,9 @@ module AppMap
15
17
  # with the method we're hooking.
16
18
  TIME_NOW = Time.method(:now)
17
19
  private_constant :TIME_NOW
18
-
19
- def initialize(hook_class, hook_method)
20
+
21
+ def initialize(hook_package, hook_class, hook_method)
22
+ @hook_package = hook_package
20
23
  @hook_class = hook_class
21
24
  @hook_method = hook_method
22
25
 
@@ -30,7 +33,7 @@ module AppMap
30
33
  msg = if method_display_name
31
34
  "#{method_display_name}"
32
35
  else
33
- "#{hook_method.name} (class resolution deferrred)"
36
+ "#{hook_method.name} (class resolution deferred)"
34
37
  end
35
38
  warn "AppMap: Hooking " + msg
36
39
  end
@@ -41,34 +44,38 @@ module AppMap
41
44
  after_hook = self.method(:after_hook)
42
45
  with_disabled_hook = self.method(:with_disabled_hook)
43
46
 
44
- hook_class.define_method hook_method.name do |*args, &block|
45
- instance_method = hook_method.bind(self).to_proc
47
+ hook_method_def = nil
48
+ hook_class.instance_eval do
49
+ hook_method_def = Proc.new do |*args, &block|
50
+ instance_method = hook_method.bind(self).to_proc
46
51
 
47
- # We may not have gotten the class for the method during
48
- # initialization (e.g. for a singleton method on an embedded
49
- # struct), so make sure we have it now.
50
- defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
52
+ # We may not have gotten the class for the method during
53
+ # initialization (e.g. for a singleton method on an embedded
54
+ # struct), so make sure we have it now.
55
+ defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
51
56
 
52
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
53
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
54
- return instance_method.call(*args, &block) unless enabled
57
+ hook_disabled = Thread.current[HOOK_DISABLE_KEY]
58
+ enabled = true if !hook_disabled && AppMap.tracing.enabled?
59
+ return instance_method.call(*args, &block) unless enabled
55
60
 
56
- call_event, start_time = with_disabled_hook.() do
57
- before_hook.(self, defined_class, args)
58
- end
59
- return_value = nil
60
- exception = nil
61
- begin
62
- return_value = instance_method.(*args, &block)
63
- rescue
64
- exception = $ERROR_INFO
65
- raise
66
- ensure
67
- with_disabled_hook.() do
68
- after_hook.(call_event, start_time, return_value, exception)
61
+ call_event, start_time = with_disabled_hook.() do
62
+ before_hook.(self, defined_class, args)
63
+ end
64
+ return_value = nil
65
+ exception = nil
66
+ begin
67
+ return_value = instance_method.(*args, &block)
68
+ rescue
69
+ exception = $ERROR_INFO
70
+ raise
71
+ ensure
72
+ with_disabled_hook.() do
73
+ after_hook.(self, call_event, start_time, return_value, exception)
74
+ end
69
75
  end
70
76
  end
71
77
  end
78
+ hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
72
79
  end
73
80
 
74
81
  protected
@@ -76,11 +83,11 @@ module AppMap
76
83
  def before_hook(receiver, defined_class, args)
77
84
  require 'appmap/event'
78
85
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
79
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
86
+ AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
80
87
  [ call_event, TIME_NOW.call ]
81
88
  end
82
89
 
83
- def after_hook(call_event, start_time, return_value, exception)
90
+ def after_hook(receiver, call_event, start_time, return_value, exception)
84
91
  require 'appmap/event'
85
92
  elapsed = TIME_NOW.call - start_time
86
93
  return_event = \
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+ require 'appmap/hook'
5
+
6
+ module AppMap
7
+ module Rails
8
+ module RequestHandler
9
+ class HTTPServerRequest < AppMap::Event::MethodEvent
10
+ attr_accessor :request_method, :path_info, :params
11
+
12
+ def initialize(request)
13
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
+
15
+ @request_method = request.request_method
16
+ @path_info = request.path_info.split('?')[0]
17
+ @params = ActionDispatch::Http::ParameterFilter.new(::Rails.application.config.filter_parameters).filter(request.params)
18
+ end
19
+
20
+ def to_h
21
+ super.tap do |h|
22
+ h[:http_server_request] = {
23
+ request_method: request_method,
24
+ path_info: path_info
25
+ }
26
+
27
+ h[:message] = params.keys.map do |key|
28
+ val = params[key]
29
+ {
30
+ name: key,
31
+ class: val.class.name,
32
+ value: self.class.display_string(val),
33
+ object_id: val.__id__
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
41
+ attr_accessor :status, :mime_type
42
+
43
+ def initialize(response, parent_id, elapsed)
44
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
45
+
46
+ self.status = response.status
47
+ self.mime_type = response.headers['Content-Type']
48
+ self.parent_id = parent_id
49
+ self.elapsed = elapsed
50
+ end
51
+
52
+ def to_h
53
+ super.tap do |h|
54
+ h[:http_server_response] = {
55
+ status: status,
56
+ mime_type: mime_type
57
+ }.compact
58
+ end
59
+ end
60
+ end
61
+
62
+ class HookMethod < AppMap::Hook::Method
63
+ def initialize
64
+ # ActionController::Instrumentation has issued start_processing.action_controller and
65
+ # process_action.action_controller since Rails 3. Therefore it's a stable place to hook
66
+ # the request. Rails controller notifications can't be used directly because they don't
67
+ # provide response headers, and we want the Content-Type.
68
+ super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
69
+ end
70
+
71
+ protected
72
+
73
+ def before_hook(receiver, defined_class, _) # args
74
+ call_event = HTTPServerRequest.new(receiver.request)
75
+ # http_server_request events are i/o and do not require a package name.
76
+ AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
77
+ [ call_event, TIME_NOW.call ]
78
+ end
79
+
80
+ def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
81
+ elapsed = TIME_NOW.call - start_time
82
+ return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
83
+ AppMap.tracing.record_event return_event
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -72,9 +72,17 @@ module AppMap
72
72
  end
73
73
 
74
74
  class ActiveRecordExaminer
75
+ @@db_version_warning_issued = {}
76
+
77
+ def issue_warning
78
+ db_type = database_type
79
+ return if @@db_version_warning_issued[db_type]
80
+ warn("AppMap: Unable to determine database version for #{db_type.inspect}")
81
+ @@db_version_warning_issued[db_type] = true
82
+ end
83
+
75
84
  def server_version
76
- ActiveRecord::Base.connection.try(:database_version) ||\
77
- warn("Unable to determine database version for #{database_type.inspect}")
85
+ ActiveRecord::Base.connection.try(:database_version) || issue_warning
78
86
  end
79
87
 
80
88
  def database_type
@@ -13,13 +13,11 @@ module AppMap
13
13
  # AppMap events.
14
14
  initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
15
15
  require 'appmap/rails/sql_handler'
16
- require 'appmap/rails/action_handler'
16
+ require 'appmap/rails/request_handler'
17
17
  ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
18
18
  ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
19
- ActiveSupport::Notifications.subscribe \
20
- 'start_processing.action_controller', AppMap::Rails::ActionHandler::HTTPServerRequest.new
21
- ActiveSupport::Notifications.subscribe \
22
- 'process_action.action_controller', AppMap::Rails::ActionHandler::HTTPServerResponse.new
19
+
20
+ AppMap::Rails::RequestHandler::HookMethod.new.activate
23
21
  end
24
22
 
25
23
  # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
@@ -42,4 +40,4 @@ module AppMap
42
40
  end.call
43
41
  end
44
42
  end
45
- end
43
+ end unless ENV['APPMAP_INITIALIZE'] == 'false'
@@ -124,8 +124,18 @@ module AppMap
124
124
  def initialize(example)
125
125
  super
126
126
 
127
+ webdriver_port = lambda do
128
+ return unless defined?(page) && page&.driver
129
+
130
+ # This is the ugliest thing ever but I don't want to lose it.
131
+ # All the WebDriver calls are getting app-mapped and it's really unclear
132
+ # what they are.
133
+ page.driver.options[:http_client].instance_variable_get('@server_url').port
134
+ end
135
+
127
136
  warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
128
137
  @trace = AppMap.tracing.trace
138
+ @webdriver_port = webdriver_port.()
129
139
  end
130
140
 
131
141
  def finish
@@ -3,9 +3,10 @@
3
3
  module AppMap
4
4
  module Trace
5
5
  class ScopedMethod < SimpleDelegator
6
- attr_reader :defined_class, :static
7
-
8
- def initialize(defined_class, method, static)
6
+ attr_reader :package, :defined_class, :static
7
+
8
+ def initialize(package, defined_class, method, static)
9
+ @package = package
9
10
  @defined_class = defined_class
10
11
  @static = static
11
12
  super(method)
@@ -32,9 +33,9 @@ module AppMap
32
33
  @tracing.any?(&:enabled?)
33
34
  end
34
35
 
35
- def record_event(event, defined_class: nil, method: nil)
36
+ def record_event(event, package: nil, defined_class: nil, method: nil)
36
37
  @tracing.each do |tracer|
37
- tracer.record_event(event, defined_class: defined_class, method: method)
38
+ tracer.record_event(event, package: package, defined_class: defined_class, method: method)
38
39
  end
39
40
  end
40
41
 
@@ -71,11 +72,12 @@ module AppMap
71
72
  # Record a program execution event.
72
73
  #
73
74
  # The event should be one of the MethodEvent subclasses.
74
- def record_event(event, defined_class: nil, method: nil)
75
+ def record_event(event, package: nil, defined_class: nil, method: nil)
75
76
  return unless @enabled
76
77
 
77
78
  @events << event
78
- @methods << Trace::ScopedMethod.new(defined_class, method, event.static) if (defined_class && method && event.event == :call)
79
+ @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
80
+ if package && defined_class && method && (event.event == :call)
79
81
  end
80
82
 
81
83
  # Gets a unique list of the methods that were invoked by the program.
@@ -43,7 +43,7 @@ module AppMap
43
43
  require 'hashie'
44
44
  h.extend(Hashie::Extensions::DeepLocate)
45
45
  keys = %i(path location)
46
- entries = h.deep_locate ->(k,v,o) {
46
+ h.deep_locate ->(k,v,o) {
47
47
  next unless keys.include?(k)
48
48
 
49
49
  fix = ->(v) {v.gsub(%r{#{Gem.dir}/gems/.*(?=lib)}, '')}
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.34.4'
6
+ VERSION = '0.36.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -39,7 +39,7 @@ describe 'AbstractControllerBase' do
39
39
  expect(appmap).to include(<<-SERVER_REQUEST.strip)
40
40
  http_server_request:
41
41
  request_method: POST
42
- path_info: "/api/users?login=alice&password=foobar
42
+ path_info: "/api/users"
43
43
  SERVER_REQUEST
44
44
  end
45
45
  it 'Properly captures method parameters in the appmap' do
@@ -45,6 +45,12 @@ describe 'AbstractControllerBase' do
45
45
  request_method: POST
46
46
  path_info: "/api/users"
47
47
  SERVER_REQUEST
48
+
49
+ expect(appmap).to include(<<-SERVER_RESPONSE.strip)
50
+ http_server_response:
51
+ status: 201
52
+ mime_type: application/json; charset=utf-8
53
+ SERVER_RESPONSE
48
54
  end
49
55
 
50
56
  it 'properly captures method parameters in the appmap' do
@@ -9,3 +9,47 @@ class ExceptionMethod
9
9
  raise 'Exception occurred in raise_exception'
10
10
  end
11
11
  end
12
+
13
+ # subclass from BasicObject so we don't get #to_s. Requires some
14
+ # hackery to implement the other methods normally provided by Object.
15
+ class NoToSMethod < BasicObject
16
+ def is_a?(*args)
17
+ return false
18
+ end
19
+
20
+ def class
21
+ return ::Class
22
+ end
23
+
24
+ def respond_to?(*args)
25
+ return false
26
+ end
27
+
28
+ def inspect
29
+ "NoToSMethod"
30
+ end
31
+
32
+ def say_hello
33
+ "hello"
34
+ end
35
+ end
36
+
37
+ class InspectRaises < NoToSMethod
38
+ def inspect
39
+ ::Kernel.raise "#to_s missing, #inspect raises"
40
+ end
41
+
42
+ def say_hello
43
+ "hello"
44
+ end
45
+ end
46
+
47
+ class ToSRaises
48
+ def to_s
49
+ raise "#to_s raises"
50
+ end
51
+
52
+ def say_hello
53
+ "hello"
54
+ end
55
+ end
@@ -27,7 +27,7 @@ describe 'AppMap class Hooking', docker: false do
27
27
 
28
28
  def invoke_test_file(file, setup: nil, &block)
29
29
  AppMap.configuration = nil
30
- package = AppMap::Config::Package.new(file)
30
+ package = AppMap::Config::Package.build_from_path(file)
31
31
  config = AppMap::Config.new('hook_spec', [ package ])
32
32
  AppMap.configuration = config
33
33
  tracer = nil
@@ -465,6 +465,132 @@ describe 'AppMap class Hooking', docker: false do
465
465
  end
466
466
  end
467
467
 
468
+ context 'string conversions works for the receiver when' do
469
+
470
+ it 'is missing #to_s' do
471
+ events_yaml = <<~YAML
472
+ ---
473
+ - :id: 1
474
+ :event: :call
475
+ :defined_class: NoToSMethod
476
+ :method_id: respond_to?
477
+ :path: spec/fixtures/hook/exception_method.rb
478
+ :lineno: 24
479
+ :static: false
480
+ :parameters:
481
+ - :name: :args
482
+ :class: Symbol
483
+ :value: to_s
484
+ :kind: :rest
485
+ :receiver:
486
+ :class: Class
487
+ :value: NoToSMethod
488
+ - :id: 2
489
+ :event: :return
490
+ :parent_id: 1
491
+ - :id: 3
492
+ :event: :call
493
+ :defined_class: NoToSMethod
494
+ :method_id: say_hello
495
+ :path: spec/fixtures/hook/exception_method.rb
496
+ :lineno: 32
497
+ :static: false
498
+ :parameters: []
499
+ :receiver:
500
+ :class: Class
501
+ :value: NoToSMethod
502
+ - :id: 4
503
+ :event: :return
504
+ :parent_id: 3
505
+ :return_value:
506
+ :class: String
507
+ :value: hello
508
+ YAML
509
+
510
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
511
+ inst = NoToSMethod.new
512
+ # sanity check
513
+ expect(inst).not_to respond_to(:to_s)
514
+ inst.say_hello
515
+ end
516
+ end
517
+
518
+ it 'it is missing #to_s and it raises an exception in #inspect' do
519
+ events_yaml = <<~YAML
520
+ ---
521
+ - :id: 1
522
+ :event: :call
523
+ :defined_class: NoToSMethod
524
+ :method_id: respond_to?
525
+ :path: spec/fixtures/hook/exception_method.rb
526
+ :lineno: 24
527
+ :static: false
528
+ :parameters:
529
+ - :name: :args
530
+ :class: Symbol
531
+ :value: to_s
532
+ :kind: :rest
533
+ :receiver:
534
+ :class: Class
535
+ :value: "*Error inspecting variable*"
536
+ - :id: 2
537
+ :event: :return
538
+ :parent_id: 1
539
+ - :id: 3
540
+ :event: :call
541
+ :defined_class: InspectRaises
542
+ :method_id: say_hello
543
+ :path: spec/fixtures/hook/exception_method.rb
544
+ :lineno: 42
545
+ :static: false
546
+ :parameters: []
547
+ :receiver:
548
+ :class: Class
549
+ :value: "*Error inspecting variable*"
550
+ - :id: 4
551
+ :event: :return
552
+ :parent_id: 3
553
+ :return_value:
554
+ :class: String
555
+ :value: hello
556
+ YAML
557
+
558
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
559
+ inst = InspectRaises.new
560
+ # sanity check
561
+ expect(inst).not_to respond_to(:to_s)
562
+ inst.say_hello
563
+ end
564
+ end
565
+
566
+ it 'it raises an exception in #to_s' do
567
+ events_yaml = <<~YAML
568
+ ---
569
+ - :id: 1
570
+ :event: :call
571
+ :defined_class: ToSRaises
572
+ :method_id: say_hello
573
+ :path: spec/fixtures/hook/exception_method.rb
574
+ :lineno: 52
575
+ :static: false
576
+ :parameters: []
577
+ :receiver:
578
+ :class: ToSRaises
579
+ :value: "*Error inspecting variable*"
580
+ - :id: 2
581
+ :event: :return
582
+ :parent_id: 1
583
+ :return_value:
584
+ :class: String
585
+ :value: hello
586
+ YAML
587
+
588
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
589
+ ToSRaises.new.say_hello
590
+ end
591
+ end
592
+ end
593
+
468
594
  it 're-raises exceptions' do
469
595
  RSpec::Expectations.configuration.on_potential_false_positives = :nothing
470
596
 
@@ -624,7 +750,7 @@ describe 'AppMap class Hooking', docker: false do
624
750
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
625
751
  expect(Compare.compare('string', 'string')).to be_truthy
626
752
  end
627
- cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(config, tracer.event_methods))
753
+ cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(tracer.event_methods))
628
754
  entry = cm[1][:children][0][:children][0][:children][0]
629
755
  # Sanity check, make sure we got the right one
630
756
  expect(entry[:name]).to eq('secure_compare')
@@ -669,4 +795,11 @@ describe 'AppMap class Hooking', docker: false do
669
795
  end
670
796
  end
671
797
  end
798
+
799
+ it "preserves the arity of hooked methods" do
800
+ invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
801
+ expect(InstanceMethod.instance_method(:say_echo).arity).to be(1)
802
+ expect(InstanceMethod.new.method(:say_echo).arity).to be(1)
803
+ end
804
+ end
672
805
  end