appmap 0.31.0 → 0.34.2

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rbenv-gemsets +1 -0
  4. data/CHANGELOG.md +22 -0
  5. data/README.md +38 -4
  6. data/Rakefile +10 -3
  7. data/appmap.gemspec +5 -0
  8. data/ext/appmap/appmap.c +26 -0
  9. data/ext/appmap/extconf.rb +6 -0
  10. data/lib/appmap.rb +23 -10
  11. data/lib/appmap/class_map.rb +13 -7
  12. data/lib/appmap/config.rb +54 -30
  13. data/lib/appmap/cucumber.rb +19 -2
  14. data/lib/appmap/event.rb +25 -16
  15. data/lib/appmap/hook.rb +52 -77
  16. data/lib/appmap/hook/method.rb +103 -0
  17. data/lib/appmap/open.rb +57 -0
  18. data/lib/appmap/rails/action_handler.rb +7 -7
  19. data/lib/appmap/rails/sql_handler.rb +10 -8
  20. data/lib/appmap/rspec.rb +1 -1
  21. data/lib/appmap/trace.rb +7 -7
  22. data/lib/appmap/util.rb +19 -0
  23. data/lib/appmap/version.rb +1 -1
  24. data/spec/abstract_controller4_base_spec.rb +1 -1
  25. data/spec/abstract_controller_base_spec.rb +9 -2
  26. data/spec/fixtures/hook/instance_method.rb +4 -0
  27. data/spec/fixtures/hook/singleton_method.rb +21 -12
  28. data/spec/hook_spec.rb +140 -44
  29. data/spec/open_spec.rb +19 -0
  30. data/spec/record_sql_rails_pg_spec.rb +56 -33
  31. data/test/cli_test.rb +12 -2
  32. data/test/fixtures/openssl_recorder/Gemfile +3 -0
  33. data/test/fixtures/openssl_recorder/appmap.yml +3 -0
  34. data/{spec/fixtures/hook/openssl_sign.rb → test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb} +11 -4
  35. data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
  36. data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
  37. data/test/openssl_test.rb +203 -0
  38. data/test/test_helper.rb +1 -0
  39. metadata +58 -4
@@ -16,11 +16,11 @@ module AppMap
16
16
  class HTTPServerRequest
17
17
  include ContextKey
18
18
 
19
- class Call < AppMap::Event::MethodEvent
19
+ class Call < AppMap::Event::MethodCall
20
20
  attr_accessor :payload
21
21
 
22
- def initialize(path, lineno, payload)
23
- super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, Thread.current.object_id
22
+ def initialize(payload)
23
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
24
24
 
25
25
  self.payload = payload
26
26
  end
@@ -47,7 +47,7 @@ module AppMap
47
47
  end
48
48
 
49
49
  def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
50
- event = Call.new(__FILE__, __LINE__, payload)
50
+ event = Call.new(payload)
51
51
  Thread.current[context_key] = Context.new(event.id, Time.now)
52
52
  AppMap.tracing.record_event(event)
53
53
  end
@@ -59,8 +59,8 @@ module AppMap
59
59
  class Call < AppMap::Event::MethodReturnIgnoreValue
60
60
  attr_accessor :payload
61
61
 
62
- def initialize(path, lineno, payload, parent_id, elapsed)
63
- super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, Thread.current.object_id
62
+ def initialize(payload, parent_id, elapsed)
63
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
64
64
 
65
65
  self.payload = payload
66
66
  self.parent_id = parent_id
@@ -82,7 +82,7 @@ module AppMap
82
82
  context = Thread.current[context_key]
83
83
  Thread.current[context_key] = nil
84
84
 
85
- event = Call.new(__FILE__, __LINE__, payload, context.id, Time.now - context.start_time)
85
+ event = Call.new(payload, context.id, Time.now - context.start_time)
86
86
  AppMap.tracing.record_event(event)
87
87
  end
88
88
  end
@@ -5,11 +5,11 @@ require 'appmap/event'
5
5
  module AppMap
6
6
  module Rails
7
7
  class SQLHandler
