appmap 0.31.0 → 0.34.2

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