appmap 0.34.2 → 0.35.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81605d2c95140e963991e04af89235ee903cce0e3a86b2477f0f80961704e2d6
4
- data.tar.gz: 7e526d9ffbc559bb3968b8a973dcd757147676d17a5028f737192fd8956d1b43
3
+ metadata.gz: 9e7d5c58bd3addf395c32591fe468c4a0b0e886d715cb711ffb6c898049455e5
4
+ data.tar.gz: 271ac50ee1ebe139ed8e52262a98a13e6b3d471ddcab49316669e66d0b7be57a
5
5
  SHA512:
6
- metadata.gz: 6a34ba2662d48a7e0083e16d6abe4693b57711f9ac0020da6631ca6c40172aeb8f3ef734edd504cec00f2f437742e45fc0cf631b425cb960c9d51239556bc461
7
- data.tar.gz: 156f126aac6f63e819c175d6b4d481679cbced9184007871c12fb174f187198afd8e7788befc4ab0ff35b10293e1e163eee25a985b35bd93853c03aa8268d192
6
+ metadata.gz: b84b1d76d8890a72fb376e95c7ab3811814b75e99304d2f1c15bb091cba1aef1e81f7688fd69c35b8c7735fd72167f89c67eeb25f76e54253503be610fff52cc
7
+ data.tar.gz: 12b27887ae25d8f91fa6321ea59c222c7ec7d1e2d6a198f6672fa166fcd3c32456fe0d3969dda729d4c4817129a3eb874f8bae0afb5dca6b74253ccf457bd51b
@@ -1,3 +1,32 @@
1
+ # v0.35.2
2
+ * Make sure `MethodEvent#display_string` works when the value's `#to_s` and/or `#inspect`
3
+ methods have problems.
4
+
5
+ # v0.35.1
6
+ * Take out hooking of `IO` and `Logger` methods.
7
+ * Enable logging if either `APPMAP_DEBUG` or `DEBUG` is `true`.
8
+
9
+ # v0.35.0
10
+ * Provide a custom display string for files and HTTP requests.
11
+ * Report `mime_type` on HTTP response.
12
+
13
+ # v0.34.6
14
+ * Only warn once about problems determining database version for an ActiveRecord
15
+ connection.
16
+
17
+ # v0.34.5
18
+ * Ensure that hooking a method doesn't change its arity.
19
+
20
+ # v0.34.4
21
+ * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
22
+ `ActiveRecord::Base.connection.database_version` if it's available.
23
+ * Fix `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.database_type` returns `:postgres`
24
+ in all supported versions of Rails.
25
+
26
+ # v0.34.3
27
+ * Fix a crash in `singleton_method_owner_name` that occurred if `__attached__.class` returned
28
+ something other than a `Module` or a `Class`.
29
+
1
30
  # v0.34.2
2
31
  * Add an extension that gets the name of the owner of a singleton method without calling
3
32
  any methods that may have been redefined (e.g. `#to_s` or `.name`).
data/README.md CHANGED
@@ -57,6 +57,15 @@ end
57
57
 
58
58
  Then install with `bundle`.
59
59
 
60
+ **Railtie**
61
+
62
+ If you are using Ruby on Rails, require the railtie after Rails is loaded.
63
+
64
+ ```
65
+ # application.rb is a good place to do this, along with all the other railties.
66
+ require 'appmap/railtie'
67
+ ```
68
+
60
69
  # Configuration
61
70
 
62
71
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -46,4 +46,5 @@ Gem::Specification.new do |spec|
46
46
  spec.add_development_dependency 'selenium-webdriver'
47
47
  spec.add_development_dependency 'webdrivers', '~> 4.0'
48
48
  spec.add_development_dependency 'timecop'
49
+ spec.add_development_dependency 'hashie'
49
50
  end
@@ -8,19 +8,88 @@
8
8
  (!SPECIAL_CONST_P(obj) && \
9
9
  (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE))
10
10
 
11
- static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
11
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
12
17
  {
13
18
  VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
14
19
  VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
15
20
  if (!CLASS_OR_MODULE_P(attached)) {
16
21
  attached = rb_funcall(attached, rb_intern("class"), 0);
17
22
  }
18
- return rb_mod_name(attached);
23
+
24
+ // Did __attached__.class return an object that's a Module or a
25
+ // Class?
26
+ if (CLASS_OR_MODULE_P(attached)) {
27
+ // Yup, get it's name
28
+ return rb_mod_name(attached);
29
+ }
30
+
31
+ // Nope (which seems weird, but whatever). Fall back to calling
32
+ // #to_s on the method's owner and hope for the best.
33
+ return rb_funcall(owner, rb_intern("to_s"), 0);
34
+ }
35
+
36
+
37
+ static VALUE
38
+ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
+ {
40
+ VALUE arities_key = rb_intern(ARITIES_KEY);
41
+ VALUE arities = rb_ivar_get(mod, arities_key);
42
+
43
+ if (arities == Qundef || NIL_P(arities)) {
44
+ arities = rb_hash_new();
45
+ rb_ivar_set(mod, arities_key, arities);
46
+ }
47
+ rb_hash_aset(arities, name, arity);
48
+
49
+ return rb_funcall(mod, rb_intern("define_method"), 2, name, proc);
50
+ }
51
+
52
+ static VALUE
53
+ am_get_method_arity(VALUE method, VALUE orig_arity_method)
54
+ {
55
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
56
+ VALUE arities = rb_ivar_get(owner, rb_intern(ARITIES_KEY));
57
+ VALUE name = rb_funcall(method, rb_intern("name"), 0);
58
+ VALUE arity = Qnil;
59
+ // See if we saved an arity for the method.
60
+ if (!NIL_P(arities)) {
61
+ arity = rb_hash_aref(arities, name);
62
+ }
63
+ // Didn't find one, call the original method.
64
+ if (NIL_P(arity)) {
65
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
+ }
68
+
69
+ return arity;
19
70
  }
