appmap 0.25.2 → 0.28.1

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 (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
  ---