appmap 0.38.1 → 0.41.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/.travis.yml +2 -23
  4. data/CHANGELOG.md +26 -1
  5. data/CONTRIBUTING.md +22 -0
  6. data/README.md +119 -53
  7. data/Rakefile +3 -3
  8. data/lib/appmap/class_map.rb +25 -8
  9. data/lib/appmap/config.rb +41 -21
  10. data/lib/appmap/event.rb +14 -4
  11. data/lib/appmap/hook.rb +18 -3
  12. data/lib/appmap/rails/request_handler.rb +17 -3
  13. data/lib/appmap/railtie.rb +1 -5
  14. data/lib/appmap/version.rb +2 -2
  15. data/spec/abstract_controller_base_spec.rb +116 -86
  16. data/spec/config_spec.rb +1 -0
  17. data/spec/fixtures/hook/exclude.rb +15 -0
  18. data/spec/fixtures/hook/labels.rb +6 -0
  19. data/spec/fixtures/rails5_users_app/Gemfile +2 -3
  20. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +8 -0
  21. data/spec/fixtures/rails5_users_app/appmap.yml +4 -1
  22. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  23. data/spec/fixtures/rails5_users_app/config/routes.rb +1 -1
  24. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +11 -0
  25. data/spec/fixtures/rails6_users_app/Gemfile +2 -3
  26. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +8 -0
  27. data/spec/fixtures/rails6_users_app/appmap.yml +4 -1
  28. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  29. data/spec/fixtures/rails6_users_app/config/routes.rb +1 -1
  30. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +11 -0
  31. data/spec/hook_spec.rb +41 -41
  32. data/spec/rails_spec_helper.rb +2 -2
  33. data/spec/record_sql_rails_pg_spec.rb +1 -1
  34. data/spec/rspec_feature_metadata_spec.rb +1 -1
  35. data/spec/spec_helper.rb +1 -0
  36. data/test/fixtures/gem_test/appmap.yml +1 -1
  37. data/test/fixtures/gem_test/test/parser_test.rb +12 -0
  38. data/test/gem_test.rb +4 -4
  39. metadata +6 -69
  40. data/spec/abstract_controller4_base_spec.rb +0 -66
  41. data/spec/fixtures/rails4_users_app/.gitignore +0 -13
  42. data/spec/fixtures/rails4_users_app/.rbenv-gemsets +0 -2
  43. data/spec/fixtures/rails4_users_app/.ruby-version +0 -1
  44. data/spec/fixtures/rails4_users_app/Dockerfile +0 -30
  45. data/spec/fixtures/rails4_users_app/Dockerfile.pg +0 -3
  46. data/spec/fixtures/rails4_users_app/Gemfile +0 -77
  47. data/spec/fixtures/rails4_users_app/README.rdoc +0 -28
  48. data/spec/fixtures/rails4_users_app/Rakefile +0 -6
  49. data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
  50. data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +0 -16
  51. data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +0 -15
  52. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +0 -27
  53. data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +0 -5
  54. data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
  55. data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +0 -5
  56. data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +0 -5
  57. data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +0 -2
  58. data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
  59. data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
  60. data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
  61. data/spec/fixtures/rails4_users_app/app/models/user.rb +0 -18
  62. data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +0 -7
  63. data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +0 -7
  64. data/spec/fixtures/rails4_users_app/appmap.yml +0 -3
  65. data/spec/fixtures/rails4_users_app/bin/rails +0 -9
  66. data/spec/fixtures/rails4_users_app/bin/setup +0 -29
  67. data/spec/fixtures/rails4_users_app/bin/spring +0 -17
  68. data/spec/fixtures/rails4_users_app/config.ru +0 -4
  69. data/spec/fixtures/rails4_users_app/config/application.rb +0 -26
  70. data/spec/fixtures/rails4_users_app/config/boot.rb +0 -3
  71. data/spec/fixtures/rails4_users_app/config/database.yml +0 -18
  72. data/spec/fixtures/rails4_users_app/config/environment.rb +0 -5
  73. data/spec/fixtures/rails4_users_app/config/environments/development.rb +0 -41
  74. data/spec/fixtures/rails4_users_app/config/environments/production.rb +0 -79
  75. data/spec/fixtures/rails4_users_app/config/environments/test.rb +0 -42
  76. data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +0 -11
  77. data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +0 -7
  78. data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +0 -3
  79. data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +0 -4
  80. data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +0 -16
  81. data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +0 -4
  82. data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +0 -3
  83. data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +0 -10
  84. data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +0 -14
  85. data/spec/fixtures/rails4_users_app/config/locales/en.yml +0 -23
  86. data/spec/fixtures/rails4_users_app/config/routes.rb +0 -12
  87. data/spec/fixtures/rails4_users_app/config/secrets.yml +0 -22
  88. data/spec/fixtures/rails4_users_app/create_app +0 -23
  89. data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +0 -10
  90. data/spec/fixtures/rails4_users_app/db/schema.rb +0 -26
  91. data/spec/fixtures/rails4_users_app/db/seeds.rb +0 -7
  92. data/spec/fixtures/rails4_users_app/docker-compose.yml +0 -26
  93. data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
  94. data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
  95. data/spec/fixtures/rails4_users_app/log/.keep +0 -0
  96. data/spec/fixtures/rails4_users_app/public/404.html +0 -67
  97. data/spec/fixtures/rails4_users_app/public/422.html +0 -67
  98. data/spec/fixtures/rails4_users_app/public/500.html +0 -66
  99. data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
  100. data/spec/fixtures/rails4_users_app/public/robots.txt +0 -5
  101. data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +0 -49
  102. data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +0 -95
  103. data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +0 -96
  104. data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +0 -9
  105. data/spec/record_sql_rails4_pg_spec.rb +0 -75
  106. data/test/fixtures/gem_test/test/to_param_test.rb +0 -14
