appmap 0.25.2 → 0.28.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +30 -0
  4. data/README.md +123 -39
  5. data/exe/appmap +3 -57
  6. data/lib/appmap.rb +51 -32
  7. data/lib/appmap/algorithm/stats.rb +2 -1
  8. data/lib/appmap/class_map.rb +1 -1
  9. data/lib/appmap/command/record.rb +2 -61
  10. data/lib/appmap/cucumber.rb +89 -0
  11. data/lib/appmap/event.rb +6 -6
  12. data/lib/appmap/hook.rb +27 -13
  13. data/lib/appmap/metadata.rb +62 -0
  14. data/lib/appmap/middleware/remote_recording.rb +2 -7
  15. data/lib/appmap/rails/action_handler.rb +2 -2
  16. data/lib/appmap/rails/sql_handler.rb +2 -2
  17. data/lib/appmap/railtie.rb +2 -2
  18. data/lib/appmap/rspec.rb +20 -38
  19. data/lib/appmap/trace.rb +11 -11
  20. data/lib/appmap/util.rb +40 -0
  21. data/lib/appmap/version.rb +1 -1
  22. data/package-lock.json +3 -3
  23. data/spec/abstract_controller4_base_spec.rb +1 -1
  24. data/spec/abstract_controller_base_spec.rb +1 -1
  25. data/spec/fixtures/hook/singleton_method.rb +54 -0
  26. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  27. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  28. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  29. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  30. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  31. data/spec/hook_spec.rb +107 -23
  32. data/spec/rails_spec_helper.rb +2 -0
  33. data/spec/rspec_feature_metadata_spec.rb +2 -0
  34. data/spec/spec_helper.rb +4 -0
  35. data/spec/util_spec.rb +21 -0
  36. data/test/cli_test.rb +2 -15
  37. data/test/cucumber_test.rb +72 -0
  38. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  39. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  40. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  41. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  42. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  43. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  44. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  45. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  46. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  47. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  48. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  49. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  50. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  51. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  52. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  53. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  54. data/test/rspec_test.rb +5 -0
  55. metadata +26 -4
  56. data/lib/appmap/command/upload.rb +0 -101
  57. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'appmap/util'
4
+
3
5
  module AppMap
4
6
  # Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will
5
7
  # be activated around each scenario which has the metadata key `:appmap`.
@@ -8,10 +10,7 @@ module AppMap
8
10
  LOG = false
9
11
 
10
12
  def self.metadata
11
- require 'appmap/command/record'
12
- @metadata ||= AppMap::Command::Record.detect_metadata
13
- @metadata.freeze
14
- @metadata.dup
13
+ AppMap.detect_metadata
15
14
  end
16
15
 
17
16
  module FeatureAnnotations
@@ -107,7 +106,17 @@ module AppMap
107
106
 
108
107
  def parent
109
108
  # An example group always has a parent; but it might be 'self'...
110
- example_group.parent != example_group ? ScopeExampleGroup.new(example_group.parent) : nil
109
+
110
+ # DEPRECATION WARNING: `Module#parent` has been renamed to `module_parent`. `parent` is deprecated and will be
111
+ # removed in Rails 6.1. (called from parent at /Users/kgilpin/source/appland/appmap-ruby/lib/appmap/rspec.rb:110)
112
+ example_group_parent = \
113
+ if example_group.respond_to?(:module_parent)
114
+ example_group.module_parent
115
+ else
116
+ example_group.parent
117
+ end
118
+
119
+ example_group_parent != example_group ? ScopeExampleGroup.new(example_group_parent) : nil
111
120
  end
112
121
  end
113
122
 
@@ -129,7 +138,7 @@ module AppMap
129
138
 
130
139
  AppMap::RSpec.add_event_methods @trace.event_methods
131
140
 
132
- class_map = AppMap.class_map(AppMap::RSpec.config, @trace.event_methods)
141
+ class_map = AppMap.class_map(@trace.event_methods)
133
142
 
134
143
  description = []
135
144
  scope = ScopeExample.new(example)
@@ -180,7 +189,6 @@ module AppMap
180
189
  end
181
190
 
182
191
  @recordings_by_example = {}
183
- @config = nil
184
192
  @event_methods = Set.new
185
193
 
186
194
  class << self
@@ -188,10 +196,6 @@ module AppMap
188
196
  warn 'Configuring AppMap recorder for RSpec'
