appmap 0.26.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -3
  3. data/CHANGELOG.md +37 -0
  4. data/README.md +170 -29
  5. data/Rakefile +1 -1
  6. data/exe/appmap +3 -1
  7. data/lib/appmap.rb +56 -35
  8. data/lib/appmap/algorithm/stats.rb +2 -1
  9. data/lib/appmap/class_map.rb +21 -28
  10. data/lib/appmap/command/record.rb +2 -61
  11. data/lib/appmap/config.rb +89 -0
  12. data/lib/appmap/cucumber.rb +89 -0
  13. data/lib/appmap/event.rb +28 -19
  14. data/lib/appmap/hook.rb +56 -128
  15. data/lib/appmap/hook/method.rb +78 -0
  16. data/lib/appmap/metadata.rb +62 -0
  17. data/lib/appmap/middleware/remote_recording.rb +2 -6
  18. data/lib/appmap/minitest.rb +141 -0
  19. data/lib/appmap/rails/action_handler.rb +7 -7
  20. data/lib/appmap/rails/sql_handler.rb +10 -8
  21. data/lib/appmap/railtie.rb +2 -2
  22. data/lib/appmap/record.rb +27 -0
  23. data/lib/appmap/rspec.rb +20 -38
  24. data/lib/appmap/trace.rb +19 -11
  25. data/lib/appmap/util.rb +59 -0
  26. data/lib/appmap/version.rb +1 -1
  27. data/package-lock.json +3 -3
  28. data/spec/abstract_controller4_base_spec.rb +1 -1
  29. data/spec/abstract_controller_base_spec.rb +9 -2
  30. data/spec/config_spec.rb +3 -3
  31. data/spec/fixtures/hook/compare.rb +7 -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 +228 -53
  39. data/spec/rails_spec_helper.rb +2 -0
  40. data/spec/record_sql_rails_pg_spec.rb +56 -33
  41. data/spec/rspec_feature_metadata_spec.rb +2 -0
  42. data/spec/spec_helper.rb +4 -0
  43. data/spec/util_spec.rb +21 -0
  44. data/test/cli_test.rb +4 -4
  45. data/test/cucumber_test.rb +72 -0
  46. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  47. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  48. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  49. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  50. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  51. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  52. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  53. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  54. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  55. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  56. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  57. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  58. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  59. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  60. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  61. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  62. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  63. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  64. data/test/fixtures/process_recorder/appmap.yml +3 -0
  65. data/test/fixtures/process_recorder/hello.rb +9 -0
  66. data/test/minitest_test.rb +38 -0
  67. data/test/record_process_test.rb +35 -0
  68. data/test/test_helper.rb +1 -0
  69. metadata +39 -3
  70. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -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,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,49 +5,50 @@ 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
19
+ require 'appmap/util'
9
20
  def collect_events(tracer)
10
21
  [].tap do |events|
11
22
  while tracer.event?
12
23
  events << tracer.next_event.to_h
13
24
  end
14
- end.map do |event|
15
- event.delete(:thread_id)
16
- event.delete(:elapsed)
17
- delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
18
- delete_object_id.call(event[:receiver])
19
- delete_object_id.call(event[:return_value])
20
- (event[:parameters] || []).each(&delete_object_id)
21
- (event[:exceptions] || []).each(&delete_object_id)
22
-
23
- if event[:event] == :return
24
- # These should be removed from the appmap spec
25
- %i[defined_class method_id path lineno static].each do |obsolete_field|
26
- event.delete(obsolete_field)
27
- end
28
- end
29
- event
30
- end.to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
31
26
  end
32
27
 
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)
28
+ def invoke_test_file(file, setup: nil, &block)
29
+ AppMap.configuration = nil
30
+ package = AppMap::Package.new(file, nil, [])
31
+ config = AppMap::Config.new('hook_spec', [ package ])
32
+ AppMap.configuration = config
33
+ tracer = nil
34
+ AppMap::Hook.new(config).enable do
35
+ setup_result = setup.call if setup
36
+
37
+ tracer = AppMap.tracing.trace
38
+ AppMap::Event.reset_id_counter
39
+ begin
40
+ load file
41
+ yield setup_result
42
+ ensure
43
+ AppMap.tracing.delete(tracer)
44
+ end
45
45
  end