data/lib/appmap/config.rb CHANGED
@@ -16,6 +16,10 @@ module AppMap
16
16
  end
17
17
 
18
18
  def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
19
+ if %w[method_source activesupport].member?(gem)
20
+ warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
21
+ return
22
+ end
19
23
  gem_paths(gem).map do |gem_path|
20
24
  Package.new(gem_path, gem, package_name, exclude, labels, shallow)
21
25
  end
@@ -57,32 +61,32 @@ module AppMap
57
61
  # Methods that should always be hooked, with their containing
58
62
  # package and labels that should be applied to them.
59
63
  HOOKED_METHODS = {
60
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[security crypto])),
61
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[view]))
64
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[provider.secure_compare])),
65
+ 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', package_name: 'action_view', labels: %w[mvc.view]))
62
66
  }.freeze
63
67
 
64
68
  BUILTIN_METHODS = {
65
69
  'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
66
- 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGES),
67
70
  'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
68
71
  'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
69
72
  'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
70
73
  'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
71
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[http io])),
72
- 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
73
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
74
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
75
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[serialization marshal])),
76
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[serialization yaml])),
77
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
78
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
74
+ 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http io])),
75
+ 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
76
+ 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
77
+ 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
78
+ 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization marshal])),
79
+ 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml provider.serialization])),
80
+ 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
81
+ 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
79
82
  }.freeze
80
83
 
81
- attr_reader :name, :packages
84
+ attr_reader :name, :packages, :exclude
82
85
 
83
- def initialize(name, packages = [])
86
+ def initialize(name, packages = [], exclude = [])
84
87
  @name = name
85
88
  @packages = packages
89
+ @exclude = exclude
86
90
  end
87
91
 
88
92
  class << self
@@ -107,42 +111,58 @@ module AppMap
107
111
  else