189
197
 
190
198
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
191
-
192
- require 'appmap/hook'
193
- @config = AppMap.configure
194
- AppMap::Hook.hook(@config)
195
199
  end
196
200
 
197
201
  def begin_spec(example)
@@ -214,9 +218,9 @@ module AppMap
214
218
  end
215
219
 
216
220
  def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
217
- metadata = RSpec.metadata.dup.tap do |m|
221
+ metadata = RSpec.metadata.tap do |m|
218
222
  m[:name] = example_name
219
- m[:app] = @config.name
223
+ m[:app] = AppMap.configuration.name
220
224
  m[:feature] = feature_name if feature_name
221
225
  m[:feature_group] = feature_group_name if feature_group_name
222
226
  m[:labels] = labels if labels
@@ -236,13 +240,13 @@ module AppMap
236
240
  classMap: class_map,
237
241
  events: events
238
242
  }.compact
239
- fname = sanitize_filename(example_name)
243
+ fname = AppMap::Util.scenario_filename(example_name)
240
244
 
241
- File.write(File.join(APPMAP_OUTPUT_DIR, "#{fname}.appmap.json"), JSON.generate(appmap))
245
+ File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
242
246
  end
243
247
 
244
248
  def print_inventory
245
- class_map = AppMap.class_map(@config, @event_methods)
249
+ class_map = AppMap.class_map(@event_methods)
246
250
  save 'Inventory', class_map, labels: %w[inventory]
247
251
  end
248
252
 
@@ -256,28 +260,6 @@ module AppMap
256
260
  print_inventory
257
261
  end
258
262
  end
259
-
260
- private
261
-
262
- # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
263
- # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
264
- def sanitize_filename(fname, separator: '_')
265
- # Replace accented chars with their ASCII equivalents.
266
- fname = fname.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
267
-
268
- # Turn unwanted chars into the separator.
269
- fname.gsub!(/[^a-z0-9\-_]+/i, separator)
270
-
271
- re_sep = Regexp.escape(separator)
272
- re_duplicate_separator = /#{re_sep}{2,}/
273
- re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
274
-
275
- # No more than one of the separator in a row.
276
- fname.gsub!(re_duplicate_separator, separator)
277
-
278
- # Finally, Remove leading/trailing separator.
279
- fname.gsub(re_leading_trailing_separator, '')
280
- end
281
263
  end
282
264
  end
283
265
  end
@@ -2,38 +2,38 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- ScopedMethod = Struct.new(:defined_class, :method)
5
+ ScopedMethod = Struct.new(:defined_class, :method, :static)
6
6
 
7
- class Tracers
7
+ class Tracing
8
8
  def initialize
9
- @tracers = []
9
+ @Tracing = []
10
10
  end
11
11
 
12
12
  def empty?
13
- @tracers.empty?
13
+ @Tracing.empty?
14
14
  end
15
15
 
16
16
  def trace(enable: true)
17
17
  Tracer.new.tap do |tracer|
18
- @tracers << tracer
18
+ @Tracing << tracer
19
19
  tracer.enable if enable
20
20
  end
21
- end
21
+ end
22
22
 
23
23
  def enabled?
24
- @tracers.any?(&:enabled?)
24
+ @Tracing.any?(&:enabled?)
25
25
  end
26
26
 
27
27
  def record_event(event, defined_class: nil, method: nil)
28
- @tracers.each do |tracer|
28
+ @Tracing.each do |tracer|
29
29
  tracer.record_event(event, defined_class: defined_class, method: method)
30
30
  end
31
31
  end
32
32
 
33
33
  def delete(tracer)
34
- return unless @tracers.member?(tracer)
34
+ return unless @Tracing.member?(tracer)
35
35
 
36
- @tracers.delete(tracer)
36
+ @Tracing.delete(tracer)
37
37
  tracer.disable
38
38
  end
39
39
  end
@@ -67,7 +67,7 @@ module AppMap
67
67
  return unless @enabled
68
68
 
69
69
  @events << event
70
- @methods << Trace::ScopedMethod.new(defined_class, method) if defined_class && method
70
+ @methods << Trace::ScopedMethod.new(defined_class, method, event.static) if (defined_class && method && event.event == :call)
71
71
  end
72
72
 
73
73
  # Gets a unique list of the methods that were invoked by the program.
