appmap 0.26.0 → 0.31.0

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