8
- class SQLCall < AppMap::Event::MethodEvent
8
+ class SQLCall < AppMap::Event::MethodCall
9
9
  attr_accessor :payload
10
10
 
11
- def initialize(path, lineno, payload)
12
- super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, Thread.current.object_id
11
+ def initialize(payload)
12
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
13
13
 
14
14
  self.payload = payload
15
15
  end
@@ -20,7 +20,7 @@ module AppMap
20
20
  sql: payload[:sql],
21
21
  database_type: payload[:database_type]
22
22
  }.tap do |sql_query|
23
- %i[server_version explain_sql].each do |attribute|
23
+ %i[server_version].each do |attribute|
24
24
  sql_query[attribute] = payload[attribute] if payload[attribute]
25
25
  end
26
26
  end
@@ -29,8 +29,8 @@ module AppMap
29
29
  end
30
30
 
31
31
  class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
32
- def initialize(path, lineno, parent_id, elapsed)
33
- super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, Thread.current.object_id
32
+ def initialize(parent_id, elapsed)
33
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
34
34
 
35
35
  self.parent_id = parent_id
36
36
  self.elapsed = elapsed
@@ -76,6 +76,8 @@ module AppMap
76
76
  case database_type
77
77
  when :postgres
78
78
  ActiveRecord::Base.connection.postgresql_version
79
+ when :sqlite
80
+ ActiveRecord::Base.connection.database_version.to_s
79
81
  else
80
82
  warn "Unable to determine database version for #{database_type.inspect}"
81
83
  end
@@ -133,9 +135,9 @@ module AppMap
133
135
 
134
136
  SQLExaminer.examine payload, sql: sql
135
137
 
136
- call = SQLCall.new(__FILE__, __LINE__, payload)
138
+ call = SQLCall.new(payload)
137
139
  AppMap.tracing.record_event(call)
138
- AppMap.tracing.record_event(SQLReturn.new(__FILE__, __LINE__, call.id, finished - started))
140
+ AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
139
141
  ensure
140
142
  Thread.current[reentry_key] = nil
141
143
  end
@@ -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
 
@@ -14,34 +14,34 @@ module AppMap
14
14
 
15
15
  class Tracing
16
16
  def initialize
17
- @Tracing = []
17
+ @tracing = []
18
18
  end
19
19
 
20
20
  def empty?
21
- @Tracing.empty?
21
+ @tracing.empty?
22
22
  end
23
23
 
24
24
  def trace(enable: true)
25
25
  Tracer.new.tap do |tracer|
26
- @Tracing << tracer
26
+ @tracing << tracer
27
27
  tracer.enable if enable
28
28
  end
29
29
  end
30
30
 
31
31
  def enabled?
32
- @Tracing.any?(&:enabled?)
32
+ @tracing.any?(&:enabled?)
33
33
  end
34
34
 
35
35
  def record_event(event, defined_class: nil, method: nil)
36
- @Tracing.each do |tracer|
36
+ @tracing.each do |tracer|
37
37
  tracer.record_event(event, defined_class: defined_class, method: method)
38
38
  end
39
39
  end
40
40
 
41
41
  def delete(tracer)
42
- return unless @Tracing.member?(tracer)
42
+ return unless @tracing.member?(tracer)
43
43
 
44
- @Tracing.delete(tracer)
44
+ @tracing.delete(tracer)
45
45
  tracer.disable
46
46
  end
47
47
  end
@@ -35,6 +35,25 @@ module AppMap
35
35
 
36
36
  [ fname, extension ].join
37
37
  end
38
+
39
+ # sanitize_event removes ephemeral values from an event, making
40
+ # events easier to compare across runs.
41
+ def sanitize_event(event, &block)
42
+ event.delete(:thread_id)
43
+ event.delete(:elapsed)
44
+ delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
45
+ delete_object_id.call(event[:receiver])
46
+ delete_object_id.call(event[:return_value])
47
+ (event[:parameters] || []).each(&delete_object_id)
48
+ (event[:exceptions] || []).each(&delete_object_id)
49
+
50
+ case event[:event]
51
+ when :call
52
+ event[:path] = event[:path].gsub(Gem.dir + '/', '')
53
+ end
54
+
55
+ event
56
+ end
38
57
  end