@@ -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.25.2'
6
+ VERSION = '0.28.1'
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
@@ -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,6 +5,16 @@ require 'appmap/hook'
5
5
  require 'appmap/event'
6
6
  require 'diffy'
7
7
 
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
+
8
18
  describe 'AppMap class Hooking' do
9
19
  def collect_events(tracer)
10
20
  [].tap do |events|
@@ -30,24 +40,28 @@ describe 'AppMap class Hooking' do
30
40
  end.to_yaml
31
41
  end
32
42
 
33
- def invoke_test_file(file, &block)
43
+ def invoke_test_file(file, setup: nil, &block)
44
+ AppMap.configuration = nil
34
45
  package = AppMap::Hook::Package.new(file, [])
35
46
  config = AppMap::Hook::Config.new('hook_spec', [ package ])
47
+ AppMap.configuration = config
36
48
  AppMap::Hook.hook(config)
49
+
50
+ setup_result = setup.call if setup
37
51
 
38
52
  tracer = AppMap.tracing.trace
39
53
  AppMap::Event.reset_id_counter
40
54
  begin
41
55
  load file
42
- yield
56
+ yield setup_result
43
57
  ensure
44
58
  AppMap.tracing.delete(tracer)
45
59
  end
46
60
  [ config, tracer ]
47
61
  end
48
62
 
49
- def test_hook_behavior(file, events_yaml, &block)
50
- config, tracer = invoke_test_file(file, &block)
63
+ def test_hook_behavior(file, events_yaml, setup: nil, &block)
64
+ config, tracer = invoke_test_file(file, setup: setup, &block)
51
65
 
52
66
  events = collect_events(tracer)
53
67
  expect(Diffy::Diff.new(events, events_yaml).to_s).to eq('')
@@ -55,6 +69,10 @@ describe 'AppMap class Hooking' do
55
69
  [ config, tracer ]
56
70
  end
57
71
 
72
+ after do
73
+ AppMap.configuration = nil
74
+ end
75
+
58
76
  it 'hooks an instance method that takes no arguments' do
59
77
  events_yaml = <<~YAML
60
78
  ---
@@ -90,10 +108,10 @@ describe 'AppMap class Hooking' do
90
108
  end
91
109
 
92
110
  it 'builds a class map of invoked methods' do
93
- config, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
111
+ _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
94
112
  InstanceMethod.new.say_default
95
113
  end
96
- class_map = AppMap.class_map(config, tracer.event_methods).to_yaml
114
+ class_map = AppMap.class_map(tracer.event_methods).to_yaml
97
115
  expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
98
116
  ---
99
117
  - :name: spec/fixtures/hook/instance_method.rb
@@ -202,7 +220,7 @@ describe 'AppMap class Hooking' do
202
220
  :parameters:
203
221
  - :name: :kw
204
222
  :class: NilClass
205
- :value:
223
+ :value: null
206
224
  :kind: :key
207
225
  :receiver:
208
226
  :class: InstanceMethod
@@ -232,7 +250,7 @@ describe 'AppMap class Hooking' do
232
250
  :parameters:
233
251
  - :name: :block
234
252
  :class: NilClass
235
- :value:
253
+ :value: null
236
254
  :kind: :block
237
255
  :receiver:
238
256
  :class: InstanceMethod
@@ -254,15 +272,15 @@ describe 'AppMap class Hooking' do
254
272
  ---
255
273
  - :id: 1
256
274
  :event: :call
257
- :defined_class: ClassMethod
275
+ :defined_class: SingletonMethod
258
276
  :method_id: say_default
259
- :path: spec/fixtures/hook/class_method.rb
277
+ :path: spec/fixtures/hook/singleton_method.rb
260
278
  :lineno: 5
261
279
  :static: true
262
280
  :parameters: []
263
281
  :receiver:
264
282
  :class: Class
265
- :value: ClassMethod
283
+ :value: SingletonMethod
266
284
  - :id: 2
267
285
  :event: :return
268
286
  :parent_id: 1
@@ -270,8 +288,8 @@ describe 'AppMap class Hooking' do
270
288
  :class: String
271
289
  :value: default
272
290
  YAML
273
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
274
- expect(ClassMethod.say_default).to eq('default')
291
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
292
+ expect(SingletonMethod.say_default).to eq('default')
275
293
  end
276
294
  end
277
295
 
@@ -280,15 +298,15 @@ describe 'AppMap class Hooking' do
280
298
  ---
