appmap 0.77.2 → 0.78.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.travis.yml +0 -7
  4. data/CHANGELOG.md +21 -0
  5. data/Rakefile +29 -3
  6. data/ext/appmap/appmap.c +21 -2
  7. data/lib/appmap/builtin_hooks/ruby.yml +6 -3
  8. data/lib/appmap/config.rb +9 -1
  9. data/lib/appmap/handler/eval.rb +41 -0
  10. data/lib/appmap/handler/function.rb +8 -8
  11. data/lib/appmap/handler/net_http.rb +19 -18
  12. data/lib/appmap/handler/rails/request_handler.rb +3 -4
  13. data/lib/appmap/handler/rails/template.rb +68 -62
  14. data/lib/appmap/hook/method/ruby2.rb +56 -0
  15. data/lib/appmap/hook/method/ruby3.rb +56 -0
  16. data/lib/appmap/hook/method.rb +42 -98
  17. data/lib/appmap/hook.rb +2 -2
  18. data/lib/appmap/version.rb +1 -1
  19. data/spec/fixtures/rails7_users_app/.dockerignore +1 -0
  20. data/spec/fixtures/rails7_users_app/.gitattributes +7 -0
  21. data/spec/fixtures/rails7_users_app/.gitignore +31 -0
  22. data/spec/fixtures/rails7_users_app/.rspec +1 -0
  23. data/spec/fixtures/rails7_users_app/.ruby-version +1 -0
  24. data/spec/fixtures/rails7_users_app/Dockerfile +30 -0
  25. data/spec/fixtures/rails7_users_app/Dockerfile.pg +3 -0
  26. data/spec/fixtures/rails7_users_app/Gemfile +99 -0
  27. data/spec/fixtures/rails7_users_app/README.md +24 -0
  28. data/spec/fixtures/rails7_users_app/Rakefile +6 -0
  29. data/spec/fixtures/rails7_users_app/app/assets/config/manifest.js +4 -0
  30. data/spec/fixtures/rails7_users_app/app/assets/images/.keep +0 -0
  31. data/spec/fixtures/rails7_users_app/app/assets/stylesheets/application.css +15 -0
  32. data/spec/fixtures/rails7_users_app/app/channels/application_cable/channel.rb +4 -0
  33. data/spec/fixtures/rails7_users_app/app/channels/application_cable/connection.rb +4 -0
  34. data/spec/fixtures/rails7_users_app/app/controllers/application_controller.rb +2 -0
  35. data/spec/fixtures/rails7_users_app/app/controllers/concerns/.keep +0 -0
  36. data/spec/fixtures/rails7_users_app/app/helpers/application_helper.rb +2 -0
  37. data/spec/fixtures/rails7_users_app/app/javascript/application.js +3 -0
  38. data/spec/fixtures/rails7_users_app/app/javascript/controllers/application.js +9 -0
  39. data/spec/fixtures/rails7_users_app/app/javascript/controllers/hello_controller.js +7 -0
  40. data/spec/fixtures/rails7_users_app/app/javascript/controllers/index.js +11 -0
  41. data/spec/fixtures/rails7_users_app/app/jobs/application_job.rb +7 -0
  42. data/spec/fixtures/rails7_users_app/app/mailers/application_mailer.rb +4 -0
  43. data/spec/fixtures/rails7_users_app/app/models/application_record.rb +3 -0
  44. data/spec/fixtures/rails7_users_app/app/models/concerns/.keep +0 -0
  45. data/spec/fixtures/rails7_users_app/app/models/instance.rb +7 -0
  46. data/spec/fixtures/rails7_users_app/app/models/instructor.rb +7 -0
  47. data/spec/fixtures/rails7_users_app/app/views/layouts/application.html.erb +16 -0
  48. data/spec/fixtures/rails7_users_app/app/views/layouts/mailer.html.erb +13 -0
  49. data/spec/fixtures/rails7_users_app/app/views/layouts/mailer.text.erb +1 -0
  50. data/spec/fixtures/rails7_users_app/appmap.yml +3 -0
  51. data/spec/fixtures/rails7_users_app/bin/bundle +114 -0
  52. data/spec/fixtures/rails7_users_app/bin/importmap +4 -0
  53. data/spec/fixtures/rails7_users_app/bin/rails +4 -0
  54. data/spec/fixtures/rails7_users_app/bin/rake +4 -0
  55. data/spec/fixtures/rails7_users_app/bin/setup +33 -0
  56. data/spec/fixtures/rails7_users_app/config/application.rb +22 -0
  57. data/spec/fixtures/rails7_users_app/config/boot.rb +4 -0
  58. data/spec/fixtures/rails7_users_app/config/cable.yml +10 -0
  59. data/spec/fixtures/rails7_users_app/config/credentials.yml.enc +1 -0
  60. data/spec/fixtures/rails7_users_app/config/database.yml +86 -0
  61. data/spec/fixtures/rails7_users_app/config/environment.rb +5 -0
  62. data/spec/fixtures/rails7_users_app/config/environments/development.rb +70 -0
  63. data/spec/fixtures/rails7_users_app/config/environments/production.rb +93 -0
  64. data/spec/fixtures/rails7_users_app/config/environments/test.rb +60 -0
  65. data/spec/fixtures/rails7_users_app/config/importmap.rb +7 -0
  66. data/spec/fixtures/rails7_users_app/config/initializers/assets.rb +12 -0
  67. data/spec/fixtures/rails7_users_app/config/initializers/content_security_policy.rb +26 -0
  68. data/spec/fixtures/rails7_users_app/config/initializers/filter_parameter_logging.rb +8 -0
  69. data/spec/fixtures/rails7_users_app/config/initializers/inflections.rb +16 -0
  70. data/spec/fixtures/rails7_users_app/config/initializers/permissions_policy.rb +11 -0
  71. data/spec/fixtures/rails7_users_app/config/locales/en.yml +33 -0
  72. data/spec/fixtures/rails7_users_app/config/puma.rb +43 -0
  73. data/spec/fixtures/rails7_users_app/config/routes.rb +6 -0
  74. data/spec/fixtures/rails7_users_app/config/storage.yml +34 -0
  75. data/spec/fixtures/rails7_users_app/config.ru +6 -0
  76. data/spec/fixtures/rails7_users_app/create_app +31 -0
  77. data/spec/fixtures/rails7_users_app/db/migrate/20220328093141_create_instances.rb +8 -0
  78. data/spec/fixtures/rails7_users_app/db/migrate/20220328093154_create_instructors.rb +8 -0
  79. data/spec/fixtures/rails7_users_app/db/schema.rb +27 -0
  80. data/spec/fixtures/rails7_users_app/db/seeds.rb +7 -0
  81. data/spec/fixtures/rails7_users_app/docker-compose.yml +31 -0
  82. data/spec/fixtures/rails7_users_app/lib/assets/.keep +0 -0
  83. data/spec/fixtures/rails7_users_app/lib/tasks/.keep +0 -0
  84. data/spec/fixtures/rails7_users_app/log/.keep +0 -0
  85. data/spec/fixtures/rails7_users_app/public/404.html +67 -0
  86. data/spec/fixtures/rails7_users_app/public/422.html +67 -0
  87. data/spec/fixtures/rails7_users_app/public/500.html +66 -0
  88. data/spec/fixtures/rails7_users_app/public/apple-touch-icon-precomposed.png +0 -0
  89. data/spec/fixtures/rails7_users_app/public/apple-touch-icon.png +0 -0
  90. data/spec/fixtures/rails7_users_app/public/favicon.ico +0 -0
  91. data/spec/fixtures/rails7_users_app/public/robots.txt +1 -0
  92. data/spec/fixtures/rails7_users_app/storage/.keep +0 -0
  93. data/spec/fixtures/rails7_users_app/test/application_system_test_case.rb +5 -0
  94. data/spec/fixtures/rails7_users_app/test/channels/application_cable/connection_test.rb +11 -0
  95. data/spec/fixtures/rails7_users_app/test/controllers/.keep +0 -0
  96. data/spec/fixtures/rails7_users_app/test/fixtures/files/.keep +0 -0
  97. data/spec/fixtures/rails7_users_app/test/fixtures/instances.yml +11 -0
  98. data/spec/fixtures/rails7_users_app/test/fixtures/instructors.yml +11 -0
  99. data/spec/fixtures/rails7_users_app/test/helpers/.keep +0 -0
  100. data/spec/fixtures/rails7_users_app/test/integration/.keep +0 -0
  101. data/spec/fixtures/rails7_users_app/test/mailers/.keep +0 -0
  102. data/spec/fixtures/rails7_users_app/test/models/.keep +0 -0
  103. data/spec/fixtures/rails7_users_app/test/models/instance_test.rb +6 -0
  104. data/spec/fixtures/rails7_users_app/test/models/instructor_test.rb +7 -0
  105. data/spec/fixtures/rails7_users_app/test/system/.keep +0 -0
  106. data/spec/fixtures/rails7_users_app/test/test_helper.rb +13 -0
  107. data/spec/handler/eval_spec.rb +66 -0
  108. data/spec/hook_spec.rb +2 -2
  109. data/spec/rails_recording_spec.rb +6 -4
  110. data/spec/rails_spec_helper.rb +7 -0
  111. data/spec/rails_test_spec.rb +45 -0
  112. metadata +95 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c287b504d552b0f627f6343e61f42835b144d12f3dea4e9f8a6e33bd72dbc581
