appmap 0.32.0 → 0.34.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,22 +7,17 @@ module AppMap
7
7
  LOG = (ENV['DEBUG'] == 'true')
8
8
 
9
9
  class << self
10
+ def lock_builtins
11
+ return if @builtins_hooked
12
+
13
+ @builtins_hooked = true
14
+ end
15
+
10
16
  # Return the class, separator ('.' or '#'), and method name for
11
17
  # the given method.
12
18
  def qualify_method_name(method)
13
19
  if method.owner.singleton_class?
14
- # Singleton class names can take two forms:
15
- # #<Class:Foo> or
16
- # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
17
- # the class from the string.
18
- #
19
- # (There really isn't a better way to do this. The
20
- # singleton's reference to the class it was created
21
- # from is stored in an instance variable named
22
- # '__attached__'. It doesn't have the '@' prefix, so
23
- # it's internal only, and not accessible from user
24
- # code.)
25
- class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
20
+ class_name = singleton_method_owner_name(method)
26
21
  [ class_name, '.', method.name ]
27
22
  else
28
23
  [ method.owner.name, '#', method.name ]
@@ -39,6 +34,8 @@ module AppMap
39
34
  def enable &block
40
35
  require 'appmap/hook/method'
41
36
 
37
+ hook_builtins
38
+
42
39
  tp = TracePoint.new(:end) do |trace_point|
43
40
  cls = trace_point.self
44
41
 
@@ -47,12 +44,10 @@ module AppMap
47
44
 
48
45
  hook = lambda do |hook_cls|
49
46
  lambda do |method_id|
50
- next if method_id.to_s =~ /_hooked_by_appmap$/
51
-
52
47
  method = hook_cls.public_instance_method(method_id)
53
48
  hook_method = Hook::Method.new(hook_cls, method)
54
49
 
55
- warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
50
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
56
51
 
57
52
  disasm = RubyVM::InstructionSequence.disasm(method)
58
53
  # Skip methods that have no instruction sequence, as they are obviously trivial.
@@ -63,7 +58,7 @@ module AppMap
63
58
  next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
64
59
 
65
60
  next unless \
66
- config.always_hook?(hook_method.defined_class, method.name) ||
61
+ config.always_hook?(hook_cls, method.name) ||
67
62
  config.included_by_location?(method)
68
63
 
69
64
  hook_method.activate
@@ -76,5 +71,35 @@ module AppMap
76
71
 
77
72
  tp.enable(&block)
78
73
  end
74
+
75
+ def hook_builtins
76
+ return unless self.class.lock_builtins
77
+
78
+ class_from_string = lambda do |fq_class|
79
+ fq_class.split('::').inject(Object) do |mod, class_name|
80
+ mod.const_get(class_name)
81
+ end
82
+ end
83
+
84
+ Config::BUILTIN_METHODS.each do |class_name, hook|
85
+ require hook.package.package_name if hook.package.package_name
86
+ Array(hook.method_names).each do |method_name|
87
+ method_name = method_name.to_sym
88
+ cls = class_from_string.(class_name)
89
+ method = \
90
+ begin
91
+ cls.instance_method(method_name)
92
+ rescue NameError
93
+ cls.method(method_name) rescue nil
94
+ end
95
+
96
+ if method
97
+ Hook::Method.new(cls, method).activate
98
+ else
99
+ warn "Method #{method_name} not found on #{cls.name}"
100
+ end
101
+ end
102
+ end
103
+ end
79
104
  end
80
105
  end
@@ -1,21 +1,41 @@
1
1
  module AppMap
2
2
  class Hook
3
3
  class Method
4
- attr_reader :hook_class, :hook_method, :defined_class, :method_display_name
4
+ attr_reader :hook_class, :hook_method
5
+
6
+ # +method_display_name+ may be nil if name resolution gets
7
+ # deferred until runtime (e.g. for a singleton method on an
8
+ # embedded Struct).
9
+ attr_reader :method_display_name
5
10
 
6
11
  HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
7
12
  private_constant :HOOK_DISABLE_KEY
8
13
 
14
+ # Grab the definition of Time.now here, to avoid interfering
15
+ # with the method we're hooking.
16
+ TIME_NOW = Time.method(:now)
17
+ private_constant :TIME_NOW
18
+
9
19
  def initialize(hook_class, hook_method)
10
20
  @hook_class = hook_class
