appmap 0.34.5 → 0.37.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
 
@@ -67,23 +70,24 @@ module AppMap
67
70
  raise
68
71
  ensure
69
72
  with_disabled_hook.() do
70
- after_hook.(call_event, start_time, return_value, exception)
73
+ after_hook.(self, call_event, start_time, return_value, exception)
71
74
  end
72
75
  end
73
76
  end
74
77
  end
75
78
  hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
76
79
  end
80
+
77
81
  protected
78
82
 
79
83
  def before_hook(receiver, defined_class, args)
80
84
  require 'appmap/event'
81
85
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
82
- 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
83
87
  [ call_event, TIME_NOW.call ]
84
88
  end
85
89
 
86
- def after_hook(call_event, start_time, return_value, exception)
90
+ def after_hook(receiver, call_event, start_time, return_value, exception)
87
91
  require 'appmap/event'
88
92
  elapsed = TIME_NOW.call - start_time
89
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
@@ -138,7 +148,7 @@ module AppMap
138
148
 
139
149
  AppMap::RSpec.add_event_methods @trace.event_methods
140
150
 
141
- class_map = AppMap.class_map(@trace.event_methods)
151
+ class_map = AppMap.class_map(@trace.event_methods, include_source: false)
142
152
 
143
153
  description = []
144
154
  scope = ScopeExample.new(example)
@@ -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.
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.34.5'
6
+ VERSION = '0.37.0'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.2'
8
+ APPMAP_FORMAT_VERSION = '1.3'
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
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'AppMap::ClassMap' do
6
+ describe '.build_from_methods' do
7
+ it 'includes source code if available' do
8
+ map = AppMap.class_map([scoped_method(method(:test_method))])
9
+ function = dig_map(map, 5)[0]
10
+ expect(function[:source]).to include 'test method body'
11
+ expect(function[:comment]).to include 'test method comment'
12
+ end
13
+
14
+ it 'can omit source code even if available' do
15
+ map = AppMap.class_map([scoped_method((method :test_method))], include_source: false)
16
+ function = dig_map(map, 5)[0]
17
+ expect(function).to_not include(:source)
18
+ expect(function).to_not include(:comment)
19
+ end
20
+
21
+ # test method comment
22
+ def test_method
23
+ 'test method body'
24
+ end
25
+
26
+ def scoped_method(method)
27
+ AppMap::Trace::ScopedMethod.new AppMap::Config::Package.new, method.receiver.class.name, method, false
28
+ end
29
+
30
+ def dig_map(map, depth)
31
+ return map if depth == 0
32
+
33
+ dig_map map[0][:children], depth - 1
34
+ end
35
+ end
36
+ end
@@ -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
@@ -112,6 +112,10 @@ describe 'AppMap class Hooking', docker: false do
112
112
  :type: function
113
113
  :location: spec/fixtures/hook/instance_method.rb:8
114
114
  :static: false
115
+ :source: |2
116
+ def say_default
117
+ 'default'
118
+ end
115
119
  YAML
116
120
  end
117
121
 
@@ -465,6 +469,132 @@ describe 'AppMap class Hooking', docker: false do
465
469
  end
466
470
  end
467
471
 
