appmap 0.28.1 → 0.31.0

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.
@@ -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