20
-
71
+
72
+ static VALUE
73
+ am_unbound_method_arity(VALUE method)
74
+ {
75
+ VALUE orig_unbound_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@unbound_method_arity"));
76
+ return am_get_method_arity(method, orig_unbound_method_arity);
77
+ }
78
+
79
+ static VALUE
80
+ am_method_arity(VALUE method)
81
+ {
82
+ VALUE orig_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@method_arity"));
83
+ return am_get_method_arity(method, orig_method_arity);
84
+ }
85
+
21
86
  void Init_appmap() {
22
87
  VALUE appmap = rb_define_module("AppMap");
23
- VALUE hook = rb_define_class_under(appmap, "Hook", rb_cObject);
88
+ am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
+
90
+ rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
24
91
 
25
- rb_define_singleton_method(hook, "singleton_method_owner_name", singleton_method_owner_name, 1);
92
+ rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
+ rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
94
+ rb_define_method(rb_cMethod, "arity", am_method_arity, 0);
26
95
  }
@@ -84,7 +84,7 @@ module AppMap
84
84
 
85
85
  # Builds a class map from a config and a list of Ruby methods.
86
86
  def class_map(methods)
87
- ClassMap.build_from_methods(configuration, methods)
87
+ ClassMap.build_from_methods(methods)
88
88
  end
89
89
 
90
90
  # Returns default metadata detected from the Ruby system and from the
@@ -61,25 +61,24 @@ module AppMap
61
61
  location: location,
62
62
  static: static,
63
63
  labels: labels
64
- }.delete_if { |k,v| v.nil? || v == [] }
64
+ }.delete_if { |_, v| v.nil? || v == [] }
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
69
  class << self
70
- def build_from_methods(config, methods)
70
+ def build_from_methods(methods)
71
71
  root = Types::Root.new
72
72
  methods.each do |method|
73
- package = config.package_for_method(method) \
74
- or raise "No package found for method #{method}"
75
- add_function root, package, method
73
+ add_function root, method
76
74
  end
77
75
  root.children.map(&:to_h)
78
76
  end
79
77
 
80
78
  protected
81
79
 
82
- def add_function(root, package, method)
80
+ def add_function(root, method)
81
+ package = method.package
83
82
  static = method.static
84
83
 
