appmap 0.26.0 → 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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +144 -31
  5. data/Rakefile +1 -1
  6. data/exe/appmap +3 -1
  7. data/lib/appmap.rb +55 -35
  8. data/lib/appmap/algorithm/stats.rb +2 -1
  9. data/lib/appmap/class_map.rb +16 -24
  10. data/lib/appmap/command/record.rb +2 -61
  11. data/lib/appmap/config.rb +91 -0
  12. data/lib/appmap/cucumber.rb +89 -0
  13. data/lib/appmap/event.rb +6 -6
  14. data/lib/appmap/hook.rb +94 -116
  15. data/lib/appmap/metadata.rb +62 -0
  16. data/lib/appmap/middleware/remote_recording.rb +2 -6
  17. data/lib/appmap/minitest.rb +141 -0
  18. data/lib/appmap/rails/action_handler.rb +2 -2
  19. data/lib/appmap/rails/sql_handler.rb +2 -2
  20. data/lib/appmap/railtie.rb +2 -2
  21. data/lib/appmap/record.rb +27 -0
  22. data/lib/appmap/rspec.rb +20 -38
  23. data/lib/appmap/trace.rb +19 -11
  24. data/lib/appmap/util.rb +40 -0
  25. data/lib/appmap/version.rb +1 -1
  26. data/package-lock.json +3 -3
  27. data/spec/abstract_controller4_base_spec.rb +1 -1
  28. data/spec/abstract_controller_base_spec.rb +1 -1
  29. data/spec/config_spec.rb +3 -3
  30. data/spec/fixtures/hook/compare.rb +7 -0
  31. data/spec/fixtures/hook/openssl_sign.rb +87 -0
  32. data/spec/fixtures/hook/singleton_method.rb +54 -0
  33. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  34. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  35. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  36. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  37. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  38. data/spec/hook_spec.rb +243 -36
  39. data/spec/rails_spec_helper.rb +2 -0
  40. data/spec/rspec_feature_metadata_spec.rb +2 -0
  41. data/spec/spec_helper.rb +4 -0
  42. data/spec/util_spec.rb +21 -0
  43. data/test/cli_test.rb +2 -2
  44. data/test/cucumber_test.rb +72 -0
  45. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  46. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  47. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  48. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  49. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  50. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  51. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  52. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  53. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  54. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  55. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  56. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  57. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  58. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  59. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  60. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  61. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  62. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  63. data/test/fixtures/process_recorder/appmap.yml +3 -0
  64. data/test/fixtures/process_recorder/hello.rb +9 -0
  65. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  66. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  67. data/test/minitest_test.rb +38 -0
  68. data/test/record_process_test.rb +35 -0
  69. data/test/rspec_test.rb +5 -0
  70. metadata +39 -3
  71. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ module Util
5
+ class << self
6
+ # scenario_filename builds a suitable file name from a scenario name.
7
+ # Special characters are removed, and the file name is truncated to fit within
8
+ # shell limitations.
9
+ def scenario_filename(name, max_length: 255, separator: '_', extension: '.appmap.json')
10
+ # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
11
+ # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
12
+ # Replace accented chars with their ASCII equivalents.
13
+
14
+ fname = name.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
15
+
16
+ # Turn unwanted chars into the separator.
17
+ fname.gsub!(/[^a-z0-9\-_]+/i, separator)
18
+
19
+ re_sep = Regexp.escape(separator)
20
+ re_duplicate_separator = /#{re_sep}{2,}/
21
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
22
+
23
+ # No more than one of the separator in a row.
24
+ fname.gsub!(re_duplicate_separator, separator)
25
+
26
+ # Finally, Remove leading/trailing separator.
27
+ fname.gsub!(re_leading_trailing_separator, '')
28
+
29
+ if (fname.length + extension.length) > max_length
30
+ require 'base64'
31
+ require 'digest'
32
+ fname_digest = Base64.urlsafe_encode64 Digest::MD5.digest(fname), padding: false
33
+ fname[max_length - fname_digest.length - extension.length - 1..-1] = [ '-', fname_digest ].join
34
+ end
35
+
36
+ [ fname, extension ].join
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.26.0'
6
+ VERSION = '0.31.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -551,9 +551,9 @@
551
551
  }
552
552
  },
553
553
  "lodash": {
554
- "version": "4.17.15",
555
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
556
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
554
+ "version": "4.17.19",
555
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
556
+ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
557
557
  },