11
21
  @hook_method = hook_method
22
+
23
+ # Get the class for the method, if it's known.
12
24
  @defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
13
- @method_display_name = [@defined_class, method_symbol, @hook_method.name].join
25
+ @method_display_name = [@defined_class, method_symbol, @hook_method.name].join if @defined_class
14
26
  end
15
27
 
16
28
  def activate
17
- warn "AppMap: Hooking #{method_display_name}" if Hook::LOG
29
+ if Hook::LOG
30
+ msg = if method_display_name
31
+ "#{method_display_name}"
32
+ else
33
+ "#{hook_method.name} (class resolution deferrred)"
34
+ end
35
+ warn "AppMap: Hooking " + msg
36
+ end
18
37
 
38
+ defined_class = @defined_class
19
39
  hook_method = self.hook_method
20
40
  before_hook = self.method(:before_hook)
21
41
  after_hook = self.method(:after_hook)
@@ -24,12 +44,17 @@ module AppMap
24
44
  hook_class.define_method hook_method.name do |*args, &block|
25
45
  instance_method = hook_method.bind(self).to_proc
26
46
 
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
51
+
27
52
  hook_disabled = Thread.current[HOOK_DISABLE_KEY]
28
53
  enabled = true if !hook_disabled && AppMap.tracing.enabled?
29
54
  return instance_method.call(*args, &block) unless enabled
30
55
 
31
56
  call_event, start_time = with_disabled_hook.() do
32
- before_hook.(self, args)
57
+ before_hook.(self, defined_class, args)
33
58
  end
34
59
  return_value = nil
35
60
  exception = nil
@@ -48,16 +73,16 @@ module AppMap
48
73
 
49
74
  protected
50
75
 
51
- def before_hook(receiver, args)
76
+ def before_hook(receiver, defined_class, args)
52
77
  require 'appmap/event'
53
78
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
54
79
  AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
55
- [ call_event, Time.now ]
80
+ [ call_event, TIME_NOW.call ]
56
81
  end
57
82
 
58
83
  def after_hook(call_event, start_time, return_value, exception)
59
84
  require 'appmap/event'
60
- elapsed = Time.now - start_time
85
+ elapsed = TIME_NOW.call - start_time
61
86
  return_event = \
62
87
  AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
63
88
  AppMap.tracing.record_event return_event
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ OpenStruct = Struct.new(:appmap)
5
+
6
+ class Open < OpenStruct
7
+ attr_reader :port
8
+
9
+ def perform
10
+ server = run_server
11
+ open_browser
12
+ server.kill
13
+ end
14
+
15
+ def page
16
+ require 'rack/utils'
17
+ <<~PAGE
18
+ <!DOCTYPE html>
19
+ <html>
20
+ <head>
21
+ <title>&hellip;</title>
22
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
23
+ <script type="text/javascript">
24
+ function dosubmit() { document.forms[0].submit(); }
25
+ </script>
26
+ </head>
27
+ <body onload="dosubmit();">
28
+ <form action="https://app.land/scenario_uploads" method="POST" accept-charset="utf-8">
29
+ <input type="hidden" name="data" value='#{Rack::Utils.escape_html appmap.to_json}'>
30
+ </form>
31
+ </body>
32
+ </html>
33
+ PAGE
34
+ end
35
+
36
+ def run_server
37
+ require 'rack'
38
+ Thread.new do
39
+ Rack::Handler::WEBrick.run(
40
+ lambda do |env|
41
+ return [200, { 'Content-Type' => 'text/html' }, [page]]
42
+ end,
43
+ :Port => 0
44
+ ) do |server|
45
+ @port = server.config[:Port]
46
+ end
47
+ end.tap do
48
+ sleep 1.0
49
+ end
50
+ end
51
+
52
+ def open_browser
53
+ system 'open', "http://localhost:#{@port}"
54
+ sleep 5.0
55
+ end
56
+ end
57
+ end
@@ -73,20 +73,15 @@ module AppMap
73
73
 
74
74
  class ActiveRecordExaminer
75
75
  def server_version
76
- case database_type
77
- when :postgres
78
- ActiveRecord::Base.connection.postgresql_version
79
- when :sqlite
80
- ActiveRecord::Base.connection.database_version.to_s
81
- else
82
- warn "Unable to determine database version for #{database_type.inspect}"
83
- end
76
+ ActiveRecord::Base.connection.try(:database_version) ||\
77
+ warn("Unable to determine database version for #{database_type.inspect}")
84
78
  end