108
112
  [ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow']) ]
109
113
  end
110
- end.flatten
111
- Config.new config_data['name'], packages
114
+ end.flatten.compact
115
+ Config.new config_data['name'], packages, config_data['exclude'] || []
112
116
  end
113
117
  end
114
118
 
115
119
  def to_h
116
120
  {
117
121
  name: name,
118
- packages: packages.map(&:to_h)
122
+ packages: packages.map(&:to_h),
123
+ exclude: exclude
119
124
  }
120
125
  end
121
126
 
127
+ # package_for_method finds the Package, if any, which configures the hook
128
+ # for a method.
122
129
  def package_for_method(method)
130
+ package_hooked_by_class(method) || package_hooked_by_source_location(method)
131
+ end
132
+
133
+ def package_hooked_by_class(method)
123
134
  defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
124
- package = find_package(defined_class, method_name)
125
- return package if package
135
+ return find_package(defined_class, method_name)
136
+ end
126
137
 
138
+ def package_hooked_by_source_location(method)
127
139
  location = method.source_location
128
140
  location_file, = location
129
141
  return unless location_file
130
142
 
131
143
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
132
- packages.find do |pkg|
144
+ packages.select { |pkg| pkg.path }.find do |pkg|
133
145
  (location_file.index(pkg.path) == 0) &&
134
146
  !pkg.exclude.find { |p| location_file.index(p) }
135
147
  end
136
148
  end
137
149
 
138
- def included_by_location?(method)
139
- !!package_for_method(method)
150
+ def never_hook?(method)
151
+ defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
152
+ return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
140
153
  end
141
154
 
155
+ # always_hook? indicates a method that should always be hooked.
142
156
  def always_hook?(defined_class, method_name)
143
157
  !!find_package(defined_class, method_name)
144
158
  end
145
159
 
160
+ # included_by_location? indicates a method whose source location matches a method definition that has been
161
+ # configured for inclusion.
162
+ def included_by_location?(method)
163
+ !!package_for_method(method)
164
+ end
165
+
146
166
  def find_package(defined_class, method_name)
147
167
  hook = find_hook(defined_class)
148
168
  return nil unless hook
data/lib/appmap/event.rb CHANGED
@@ -38,6 +38,15 @@ module AppMap
38
38
 
39
39
  protected
40
40
 
41
+ # Heuristic for dynamically defined class whose name can be nil
42
+ def best_class_name(value)
43
+ value_cls = value.class
44
+ while value_cls.name.nil?
45
+ value_cls = value_cls.superclass
46
+ end
47
+ value_cls.name
48
+ end
49
+
41
50
  def custom_display_string(value)
42
51
  case value
43
52
  when File
@@ -77,6 +86,7 @@ module AppMap
77
86
 
78
87
  class << self
79
88
  def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
89
+ defined_class ||= 'Class'
80
90
  mc.tap do
81
91
  static = receiver.is_a?(Module)
82
92
  mc.defined_class = defined_class
@@ -110,14 +120,14 @@ module AppMap
110
120
  end
111
121
  {
112
122
  name: param_name,
113
- class: value.class.name,
123
+ class: best_class_name(value),
114
124
  object_id: value.__id__,
115
125
  value: display_string(value),
116
126
  kind: param_type
117
127
  }
118
128
  end
119
129
  mc.receiver = {
120
- class: receiver.class.name,
130
+ class: best_class_name(receiver),
121
131
  object_id: receiver.__id__,
122
132
  value: display_string(receiver)
123
133
  }
@@ -172,7 +182,7 @@ module AppMap
172
182
  mr.tap do |_|
173
183
  if return_value
174
184
  mr.return_value = {
175
- class: return_value.class.name,
185
+ class: best_class_name(return_value),
176
186
  value: display_string(return_value),
177
187
  object_id: return_value.__id__
178
188
  }
@@ -183,7 +193,7 @@ module AppMap
183
193
  while next_exception
184
194
  exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
185
195
  exceptions << {
186
- class: next_exception.class.name,
196
+ class: best_class_name(next_exception),
187
197
  message: next_exception.message,
188
198
  object_id: next_exception.__id__,
189
199
  path: exception_backtrace&.path,
data/lib/appmap/hook.rb CHANGED
@@ -6,6 +6,9 @@ module AppMap
6
6
  class Hook
7
7
  LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
8
 
9
+ OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self].freeze
10
+ OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self].freeze
11
+
9
12
  @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
13
  @method_arity = ::Method.instance_method(:arity)
11
14
 
@@ -42,12 +45,17 @@ module AppMap
42
45
  tp = TracePoint.new(:end) do |trace_point|
43
46
  cls = trace_point.self
44
47
 
45
- instance_methods = cls.public_instance_methods(false)
46
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
48
+ instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
49
+ class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
47
50
 