558
558
  "longest": {
559
559
  "version": "1.0.1",
@@ -52,8 +52,8 @@ describe 'AbstractControllerBase' do
52
52
  method_id: build_user
53
53
  path: app/controllers/api/users_controller.rb
54
54
  lineno: 23
55
- static: false
56
55
  thread_id: .*
56
+ static: false
57
57
  parameters:
58
58
  - name: params
59
59
  class: Hash
@@ -57,8 +57,8 @@ describe 'AbstractControllerBase' do
57
57
  method_id: build_user
58
58
  path: app/controllers/api/users_controller.rb
59
59
  lineno: 23
60
- static: false
61
60
  thread_id: .*
61
+ static: false
62
62
  parameters:
63
63
  - name: params
64
64
  class: ActiveSupport::HashWithIndifferentAccess
@@ -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
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SingletonMethod
4
+ class << self
5
+ def say_default
6
+ 'default'
7
+ end
8
+ end
9
+
10
+ def SingletonMethod.say_class_defined
11
+ 'defined with explicit class scope'
12
+ end
13
+
14
+ def self.say_self_defined
15
+ 'defined with self class scope'
16
+ end
17
+
18
+ # When called, do_include calls +include+ to bring in the module
19
+ # AddMethod. AddMethod defines a new instance method, which gets
20
+ # added to the singleton class of SingletonMethod.
21
+ def do_include
22
+ class << self
23
+ SingletonMethod.include(AddMethod)
24
+ end
25
+ self
26
+ end
27
+
28
+ def self.new_with_instance_method
29
+ SingletonMethod.new.tap do |m|
30
+ def m.say_instance_defined
31
+ 'defined for an instance'
32
+ end
33
+ end
34
+ end
35
+
36
+ def to_s
37
+ 'Singleton Method fixture'
38
+ end
39
+ end
40
+
41
+ module AddMethod
42
+ def self.included(base)
43
+ base.module_eval do
44
+ define_method "added_method" do
45
+ _added_method
46
+ end
47
+ end
48
+ end
49
+
50
+ def _added_method
51
+ 'defined by including a module'
52
+ end
53
+ end
54
+
@@ -39,6 +39,7 @@ appmap_options = \
39
39
  gem 'appmap', appmap_options
40
40
 
41
41
  group :development, :test do
42
+ gem 'cucumber-rails', require: false
42
43
  gem 'rspec-rails'
43
44
  # Required for Sequel, since without ActiveRecord, the Rails transactional fixture support
44
45
  # isn't activated.
@@ -0,0 +1,13 @@
1
+ Feature: /api/users
2
+
3
+ @appmap-disable
4
+ Scenario: A user can be created
5
+ When I create a user
6
+ Then the response status should be 201
7
+
8
+ Scenario: When a user is created, it should be in the user list
9
+ Given I create a user
10
+ And the response status should be 201
11
+ When I list the users
12
+ Then the response status should be 200
13
+ And the response should include the user
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/rails'
4
+ require 'appmap/cucumber'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ if AppMap::Cucumber.enabled?
4
+ Around('not @appmap-disable') do |scenario, block|
5
+ appmap = AppMap.record do
6
+ block.call
7
+ end
8
+
9
+ AppMap::Cucumber.write_scenario(scenario, appmap)
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ When 'I create a user' do
4
+ @response = post '/api/users', login: 'alice'
5
+ end
6
+
7
+ Then(/the response status should be (\d+)/) do |status|
8
+ expect(@response.status).to eq(status.to_i)
9
+ end
10
+
11
+ When 'I list the users' do
12
+ @response = get '/api/users'
13
+ @users = JSON.parse(@response.body)
14
+ end
15
+
16
+ Then 'the response should include the user' do
17
+ expect(@users.map { |u| u['login'] }).to include('alice')
18
+ end
@@ -5,7 +5,17 @@ require 'appmap/hook'
5
5
  require 'appmap/event'
6
6
  require 'diffy'
7
7
 
8
- describe 'AppMap class Hooking' do
8
+ # Show nulls as the literal +null+, rather than just leaving the field
9
+ # empty. This make some of the expected YAML below easier to
10
+ # understand.
11
+ module ShowYamlNulls
12
+ def visit_NilClass(o)
13
+ @emitter.scalar('null', nil, 'tag:yaml.org,2002:null', true, false, Psych::Nodes::Scalar::ANY)
14
+ end
15
+ end
16
+ Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
17
+
18
+ describe 'AppMap class Hooking', docker: false do
9
19
  def collect_events(tracer)
10
20
  [].tap do |events|
11
21
  while tracer.event?
@@ -20,7 +30,10 @@ describe 'AppMap class Hooking' do
20
30
  (event[:parameters] || []).each(&delete_object_id)
21
31
  (event[:exceptions] || []).each(&delete_object_id)
22
32
 
23
- if event[:event] == :return
33
+ case event[:event]
34
+ when :call
35
+ event[:path] = event[:path].gsub(Gem.dir + '/', '')
36
+ when :return
24
37
  # These should be removed from the appmap spec
25
38
  %i[defined_class method_id path lineno static].each do |obsolete_field|
26
39
  event.delete(obsolete_field)
@@ -30,24 +43,30 @@ describe 'AppMap class Hooking' do
30
43
  end.to_yaml
31
44
  end
32
45
 
33
- def invoke_test_file(file, &block)
34
- package = AppMap::Hook::Package.new(file, [])
35
- config = AppMap::Hook::Config.new('hook_spec', [ package ])
36
- AppMap::Hook.hook(config)
37
-
38
- tracer = AppMap.tracing.trace
39
- AppMap::Event.reset_id_counter
40
- begin
41
- load file
42
- yield
43
- ensure
44
- AppMap.tracing.delete(tracer)
46
+ def invoke_test_file(file, setup: nil, &block)
47
+ AppMap.configuration = nil
48
+ package = AppMap::Package.new(file, [])
49
+ config = AppMap::Config.new('hook_spec', [ package ])
50
+ AppMap.configuration = config
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
45
63
  end
64
+
46
65
  [ config, tracer ]
47
66
  end
48
67
 
49
- def test_hook_behavior(file, events_yaml, &block)
50
- config, tracer = invoke_test_file(file, &block)
68
+ def test_hook_behavior(file, events_yaml, setup: nil, &block)
69
+ config, tracer = invoke_test_file(file, setup: setup, &block)
51
70
 
52
71
  events = collect_events(tracer)
53
72
  expect(Diffy::Diff.new(events, events_yaml).to_s).to eq('')
@@ -55,6 +74,10 @@ describe 'AppMap class Hooking' do
55
74
  [ config, tracer ]
56
75
  end
57
76
 
77
+ after do
78
+ AppMap.configuration = nil
79
+ end
80
+
58
81
  it 'hooks an instance method that takes no arguments' do
59
82
  events_yaml = <<~YAML
60
83
  ---
@@ -86,14 +109,14 @@ describe 'AppMap class Hooking' do
86
109
  InstanceMethod.new.say_default
87
110
  end
88
111
  expect(tracer.event_methods.to_a.map(&:defined_class)).to eq([ 'InstanceMethod' ])
89
- 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 ])
90
113
  end