46
+
46
47
  [ config, tracer ]
47
48
  end
48
49
 
49
- def test_hook_behavior(file, events_yaml, &block)
50
- config, tracer = invoke_test_file(file, &block)
50
+ def test_hook_behavior(file, events_yaml, setup: nil, &block)
51
+ config, tracer = invoke_test_file(file, setup: setup, &block)
51
52
 
52
53
  events = collect_events(tracer)
53
54
  expect(Diffy::Diff.new(events, events_yaml).to_s).to eq('')
@@ -55,6 +56,10 @@ describe 'AppMap class Hooking' do
55
56
  [ config, tracer ]
56
57
  end
57
58
 
59
+ after do
60
+ AppMap.configuration = nil
61
+ end
62
+
58
63
  it 'hooks an instance method that takes no arguments' do
59
64
  events_yaml = <<~YAML
60
65
  ---
@@ -76,7 +81,7 @@ describe 'AppMap class Hooking' do
76
81
  :class: String
77
82
  :value: default
78
83
  YAML
79
- config, tracer = test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
84
+ test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
80
85
  expect(InstanceMethod.new.say_default).to eq('default')
81
86
  end
82
87
  end
@@ -86,14 +91,14 @@ describe 'AppMap class Hooking' do
86
91
  InstanceMethod.new.say_default
87
92
  end
88
93
  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 ])
94
+ expect(tracer.event_methods.to_a.map(&:to_s)).to eq([ InstanceMethod.public_instance_method(:say_default).to_s ])
90
95
  end
91
96
 
92
97
  it 'builds a class map of invoked methods' do
93
- config, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
98
+ _, tracer = invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
94
99
  InstanceMethod.new.say_default
95
100
  end
96
- class_map = AppMap.class_map(config, tracer.event_methods).to_yaml
101
+ class_map = AppMap.class_map(tracer.event_methods).to_yaml
97
102
  expect(Diffy::Diff.new(class_map, <<~YAML).to_s).to eq('')
98
103
  ---
99
104
  - :name: spec/fixtures/hook/instance_method.rb
@@ -202,7 +207,7 @@ describe 'AppMap class Hooking' do
202
207
  :parameters:
203
208
  - :name: :kw
204
209
  :class: NilClass
205
- :value:
210
+ :value: null
206
211
  :kind: :key
207
212
  :receiver:
208
213
  :class: InstanceMethod
@@ -232,7 +237,7 @@ describe 'AppMap class Hooking' do
232
237
  :parameters:
233
238
  - :name: :block
234
239
  :class: NilClass
235
- :value:
240
+ :value: null
236
241
  :kind: :block
237
242
  :receiver:
238
243
  :class: InstanceMethod
@@ -254,15 +259,15 @@ describe 'AppMap class Hooking' do
254
259
  ---
255
260
  - :id: 1
256
261
  :event: :call
257
- :defined_class: ClassMethod
262
+ :defined_class: SingletonMethod
258
263
  :method_id: say_default
259
- :path: spec/fixtures/hook/class_method.rb
264
+ :path: spec/fixtures/hook/singleton_method.rb
260
265
  :lineno: 5
261
266
  :static: true
262
267
  :parameters: []
263
268
  :receiver:
264
269
  :class: Class
265
- :value: ClassMethod
270
+ :value: SingletonMethod
266
271
  - :id: 2
267
272
  :event: :return
268
273
  :parent_id: 1
@@ -270,8 +275,8 @@ describe 'AppMap class Hooking' do
270
275
  :class: String
271
276
  :value: default
272
277
  YAML
273
- test_hook_behavior 'spec/fixtures/hook/class_method.rb', events_yaml do
274
- expect(ClassMethod.say_default).to eq('default')
278
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
279
+ expect(SingletonMethod.say_default).to eq('default')
275
280
  end