85
79
 
86
80
  def database_type
87
- return :postgres if ActiveRecord::Base.connection.respond_to?(:postgresql_version)
81
+ type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
82
+ type = :postgres if type == :postgresql
88
83
 
89
- ActiveRecord::Base.connection.adapter_name.downcase.to_sym
84
+ type
90
85
  end
91
86
 
92
87
  def execute_query(sql)
@@ -154,7 +154,7 @@ module AppMap
154
154
  end
155
155
 
156
156
  labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
157
- description.reject!(&:nil?).reject(&:blank?)
157
+ description.reject!(&:nil?).reject!(&:blank?)
158
158
  default_description = description.last
159
159
  description.reverse!
160
160
 
@@ -36,6 +36,23 @@ module AppMap
36
36
  [ fname, extension ].join
37
37
  end
38
38
 
39
+ # sanitize_paths removes ephemeral values from objects with
40
+ # embedded paths (e.g. an event or a classmap), making events
41
+ # easier to compare across runs.
42
+ def sanitize_paths(h)
43
+ require 'hashie'
44
+ h.extend(Hashie::Extensions::DeepLocate)
45
+ keys = %i(path location)
46
+ entries = h.deep_locate ->(k,v,o) {
47
+ next unless keys.include?(k)
48
+
49
+ fix = ->(v) {v.gsub(%r{#{Gem.dir}/gems/.*(?=lib)}, '')}
50
+ keys.each {|k| o[k] = fix.(o[k]) if o[k] }
51
+ }
52
+
53
+ h
54
+ end
55
+
39
56
  # sanitize_event removes ephemeral values from an event, making
40
57
  # events easier to compare across runs.
41
58
  def sanitize_event(event, &block)
@@ -49,7 +66,7 @@ module AppMap
49
66
 
50
67
  case event[:event]
51
68
  when :call
52
- event[:path] = event[:path].gsub(Gem.dir + '/', '')
69
+ sanitize_paths(event)
53
70
  end
54
71
 
55
72
  event
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.32.0'
6
+ VERSION = '0.34.4'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -20,4 +20,8 @@ class InstanceMethod
20
20
  def say_block(&block)
21
21
  yield
22
22
  end
23
+
24
+ def say_the_time
25
+ Time.now.to_s
26
+ end
23
27
  end
@@ -15,6 +15,20 @@ class SingletonMethod
15
15
  'defined with self class scope'
16
16
  end
17
17
 
18
+ module AddMethod
19
+ def self.included(base)
20
+ base.module_eval do
21
+ define_method "added_method" do
22
+ _added_method
23
+ end
24
+ end
25
+ end
26
+
27
+ def _added_method
28
+ 'defined by including a module'
29
+ end
30
+ end
31
+
18
32
  # When called, do_include calls +include+ to bring in the module
19
33
  # AddMethod. AddMethod defines a new instance method, which gets
20
34
  # added to the singleton class of SingletonMethod.
@@ -32,23 +46,18 @@ class SingletonMethod
32
46
  end
33
47
  end
34
48
  end
35
-
36
- def to_s
37
- 'Singleton Method fixture'
38
- end
39
- end
40
49
 
41
- module AddMethod
42
- def self.included(base)
43
- base.module_eval do
44
- define_method "added_method" do
45
- _added_method
50
+ STRUCT_TEST = Struct.new(:attr) do
51
+ class << self
52
+ def say_struct_singleton
53
+ 'singleton for a struct'
46
54
  end
47
55
  end
48
56
  end
49
57
 
50
- def _added_method
51
- 'defined by including a module'
58
+ def to_s
59
+ 'Singleton Method fixture'
52
60
  end
53
61
  end
54
62
 
63
+
@@ -22,12 +22,12 @@ describe 'AppMap class Hooking', docker: false do
22
22
  while tracer.event?
23
23
  events << tracer.next_event.to_h
24
24
  end
25
- end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event))
26
26
  end
27
27
 
28
28
  def invoke_test_file(file, setup: nil, &block)
29
29
  AppMap.configuration = nil
30
- package = AppMap::Package.new(file, nil, [])
30
+ package = AppMap::Config::Package.new(file)
31
31
  config = AppMap::Config.new('hook_spec', [ package ])
32
32
  AppMap.configuration = config
33
33
  tracer = nil
@@ -50,8 +50,9 @@ describe 'AppMap class Hooking', docker: false do
50
50
  def test_hook_behavior(file, events_yaml, setup: nil, &block)
51
51
  config, tracer = invoke_test_file(file, setup: setup, &block)
52
52
 
53
- events = collect_events(tracer)
54
- expect(Diffy::Diff.new(events, events_yaml).to_s).to eq('')
53
+ events = collect_events(tracer).to_yaml
54
+
55
+ expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
55
56
 
56
57
  [ config, tracer ]
57
58
  end
@@ -99,7 +100,7 @@ describe 'AppMap class Hooking', docker: false do
99
100
  InstanceMethod.new.say_default
100
101
  end
101
102
  class_map = AppMap.class_map(tracer.event_methods).to_yaml
102
- expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
103
+ expect(Diffy::Diff.new(<<~YAML, class_map).to_s).to eq('')
103
104
  ---
104
105
  - :name: spec/fixtures/hook/instance_method.rb
105
106
  :type: package
@@ -341,7 +342,7 @@ describe 'AppMap class Hooking', docker: false do
341
342
  :defined_class: SingletonMethod
342
343
  :method_id: added_method
343
344
  :path: spec/fixtures/hook/singleton_method.rb
344
- :lineno: 44
345
+ :lineno: 21
345
346
  :static: false
346
347
  :parameters: []
347
348
  :receiver:
@@ -349,10 +350,10 @@ describe 'AppMap class Hooking', docker: false do
349
350
  :value: Singleton Method fixture
350
351
  - :id: 2
351
352
  :event: :call
352
- :defined_class: AddMethod
353
+ :defined_class: SingletonMethod::AddMethod
353
354
  :method_id: _added_method
354
355
  :path: spec/fixtures/hook/singleton_method.rb
355
- :lineno: 50
356
+ :lineno: 27
356
357
  :static: false
357
358
  :parameters: []
358
359
  :receiver:
@@ -394,10 +395,44 @@ describe 'AppMap class Hooking', docker: false do
394
395
  load 'spec/fixtures/hook/singleton_method.rb'
395
396
  setup = -> { SingletonMethod.new_with_instance_method }
396
397
  test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
398
+ # Make sure we're testing the right thing
399
+ say_instance_defined = s.method(:say_instance_defined)
400
+ expect(say_instance_defined.owner.to_s).to start_with('#<Class:#<SingletonMethod:')
401
+
402
+ # Verify the native extension works as expected
403
+ expect(AppMap::Hook.singleton_method_owner_name(say_instance_defined)).to eq('SingletonMethod')
404
+
397
405
  expect(s.say_instance_defined).to eq('defined for an instance')
398
406
  end
399
407
  end
400
408
 
409
+ it 'hooks a singleton method on an embedded struct' do
410
+ events_yaml = <<~YAML
411
+ ---
412
+ - :id: 1
413
+ :event: :call
414
+ :defined_class: SingletonMethod::STRUCT_TEST
415
+ :method_id: say_struct_singleton
416
+ :path: spec/fixtures/hook/singleton_method.rb
417
+ :lineno: 52
418
+ :static: true
419
+ :parameters: []
420
+ :receiver:
421
+ :class: Class
422
+ :value: SingletonMethod::STRUCT_TEST
423
+ - :id: 2
424
+ :event: :return
425
+ :parent_id: 1
426
+ :return_value:
427
+ :class: String
428
+ :value: singleton for a struct
429
+ YAML
430
+
431
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
432
+ expect(SingletonMethod::STRUCT_TEST.say_struct_singleton).to eq('singleton for a struct')
433
+ end
434
+ end
435
+
401
436
  it 'Reports exceptions' do
402
437
  events_yaml = <<~YAML
403
438
  ---
@@ -465,7 +500,7 @@ describe 'AppMap class Hooking', docker: false do
465
500
  :event: :call
466
501
  :defined_class: ActiveSupport::SecurityUtils
467
502
  :method_id: secure_compare
468
- :path: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb
503
+ :path: lib/active_support/security_utils.rb
469
504
  :lineno: 26
470
505
  :static: true
471
506
  :parameters:
@@ -481,12 +516,52 @@ describe 'AppMap class Hooking', docker: false do
481
516
  :class: Module
482
517
  :value: ActiveSupport::SecurityUtils
483
518
  - :id: 3
519
+ :event: :call
520
+ :defined_class: Digest::Instance
521
+ :method_id: digest
522
+ :path: Digest::Instance#digest
523
+ :static: false
524
+ :parameters:
525
+ - :name: arg
526
+ :class: String
527
+ :value: string
528
+ :kind: :rest
529
+ :receiver:
530
+ :class: Digest::SHA256
531
+ :value: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
532
+ - :id: 4
533
+ :event: :return
534
+ :parent_id: 3
535
+ :return_value:
536
+ :class: String
537
+ :value: "G2__)__qc____X____3_].\\x02y__.___/_"
538
+ - :id: 5
539
+ :event: :call
540
+ :defined_class: Digest::Instance
541
+ :method_id: digest
542
+ :path: Digest::Instance#digest
543
+ :static: false
544
+ :parameters:
545
+ - :name: arg
546
+ :class: String
547
+ :value: string
548
+ :kind: :rest
549
+ :receiver:
550
+ :class: Digest::SHA256
551
+ :value: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
552
+ - :id: 6
553
+ :event: :return
554
+ :parent_id: 5
555
+ :return_value:
556
+ :class: String
557
+ :value: "G2__)__qc____X____3_].\\x02y__.___/_"
558
+ - :id: 7
484
559
  :event: :return
485
560
  :parent_id: 2
486
561
  :return_value:
487
562
  :class: TrueClass
488
563
  :value: 'true'
489
- - :id: 4
564
+ - :id: 8
490
565
  :event: :return
491
566
  :parent_id: 1
492
567
  :return_value:
@@ -523,22 +598,75 @@ describe 'AppMap class Hooking', docker: false do
523
598
  :children:
524
599
  - :name: secure_compare
525
600
  :type: function
526
- :location: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb:26
601
+ :location: lib/active_support/security_utils.rb:26
527
602
  :static: true
528
603
  :labels:
529
604
  - security
605
+ - crypto
606
+ - :name: openssl
607
+ :type: package
608
+ :children:
609
+ - :name: Digest
610
+ :type: class
611
+ :children:
612
+ - :name: Instance
613
+ :type: class
614
+ :children:
615
+ - :name: digest
616
+ :type: function
617
+ :location: Digest::Instance#digest
618
+ :static: false
619
+ :labels:
620
+ - security
621
+ - crypto
530
622
  YAML
531
623
 
532
624
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
533
625
  expect(Compare.compare('string', 'string')).to be_truthy
534
626
  end
535
- cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
627
+ cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(config, tracer.event_methods))
536
628
  entry = cm[1][:children][0][:children][0][:children][0]