91
114
 
92
115
  it 'builds a class map of invoked methods' do
93
- config, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
116
+ _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
94
117
  InstanceMethod.new.say_default
95
118
  end
96
- class_map = AppMap.class_map(config, tracer.event_methods).to_yaml
119
+ class_map = AppMap.class_map(tracer.event_methods).to_yaml
97
120
  expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
98
121
  ---
99
122
  - :name: spec/fixtures/hook/instance_method.rb
@@ -202,7 +225,7 @@ describe 'AppMap class Hooking' do
202
225
  :parameters:
203
226
  - :name: :kw
204
227
  :class: NilClass
205
- :value:
228
+ :value: null
206
229
  :kind: :key
207
230
  :receiver:
208
231
  :class: InstanceMethod
@@ -232,7 +255,7 @@ describe 'AppMap class Hooking' do
232
255
  :parameters:
233
256
  - :name: :block
234
257
  :class: NilClass
235
- :value:
258
+ :value: null
236
259
  :kind: :block
237
260
  :receiver:
238
261
  :class: InstanceMethod
@@ -254,15 +277,15 @@ describe 'AppMap class Hooking' do
254
277
  ---
255
278
  - :id: 1
256
279
  :event: :call
257
- :defined_class: ClassMethod
280
+ :defined_class: SingletonMethod
258
281
  :method_id: say_default
259
- :path: spec/fixtures/hook/class_method.rb
282
+ :path: spec/fixtures/hook/singleton_method.rb
260
283
  :lineno: 5
261
284
  :static: true
262
285
  :parameters: []
263
286
  :receiver:
264
287
  :class: Class
265
- :value: ClassMethod
288
+ :value: SingletonMethod
266
289
  - :id: 2
267
290
  :event: :return
268
291
  :parent_id: 1
@@ -270,8 +293,8 @@ describe 'AppMap class Hooking' do
270
293
  :class: String
271
294
  :value: default
272
295
  YAML
273
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
274
- expect(ClassMethod.say_default).to eq('default')
296
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
297
+ expect(SingletonMethod.say_default).to eq('default')
275
298
  end
276
299
  end
277
300
 
@@ -280,15 +303,15 @@ describe 'AppMap class Hooking' do
280
303
  ---
