appmap 0.38.1 → 0.41.1

Sign up to get free protection for your applications and to get access to all the features.
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