appmap 0.28.1 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,7 +17,7 @@ module AppMap
17
17
  version: AppMap::VERSION
18
18
  }
19
19
  }.tap do |m|
20
- if defined?(::Rails)
20
+ if defined?(::Rails) && defined?(::Rails.version)
21
21
  m[:frameworks] ||= []
22
22
  m[:frameworks] << {
23
23
  name: 'rails',
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/util'
4
+
5
+ module AppMap
6
+ # Integration of AppMap with Minitest. When enabled with APPMAP=true, the AppMap tracer will
7
+ # be activated around each test.
8
+ module Minitest
9
+ APPMAP_OUTPUT_DIR = 'tmp/appmap/minitest'
10
+ LOG = false
11
+
12
+ def self.metadata
13
+ AppMap.detect_metadata
14
+ end
15
+
16
+ Recording = Struct.new(:test) do
17
+ def initialize(test)
18
+ super
19
+
20
+ warn "Starting recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
21
+ @trace = AppMap.tracing.trace
22
+ end
23
+
24
+ def finish
25
+ warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
26
+
27
+ events = []
28
+ AppMap.tracing.delete @trace
29
+
30
+ events << @trace.next_event.to_h while @trace.event?
31
+
32
+ AppMap::Minitest.add_event_methods @trace.event_methods
33
+
34
+ class_map = AppMap.class_map(@trace.event_methods)
35
+
36
+ feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
37
+ feature_name = test.name.split('_')[1..-1].join(' ')
38
+ scenario_name = [ feature_group, feature_name ].join(' ')
39
+
40
+ AppMap::Minitest.save scenario_name,
41
+ class_map,
42
+ events: events,
43
+ feature_name: feature_name,
44
+ feature_group_name: feature_group
45
+ end
46
+ end
47
+
48
+ @recordings_by_test = {}
49
+ @event_methods = Set.new
50
+
51
+ class << self
52
+ def init
53
+ warn 'Configuring AppMap recorder for Minitest'
54
+
55
+ FileUtils.mkdir_p APPMAP_OUTPUT_DIR
56
+ end
57
+
58
+ def begin_test(test)
59
+ @recordings_by_test[test.object_id] = Recording.new(test)
60
+ end
61
+
62
+ def end_test(test)
63
+ recording = @recordings_by_test.delete(test.object_id)
64
+ return warn "No recording found for #{test}" unless recording
65
+
66
+ recording.finish
67
+ end
68
+
69
+ def config
70
+ @config or raise "AppMap is not configured"
71
+ end
72
+
73
+ def add_event_methods(event_methods)
74
+ @event_methods += event_methods
75
+ end
76
+
77
+ def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
78
+ metadata = AppMap::Minitest.metadata.tap do |m|
79
+ m[:name] = example_name
80
+ m[:app] = AppMap.configuration.name
81
+ m[:feature] = feature_name if feature_name
82
+ m[:feature_group] = feature_group_name if feature_group_name
83
+ m[:frameworks] ||= []
84
+ m[:frameworks] << {
85
+ name: 'minitest',
86
+ version: Gem.loaded_specs['minitest']&.version&.to_s
87
+ }
88
+ m[:recorder] = {
89
+ name: 'minitest'
90
+ }
91
+ end
92
+
93
+ appmap = {
94
+ version: AppMap::APPMAP_FORMAT_VERSION,
95
+ metadata: metadata,
96
+ classMap: class_map,
97
+ events: events
98
+ }.compact
99
+ fname = AppMap::Util.scenario_filename(example_name)
100
+
101
+ File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
102
+ end
103
+
104
+ def print_inventory
105
+ class_map = AppMap.class_map(@event_methods)
106
+ save 'Inventory', class_map, labels: %w[inventory]
107
+ end
108
+
109
+ def enabled?
110
+ ENV['APPMAP'] == 'true'
111
+ end
112
+
113
+ def run
114
+ init
115
+ at_exit do
116
+ print_inventory
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ if AppMap::Minitest.enabled?
124
+ require 'appmap'
125
+ require 'minitest/test'
126
+
127
+ class ::Minitest::Test
128
+ alias run_without_hook run
129
+
130
+ def run
131
+ AppMap::Minitest.begin_test self
132
+ begin
133
+ run_without_hook
134
+ ensure
135
+ AppMap::Minitest.end_test self
136
+ end
137
+ end
138
+ end
139
+
140
+ AppMap::Minitest.run
141
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap'
4
+ require 'json'
5
+
6
+ tracer = AppMap.tracing.trace
7
+
8
+ at_exit do
9
+ AppMap.tracing.delete(tracer)
10
+
11
+ events = [].tap do |event_list|
12
+ event_list << tracer.next_event.to_h while tracer.event?
13
+ end
14
+
15
+ metadata = AppMap.detect_metadata
16
+ metadata[:recorder] = {
17
+ name: 'record_process'
18
+ }
19
+
20
+ appmap = {
21
+ 'version' => AppMap::APPMAP_FORMAT_VERSION,
22
+ 'metadata' => metadata,
23
+ 'classMap' => AppMap.class_map(tracer.event_methods),
24
+ 'events' => events
25
+ }
26
+ File.write 'appmap.json', JSON.generate(appmap)
27
+ end
@@ -218,7 +218,7 @@ module AppMap
218
218
  end
219
219
 
220
220
  def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
221
- metadata = RSpec.metadata.tap do |m|
221
+ metadata = AppMap::RSpec.metadata.tap do |m|
222
222
  m[:name] = example_name
223
223
  m[:app] = AppMap.configuration.name
224
224
  m[:feature] = feature_name if feature_name
@@ -2,7 +2,15 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- ScopedMethod = Struct.new(:defined_class, :method, :static)
5
+ class ScopedMethod < SimpleDelegator
6
+ attr_reader :defined_class, :static
7
+
8
+ def initialize(defined_class, method, static)
9
+ @defined_class = defined_class
10
+ @static = static
11
+ super(method)
12
+ end
13
+ end
6
14
 
7
15
  class Tracing
8
16
  def initialize
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.28.1'
6
+ VERSION = '0.31.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'rails_spec_helper'
4
4
  require 'active_support/core_ext'
5
- require 'appmap/hook'
5
+ require 'appmap/config'
6
6
 
7
- describe AppMap::Hook::Config do
7
+ describe AppMap::Config, docker: false do
8
8
  it 'loads from a Hash' do
9
9
  config_data = {
10
10
  name: 'test',
@@ -18,7 +18,7 @@ describe AppMap::Hook::Config do
18
18
  }
19
19
  ]