48
51
  hook = lambda do |hook_cls|
49
52
  lambda do |method_id|
50
- method = hook_cls.public_instance_method(method_id)
53
+ method = begin
54
+ hook_cls.public_instance_method(method_id)
55
+ rescue NameError
56
+ warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
57
+ return
58
+ end
51
59
 
52
60
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
53
61
 
@@ -55,6 +63,8 @@ module AppMap
55
63
  # Skip methods that have no instruction sequence, as they are obviously trivial.
56
64
  next unless disasm
57
65
 
66
+ next if config.never_hook?(method)
67
+
58
68
  next unless \
59
69
  config.always_hook?(hook_cls, method.name) ||
60
70
  config.included_by_location?(method)
@@ -76,6 +86,8 @@ module AppMap
76
86
  tp.enable(&block)
77
87
  end
78
88
 
89
+ # hook_builtins builds hooks for code that is built in to the Ruby standard library.
90
+ # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
79
91
  def hook_builtins
80
92
  return unless self.class.lock_builtins
81
93
 
@@ -89,6 +101,7 @@ module AppMap
89
101
  require hook.package.package_name if hook.package.package_name
90
102
  Array(hook.method_names).each do |method_name|
91
103
  method_name = method_name.to_sym
104
+
92
105
  cls = class_from_string.(class_name)
93
106
  method = \
94
107
  begin
@@ -97,6 +110,8 @@ module AppMap
97
110
  cls.method(method_name) rescue nil
98
111
  end
99
112
 
113
+ next if config.never_hook?(method)
114
+
100
115
  if method
101
116
  Hook::Method.new(hook.package, cls, method).activate
102
117
  else
@@ -7,12 +7,13 @@ module AppMap
7
7
  module Rails
8
8
  module RequestHandler
9
9
  class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :request_method, :path_info, :params
10
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params
11
11
 
12
12
  def initialize(request)
13
13
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
14
 
15
15
  @request_method = request.request_method
16
+ @normalized_path_info = normalized_path request
16
17
  @path_info = request.path_info.split('?')[0]
17
18
  # ActionDispatch::Http::ParameterFilter is deprecated
18
19
  parameter_filter_cls = \
@@ -28,8 +29,9 @@ module AppMap
28
29
  super.tap do |h|
29
30
  h[:http_server_request] = {
30
31
  request_method: request_method,
31
- path_info: path_info
32
- }
32
+ path_info: path_info,
33
+ normalized_path_info: normalized_path_info
34
+ }.compact
33
35
 
34
36
  h[:message] = params.keys.map do |key|
35
37
  val = params[key]
@@ -42,6 +44,18 @@ module AppMap
42
44
  end
43
45
  end
44
46
  end
47
+
48
+ private
49
+
50
+ def normalized_path(request, router = ::Rails.application.routes.router)
51
+ router.recognize request do |route, _|
52
+ app = route.app
53
+ next unless app.matches? request
54
+ return normalized_path request, app.rack_app.routes.router if app.engine?
55
+
56
+ return route.path.spec.to_s
57
+ end
58
+ end
45
59
  end
46
60
 
47
61
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
@@ -5,13 +5,9 @@ module AppMap
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.appmap = ActiveSupport::OrderedOptions.new
7
7
 
8
- initializer 'appmap.init' do |_| # params: app
9
- require 'appmap'
10
- end
11
-
12
8
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
13
9
  # AppMap events.
14
- initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
10
+ initializer 'appmap.subscribe' do |_| # params: app
15
11
  require 'appmap/rails/sql_handler'
16
12
  require 'appmap/rails/request_handler'
17
13
  ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.38.1'
6
+ VERSION = '0.41.1'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.3'
8
+ APPMAP_FORMAT_VERSION = '1.4'
9
9
  end
@@ -1,120 +1,150 @@
1
1
  require 'rails_spec_helper'
2
2
 
3
3
  describe 'AbstractControllerBase' do
4
- shared_examples 'rails version' do |rails_major_version|
5
- include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app" do
4
+ %w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
5
+ context "in Rails #{rails_major_version}" do
6
+ include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app"
6
7
  def run_spec(spec_name)