39
58
  end
40
59
  end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.31.0'
6
+ VERSION = '0.34.2'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -48,11 +48,11 @@ describe 'AbstractControllerBase' do
48
48
 
49
49
  expect(appmap).to match(<<-CREATE_CALL.strip)
50
50
  event: call
51
+ thread_id: .*
51
52
  defined_class: Api::UsersController
52
53
  method_id: build_user
53
54
  path: app/controllers/api/users_controller.rb
54
55
  lineno: 23
55
- thread_id: .*
56
56
  static: false
57
57
  parameters:
58
58
  - name: params
@@ -47,17 +47,17 @@ describe 'AbstractControllerBase' do
47
47
  SERVER_REQUEST
48
48
  end
49
49
 
50
- it 'Properly captures method parameters in the appmap' do
50
+ it 'properly captures method parameters in the appmap' do
51
51
  expect(File).to exist(appmap_json)
52
52
  appmap = JSON.parse(File.read(appmap_json)).to_yaml
53
53
 
54
54
  expect(appmap).to match(<<-CREATE_CALL.strip)
55
55
  event: call
56
+ thread_id: .*
56
57
  defined_class: Api::UsersController
57
58
  method_id: build_user
58
59
  path: app/controllers/api/users_controller.rb
59
60
  lineno: 23
60
- thread_id: .*
61
61
  static: false
62
62
  parameters:
63
63
  - name: params
@@ -68,5 +68,12 @@ describe 'AbstractControllerBase' do
68
68
  receiver:
69
69
  CREATE_CALL
70
70
  end
71
+
72
+ it 'returns a minimal event' do
73
+ expect(File).to exist(appmap_json)
74
+ appmap = JSON.parse(File.read(appmap_json))
75
+ event = appmap['events'].find { |event| event['event'] == 'return' && event['return_value'] }
76
+ expect(event.keys).to eq(%w[id event thread_id parent_id elapsed return_value])
77
+ end
71
78
  end
72
79
  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
+
@@ -16,36 +16,18 @@ end
16
16
  Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
17
17
 
18
18
  describe 'AppMap class Hooking', docker: false do
19
+ require 'appmap/util'
19
20
  def collect_events(tracer)
20
21
  [].tap do |events|
21
22
  while tracer.event?
22
23
  events << tracer.next_event.to_h
23
24
  end
24
- end.map do |event|
25
- event.delete(:thread_id)
26
- event.delete(:elapsed)
27
- delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
28
- delete_object_id.call(event[:receiver])
29
- delete_object_id.call(event[:return_value])
30
- (event[:parameters] || []).each(&delete_object_id)
31
- (event[:exceptions] || []).each(&delete_object_id)
32
-
33
- case event[:event]
34
- when :call
35
- event[:path] = event[:path].gsub(Gem.dir + '/', '')
36
- when :return
37
- # These should be removed from the appmap spec
38
- %i[defined_class method_id path lineno static].each do |obsolete_field|
39
- event.delete(obsolete_field)
40
- end
41
- end
42
- event
43
- end.to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
44
26
  end
45
27
 
46
28
  def invoke_test_file(file, setup: nil, &block)
47
29
  AppMap.configuration = nil
48
- package = AppMap::Package.new(file, [])
30
+ package = AppMap::Config::Package.new(file)
49
31
  config = AppMap::Config.new('hook_spec', [ package ])
50
32
  AppMap.configuration = config
51
33
  tracer = nil
@@ -69,7 +51,8 @@ describe 'AppMap class Hooking', docker: false do
69
51
  config, tracer = invoke_test_file(file, setup: setup, &block)
70
52
 
71
53
  events = collect_events(tracer)
72
- expect(Diffy::Diff.new(events, events_yaml).to_s).to eq('')
54
+
55
+ expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
73
56
 
74
57
  [ config, tracer ]
75
58
  end
@@ -99,7 +82,7 @@ describe 'AppMap class Hooking', docker: false do
99
82
  :class: String
100
83
  :value: default