281
304
  - :id: 1
282
305
  :event: :call
283
- :defined_class: ClassMethod
306
+ :defined_class: SingletonMethod
284
307
  :method_id: say_class_defined
285
- :path: spec/fixtures/hook/class_method.rb
308
+ :path: spec/fixtures/hook/singleton_method.rb
286
309
  :lineno: 10
287
310
  :static: true
288
311
  :parameters: []
289
312
  :receiver:
290
313
  :class: Class
291
- :value: ClassMethod
314
+ :value: SingletonMethod
292
315
  - :id: 2
293
316
  :event: :return
294
317
  :parent_id: 1
@@ -296,8 +319,8 @@ describe 'AppMap class Hooking' do
296
319
  :class: String
297
320
  :value: defined with explicit class scope
298
321
  YAML
299
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
300
- expect(ClassMethod.say_class_defined).to eq('defined with explicit class scope')
322
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
323
+ expect(SingletonMethod.say_class_defined).to eq('defined with explicit class scope')
301
324
  end
302
325
  end
303
326
 
@@ -306,15 +329,15 @@ describe 'AppMap class Hooking' do
306
329
  ---
307
330
  - :id: 1
308
331
  :event: :call
309
- :defined_class: ClassMethod
332
+ :defined_class: SingletonMethod
310
333
  :method_id: say_self_defined
311
- :path: spec/fixtures/hook/class_method.rb
334
+ :path: spec/fixtures/hook/singleton_method.rb
312
335
  :lineno: 14
313
336
  :static: true
314
337
  :parameters: []
315
338
  :receiver:
316
339
  :class: Class
317
- :value: ClassMethod
340
+ :value: SingletonMethod
318
341
  - :id: 2
319
342
  :event: :return
320
343
  :parent_id: 1
@@ -322,8 +345,74 @@ describe 'AppMap class Hooking' do
322
345
  :class: String
323
346
  :value: defined with self class scope
324
347
  YAML
325
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
326
- expect(ClassMethod.say_self_defined).to eq('defined with self class scope')
348
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
349
+ expect(SingletonMethod.say_self_defined).to eq('defined with self class scope')
350
+ end
351
+ end
352
+
353
+
354
+ it 'hooks an included method' do
355
+ events_yaml = <<~YAML
356
+ ---
357
+ - :id: 1
358
+ :event: :call
359
+ :defined_class: SingletonMethod
360
+ :method_id: added_method
361
+ :path: spec/fixtures/hook/singleton_method.rb
362
+ :lineno: 44
363
+ :static: false
364
+ :parameters: []
365
+ :receiver:
366
+ :class: SingletonMethod
367
+ :value: Singleton Method fixture
368
+ - :id: 2
369
+ :event: :call
370
+ :defined_class: AddMethod
371
+ :method_id: _added_method
372
+ :path: spec/fixtures/hook/singleton_method.rb
373
+ :lineno: 50
374
+ :static: false
375
+ :parameters: []
376
+ :receiver:
377
+ :class: SingletonMethod
378
+ :value: Singleton Method fixture
379
+ - :id: 3
380
+ :event: :return
381
+ :parent_id: 2
382
+ :return_value:
383
+ :class: String
384
+ :value: defined by including a module
385
+ - :id: 4
386
+ :event: :return
387
+ :parent_id: 1
388
+ :return_value:
389
+ :class: String
390
+ :value: defined by including a module
391
+ YAML
392
+
393
+ load 'spec/fixtures/hook/singleton_method.rb'
394
+ setup = -> { SingletonMethod.new.do_include }
395
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
396
+ expect(s.added_method).to eq('defined by including a module')
397
+ end
398
+ end
399
+
400
+ it "doesn't hook a singleton method defined for an instance" do
401
+ # Ideally, Ruby would fire a TracePoint event when a singleton
402
+ # class gets created by defining a method on an instance. It
403
+ # currently doesn't, though, so there's no way for us to hook such
404
+ # a method.
405
+ #
406
+ # This example will fail if Ruby's behavior changes at some point
407
+ # in the future.
408
+ events_yaml = <<~YAML
409
+ --- []
410
+ YAML
411
+
412
+ load 'spec/fixtures/hook/singleton_method.rb'
413
+ setup = -> { SingletonMethod.new_with_instance_method }
414
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
415
+ expect(s.say_instance_defined).to eq('defined for an instance')
327
416
  end
328
417
  end
329
418
 
@@ -366,4 +455,122 @@ describe 'AppMap class Hooking' do
366
455
  expect { ExceptionMethod.new.raise_exception }.to raise_exception
367
456
  end
368
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
369
576
  end