appmap 0.34.4 → 0.36.0

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