7
- cmd = "docker-compose run --rm -e APPMAP=true -v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}"
8
+ FileUtils.rm_rf tmpdir
9
+ FileUtils.mkdir_p tmpdir
10
+ cmd = <<~CMD.gsub "\n", ' '
11
+ docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true
12
+ -v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
13
+ CMD
8
14
  run_cmd cmd, chdir: fixture_dir
9
15
  end
10
16
 
11
- before do
12
- FileUtils.rm_rf tmpdir
13
- FileUtils.mkdir_p tmpdir
14
- run_spec spec_name
17
+ def tmpdir
18
+ 'tmp/spec/AbstractControllerBase'
15
19
  end
16
20
 
17
- let(:tmpdir) { 'tmp/spec/AbstractControllerBase' }
21
+ let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
22
+ let(:events) { appmap['events'] }
18
23
 
19
24
  describe 'testing with rspec' do
20
- let(:spec_name) { 'spec/controllers/users_controller_api_spec.rb:8' }
21
- let(:appmap_json_file) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json') }
22
-
23
25
  describe 'creating a user' do
26
+ before(:all) { run_spec 'spec/controllers/users_controller_api_spec.rb:8' }
27
+ let(:appmap_json_file) do
28
+ 'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
29
+ end
30
+
24
31
  it 'inventory file is printed' do
25
32
  expect(File).to exist(File.join(tmpdir, 'appmap/rspec/Inventory.appmap.json'))
26
33
  end
27
34
 
28
35
  it 'message fields are recorded in the appmap' do
29
- expect(File).to exist(appmap_json_file)
30
- appmap = JSON.parse(File.read(appmap_json_file)).to_yaml
31
-
32
- expect(appmap).to include(<<-MESSAGE.strip)
33
- message:
34
- - name: login
35
- class: String
36
- value: alice
37
- object_id:
38
- MESSAGE
39
-
40
- expect(appmap).to include(<<-MESSAGE.strip)
41
- - name: password
42
- class: String
43
- value: "[FILTERED]"
44
- object_id:
45
- MESSAGE
46
-
47
- expect(appmap).to include(<<-SERVER_REQUEST.strip)
48
- http_server_request:
49
- request_method: POST
50
- path_info: "/api/users"
51
- SERVER_REQUEST
52
-
53
- expect(appmap).to include(<<-SERVER_RESPONSE.strip)
54
- http_server_response:
55
- status: 201
56
- mime_type: application/json; charset=utf-8
57
- SERVER_RESPONSE
36
+ expect(events).to include(
37
+ hash_including(
38
+ 'http_server_request' => hash_including(
39
+ 'request_method' => 'POST',
40
+ 'path_info' => '/api/users'
41
+ ),
42
+ 'message' => include(
43
+ hash_including(
44
+ 'name' => 'login',
45
+ 'class' => 'String',
46
+ 'value' => 'alice',
47
+ 'object_id' => Integer
48
+ ),
49
+ hash_including(
50
+ 'name' => 'password',
51
+ 'class' => 'String',
52
+ 'value' => '[FILTERED]',
53
+ 'object_id' => Integer
54
+ )
55
+ )
56
+ ),
57
+ hash_including(
58
+ 'http_server_response' => {
59
+ 'status' => 201,
60
+ 'mime_type' => 'application/json; charset=utf-8'
61
+ }
62
+ )
63
+ )
58
64
  end
59
65
 
60
66
  it 'properly captures method parameters in the appmap' do