276
281
  end
277
282
 
@@ -280,15 +285,15 @@ describe 'AppMap class Hooking' do
280
285
  ---
281
286
  - :id: 1
282
287
  :event: :call
283
- :defined_class: ClassMethod
288
+ :defined_class: SingletonMethod
284
289
  :method_id: say_class_defined
285
- :path: spec/fixtures/hook/class_method.rb
290
+ :path: spec/fixtures/hook/singleton_method.rb
286
291
  :lineno: 10
287
292
  :static: true
288
293
  :parameters: []
289
294
  :receiver:
290
295
  :class: Class
291
- :value: ClassMethod
296
+ :value: SingletonMethod
292
297
  - :id: 2
293
298
  :event: :return
294
299
  :parent_id: 1
@@ -296,8 +301,8 @@ describe 'AppMap class Hooking' do
296
301
  :class: String
297
302
  :value: defined with explicit class scope
298
303
  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')
304
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
305
+ expect(SingletonMethod.say_class_defined).to eq('defined with explicit class scope')
301
306
  end
302
307
  end
303
308
 
@@ -306,15 +311,15 @@ describe 'AppMap class Hooking' do
306
311
  ---
307
312
  - :id: 1
308
313
  :event: :call
309
- :defined_class: ClassMethod
314
+ :defined_class: SingletonMethod
310
315
  :method_id: say_self_defined
311
- :path: spec/fixtures/hook/class_method.rb
316
+ :path: spec/fixtures/hook/singleton_method.rb
312
317
  :lineno: 14
313
318
  :static: true
314
319
  :parameters: []
315
320
  :receiver:
316
321
  :class: Class
317
- :value: ClassMethod
322
+ :value: SingletonMethod
318
323
  - :id: 2
319
324
  :event: :return
320
325
  :parent_id: 1
@@ -322,8 +327,74 @@ describe 'AppMap class Hooking' do
322
327
  :class: String
323
328
  :value: defined with self class scope
324
329
  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')
330
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml do
331
+ expect(SingletonMethod.say_self_defined).to eq('defined with self class scope')
332
+ end
333
+ end
334
+
335
+
336
+ it 'hooks an included method' do
337
+ events_yaml = <<~YAML
338
+ ---
339
+ - :id: 1
340
+ :event: :call
341
+ :defined_class: SingletonMethod
342
+ :method_id: added_method
343
+ :path: spec/fixtures/hook/singleton_method.rb
344
+ :lineno: 44
345
+ :static: false
346
+ :parameters: []
347
+ :receiver:
348
+ :class: SingletonMethod
349
+ :value: Singleton Method fixture
350
+ - :id: 2
351
+ :event: :call
352
+ :defined_class: AddMethod
353
+ :method_id: _added_method
354
+ :path: spec/fixtures/hook/singleton_method.rb
355
+ :lineno: 50
356
+ :static: false
357
+ :parameters: []
358
+ :receiver:
359
+ :class: SingletonMethod
360
+ :value: Singleton Method fixture
361
+ - :id: 3
362
+ :event: :return
363
+ :parent_id: 2
364
+ :return_value:
365
+ :class: String
366
+ :value: defined by including a module
367
+ - :id: 4
368
+ :event: :return
369
+ :parent_id: 1
370
+ :return_value:
371
+ :class: String
372
+ :value: defined by including a module
373
+ YAML
374
+
375
+ load 'spec/fixtures/hook/singleton_method.rb'
376
+ setup = -> { SingletonMethod.new.do_include }
377
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
378
+ expect(s.added_method).to eq('defined by including a module')
379
+ end
380
+ end
381
+
382
+ it "doesn't hook a singleton method defined for an instance" do
383
+ # Ideally, Ruby would fire a TracePoint event when a singleton
384
+ # class gets created by defining a method on an instance. It
385
+ # currently doesn't, though, so there's no way for us to hook such
386
+ # a method.
387
+ #
388
+ # This example will fail if Ruby's behavior changes at some point
389
+ # in the future.
390
+ events_yaml = <<~YAML
391
+ --- []
392
+ YAML
393
+
394
+ load 'spec/fixtures/hook/singleton_method.rb'
395
+ setup = -> { SingletonMethod.new_with_instance_method }
396
+ test_hook_behavior 'spec/fixtures/hook/singleton_method.rb', events_yaml, setup: setup do |s|
397
+ expect(s.say_instance_defined).to eq('defined for an instance')
327
398
  end