281
299
  - :id: 1
282
300
  :event: :call
283
- :defined_class: ClassMethod
301
+ :defined_class: SingletonMethod
284
302
  :method_id: say_class_defined
285
- :path: spec/fixtures/hook/class_method.rb
303
+ :path: spec/fixtures/hook/singleton_method.rb
286
304
  :lineno: 10
287
305
  :static: true
288
306
  :parameters: []
289
307
  :receiver:
290
308
  :class: Class
291
- :value: ClassMethod
309
+ :value: SingletonMethod
292
310
  - :id: 2
293
311
  :event: :return
294
312
  :parent_id: 1
@@ -296,8 +314,8 @@ describe 'AppMap class Hooking' do
296
314
  :class: String
297
315
  :value: defined with explicit class scope
298
316
  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')
317
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
318
+ expect(SingletonMethod.say_class_defined).to eq('defined with explicit class scope')
301
319
  end
302
320
  end
303
321
 
@@ -306,15 +324,15 @@ describe 'AppMap class Hooking' do
306
324
  ---
307
325
  - :id: 1
308
326
  :event: :call
309
- :defined_class: ClassMethod
327
+ :defined_class: SingletonMethod
310
328
  :method_id: say_self_defined
311
- :path: spec/fixtures/hook/class_method.rb
329
+ :path: spec/fixtures/hook/singleton_method.rb
312
330
  :lineno: 14
313
331
  :static: true
314
332
  :parameters: []
315
333
  :receiver:
316
334
  :class: Class
317
- :value: ClassMethod
335
+ :value: SingletonMethod
318
336
  - :id: 2
319
337
  :event: :return
320
338
  :parent_id: 1
@@ -322,11 +340,77 @@ describe 'AppMap class Hooking' do
322
340
  :class: String
323
341
  :value: defined with self class scope
324
342
  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')
343
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
344
+ expect(SingletonMethod.say_self_defined).to eq('defined with self class scope')
345
+ end
346
+ end
347
+
348
+
349
+ it 'hooks an included method' do
350
+ events_yaml = <<~YAML
351
+ ---
352
+ - :id: 1
353
+ :event: :call
354
+ :defined_class: SingletonMethod
355
+ :method_id: added_method
356
+ :path: spec/fixtures/hook/singleton_method.rb
357
+ :lineno: 44
358
+ :static: false
359
+ :parameters: []
360
+ :receiver:
361
+ :class: SingletonMethod
362
+ :value: Singleton Method fixture
363
+ - :id: 2
364
+ :event: :call
365
+ :defined_class: AddMethod
366
+ :method_id: _added_method
367
+ :path: spec/fixtures/hook/singleton_method.rb
368
+ :lineno: 50
369
+ :static: false
370
+ :parameters: []
371
+ :receiver:
372
+ :class: SingletonMethod
373
+ :value: Singleton Method fixture
374
+ - :id: 3
375
+ :event: :return
376
+ :parent_id: 2
377
+ :return_value:
378
+ :class: String
379
+ :value: defined by including a module
380
+ - :id: 4
381
+ :event: :return
382
+ :parent_id: 1
383
+ :return_value:
384
+ :class: String
385
+ :value: defined by including a module
386
+ YAML
387
+
388
+ load 'spec/fixtures/hook/singleton_method.rb'
389
+ setup = -> { SingletonMethod.new.do_include }
390
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
391
+ expect(s.added_method).to eq('defined by including a module')
327
392
  end
328
393
  end
329
394
 
395
+ it "doesn't hook a singleton method defined for an instance" do
396
+ # Ideally, Ruby would fire a TracePoint event when a singleton
397
+ # class gets created by defining a method on an instance. It
398
+ # currently doesn't, though, so there's no way for us to hook such
399
+ # a method.
400
+ #
401
+ # This example will fail if Ruby's behavior changes at some point
402
+ # in the future.
403
+ events_yaml = <<~YAML
404
+ --- []
405
+ YAML
406
+
407
+ load 'spec/fixtures/hook/singleton_method.rb'
408
+ setup = -> { SingletonMethod.new_with_instance_method }
409
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
410
+ expect(s.say_instance_defined).to eq('defined for an instance')
411
+ end
412
+ end
413
+
330
414
  it 'Reports exceptions' do
331
415
  events_yaml = <<~YAML
332
416
  ---