4
- data.tar.gz: b00990f93c7961487a0c4ea6bc1f0828c3efa391578100de41600cc72ee1727d
3
+ metadata.gz: 8dce970e998105e2e322410b204af7a0ff48790cd177225468e9b49c75651fdb
4
+ data.tar.gz: d93b8f4f0690bf5ade69b2c08e4354d3043567fc4bf6018d6e68084eece08be9
5
5
  SHA512:
6
- metadata.gz: 0245f3bb4e483b6fba7326d759af828022e4339e05a293144e628c7fae558d744b105a0d3be8f44caa106f9b7b056fb3886176f3cdd513fe400c18126c4b7488
7
- data.tar.gz: 84cf727259f8ec5bad21b9d2ce799bc0753aeaeca0047fcf4296a0e5782d392a28340d44af0c08ab5d6a44fddb7baea4ea784ddd65e942329f159cf9eb4eac5b
6
+ metadata.gz: bea7fecb6bbae7feb68fa649ecba6276b3d497312ff99054c527d2f74117b584dd7119fa8e4a346b0091089786e90a0a2f2f375f1bc94a0e5437dfa8cff99e20
7
+ data.tar.gz: 913ab70d04467c584ecf5edfe4c8d60807974cf3d01eb9f2107ceaaa8677c157dc3b0e4130b6e90dcdaba3ba055b4a5268b40e94d0054eb6c41d1146205b0eff
data/.rubocop.yml CHANGED
@@ -19,6 +19,7 @@ Layout/LineLength:
19
19
 