61
- expect(File).to exist(appmap_json_file)
62
- appmap = JSON.parse(File.read(appmap_json_file)).to_yaml
63
-
64
- expect(appmap).to match(<<-CREATE_CALL.strip)
65
- event: call
66
- thread_id: .*
67
- defined_class: Api::UsersController
68
- method_id: build_user
69
- path: app/controllers/api/users_controller.rb
70
- lineno: 23
71
- static: false
72
- parameters:
73
- - name: params
74
- class: ActiveSupport::HashWithIndifferentAccess
75
- object_id: .*
76
- value: '{"login"=>"alice"}'
77
- kind: req
78
- receiver:
79
- CREATE_CALL
67
+ expect(events).to include hash_including(
68
+ 'event' => 'call',
69
+ 'thread_id' => Integer,
70
+ 'defined_class' => 'Api::UsersController',
71
+ 'method_id' => 'build_user',
72
+ 'path' => 'app/controllers/api/users_controller.rb',
73
+ 'lineno' => 23,
74
+ 'static' => false,
75
+ 'parameters' => include(
76
+ 'name' => 'params',
77
+ 'class' => 'ActiveSupport::HashWithIndifferentAccess',
78
+ 'object_id' => Integer,
79
+ 'value' => '{"login"=>"alice"}',
80
+ 'kind' => 'req'
81
+ ),
82
+ 'receiver' => anything
83
+ )
80
84
  end
81
85
 
82
86
  it 'returns a minimal event' do
83
- expect(File).to exist(appmap_json_file)
84
- appmap = JSON.parse(File.read(appmap_json_file))
85
- event = appmap['events'].find { |event| event['event'] == 'return' && event['return_value'] }
86
- expect(event.keys).to eq(%w[id event thread_id parent_id elapsed return_value])
87
+ expect(events).to include hash_including(
88
+ 'event' => 'return',
89
+ 'return_value' => Hash,
90
+ 'id' => Integer,
91
+ 'thread_id' => Integer,
92
+ 'parent_id' => Integer,
93
+ 'elapsed' => Numeric
94
+ )
87
95
  end
88
96
  end
89
97
 
90
- describe 'listing users' do
91
- let(:spec_name) { 'spec/controllers/users_controller_spec.rb:11' }
92
- let(:appmap_json_file) { File.join(tmpdir, 'appmap/rspec/UsersController_GET_users_lists_the_users.appmap.json') }
93
- it 'records and labels view rendering' do
94
- expect(File).to exist(appmap_json_file)
95
- appmap = JSON.parse(File.read(appmap_json_file)).to_yaml
98
+ describe 'showing a user' do
99
+ before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:22' }
100
+ let(:appmap_json_file) do
101
+ 'UsersController_GET_users_login_shows_the_user.appmap.json'
102
+ end
96
103
 
97
- expect(appmap).to match(<<-VIEW_CALL.strip)
98
- event: call
99
- thread_id: .*
100
- defined_class: ActionView::Renderer
101
- method_id: render
102
- path: .*
103
- lineno: .*
104
- static: false
105
- VIEW_CALL
104
+ it 'records the normalized path info' do
105
+ expect(events).to include(
106
+ hash_including(
107
+ 'http_server_request' => {
108
+ 'request_method' => 'GET',
109
+ 'path_info' => '/users/alice',
110
+ 'normalized_path_info' => '/users/:id(.:format)'
111
+ }
112
+ )
113
+ )
114
+ end
115
+ end
106
116
 
107
- expect(appmap).to match(<<-VIEW_LABEL.strip)
108
- labels:
109
- - view
110
- VIEW_LABEL
117
+ describe 'listing users' do
118
+ before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:11' }
119
+ let(:appmap_json_file) { 'UsersController_GET_users_lists_the_users.appmap.json' }
120
+
121
+ it 'records and labels view rendering' do
122
+ expect(events).to include hash_including(
123
+ 'event' => 'call',
124
+ 'thread_id' => Numeric,
125
+ 'defined_class' => 'ActionView::Renderer',
126
+ 'method_id' => 'render',
127
+ 'path' => String,
128
+ 'lineno' => Integer,
129
+ 'static' => false
130
+ )
131
+
132
+ expect(appmap['classMap']).to include hash_including(
133
+ 'name' => 'action_view',
134
+ 'children' => include(hash_including(
135
+ 'name' => 'ActionView',
136
+ 'children' => include(hash_including(
137
+ 'name' => 'Renderer',
138
+ 'children' => include(hash_including(
139
+ 'name' => 'render',
140
+ 'labels' => ['mvc.view']
141
+ ))
142
+ ))
143
+ ))
144
+ )
111
145
  end
112
146
  end
113
147
  end
114
148
  end
115
149
  end
116
-
117
- %w[5 6].each do |version|
118
- it_behaves_like 'rails version', version
119
- end
120
150
  end