472
+ context 'string conversions works for the receiver when' do
473
+
474
+ it 'is missing #to_s' do
475
+ events_yaml = <<~YAML
476
+ ---
477
+ - :id: 1
478
+ :event: :call
479
+ :defined_class: NoToSMethod
480
+ :method_id: respond_to?
481
+ :path: spec/fixtures/hook/exception_method.rb
482
+ :lineno: 24
483
+ :static: false
484
+ :parameters:
485
+ - :name: :args
486
+ :class: Symbol
487
+ :value: to_s
488
+ :kind: :rest
489
+ :receiver:
490
+ :class: Class
491
+ :value: NoToSMethod
492
+ - :id: 2
493
+ :event: :return
494
+ :parent_id: 1
495
+ - :id: 3
496
+ :event: :call
497
+ :defined_class: NoToSMethod
498
+ :method_id: say_hello
499
+ :path: spec/fixtures/hook/exception_method.rb
500
+ :lineno: 32
501
+ :static: false
502
+ :parameters: []
503
+ :receiver:
504
+ :class: Class
505
+ :value: NoToSMethod
506
+ - :id: 4
507
+ :event: :return
508
+ :parent_id: 3
509
+ :return_value:
510
+ :class: String
511
+ :value: hello
512
+ YAML
513
+
514
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
515
+ inst = NoToSMethod.new
516
+ # sanity check
517
+ expect(inst).not_to respond_to(:to_s)
518
+ inst.say_hello
519
+ end
520
+ end
521
+
522
+ it 'it is missing #to_s and it raises an exception in #inspect' do
523
+ events_yaml = <<~YAML
524
+ ---
525
+ - :id: 1
526
+ :event: :call
527
+ :defined_class: NoToSMethod
528
+ :method_id: respond_to?
529
+ :path: spec/fixtures/hook/exception_method.rb
530
+ :lineno: 24
531
+ :static: false
532
+ :parameters:
533
+ - :name: :args
534
+ :class: Symbol
535
+ :value: to_s
536
+ :kind: :rest
537
+ :receiver:
538
+ :class: Class
539
+ :value: "*Error inspecting variable*"
540
+ - :id: 2
541
+ :event: :return
542
+ :parent_id: 1
543
+ - :id: 3
544
+ :event: :call
545
+ :defined_class: InspectRaises
546
+ :method_id: say_hello
547
+ :path: spec/fixtures/hook/exception_method.rb
548
+ :lineno: 42
549
+ :static: false
550
+ :parameters: []
551
+ :receiver:
552
+ :class: Class
553
+ :value: "*Error inspecting variable*"
554
+ - :id: 4
555
+ :event: :return
556
+ :parent_id: 3
557
+ :return_value:
558
+ :class: String
559
+ :value: hello
560
+ YAML
561
+
562
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
563
+ inst = InspectRaises.new
564
+ # sanity check
565
+ expect(inst).not_to respond_to(:to_s)
566
+ inst.say_hello
567
+ end
568
+ end
569
+
570
+ it 'it raises an exception in #to_s' do
571
+ events_yaml = <<~YAML
572
+ ---
573
+ - :id: 1
574
+ :event: :call
575
+ :defined_class: ToSRaises
576
+ :method_id: say_hello
577
+ :path: spec/fixtures/hook/exception_method.rb
578
+ :lineno: 52
579
+ :static: false
580
+ :parameters: []
581
+ :receiver:
582
+ :class: ToSRaises
583
+ :value: "*Error inspecting variable*"
584
+ - :id: 2
585
+ :event: :return
586
+ :parent_id: 1
587
+ :return_value:
588
+ :class: String
589
+ :value: hello
590
+ YAML
591
+
592
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
593
+ ToSRaises.new.say_hello
594
+ end
595
+ end
596
+ end
597
+
468
598
  it 're-raises exceptions' do
469
599
  RSpec::Expectations.configuration.on_potential_false_positives = :nothing
470
600
 
@@ -587,6 +717,10 @@ describe 'AppMap class Hooking', docker: false do
587
717
  :type: function
588
718
  :location: spec/fixtures/hook/compare.rb:4
589
719
  :static: true
720
+ :source: |2
721
+ def self.compare(s1, s2)
722
+ ActiveSupport::SecurityUtils.secure_compare(s1, s2)
723
+ end
590
724
  - :name: active_support
591
725
  :type: package
592
726
  :children:
@@ -603,6 +737,15 @@ describe 'AppMap class Hooking', docker: false do
603
737
  :labels:
604
738
  - security
605
739
  - crypto
740
+ :comment: |
741
+ # Constant time string comparison, for variable length strings.
742
+ #
743
+ # The values are first processed by SHA256, so that we don't leak length info
744
+ # via timing attacks.
745
+ :source: |2
746
+ def secure_compare(a, b)
747
+ fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
748
+ end
606
749
  - :name: openssl
607
750
  :type: package
608
751
  :children:
@@ -624,7 +767,7 @@ describe 'AppMap class Hooking', docker: false do
624
767
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
625
768
  expect(Compare.compare('string', 'string')).to be_truthy
626
769
  end
627
- cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(config, tracer.event_methods))
770
+ cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(tracer.event_methods))
628
771
  entry = cm[1][:children][0][:children][0][:children][0]
629
772
  # Sanity check, make sure we got the right one
630
773
  expect(entry[:name]).to eq('secure_compare')