101
84
  YAML
102
- config, tracer = test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
85
+ test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
103
86
  expect(InstanceMethod.new.say_default).to eq('default')
104
87
  end
105
88
  end
@@ -117,7 +100,7 @@ describe 'AppMap class Hooking', docker: false do
117
100
  InstanceMethod.new.say_default
118
101
  end
119
102
  class_map = AppMap.class_map(tracer.event_methods).to_yaml
120
- expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
103
+ expect(Diffy::Diff.new(<<~YAML, class_map).to_s).to eq('')
121
104
  ---
122
105
  - :name: spec/fixtures/hook/instance_method.rb
123
106
  :type: package
@@ -359,7 +342,7 @@ describe 'AppMap class Hooking', docker: false do
359
342
  :defined_class: SingletonMethod
360
343
  :method_id: added_method
361
344
  :path: spec/fixtures/hook/singleton_method.rb
362
- :lineno: 44
345
+ :lineno: 21
363
346
  :static: false
364
347
  :parameters: []
365
348
  :receiver:
@@ -367,10 +350,10 @@ describe 'AppMap class Hooking', docker: false do
367
350
  :value: Singleton Method fixture
368
351
  - :id: 2
369
352
  :event: :call
370
- :defined_class: AddMethod
353
+ :defined_class: SingletonMethod::AddMethod
371
354
  :method_id: _added_method
372
355
  :path: spec/fixtures/hook/singleton_method.rb
373
- :lineno: 50
356
+ :lineno: 27
374
357
  :static: false
375
358
  :parameters: []
376
359
  :receiver:
@@ -412,10 +395,44 @@ describe 'AppMap class Hooking', docker: false do
412
395
  load 'spec/fixtures/hook/singleton_method.rb'
413
396
  setup = -> { SingletonMethod.new_with_instance_method }
414
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
+
415
405
  expect(s.say_instance_defined).to eq('defined for an instance')
416
406
  end
417
407
  end
418
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
+
419
436
  it 'Reports exceptions' do
420
437
  events_yaml = <<~YAML
421
438
  ---
@@ -456,20 +473,6 @@ describe 'AppMap class Hooking', docker: false do
456
473
  end
457
474
  end
458
475
 
459
- context 'OpenSSL::X509::Certificate.sign' do
460
- # OpenSSL::X509 is not being hooked.
461
- # This might be because the class is being loaded before AppMap, and so the TracePoint
462
- # set by AppMap doesn't see it.
463
- xit 'is hooked' do
464
- events_yaml = <<~YAML
465
- ---
466
- YAML
467
- test_hook_behavior 'spec/fixtures/hook/openssl_sign.rb', events_yaml do
468
- expect(OpenSSLExample.example).to be_truthy
469
- end
470
- end
471
- end
472
-
473
476
  context 'ActiveSupport::SecurityUtils.secure_compare' do
474
477
  it 'is hooked' do
475
478
  events_yaml = <<~YAML
@@ -513,12 +516,52 @@ describe 'AppMap class Hooking', docker: false do
513
516
  :class: Module
514
517
  :value: ActiveSupport::SecurityUtils
515
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
516
559
  :event: :return
517
560
  :parent_id: 2
518
561
  :return_value:
519
562
  :class: TrueClass
520
563
  :value: 'true'
521
- - :id: 4
564
+ - :id: 8
522
565
  :event: :return
523
566
  :parent_id: 1
524
567
  :return_value:
@@ -559,8 +602,25 @@ describe 'AppMap class Hooking', docker: false do
559
602
  :static: true
560
603
  :labels:
561
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
562
622
  YAML
563
-
623
+
564
624
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
565
625
  expect(Compare.compare('string', 'string')).to be_truthy
566
626
  end
@@ -570,7 +630,43 @@ describe 'AppMap class Hooking', docker: false do
570
630
  expect(entry[:name]).to eq('secure_compare')
571
631
  spec = Gem::Specification.find_by_name('activesupport')
572
632
  entry[:location].gsub!(spec.base_dir + '/', '')
573
- 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
574
670
  end
575
671
  end
576
672
  end