20
20
  Metrics/BlockLength:
21
21
  ExcludedMethods:
22
+ - describe
22
23
  - it
23
24
  - context
24
25
 
data/.travis.yml CHANGED
@@ -33,13 +33,6 @@ before_install:
33
33
  && nvm use --lts \
34
34
  && npm i -g yarn
35
35
 
36
- jobs:
37
- include:
38
- - stage: test
39
- env:
40
- # GEM_ALTERNATIVE_NAME only needed for deployment
41
- - GEM_ALTERNATIVE_NAME=''
42
-
43
36
  before_deploy:
44
37
  - |
45
38
  npm i -g \
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [0.78.0](https://github.com/applandinc/appmap-ruby/compare/v0.77.4...v0.78.0) (2022-04-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * Hook and label Kernel#eval ([e0c151d](https://github.com/applandinc/appmap-ruby/commit/e0c151d1371f5bed5597ffd0d3bfebb8bca247c2))
7
+
8
+ ## [0.77.4](https://github.com/applandinc/appmap-ruby/compare/v0.77.3...v0.77.4) (2022-04-04)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Update Rails request handler to the new hook architecture ([595b39a](https://github.com/applandinc/appmap-ruby/commit/595b39abb030c1dcf85c83e4717c25d4c5177d4d))
14
+
15
+ ## [0.77.3](https://github.com/applandinc/appmap-ruby/compare/v0.77.2...v0.77.3) (2022-03-29)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * Rescue exceptions when calling Class#to_s ([f59f2f6](https://github.com/applandinc/appmap-ruby/commit/f59f2f6b39664ff050486c88ff1b859ca0db48d8))
21
+
1
22
  ## [0.77.2](https://github.com/applandinc/appmap-ruby/compare/v0.77.1...v0.77.2) (2022-03-25)
2
23
 
3
24
 
data/Rakefile CHANGED
@@ -2,6 +2,21 @@ $: << File.join(__dir__, 'lib')
2
2
  require 'appmap/version'
3
3
  GEM_VERSION = AppMap::VERSION
4
4
 
5
+ # Make sure the local version is not behind the one on
6
+ # rubygems.org (it's ok if they're the same).
7
+ #
8
+ # If it is behind, the fixture images won't get updated with the gem
9
+ # built from the local source, so you'll wind up testing the rubygems
10
+ # version instead.
11
+ unless ENV['SKIP_VERSION_CHECK']
12
+ require 'json'
13
+ require 'net/http'
14
+ rubygems_version = JSON.parse(Net::HTTP.get(URI.parse('https://rubygems.org/api/v1/gems/appmap.json')))['version']
15
+ if Gem::Version.new(GEM_VERSION) < Gem::Version.new(rubygems_version)
16
+ raise "#{GEM_VERSION} < #{rubygems_version}. Rebase to avoid build issues."
17
+ end
18
+ end
19
+
5
20
  require 'rake/testtask'
6
21
  require 'rdoc/task'
7
22
 
@@ -24,7 +39,7 @@ RUBY_VERSIONS=%w[2.6 2.7 3.0 3.1].select do |version|
24
39
 
25
40
  false
26
41
  end
27
- FIXTURE_APPS=%w[rack_users_app rails6_users_app rails5_users_app]
42
+ FIXTURE_APPS=[:rack_users_app, :rails6_users_app, :rails5_users_app, :rails7_users_app => {:ruby_version => '>= 2.7'}]
28
43
 
29
44
  def run_cmd(*cmd)
30
45
  $stderr.puts "Running: #{cmd}"
@@ -83,7 +98,17 @@ namespace :build do
83
98
  RUBY_VERSIONS.each do |ruby_version|
84
99
  namespace ruby_version do
85
100
  desc "build:fixtures:#{ruby_version}"
86
- FIXTURE_APPS.each do |app|
101
+ FIXTURE_APPS.each do |app_spec|
102
+ app = if app_spec.instance_of?(Hash)
103
+ app_spec = app_spec.flatten
104
+ version_rqt = Gem::Requirement.create(app_spec[1][:ruby_version])
105
+ next unless version_rqt =~ (Gem::Version.new(ruby_version))
106
+ app = app_spec[0]
107
+ else
108
+ app = app_spec
109
+ end.to_s
110
+
111
+
87
112
  desc app
88
113
  task app => ["base:#{ruby_version}"] do
89
114
  build_app_image(app, ruby_version)
@@ -109,12 +134,13 @@ def run_specs(ruby_version, task_args)
109
134
  # description), because it's not intended to be invoked directly
110
135
  RSpec::Core::RakeTask.new("rspec_#{ruby_version}", [:specs]) do |task, args|
111
136
  task.exclude_pattern = 'spec/fixtures/**/*_spec.rb'
137
+ task.rspec_opts = '-f doc'
112
138
  if args.count > 0
113
139
  # There doesn't appear to be a value for +pattern+ that will
114
140
  # cause it to be ignored. Setting it to '' or +nil+ causes an
115
141
  # empty argument to get passed to rspec, which confuses it.
116
142
  task.pattern = 'never match this'
117
- task.rspec_opts = args.to_a.join(' ')
143
+ task.rspec_opts += " " + args.to_a.join(' ')
118
144
  end
119
145
  end
120
146
 
data/ext/appmap/appmap.c CHANGED
@@ -1,4 +1,5 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/debug.h>
2
3
  #include <ruby/intern.h>
3
4
 
4
5
  // Seems like CLASS_OR_MODULE_P should really be in a header file in
@@ -39,7 +40,7 @@ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
40
  {
40
41
  VALUE arities_key = rb_intern(ARITIES_KEY);
41
42
  VALUE arities = rb_ivar_get(mod, arities_key);
42
-
43
+
43
44
  if (arities == Qundef || NIL_P(arities)) {
44
45
  arities = rb_hash_new();
45
46
  rb_ivar_set(mod, arities_key, arities);
@@ -62,7 +63,7 @@ am_get_method_arity(VALUE method, VALUE orig_arity_method)
62
63
  }
63
64
  // Didn't find one, call the original method.
64
65
  if (NIL_P(arity)) {
65
- VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
67
  arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
68
  }
68
69
 
@@ -83,11 +84,29 @@ am_method_arity(VALUE method)
83
84
  return am_get_method_arity(method, orig_method_arity);
84
85
  }
85
86
 
87
+ static VALUE
88
+ bindings_callback(const rb_debug_inspector_t *dbg_context, void *level)
89
+ {
90
+ const int i = NUM2INT((VALUE) level);
91
+ if (i >= RARRAY_LEN(rb_debug_inspector_backtrace_locations(dbg_context))) {
92
+ return Qnil;
93
+ } else {
94
+ return rb_debug_inspector_frame_binding_get(dbg_context, i);
95
+ }
96
+ }
97
+
98
+ static VALUE
99
+ am_previous_bindings(VALUE self, VALUE level)
100
+ {
101
+ return rb_debug_inspector_open(bindings_callback, (void *) level);
102
+ }
103
+
86
104
  void Init_appmap() {
87
105
  VALUE appmap = rb_define_module("AppMap");
88
106
  am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
107
 
90
108
  rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
109
+ rb_define_module_function(appmap, "caller_binding", am_previous_bindings, 1);
91
110
 
92
111
  rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
112
  rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
@@ -14,14 +14,17 @@
14
14
  - String#unpack1
15
15
  require_name: ruby
16
16
  label: string.unpack
17
- #- methods:
17
+ - methods:
18
+ - Kernel#eval
19
+ require_name: ruby
20
+ label: lang.eval
21
+ handler_class: AppMap::Handler::Eval
18
22
  # TODO: eval does not happen in the right context, and therefore any new constants
19
23
  # which are defined are placed on the wrong module/class.
20
- # - Kernel#eval
21
24
  # - Binding#eval
22
25
  # - BasicObject#instance_eval
23
26
  # These methods cannot be hooked as far as I can tell.
24
- # Why? When calling one of these functions, the context at the point of
27
+ # Why? When calling one of these functions, the context at the point of
25
28
  # definition is used. It's not possible to bind class_eval to a new context.
26
29
  # - Module#class_eval
27
30
  # - Module#module_eval
data/lib/appmap/config.rb CHANGED
@@ -450,7 +450,15 @@ module AppMap
450
450
 
451
451
  # Hook a method which is specified by class and method name.
452
452
  def package_for_code_object
453
- class_name = cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.name
453
+ class_name = begin
454
+ cls.to_s.index('#<Class:') == 0 ? cls.to_s['#<Class:'.length...-1] : cls.name
455
+ rescue
456
+ # Calling #to_s on some Rails classes
457
+ # (e.g. those generated to represent
458
+ # associations) will raise an exception. Fall
459
+ # back to using the class name.
460
+ cls.name
461
+ end
454
462
  Array(config.gem_hooks[class_name])
455
463
  .find { |hook| hook.include_method?(method.name) }
456
464
  &.package
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/handler/function'
4
+
5
+ module AppMap
6
+ module Handler
7
+ # Handler class for Kernel#eval.
8
+ #
9
+ # Second argument to eval is a Binding, which despite the name (and
10
+ # the accessible methods) in addition to locals and receiver also
11
+ # encapsulates the entire execution context, in particular including
12
+ # the lexical scope. This is especially important for constant lookup
13
+ # and definition.
14
+ #
15
+ # If the binding is not provided, by default eval will run in the
16
+ # current frame. Since we call it here, this will mean the #do_call
17
+ # frame, which would make AppMap::Handler::Eval the lexical scope
18
+ # for constant lookup and definition; as a consequence
19
+ # eg. `eval "class Foo; end"` would define
20
+ # AppMap::Handler::Eval::Foo instead of defining it in
21
+ # the module where the original call was made.
22
+ #
23
+ # To avoid this, we explicitly substitute the correct execution
24
+ # context, up several stack frames.
25
+ class Eval < Function
26
+ # The depth of the frame we need to pluck out:
27
+ # 1. Hook::Method#do_call
28
+ # 2. Hook::Method#trace_call
29
+ # 3. Hook::Method#call
30
+ # 4. proc generated by Hook::Method#hook_method_def
31
+ # 5. the (intended) frame of the original eval that we hooked
32
+ # Note it needs to be adjusted if this call sequence changes.
33
+ FRAME_DEPTH = 5
34
+
35
+ def do_call(receiver, src = nil, context = nil, *rest)
36
+ context ||= AppMap.caller_binding FRAME_DEPTH
37
+ hook_method.bind(receiver).call(src, context, *rest)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/event'
4
+ require 'appmap/hook/method'
4
5
 
5
6
  module AppMap
6
7
  module Handler
7
- module Function
8
- class << self
9
- def handle_call(defined_class, hook_method, receiver, args)
10
- AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
11
- end
8
+ # Base handler class, will emit method call and return events.
9
+ class Function < Hook::Method
10
+ def handle_call(receiver, args)
11
+ AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
12
+ end
12
13
 
13
- def handle_return(call_event_id, elapsed, return_value, exception)
14
- AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
15
- end
14
+ def handle_return(call_event_id, elapsed, return_value, exception)
15
+ AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
16
16
  end
17
17
  end
18
18
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/event'
4
+ require 'appmap/hook/method'
4
5
  require 'appmap/util'
5
6
  require 'rack'
6
7
 
@@ -81,29 +82,29 @@ module AppMap
81
82
  end
82
83
  end
83
84
 
84
- class NetHTTP
85
- class << self
86
- def copy_headers(obj)
87
- {}.tap do |headers|
88
- obj.each_header do |key, value|
89
- key = key.split('-').map(&:capitalize).join('-')
90
- headers[key] = value
91
- end
85
+ # Handler class for HTTP requests.
86
+ # Emits HTTP request events instead of method calls.
87
+ class NetHTTP < Hook::Method
88
+ def self.copy_headers(obj)
89
+ {}.tap do |headers|
90
+ obj.each_header do |key, value|
91
+ key = key.split('-').map(&:capitalize).join('-')
92
+ headers[key] = value
92
93
  end
93
94
  end
95
+ end
94
96
 
95
- def handle_call(defined_class, hook_method, receiver, args)
96
- # request will call itself again in a start block if it's not already started.
97
- return unless receiver.started?
97
+ def handle_call(receiver, args)
98
+ # request will call itself again in a start block if it's not already started.
99
+ return unless receiver.started?
98
100
 
99
- http = receiver
100
- request = args.first
101
- HTTPClientRequest.new(http, request)
102
- end
101
+ http = receiver
102
+ request = args.first
103
+ HTTPClientRequest.new(http, request)
104
+ end
103
105
 
104
- def handle_return(call_event_id, elapsed, return_value, exception)
105
- HTTPClientResponse.new(return_value, call_event_id, elapsed)
106
- end
106
+ def handle_return(call_event_id, elapsed, return_value, exception)
107
+ HTTPClientResponse.new(return_value, call_event_id, elapsed)
107
108
  end
108
109
  end
109
110
  end
@@ -100,15 +100,14 @@ module AppMap
100
100
 
101
101
  protected
102
102
 
103
- def before_hook(receiver, defined_class, _) # args
103
+ def before_hook(receiver, *)
104
104
  call_event = HTTPServerRequest.new(receiver.request)
105
105
  # http_server_request events are i/o and do not require a package name.
106
106
  AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
107
- [ call_event, TIME_NOW.call ]
107
+ call_event
108
108
  end
109
109
 
110
- def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
111
- elapsed = TIME_NOW.call - start_time
110
+ def after_hook(receiver, call_event, elapsed, *)
112
111
  return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
113
112
  AppMap.tracing.record_event return_event
114
113
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'appmap/handler/function'
3
4
  require 'appmap/event'
4
5
 
5
6
  module AppMap
@@ -9,7 +10,7 @@ module AppMap
9
10
  LOG = (ENV['APPMAP_TEMPLATE_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
10
11
 
11
12
  # All the code which is touched by the AppMap is recorded in the classMap.
12
- # This duck-typed 'method' is used to represent a view template as a package,
13
+ # This duck-typed 'method' is used to represent a view template as a package,
13
14
  # class, and method in the classMap.
14
15
  # The class name is generated from the template path. The package name is
15
16
  # 'app/views', and the method name is 'render'. The source location of the method
@@ -41,31 +42,31 @@ module AppMap
41
42
  def package
42
43
  'app/views'
43
44
  end
44
-
45
+
45
46
  def name
46
47
  'render'
47
48
  end
48
-
49
+
49
50
  def source_location
50
51
  path
51
52
  end
52
-
53
+
53
54
  def static
54
55
  true
55
56
  end
56
-
57
+
57
58
  def comment
58
59
  nil
59
60
  end
60
-
61
+
61
62
  def labels
62
63
  [ 'mvc.template' ]
63
64
  end
64
65
  end
65
-
66
+
66
67
  # TemplateCall is a type of function call which is specialized to view template rendering. Since
67
68
  # there isn't really a perfect method in Rails to hook, this one is synthesized from the available
68
- # information.
69
+ # information.
69
70
  class TemplateCall < AppMap::Event::MethodEvent
70
71
  # This is basically the +self+ parameter.
71
72
  attr_reader :render_instance
@@ -75,19 +76,19 @@ module AppMap
75
76
  attr_accessor :ready
76
77
 
77
78
  alias ready? ready
78
-
79
+
79
80
  def initialize(render_instance)
80
81
  super :call
81
-
82
+
82
83
  AppMap::Event::MethodEvent.build_from_invocation(:call, event: self)
83
84
  @ready = false
84
85
  @render_instance = render_instance
85
86
  end
86
-
87
+
87
88
  def static?
88
89
  true
89
90
  end
90
-
91
+
91
92
  def to_h
92
93
  super.tap do |h|
93
94
  h[:defined_class] = path ? path.parameterize.underscore : 'inline_template'
@@ -103,71 +104,76 @@ module AppMap
103
104
  end.compact
104
105
  end
105
106
  end
106
-
107
+
107
108
  TEMPLATE_RENDERER = 'appmap.handler.rails.template.renderer'
108
109
 
109
110
  # Hooks the ActionView::Resolver methods +find_all+, +find_all_anywhere+. The resolver is used
110
111
  # during template rendering to lookup the template file path from parameters such as the
111
112
  # template name, prefix, and partial (boolean).
112
- class ResolverHandler
113
- class << self
114
- # Handled as a normal function call.
115
- def handle_call(defined_class, hook_method, receiver, args)
116
- name, prefix, partial = args
117
- warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
118
-
119
- AppMap::Handler::Function.handle_call(defined_class, hook_method, receiver, args)
120
- end
113
+ class ResolverHandler < AppMap::Handler::Function
114
+ def handle_call(receiver, args)
115
+ name, prefix, partial = args
116
+ warn "Resolver: #{{ name: name, prefix: prefix, partial: partial }}" if LOG
121
117
 
122
- # When the resolver returns, look to see if there is template rendering underway.
123
- # If so, populate the template path. In all cases, add a TemplateMethod so that the
124
- # template will be recorded in the classMap.
125
- def handle_return(call_event_id, elapsed, return_value, exception)
126
- renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
127
- path_obj = Array(return_value).first
128
-
129
- warn "Resolver return: #{path_obj}" if LOG
130
-
131
- if path_obj
132
- path = if path_obj.respond_to?(:identifier) && path_obj.inspect.index('#<')
133
- path_obj.identifier
134
- else
135
- path_obj.inspect
136
- end
137
- path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
138
- AppMap.tracing.record_method(TemplateMethod.new(path))
139
- renderer.path ||= path if renderer
140
- end
118
+ super
119
+ end
141
120
 
142
- AppMap::Handler::Function.handle_return(call_event_id, elapsed, return_value, exception)
143
- end
121
+ # When the resolver returns, look to see if there is template rendering underway.
122
+ # If so, populate the template path. In all cases, add a TemplateMethod so that the
123
+ # template will be recorded in the classMap.
124
+ def handle_return(call_event_id, elapsed, return_value, exception)
125
+ renderer = Array(Thread.current[TEMPLATE_RENDERER]).last
126
+ path_obj = Array(return_value).first
127
+
128
+ warn "Resolver return: #{path_obj}" if LOG
129
+
130
+ record_template_path renderer, path_obj
131
+
132
+ super
133
+ end
134
+
135
+ def record_template_path(renderer, path_obj)
136
+ return unless path_obj
137
+
138
+ path = path_from_obj path_obj
139
+ AppMap.tracing.record_method(TemplateMethod.new(path))
140
+ renderer.path ||= path if renderer
141
+ end
142
+
143
+ def path_from_obj(path_obj)
144
+ path =
145
+ if path_obj.respond_to?(:identifier) && path_obj.inspect.index('#<')
146
+ path_obj.identifier
147
+ else
148
+ path_obj.inspect
149
+ end
150
+ path = path[Dir.pwd.length + 1..] if path.index(Dir.pwd) == 0
151
+ path
144
152
  end
145
153
  end
146
154
 
147
155
  # Hooks the ActionView::Renderer method +render+. This method is used by Rails to perform
148
156
  # template rendering. The TemplateCall event which is emitted by this handler has a
149
- # +path+ parameter, which is nil until it's filled in by a ResolverHandler.
150
- class RenderHandler
151
- class << self
152
- def handle_call(defined_class, hook_method, receiver, args)
153
- # context, options
154
- _, options = args
155
-
156
- warn "Renderer: #{options}" if LOG
157
-
158
- TemplateCall.new(receiver).tap do |call|
159
- Thread.current[TEMPLATE_RENDERER] ||= []
160
- Thread.current[TEMPLATE_RENDERER] << call
161
- end
162
- end
163
-
164
- def handle_return(call_event_id, elapsed, return_value, exception)
165
- template_call = Array(Thread.current[TEMPLATE_RENDERER]).pop
166
- template_call.ready = true
157
+ # +path+ parameter, which is nil until it's filled in by a ResolverHandler.
158
+ class RenderHandler < AppMap::Hook::Method
159
+ def handle_call(receiver, args)
160
+ # context, options
161
+ _, options = args
162
+
163
+ warn "Renderer: #{options}" if LOG
167
164
 
168
- AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
165
+ TemplateCall.new(receiver).tap do |call|
166
+ Thread.current[TEMPLATE_RENDERER] ||= []
167
+ Thread.current[TEMPLATE_RENDERER] << call
169
168
  end
170
169
  end
170
+
171
+ def handle_return(call_event_id, elapsed, _return_value, _exception)
172
+ template_call = Array(Thread.current[TEMPLATE_RENDERER]).pop
173
+ template_call.ready = true
174
+
175
+ AppMap::Event::MethodReturnIgnoreValue.build_from_invocation(call_event_id, elapsed: elapsed)
176
+ end
171
177
  end
172
178
  end
173
179
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ def ruby2_keywords(*); end unless respond_to?(:ruby2_keywords, true)
4
+
5
+ module AppMap
6
+ class Hook
7
+ # Delegation methods for Ruby 2.
8
+ # cf. https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
9
+ class Method
10
+ ruby2_keywords def call(receiver, *args, &block)
11
+ return do_call(receiver, *args, &block) unless trace?
12
+
13
+ call_event = with_disabled_hook { before_hook receiver, *args }
14
+ trace_call call_event, receiver, *args, &block
15
+ end
16
+
17
+ protected
18
+
19
+ def before_hook(receiver, *args)
20
+ call_event = handle_call(receiver, args)
21
+ if call_event
22
+ AppMap.tracing.record_event \
23
+ call_event,
24
+ package: hook_package,
25
+ defined_class: defined_class,
26
+ method: hook_method
27
+ end
28
+ call_event
29
+ end
30
+
31
+ ruby2_keywords def do_call(receiver, *args, &block)
32
+ hook_method.bind(receiver).call(*args, &block)
33
+ end
34
+
35
+ ruby2_keywords def trace_call(call_event, receiver, *args, &block)
36
+ start_time = gettime
37
+ begin
38
+ return_value = do_call(receiver, *args, &block)
39
+ rescue # rubocop:disable Style/RescueStandardError
40
+ exception = $ERROR_INFO
41
+ raise
42
+ ensure
43
+ with_disabled_hook { after_hook receiver, call_event, gettime - start_time, return_value, exception } \
44
+ if call_event
45
+ end
46
+ end
47
+
48
+ def hook_method_def
49
+ this = self
50
+ proc { |*args, &block| this.call self, *args, &block }.tap do |hook|
51
+ hook.ruby2_keywords if hook.respond_to? :ruby2_keywords
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end