85
84
  object_infos = [
@@ -6,7 +6,7 @@ module AppMap
6
6
  def initialize(path, package_name: nil, exclude: [], labels: [])
7
7
  super path, package_name, exclude, labels
8
8
  end
9
-
9
+
10
10
  def to_h
11
11
  {
12
12
  path: path,
@@ -35,12 +35,10 @@ module AppMap
35
35
  'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGE),
36
36
  'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGE),
37
37
  'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGE),
38
- 'Logger' => Hook.new(:add, Package.new('logger', labels: %w[log io])),
39
38
  'Net::HTTP' => Hook.new(:request, Package.new('net/http', package_name: 'net/http', labels: %w[http io])),
40
39
  'Net::SMTP' => Hook.new(:send, Package.new('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
41
40
  'Net::POP3' => Hook.new(:mails, Package.new('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
42
41
  'Net::IMAP' => Hook.new(:send_command, Package.new('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
43
- 'IO' => Hook.new(%i[read write open close], Package.new('io', labels: %w[io])),
44
42
  'Marshal' => Hook.new(%i[dump load], Package.new('marshal', labels: %w[serialization marshal])),
45
43
  'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.new('yaml', package_name: 'psych', labels: %w[serialization yaml])),
46
44
  'JSON::Ext::Parser' => Hook.new(:parse, Package.new('json', package_name: 'json', labels: %w[serialization json])),
@@ -105,7 +103,7 @@ module AppMap
105
103
  hook = find_hook(defined_class)
106
104
  return nil unless hook
107
105
 
108
- Array(hook.method_names).include?(method_name) ? hook.package : nil
106
+ Array(hook.method_names).include?(method_name) ? hook.package : nil
109
107
  end
110
108
 
111
109
  def find_hook(defined_class)
@@ -31,25 +31,43 @@ module AppMap
31
31
  def display_string(value)
32
32
  return nil unless value
33
33
 
34
+ value_string = custom_display_string(value) || default_display_string(value)
35
+
36
+ (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
+ end
38
+
39
+ protected
40
+
41
+ def custom_display_string(value)
42
+ case value
43
+ when File
44
+ "#{value.class}[path=#{value.path}]"
45
+ when Net::HTTP
46
+ "#{value.class}[#{value.address}:#{value.port}]"
47
+ when Net::HTTPGenericRequest
48
+ "#{value.class}[#{value.method} #{value.path}]"
49
+ end
50
+ rescue StandardError
51
+ nil
52
+ end
53
+
54
+ def default_display_string(value)
34
55
  last_resort_string = lambda do
35
56
  warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
36
57
  '*Error inspecting variable*'
37
58
  end
38
59
 
39
- value_string = \
60
+ begin
61
+ value.to_s
62
+ rescue NoMethodError
40
63
  begin
41
- value.to_s
42
- rescue NoMethodError
43
- begin
44
- value.inspect
45
- rescue StandardError
46
- last_resort_string.call
47
- end
64
+ value.inspect
48
65
  rescue StandardError
49
66
  last_resort_string.call
50
67
  end
51
-
52
- (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
68
+ rescue StandardError
69
+ last_resort_string.call
70
+ end
53
71
  end
54
72
  end
55
73
  end
@@ -4,7 +4,10 @@ require 'English'
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
- LOG = (ENV['DEBUG'] == 'true')
7
+ LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+
9
+ @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
+ @method_arity = ::Method.instance_method(:arity)
8
11
 
9
12
  class << self
10
13
  def lock_builtins
@@ -45,7 +48,6 @@ module AppMap
45
48
  hook = lambda do |hook_cls|
46
49
  lambda do |method_id|
47
50
  method = hook_cls.public_instance_method(method_id)
48
- hook_method = Hook::Method.new(hook_cls, method)
49
51
 
50
52
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
51
53
 
@@ -53,14 +55,16 @@ module AppMap
53
55
  # Skip methods that have no instruction sequence, as they are obviously trivial.
54
56
  next unless disasm
55
57
 
56
- # Don't try and trace the AppMap methods or there will be
57
- # a stack overflow in the defined hook method.
58
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
59
-
60
58
  next unless \
61
59
  config.always_hook?(hook_cls, method.name) ||
62
60
  config.included_by_location?(method)
63
61
 
62
+ hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
63
+
64
+ # Don't try and trace the AppMap methods or there will be
65
+ # a stack overflow in the defined hook method.
66
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
67
+
64
68
  hook_method.activate
65
69
  end
66
70
  end
@@ -94,9 +98,9 @@ module AppMap
94
98
  end
95
99
 
96
100
  if method
97
- Hook::Method.new(cls, method).activate
101
+ Hook::Method.new(hook.package, cls, method).activate
98
102
  else
99
- warn "Method #{method_name} not found on #{cls.name}"
103
+ warn "Method #{method_name} not found on #{cls.name}"
100
104
  end
101
105
  end
102
106
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AppMap
2
4
  class Hook
3
5
  class Method
4
- attr_reader :hook_class, :hook_method
6
+ attr_reader :hook_package, :hook_class, :hook_method
5
7
 
6
8
  # +method_display_name+ may be nil if name resolution gets
7
9
  # deferred until runtime (e.g. for a singleton method on an
@@ -15,8 +17,9 @@ module AppMap
15
17
  # with the method we're hooking.
16
18
  TIME_NOW = Time.method(:now)
17
19
  private_constant :TIME_NOW
18
-
19
- def initialize(hook_class, hook_method)
20
+
21
+ def initialize(hook_package, hook_class, hook_method)
22
+ @hook_package = hook_package
20
23
  @hook_class = hook_class
21
24
  @hook_method = hook_method
22
25
 
@@ -30,7 +33,7 @@ module AppMap
30
33
  msg = if method_display_name
31
34
  "#{method_display_name}"
32
35
  else
33
- "#{hook_method.name} (class resolution deferrred)"
36
+ "#{hook_method.name} (class resolution deferred)"
34
37
  end
35
38
  warn "AppMap: Hooking " + msg
36
39
  end
@@ -41,34 +44,38 @@ module AppMap
41
44
  after_hook = self.method(:after_hook)
42
45
  with_disabled_hook = self.method(:with_disabled_hook)
43
46
 
44
- hook_class.define_method hook_method.name do |*args, &block|
45
- instance_method = hook_method.bind(self).to_proc
47
+ hook_method_def = nil
48
+ hook_class.instance_eval do
49
+ hook_method_def = Proc.new do |*args, &block|
50
+ instance_method = hook_method.bind(self).to_proc
46
51
 
47
- # We may not have gotten the class for the method during
48
- # initialization (e.g. for a singleton method on an embedded
49
- # struct), so make sure we have it now.
50
- defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
52
+ # We may not have gotten the class for the method during
53
+ # initialization (e.g. for a singleton method on an embedded
54
+ # struct), so make sure we have it now.
55
+ defined_class,_ = Hook.qualify_method_name(hook_method) unless defined_class
51
56
 
52
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
53
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
54
- return instance_method.call(*args, &block) unless enabled
57
+ hook_disabled = Thread.current[HOOK_DISABLE_KEY]
58
+ enabled = true if !hook_disabled && AppMap.tracing.enabled?
59
+ return instance_method.call(*args, &block) unless enabled
55
60
 
56
- call_event, start_time = with_disabled_hook.() do
57
- before_hook.(self, defined_class, args)
58
- end
59
- return_value = nil
60
- exception = nil
61
- begin
62
- return_value = instance_method.(*args, &block)
63
- rescue
64
- exception = $ERROR_INFO
65
- raise
66
- ensure
67
- with_disabled_hook.() do
68
- after_hook.(call_event, start_time, return_value, exception)
61
+ call_event, start_time = with_disabled_hook.() do
62
+ before_hook.(self, defined_class, args)
63
+ end
64
+ return_value = nil
65
+ exception = nil
66
+ begin
67
+ return_value = instance_method.(*args, &block)
68
+ rescue
69
+ exception = $ERROR_INFO
70
+ raise
71
+ ensure
72
+ with_disabled_hook.() do
73
+ after_hook.(self, call_event, start_time, return_value, exception)
74
+ end
69
75
  end
70
76
  end
71
77
  end
78
+ hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
72
79
  end
73
80
 
74
81
  protected
@@ -76,11 +83,11 @@ module AppMap
76
83
  def before_hook(receiver, defined_class, args)
77
84
  require 'appmap/event'
78
85
  call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
79
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
86
+ AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
80
87
  [ call_event, TIME_NOW.call ]
81
88
  end
82
89
 
83
- def after_hook(call_event, start_time, return_value, exception)
90
+ def after_hook(receiver, call_event, start_time, return_value, exception)
84
91
  require 'appmap/event'
85
92
  elapsed = TIME_NOW.call - start_time
86
93
  return_event = \
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+ require 'appmap/hook'
5
+
6
+ module AppMap
7
+ module Rails
8
+ module RequestHandler
9
+ class HTTPServerRequest < AppMap::Event::MethodEvent
10
+ attr_accessor :request_method, :path_info, :params
11
+
12
+ def initialize(request)
13
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
+
15
+ @request_method = request.request_method
16
+ @path_info = request.path_info.split('?')[0]
17
+ @params = ActionDispatch::Http::ParameterFilter.new(::Rails.application.config.filter_parameters).filter(request.params)
18
+ end
19
+
20
+ def to_h
21
+ super.tap do |h|
22
+ h[:http_server_request] = {
23
+ request_method: request_method,
24
+ path_info: path_info
25
+ }
26
+
27
+ h[:message] = params.keys.map do |key|
28
+ val = params[key]
29
+ {
30
+ name: key,
31
+ class: val.class.name,
32
+ value: self.class.display_string(val),
33
+ object_id: val.__id__
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
41
+ attr_accessor :status, :mime_type
42
+
43
+ def initialize(response, parent_id, elapsed)
44
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
45
+
46
+ self.status = response.status
47
+ self.mime_type = response.headers['Content-Type']
48
+ self.parent_id = parent_id
49
+ self.elapsed = elapsed
50
+ end
51
+
52
+ def to_h
53
+ super.tap do |h|
54
+ h[:http_server_response] = {
55
+ status: status,
56
+ mime_type: mime_type
57
+ }.compact
58
+ end
59
+ end
60
+ end
61
+
62
+ class HookMethod < AppMap::Hook::Method
63
+ def initialize
64
+ # ActionController::Instrumentation has issued start_processing.action_controller and
65
+ # process_action.action_controller since Rails 3. Therefore it's a stable place to hook
66
+ # the request. Rails controller notifications can't be used directly because they don't
67
+ # provide response headers, and we want the Content-Type.
68
+ super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
69
+ end
70
+
71
+ protected
72
+
73
+ def before_hook(receiver, defined_class, _) # args
74
+ call_event = HTTPServerRequest.new(receiver.request)
75
+ # http_server_request events are i/o and do not require a package name.
76
+ AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
77
+ [ call_event, TIME_NOW.call ]
78
+ end
79
+
80
+ def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
81
+ elapsed = TIME_NOW.call - start_time
82
+ return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
83
+ AppMap.tracing.record_event return_event
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -72,21 +72,24 @@ module AppMap
72
72
  end
73
73
 
74
74
  class ActiveRecordExaminer
75
+ @@db_version_warning_issued = {}
76
+
77
+ def issue_warning
78
+ db_type = database_type
79
+ return if @@db_version_warning_issued[db_type]
80
+ warn("AppMap: Unable to determine database version for #{db_type.inspect}")
81
+ @@db_version_warning_issued[db_type] = true
82
+ end
83
+
75
84
  def server_version
76
- case database_type
77
- when :postgres
78
- ActiveRecord::Base.connection.postgresql_version
79
- when :sqlite
80
- ActiveRecord::Base.connection.database_version.to_s
81
- else
82
- warn "Unable to determine database version for #{database_type.inspect}"
83
- end
85
+ ActiveRecord::Base.connection.try(:database_version) || issue_warning
84
86
  end
85
87
 
86
88
  def database_type
87
- return :postgres if ActiveRecord::Base.connection.respond_to?(:postgresql_version)
89
+ type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
90
+ type = :postgres if type == :postgresql
88
91
 
89
- ActiveRecord::Base.connection.adapter_name.downcase.to_sym
92
+ type
90
93
  end
91
94
 
92
95
  def execute_query(sql)
@@ -13,13 +13,11 @@ module AppMap
13
13
  # AppMap events.
14
14
  initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
15
15
  require 'appmap/rails/sql_handler'
16
- require 'appmap/rails/action_handler'
16
+ require 'appmap/rails/request_handler'
17
17
  ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
18
18
  ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
19
- ActiveSupport::Notifications.subscribe \
20
- 'start_processing.action_controller', AppMap::Rails::ActionHandler::HTTPServerRequest.new
21
- ActiveSupport::Notifications.subscribe \
22
- 'process_action.action_controller', AppMap::Rails::ActionHandler::HTTPServerResponse.new
19
+
20
+ AppMap::Rails::RequestHandler::HookMethod.new.activate
23
21
  end
24
22
 
25
23
  # appmap.trace begins recording an AppMap trace and writes it to appmap.json.
@@ -124,8 +124,18 @@ module AppMap
124
124
  def initialize(example)
125
125
  super
126
126
 
127
+ webdriver_port = lambda do
128
+ return unless defined?(page) && page&.driver
129
+
130
+ # This is the ugliest thing ever but I don't want to lose it.
131
+ # All the WebDriver calls are getting app-mapped and it's really unclear
132
+ # what they are.
133
+ page.driver.options[:http_client].instance_variable_get('@server_url').port
134
+ end
135
+
127
136
  warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
128
137
  @trace = AppMap.tracing.trace
138
+ @webdriver_port = webdriver_port.()
129
139
  end
130
140
 
131
141
  def finish
@@ -3,9 +3,10 @@
3
3
  module AppMap
4
4
  module Trace
5
5
  class ScopedMethod < SimpleDelegator
6
- attr_reader :defined_class, :static
7
-
8
- def initialize(defined_class, method, static)
6
+ attr_reader :package, :defined_class, :static
7
+
8
+ def initialize(package, defined_class, method, static)
9
+ @package = package
9
10
  @defined_class = defined_class
10
11
  @static = static
11
12
  super(method)
@@ -32,9 +33,9 @@ module AppMap
32
33
  @tracing.any?(&:enabled?)
33
34
  end
34
35
 
35
- def record_event(event, defined_class: nil, method: nil)
36
+ def record_event(event, package: nil, defined_class: nil, method: nil)
36
37
  @tracing.each do |tracer|
37
- tracer.record_event(event, defined_class: defined_class, method: method)
38
+ tracer.record_event(event, package: package, defined_class: defined_class, method: method)
38
39
  end
39
40
  end
40
41
 
@@ -71,11 +72,12 @@ module AppMap
71
72
  # Record a program execution event.
72
73
  #
73
74
  # The event should be one of the MethodEvent subclasses.
74
- def record_event(event, defined_class: nil, method: nil)
75
+ def record_event(event, package: nil, defined_class: nil, method: nil)
75
76
  return unless @enabled
76
77
 
77
78
  @events << event
78
- @methods << Trace::ScopedMethod.new(defined_class, method, event.static) if (defined_class && method && event.event == :call)
79
+ @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
80
+ if package && defined_class && method && (event.event == :call)
79
81
  end
80
82
 
81
83
  # Gets a unique list of the methods that were invoked by the program.
@@ -36,6 +36,23 @@ module AppMap
36
36
  [ fname, extension ].join
37
37
  end
38
38
 
39
+ # sanitize_paths removes ephemeral values from objects with
40
+ # embedded paths (e.g. an event or a classmap), making events
41
+ # easier to compare across runs.
42
+ def sanitize_paths(h)
43
+ require 'hashie'
44
+ h.extend(Hashie::Extensions::DeepLocate)
45
+ keys = %i(path location)
46
+ h.deep_locate ->(k,v,o) {
47
+ next unless keys.include?(k)
48
+
49
+ fix = ->(v) {v.gsub(%r{#{Gem.dir}/gems/.*(?=lib)}, '')}
50
+ keys.each {|k| o[k] = fix.(o[k]) if o[k] }
51
+ }
52
+
53
+ h
54
+ end
55
+
39
56
  # sanitize_event removes ephemeral values from an event, making
40
57
  # events easier to compare across runs.
41
58
  def sanitize_event(event, &block)
@@ -49,7 +66,7 @@ module AppMap
49
66
 
50
67
  case event[:event]
51
68
  when :call
52
- event[:path] = event[:path].gsub(Gem.dir + '/', '')
69
+ sanitize_paths(event)
53
70
  end
54
71
 
55
72
  event
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.34.2'
6
+ VERSION = '0.35.2'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -39,7 +39,7 @@ describe 'AbstractControllerBase' do
39
39
  expect(appmap).to include(<<-SERVER_REQUEST.strip)
40
40
  http_server_request:
41
41
  request_method: POST
42
- path_info: "/api/users?login=alice&password=foobar
42
+ path_info: "/api/users"
43
43
  SERVER_REQUEST
44
44
  end
45
45
  it 'Properly captures method parameters in the appmap' do
@@ -45,6 +45,12 @@ describe 'AbstractControllerBase' do
45
45
  request_method: POST
46
46
  path_info: "/api/users"
47
47
  SERVER_REQUEST
48
+
49
+ expect(appmap).to include(<<-SERVER_RESPONSE.strip)
50
+ http_server_response:
51
+ status: 201
52
+ mime_type: application/json; charset=utf-8
53
+ SERVER_RESPONSE
48
54
  end
49
55
 
50
56
  it 'properly captures method parameters in the appmap' do
@@ -9,3 +9,47 @@ class ExceptionMethod
9
9
  raise 'Exception occurred in raise_exception'
10
10
  end
11
11
  end
12
+
13
+ # subclass from BasicObject so we don't get #to_s. Requires some
14
+ # hackery to implement the other methods normally provided by Object.
15
+ class NoToSMethod < BasicObject
16
+ def is_a?(*args)
17
+ return false
18
+ end
19
+
20
+ def class
21
+ return ::Class
22
+ end
23
+
24
+ def respond_to?(*args)
25
+ return false
26
+ end
27
+
28
+ def inspect
29
+ "NoToSMethod"
30
+ end
31
+
32
+ def say_hello
33
+ "hello"
34
+ end
35
+ end
36
+
37
+ class InspectRaises < NoToSMethod
38
+ def inspect
39
+ ::Kernel.raise "#to_s missing, #inspect raises"
40
+ end
41
+
42
+ def say_hello
43
+ "hello"
44
+ end
45
+ end
46
+
47
+ class ToSRaises
48
+ def to_s
49
+ raise "#to_s raises"
50
+ end
51
+
52
+ def say_hello
53
+ "hello"
54
+ end
55
+ end
@@ -22,7 +22,7 @@ describe 'AppMap class Hooking', docker: false do
22
22
  while tracer.event?
23
23
  events << tracer.next_event.to_h
24
24
  end
25
- end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event))
26
26
  end
27
27
 
28
28
  def invoke_test_file(file, setup: nil, &block)
@@ -50,7 +50,7 @@ describe 'AppMap class Hooking', docker: false do
50
50
  def test_hook_behavior(file, events_yaml, setup: nil, &block)
51
51
  config, tracer = invoke_test_file(file, setup: setup, &block)
52
52
 
53
- events = collect_events(tracer)
53
+ events = collect_events(tracer).to_yaml
54
54
 
55
55
  expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
56
56
 
@@ -465,6 +465,132 @@ describe 'AppMap class Hooking', docker: false do
465
465
  end
466
466
  end
467
467
 
468
+ context 'string conversions works for the receiver when' do
469
+
470
+ it 'is missing #to_s' do
471
+ events_yaml = <<~YAML
472
+ ---
473
+ - :id: 1
474
+ :event: :call
475
+ :defined_class: NoToSMethod
476
+ :method_id: respond_to?
477
+ :path: spec/fixtures/hook/exception_method.rb
478
+ :lineno: 24
479
+ :static: false
480
+ :parameters:
481
+ - :name: :args
482
+ :class: Symbol
483
+ :value: to_s
484
+ :kind: :rest
485
+ :receiver:
486
+ :class: Class
487
+ :value: NoToSMethod
488
+ - :id: 2
489
+ :event: :return
490
+ :parent_id: 1
491
+ - :id: 3
492
+ :event: :call
493
+ :defined_class: NoToSMethod
494
+ :method_id: say_hello
495
+ :path: spec/fixtures/hook/exception_method.rb
496
+ :lineno: 32
497
+ :static: false
498
+ :parameters: []
499
+ :receiver:
500
+ :class: Class
501
+ :value: NoToSMethod
502
+ - :id: 4
503
+ :event: :return
504
+ :parent_id: 3
505
+ :return_value:
506
+ :class: String
507
+ :value: hello
508
+ YAML
509
+
510
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
511
+ inst = NoToSMethod.new
512
+ # sanity check
513
+ expect(inst).not_to respond_to(:to_s)
514
+ inst.say_hello
515
+ end
516
+ end
517
+
518
+ it 'it is missing #to_s and it raises an exception in #inspect' do
519
+ events_yaml = <<~YAML
520
+ ---
521
+ - :id: 1
522
+ :event: :call
523
+ :defined_class: NoToSMethod
524
+ :method_id: respond_to?
525
+ :path: spec/fixtures/hook/exception_method.rb
526
+ :lineno: 24
527
+ :static: false
528
+ :parameters:
529
+ - :name: :args
530
+ :class: Symbol
531
+ :value: to_s
532
+ :kind: :rest
533
+ :receiver:
534
+ :class: Class
535
+ :value: "*Error inspecting variable*"
536
+ - :id: 2
537
+ :event: :return
538
+ :parent_id: 1
539
+ - :id: 3
540
+ :event: :call
541
+ :defined_class: InspectRaises
542
+ :method_id: say_hello
543
+ :path: spec/fixtures/hook/exception_method.rb
544
+ :lineno: 42
545
+ :static: false
546
+ :parameters: []
547
+ :receiver:
548
+ :class: Class
549
+ :value: "*Error inspecting variable*"
550
+ - :id: 4
551
+ :event: :return
552
+ :parent_id: 3
553
+ :return_value:
554
+ :class: String
555
+ :value: hello
556
+ YAML
557
+
558
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
559
+ inst = InspectRaises.new
560
+ # sanity check
561
+ expect(inst).not_to respond_to(:to_s)
562
+ inst.say_hello
563
+ end
564
+ end
565
+
566
+ it 'it raises an exception in #to_s' do
567
+ events_yaml = <<~YAML
568
+ ---
569
+ - :id: 1
570
+ :event: :call
571
+ :defined_class: ToSRaises
572
+ :method_id: say_hello
573
+ :path: spec/fixtures/hook/exception_method.rb
574
+ :lineno: 52
575
+ :static: false
576
+ :parameters: []
577
+ :receiver:
578
+ :class: ToSRaises
579
+ :value: "*Error inspecting variable*"
580
+ - :id: 2
581
+ :event: :return
582
+ :parent_id: 1
583
+ :return_value:
584
+ :class: String
585
+ :value: hello
586
+ YAML
587
+
588
+ test_hook_behavior 'spec/fixtures/hook/exception_method.rb', events_yaml do
589
+ ToSRaises.new.say_hello
590
+ end
591
+ end
592
+ end
593
+
468
594
  it 're-raises exceptions' do
469
595
  RSpec::Expectations.configuration.on_potential_false_positives = :nothing
470
596
 
@@ -500,7 +626,7 @@ describe 'AppMap class Hooking', docker: false do
500
626
  :event: :call
501
627
  :defined_class: ActiveSupport::SecurityUtils
502
628
  :method_id: secure_compare
503
- :path: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb
629
+ :path: lib/active_support/security_utils.rb
504
630
  :lineno: 26
505
631
  :static: true
506
632
  :parameters:
@@ -598,7 +724,7 @@ describe 'AppMap class Hooking', docker: false do
598
724
  :children:
599
725
  - :name: secure_compare
600
726
  :type: function
601
- :location: gems/activesupport-6.0.3.2/lib/active_support/security_utils.rb:26
727
+ :location: lib/active_support/security_utils.rb:26
602
728
  :static: true
603
729
  :labels:
604
730
  - security
@@ -624,7 +750,7 @@ describe 'AppMap class Hooking', docker: false do
624
750
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
625
751
  expect(Compare.compare('string', 'string')).to be_truthy
626
752
  end
627
- cm = AppMap::ClassMap.build_from_methods(config, tracer.event_methods)
753
+ cm = AppMap::Util.sanitize_paths(AppMap::ClassMap.build_from_methods(tracer.event_methods))
628
754
  entry = cm[1][:children][0][:children][0][:children][0]
629
755
  # Sanity check, make sure we got the right one
630
756
  expect(entry[:name]).to eq('secure_compare')
@@ -669,4 +795,11 @@ describe 'AppMap class Hooking', docker: false do
669
795
  end
670
796
  end
671
797
  end
798
+
799
+ it "preserves the arity of hooked methods" do
800
+ invoke_test_file 'spec/fixtures/hook/instance_method.rb' do
801
+ expect(InstanceMethod.instance_method(:say_echo).arity).to be(1)
802
+ expect(InstanceMethod.new.method(:say_echo).arity).to be(1)
803
+ end
804
+ end
672
805
  end
@@ -56,12 +56,10 @@ class CLITest < Minitest::Test
56
56
  Class frequency:
57
57
  ----------------
58
58
  1 Main
59
- 1 IO
60
59
 
61
60
  Method frequency:
62
61
  ----------------
63
62
  1 Main.say_hello
64
- 1 IO#write
65
63
  OUTPUT
66
64
  end
67
65
 
@@ -82,20 +80,12 @@ class CLITest < Minitest::Test
82
80
  {
83
81
  "name": "Main",
84
82
  "count": 1
85
- },
86
- {
87
- "name": "IO",
88
- "count": 1
89
83
  }
90
84
  ],
91
85
  "method_frequency": [
92
86
  {
93
87
  "name": "Main.say_hello",
94
88
  "count": 1
95
- },
96
- {
97
- "name": "IO#write",
98
- "count": 1
99
89
  }
100
90
  ]
101
91
  }
@@ -79,27 +79,6 @@ class OpenSSLTest < Minitest::Test
79
79
  ]
80
80
  }
81
81
  ]
82
- },
83
- {
84
- "name": "io",
85
- "type": "package",
86
- "children": [
87
- {
88
- "name": "IO",
89
- "type": "class",
90
- "children": [
91
- {
92
- "name": "write",
93
- "type": "function",
94
- "location": "IO#write",
95
- "static": false,
96
- "labels": [
97
- "io"
98
- ]
99
- }
100
- ]
101
- }
102
- ]
103
82
  }
104
83
  ]
105
84
  JSON
@@ -167,33 +146,6 @@ class OpenSSLTest < Minitest::Test
167
146
  "return_value": {
168
147
  "class": "String"
169
148
  }
170
- },
171
- {
172
- "id": 5,
173
- "event": "call",
174
- "defined_class": "IO",
175
- "method_id": "write",
176
- "path": "IO#write",
177
- "static": false,
178
- "parameters": [
179
- {
180
- "name": "arg",
181
- "class": "String",
182
- "value": "Computed signature",
183
- "kind": "rest"
184
- }
185
- ],
186
- "receiver": {
187
- "class": "IO"
188
- }
189
- },
190
- {
191
- "id": 6,
192
- "event": "return",
193
- "parent_id": 5,
194
- "return_value": {
195
- "class": "Integer"
196
- }
197
149
  }
198
150
  ]
199
151
  JSON
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.2
4
+ version: 0.35.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-04 00:00:00.000000000 Z
11
+ date: 2020-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -276,6 +276,20 @@ dependencies:
276
276
  - - ">="
277
277
  - !ruby/object:Gem::Version
278
278
  version: '0'
279
+ - !ruby/object:Gem::Dependency
280
+ name: hashie
281
+ requirement: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - ">="
284
+ - !ruby/object:Gem::Version
285
+ version: '0'
286
+ type: :development
287
+ prerelease: false
288
+ version_requirements: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - ">="
291
+ - !ruby/object:Gem::Version
292
+ version: '0'
279
293
  description:
280
294
  email:
281
295
  - kgilpin@gmail.com
@@ -324,7 +338,7 @@ files:
324
338
  - lib/appmap/middleware/remote_recording.rb
325
339
  - lib/appmap/minitest.rb
326
340
  - lib/appmap/open.rb
327
- - lib/appmap/rails/action_handler.rb
341
+ - lib/appmap/rails/request_handler.rb
328
342
  - lib/appmap/rails/sql_handler.rb
329
343
  - lib/appmap/railtie.rb
330
344
  - lib/appmap/record.rb
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'appmap/event'
4
-
5
- module AppMap
6
- module Rails
7
- module ActionHandler
8
- Context = Struct.new(:id, :start_time)
9
-
10
- module ContextKey
11
- def context_key
12
- "#{HTTPServerRequest.name}#call"
13
- end
14
- end
15
-
16
- class HTTPServerRequest
17
- include ContextKey
18
-
19
- class Call < AppMap::Event::MethodCall
20
- attr_accessor :payload
21
-
22
- def initialize(payload)
23
- super AppMap::Event.next_id_counter, :call, Thread.current.object_id
24
-
25
- self.payload = payload
26
- end
27
-
28
- def to_h
29
- super.tap do |h|
30
- h[:http_server_request] = {
31
- request_method: payload[:method],
32
- path_info: payload[:path]
33
- }
34
-
35
- params = payload[:params]
36
- h[:message] = params.keys.map do |key|
37
- val = params[key]
38
- {
39
- name: key,
40
- class: val.class.name,
41
- value: self.class.display_string(val),
42
- object_id: val.__id__
43
- }
44
- end
45
- end
46
- end
47
- end
48
-
49
- def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
50
- event = Call.new(payload)
51
- Thread.current[context_key] = Context.new(event.id, Time.now)
52
- AppMap.tracing.record_event(event)
53
- end
54
- end
55
-
56
- class HTTPServerResponse
57
- include ContextKey
58
-
59
- class Call < AppMap::Event::MethodReturnIgnoreValue
60
- attr_accessor :payload
61
-
62
- def initialize(payload, parent_id, elapsed)
63
- super AppMap::Event.next_id_counter, :return, Thread.current.object_id
64
-
65
- self.payload = payload
66
- self.parent_id = parent_id
67
- self.elapsed = elapsed
68
- end
69
-
70
- def to_h
71
- super.tap do |h|
72
- h[:http_server_response] = {
73
- status: payload[:status]
74
- }
75
- end
76
- end
77
- end
78
-
79
- def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
80
- return unless Thread.current[context_key]
81
-
82
- context = Thread.current[context_key]
83
- Thread.current[context_key] = nil
84
-
85
- event = Call.new(payload, context.id, Time.now - context.start_time)
86
- AppMap.tracing.record_event(event)
87
- end
88
- end
89
- end
90
- end
91
- end