328
399
  end
329
400
 
@@ -366,4 +437,108 @@ describe 'AppMap class Hooking' do
366
437
  expect { ExceptionMethod.new.raise_exception }.to raise_exception
367
438
  end
368
439
  end
440
+
441
+ context 'ActiveSupport::SecurityUtils.secure_compare' do
442
+ it 'is hooked' do
443
+ events_yaml = <<~YAML
444
+ ---
445
+ - :id: 1
446
+ :event: :call
447
+ :defined_class: Compare
448
+ :method_id: compare
449
+ :path: spec/fixtures/hook/compare.rb
450
+ :lineno: 4
451
+ :static: true
452
+ :parameters:
453
+ - :name: :s1
454
+ :class: String
455
+ :value: string
456
+ :kind: :req
457
+ - :name: :s2
458
+ :class: String
459
+ :value: string
460
+ :kind: :req
461
+ :receiver:
462
+ :class: Class
463
+ :value: Compare
464
+ - :id: 2
465
+ :event: :call
466
+ :defined_class: ActiveSupport::SecurityUtils
467
+ :method_id: secure_compare
468
+ :path: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb
469
+ :lineno: 26
470
+ :static: true
471
+ :parameters:
472
+ - :name: :a
473
+ :class: String
474
+ :value: string
475
+ :kind: :req
476
+ - :name: :b
477
+ :class: String
478
+ :value: string
479
+ :kind: :req
480
+ :receiver:
481
+ :class: Module
482
+ :value: ActiveSupport::SecurityUtils
483
+ - :id: 3
484
+ :event: :return
485
+ :parent_id: 2
486
+ :return_value:
487
+ :class: TrueClass
488
+ :value: 'true'
489
+ - :id: 4
490
+ :event: :return
491
+ :parent_id: 1
492
+ :return_value:
493
+ :class: TrueClass
494
+ :value: 'true'
495
+ YAML
496
+
497
+ test_hook_behavior 'spec/fixtures/hook/compare.rb', events_yaml do
498
+ expect(Compare.compare('string', 'string')).to be_truthy
499
+ end
500
+ end
501
+
502
+ it 'gets labeled in the classmap' do
503
+ classmap_yaml = <<~YAML
504
+ ---
505
+ - :name: spec/fixtures/hook/compare.rb
506
+ :type: package
507
+ :children:
508
+ - :name: Compare
509
+ :type: class
510
+ :children:
511
+ - :name: compare
512
+ :type: function
513
+ :location: spec/fixtures/hook/compare.rb:4
514
+ :static: true
515
+ - :name: active_support
516
+ :type: package
517
+ :children:
518
+ - :name: ActiveSupport
519
+ :type: class
520
+ :children:
521
+ - :name: SecurityUtils
522
+ :type: class
523
+ :children:
524
+ - :name: secure_compare
525
+ :type: function
526
+ :location: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb:26
527
+ :static: true
528
+ :labels:
529
+ - security
530
+ YAML
531
+
532
+ config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
533
+ expect(Compare.compare('string', 'string')).to be_truthy
534
+ end
535
+ cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
536
+ entry = cm[1][:children][0][:children][0][:children][0]
537
+ # Sanity check, make sure we got the right one
538
+ expect(entry[:name]).to eq('secure_compare')
539
+ spec = Gem::Specification.find_by_name('activesupport')
540
+ entry[:location].gsub!(spec.base_dir + '/', '')
541
+ expect(Diffy::Diff.new(cm.to_yaml, classmap_yaml).to_s).to eq('')
542
+ end
543
+ end
369
544
  end