20
20
  }.deep_stringify_keys!
21
- config = AppMap::Hook::Config.load(config_data)
21
+ config = AppMap::Config.load(config_data)
22
22
 
23
23
  expect(config.to_h.deep_stringify_keys!).to eq(config_data)
24
24
  end
@@ -0,0 +1,7 @@
1
+ require 'active_support/security_utils'
2
+
3
+ class Compare
4
+ def self.compare(s1, s2)
5
+ ActiveSupport::SecurityUtils.secure_compare(s1, s2)
6
+ end
7
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # From the manual page https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL.html
4
+
5
+ require 'openssl'
6
+
7
+ module OpenSSLExample
8
+ def OpenSSLExample.example
9
+ ca_key = OpenSSL::PKey::RSA.new 2048
10
+ pass_phrase = 'my secure pass phrase goes here'
11
+
12
+ cipher = OpenSSL::Cipher.new 'AES-256-CBC'
13
+
14
+ open 'tmp/ca_key.pem', 'w', 0644 do |io|
15
+ io.write ca_key.export(cipher, pass_phrase)
16
+ end
17
+
18
+ ca_name = OpenSSL::X509::Name.parse '/CN=ca/DC=example'
19
+
20
+ ca_cert = OpenSSL::X509::Certificate.new
21
+ ca_cert.serial = 0
22
+ ca_cert.version = 2
23
+ ca_cert.not_before = Time.now
24
+ ca_cert.not_after = Time.now + 86400
25
+
26
+ ca_cert.public_key = ca_key.public_key
27
+ ca_cert.subject = ca_name
28
+ ca_cert.issuer = ca_name
29
+
30
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
31
+ extension_factory.subject_certificate = ca_cert
32
+ extension_factory.issuer_certificate = ca_cert
33
+
34
+ ca_cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
35
+ ca_cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
36
+
37
+ ca_cert.add_extension extension_factory.create_extension(
38
+ 'keyUsage', 'cRLSign,keyCertSign', true)
39
+
40
+ ca_cert.sign ca_key, OpenSSL::Digest::SHA1.new
41
+
42
+ open 'tmp/ca_cert.pem', 'w' do |io|
43
+ io.write ca_cert.to_pem
44
+ end
45
+
46
+ csr = OpenSSL::X509::Request.new
47
+ csr.version = 0
48
+ csr.subject = OpenSSL::X509::Name.new([ ['CN', 'the name to sign', OpenSSL::ASN1::UTF8STRING] ])
49
+ csr.public_key = ca_key.public_key
50
+ csr.sign ca_key, OpenSSL::Digest::SHA1.new
51
+
52
+ open 'tmp/csr.pem', 'w' do |io|
53
+ io.write csr.to_pem
54
+ end
55
+
56
+ csr = OpenSSL::X509::Request.new File.read 'tmp/csr.pem'
57
+
58
+ raise 'CSR can not be verified' unless csr.verify csr.public_key
59
+
60
+ csr_cert = OpenSSL::X509::Certificate.new
61
+ csr_cert.serial = 0
62
+ csr_cert.version = 2
63
+ csr_cert.not_before = Time.now
64
+ csr_cert.not_after = Time.now + 600
65
+
66
+ csr_cert.subject = csr.subject
67
+ csr_cert.public_key = csr.public_key
68
+ csr_cert.issuer = ca_cert.subject
69
+
70
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
71
+ extension_factory.subject_certificate = csr_cert
72
+ extension_factory.issuer_certificate = ca_cert
73
+
74
+ csr_cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:FALSE')
75
+
76
+ csr_cert.add_extension extension_factory.create_extension(
77
+ 'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
78
+
79
+ csr_cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
80
+
81
+ csr_cert.sign ca_key, OpenSSL::Digest::SHA1.new
82
+
83
+ open 'tmp/csr_cert.pem', 'w' do |io|
84
+ io.write csr_cert.to_pem
85
+ end
86
+ end
87
+ end
@@ -15,7 +15,7 @@ module ShowYamlNulls
15
15
  end
16
16
  Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
17
17
 
18
- describe 'AppMap class Hooking' do
18
+ describe 'AppMap class Hooking', docker: false do
19
19
  def collect_events(tracer)
20
20
  [].tap do |events|
21
21
  while tracer.event?
@@ -30,7 +30,10 @@ describe 'AppMap class Hooking' do
30
30
  (event[:parameters] || []).each(&delete_object_id)
31
31
  (event[:exceptions] || []).each(&delete_object_id)
32
32
 
33
- if event[:event] == :return
33
+ case event[:event]
34
+ when :call
35
+ event[:path] = event[:path].gsub(Gem.dir + '/', '')
36
+ when :return
34
37
  # These should be removed from the appmap spec
35
38
  %i[defined_class method_id path lineno static].each do |obsolete_field|
36
39
  event.delete(obsolete_field)
@@ -42,21 +45,23 @@ describe 'AppMap class Hooking' do
42
45
 
43
46
  def invoke_test_file(file, setup: nil, &block)
44
47
  AppMap.configuration = nil
45
- package = AppMap::Hook::Package.new(file, [])
46
- config = AppMap::Hook::Config.new('hook_spec', [ package ])
48
+ package = AppMap::Package.new(file, [])
49
+ config = AppMap::Config.new('hook_spec', [ package ])
47
50
  AppMap.configuration = config
48
- AppMap::Hook.hook(config)
49
-
50
- setup_result = setup.call if setup
51
-
52
- tracer = AppMap.tracing.trace
53
- AppMap::Event.reset_id_counter
54
- begin
55
- load file
56
- yield setup_result
57
- ensure
58
- AppMap.tracing.delete(tracer)
51
+ tracer = nil
52
+ AppMap::Hook.new(config).enable do
53
+ setup_result = setup.call if setup
54
+
55
+ tracer = AppMap.tracing.trace
56
+ AppMap::Event.reset_id_counter
57
+ begin
58
+ load file
59
+ yield setup_result
60
+ ensure
61
+ AppMap.tracing.delete(tracer)
62
+ end
59
63
  end
64
+
60
65
  [ config, tracer ]
61
66
  end
62
67
 
@@ -104,7 +109,7 @@ describe 'AppMap class Hooking' do
104
109
  InstanceMethod.new.say_default
105
110
  end
106
111
  expect(tracer.event_methods.to_a.map(&:defined_class)).to eq([ 'InstanceMethod' ])
107
- expect(tracer.event_methods.to_a.map(&:method).map(&:to_s)).to eq([ InstanceMethod.public_instance_method(:say_default).to_s ])
112
+ expect(tracer.event_methods.to_a.map(&:to_s)).to eq([ InstanceMethod.public_instance_method(:say_default).to_s ])
108
113
  end
109
114
 
110
115
  it 'builds a class map of invoked methods' do
@@ -403,14 +408,14 @@ describe 'AppMap class Hooking' do
403
408
  events_yaml = <<~YAML
404
409
  --- []
405
410
  YAML
406
-
411
+
407
412
  load 'spec/fixtures/hook/singleton_method.rb'
408
413
  setup = -> { SingletonMethod.new_with_instance_method }
409
414
  test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
410
415
  expect(s.say_instance_defined).to eq('defined for an instance')
411
416
  end
412
417
  end
413
-
418
+
414
419
  it 'Reports exceptions' do
415
420
  events_yaml = <<~YAML
416
421
  ---
@@ -450,4 +455,122 @@ describe 'AppMap class Hooking' do
450
455
  expect { ExceptionMethod.new.raise_exception }.to raise_exception
451
456
  end
452
457
  end
458
+
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
+ context 'ActiveSupport::SecurityUtils.secure_compare' do
474
+ it 'is hooked' do
475
+ events_yaml = <<~YAML
476
+ ---
477
+ - :id: 1
478
+ :event: :call
479
+ :defined_class: Compare
480
+ :method_id: compare
481
+ :path: spec/fixtures/hook/compare.rb
482
+ :lineno: 4
483
+ :static: true
484
+ :parameters:
485
+ - :name: :s1
486
+ :class: String
487
+ :value: string
488
+ :kind: :req
489
+ - :name: :s2
490
+ :class: String
491
+ :value: string
492
+ :kind: :req
493
+ :receiver:
494
+ :class: Class
495
+ :value: Compare
496
+ - :id: 2
497
+ :event: :call
498
+ :defined_class: ActiveSupport::SecurityUtils
499
+ :method_id: secure_compare
500
+ :path: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb
501
+ :lineno: 26
502
+ :static: true
503
+ :parameters:
504
+ - :name: :a
505
+ :class: String
506
+ :value: string
507
+ :kind: :req
508
+ - :name: :b
509
+ :class: String
510
+ :value: string
511
+ :kind: :req
512
+ :receiver:
513
+ :class: Module
514
+ :value: ActiveSupport::SecurityUtils
515
+ - :id: 3
516
+ :event: :return
517
+ :parent_id: 2
518
+ :return_value:
519
+ :class: TrueClass
520
+ :value: 'true'
521
+ - :id: 4
522
+ :event: :return
523
+ :parent_id: 1
524
+ :return_value:
525
+ :class: TrueClass
526
+ :value: 'true'
527
+ YAML
528
+
529
+ test_hook_behavior 'spec/fixtures/hook/compare.rb', events_yaml do
530
+ expect(Compare.compare('string', 'string')).to be_truthy
531
+ end
532
+ end
533
+
534
+ it 'gets labeled in the classmap' do
535
+ classmap_yaml = <<~YAML
536
+ ---
537
+ - :name: spec/fixtures/hook/compare.rb
538
+ :type: package
539
+ :children:
540
+ - :name: Compare
541
+ :type: class
542
+ :children:
543
+ - :name: compare
544
+ :type: function
545
+ :location: spec/fixtures/hook/compare.rb:4
546
+ :static: true
547
+ - :name: active_support
548
+ :type: package
549
+ :children:
550
+ - :name: ActiveSupport
551
+ :type: class
552
+ :children:
553
+ - :name: SecurityUtils
554
+ :type: class
555
+ :children:
556
+ - :name: secure_compare
557
+ :type: function
558
+ :location: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb:26
559
+ :static: true
560
+ :labels:
561
+ - security
562
+ YAML
563
+
564
+ config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
565
+ expect(Compare.compare('string', 'string')).to be_truthy
566
+ end
567
+ cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
568
+ entry = cm[1][:children][0][:children][0][:children][0]
569
+ # Sanity check, make sure we got the right one
570
+ expect(entry[:name]).to eq('secure_compare')
571
+ spec = Gem::Specification.find_by_name('activesupport')
572
+ entry[:location].gsub!(spec.base_dir + '/', '')
573
+ expect(Diffy::Diff.new(cm.to_yaml, classmap_yaml).to_s).to eq('')
574
+ end
575
+ end
453
576
  end