537
629
  # Sanity check, make sure we got the right one
538
630
  expect(entry[:name]).to eq('secure_compare')
539
631
  spec = Gem::Specification.find_by_name('activesupport')
540
632
  entry[:location].gsub!(spec.base_dir + '/', '')
541
- expect(Diffy::Diff.new(cm.to_yaml, classmap_yaml).to_s).to eq('')
633
+ expect(Diffy::Diff.new(classmap_yaml, cm.to_yaml).to_s).to eq('')
634
+ end
635
+ end
636
+
637
+ it "doesn't cause expectations on Time.now to fail" do
638
+ events_yaml = <<~YAML
639
+ ---
640
+ - :id: 1
641
+ :event: :call
642
+ :defined_class: InstanceMethod
643
+ :method_id: say_the_time
644
+ :path: spec/fixtures/hook/instance_method.rb
645
+ :lineno: 24
646
+ :static: false
647
+ :parameters: []
648
+ :receiver:
649
+ :class: InstanceMethod
650
+ :value: Instance Method fixture
651
+ - :id: 2
652
+ :event: :return
653
+ :parent_id: 1
654
+ :return_value:
655
+ :class: String
656
+ :value: '2020-01-01 00:00:00 +0000'
657
+ YAML
658
+ test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
659
+ require 'timecop'
660
+ begin
661
+ tz = ENV['TZ']
662
+ ENV['TZ'] = 'UTC'
663
+ Timecop.freeze(Time.utc('2020-01-01')) do
664
+ expect(Time).to receive(:now).exactly(3).times.and_call_original
665
+ expect(InstanceMethod.new.say_the_time).to be
666
+ end
667
+ ensure
668
+ ENV['TZ'] = tz
669
+ end
542
670
  end
543
671
  end
544
672
  end