appmap 0.34.1 → 0.35.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39576d59d2648a009f888fa9c5720d8fdb91b04fcb4d08949a05a0a5c453d0c8
4
- data.tar.gz: 8b1d5442421d9dd37c3db4fa0329321e6b6e6ebc87e94bb67be3da9b3f1467b0
3
+ metadata.gz: baf04e5395f8456142064fd8413363f6e29aa1bf2b68150e79b904d3879b8b7f
4
+ data.tar.gz: ca27e18b4778b9341801ac81e25e9025ab26a047824b58ab4bcf113ab91fd5b9
5
5
  SHA512:
6
- metadata.gz: 40bc1a630b65a83f7516085ec4cd25a72f83e574a9c2f150f8e302c9756c376a1fc820d490393d9625aeb3f5b781ca44d34431efe2722a7a0031b49334ccbca7
7
- data.tar.gz: 9d32eb02deebdfb16e99ee0f157f5c9de6cb77cafa46233867e0173577609d4390b98a675f67c11868d57370066666bf9f9da39b2ee2ab25ba16cbaf4ebcd8ca
6
+ metadata.gz: f7a97d2d7a4e8bd5ae7db133e6287775dcff0373af82f187bc7d82411f868731e30fdb1a1267b173578a38d44017b20127cb20dcd7ddf3924d813e9ba6b32eb5
7
+ data.tar.gz: 784ea7d032b9186d50352c0222b168887249b0a9bde5d850f4b2b98e12555ed728f1dc95c1604e2e45d7d6a15c6765eda467ddbd55530af859c82842a1aca4ca
data/.gitignore CHANGED
@@ -14,4 +14,4 @@ Gemfile.lock
14
14
  appmap.json
15
15
  .vscode
16
16
  .byebug_history
17
-
17
+ /lib/appmap/appmap.bundle
@@ -0,0 +1 @@
1
+ appmap-ruby
@@ -1,3 +1,32 @@
1
+ # v0.35.1
2
+ * Take out hooking of `IO` and `Logger` methods.
3
+ * Enable logging if either `APPMAP_DEBUG` or `DEBUG` is `true`.
4
+
5
+ # v0.35.0
6
+ * Provide a custom display string for files and HTTP requests.
7
+ * Report `mime_type` on HTTP response.
8
+
9
+ # v0.34.6
10
+ * Only warn once about problems determining database version for an ActiveRecord
11
+ connection.
12
+
13
+ # v0.34.5
14
+ * Ensure that hooking a method doesn't change its arity.
15
+
16
+ # v0.34.4
17
+ * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
18
+ `ActiveRecord::Base.connection.database_version` if it's available.
19
+ * Fix `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.database_type` returns `:postgres`
20
+ in all supported versions of Rails.
21
+
22
+ # v0.34.3
23
+ * Fix a crash in `singleton_method_owner_name` that occurred if `__attached__.class` returned
24
+ something other than a `Module` or a `Class`.
25
+
26
+ # v0.34.2
27
+ * Add an extension that gets the name of the owner of a singleton method without calling
28
+ any methods that may have been redefined (e.g. `#to_s` or `.name`).
29
+
1
30
  # v0.34.1
2
31
  * Ensure that capturing events doesn't change the behavior of a hooked method that uses
3
32
  `Time.now`. For example, if a test expects that `Time.now` will be called a certain
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
@@ -267,7 +276,13 @@ $ bundle config local.appmap $(pwd)
267
276
  Run the tests via `rake`:
268
277
  ```
269
278
  $ bundle exec rake test
270
- ```
279
+ ```
280
+
281
+ The `test` target will build the native extension first, then run the tests. If you need
282
+ to build the extension separately, run
283
+ ```
284
+ $ bundle exec rake compile
285
+ ```
271
286
 
272
287
  ## Using fixture apps
273
288
 
@@ -286,7 +301,7 @@ resources such as a PostgreSQL database.
286
301
  To build the fixture container images, first run:
287
302
 
288
303
  ```sh-session
289
- $ bundle exec rake fixtures:all
304
+ $ bundle exec rake build:fixtures:all
290
305
  ```
291
306
 
292
307
  This will build the `appmap.gem`, along with a Docker image for each fixture app.
data/Rakefile CHANGED
@@ -6,6 +6,13 @@ require 'rdoc/task'
6
6
 
7
7
  require 'open3'
8
8
 
9
+ require "rake/extensiontask"
10
+
11
+ desc 'build the native extension'
12
+ Rake::ExtensionTask.new("appmap") do |ext|
13
+ ext.lib_dir = "lib/appmap"
14
+ end
15
+
9
16
  namespace 'gem' do
10
17
  require 'bundler/gem_tasks'
11
18
  end
@@ -104,7 +111,7 @@ end
104
111
  namespace :spec do
105
112
  RUBY_VERSIONS.each do |ruby_version|
106
113
  desc ruby_version
107
- task ruby_version, [:specs] => ["build:fixtures:#{ruby_version}:all"] do |_, task_args|
114
+ task ruby_version, [:specs] => ["compile", "build:fixtures:#{ruby_version}:all"] do |_, task_args|
108
115
  run_specs(ruby_version, task_args)
109
116
  end.tap do|t|
110
117
  desc "Run all specs"
@@ -119,13 +126,13 @@ Rake::RDocTask.new do |rd|
119
126
  rd.title = 'AppMap'
120
127
  end
121
128
 
122
- Rake::TestTask.new(:minitest) do |t|
129
+ Rake::TestTask.new(minitest: 'compile') do |t|
123
130
  t.libs << 'test'
124
131
  t.libs << 'lib'
125
132
  t.test_files = FileList['test/*_test.rb']
126
133
  end
127
134
 
128
- task spec: "spec:all"
135
+ task spec: %i[spec:all]
129
136
 
130
137
  task test: %i[spec:all minitest]
131
138
 
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
19
  spec.files = `git ls-files --no-deleted`.split("
20
20
  ")
21
+ spec.extensions << "ext/appmap/extconf.rb"
22
+
21
23
  spec.bindir = 'exe'
22
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
25
  spec.require_paths = ['lib']
@@ -34,6 +36,7 @@ Gem::Specification.new do |spec|
34
36
  spec.add_development_dependency 'rake', '>= 12.3.3'
35
37
  spec.add_development_dependency 'rdoc'
36
38
  spec.add_development_dependency 'rubocop'
39
+ spec.add_development_dependency "rake-compiler"
37
40
 
38
41
  # Testing
39
42
  spec.add_development_dependency 'climate_control'
@@ -43,4 +46,5 @@ Gem::Specification.new do |spec|
43
46
  spec.add_development_dependency 'selenium-webdriver'
44
47
  spec.add_development_dependency 'webdrivers', '~> 4.0'
45
48
  spec.add_development_dependency 'timecop'
49
+ spec.add_development_dependency 'hashie'
46
50
  end
@@ -0,0 +1,95 @@
1
+ #include <ruby.h>
2
+ #include <ruby/intern.h>
3
+
4
+ // Seems like CLASS_OR_MODULE_P should really be in a header file in
5
+ // the ruby source -- it's in object.c and duplicated in eval.c. In
6
+ // the future, we'll fail if it does get moved to a header.
7
+ #define CLASS_OR_MODULE_P(obj) \
8
+ (!SPECIAL_CONST_P(obj) && \
9
+ (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE))
10
+
11
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
17
+ {
18
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
19
+ VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
20
+ if (!CLASS_OR_MODULE_P(attached)) {
21
+ attached = rb_funcall(attached, rb_intern("class"), 0);
22
+ }
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;
70
+ }
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
+
86
+ void Init_appmap() {
87
+ VALUE appmap = rb_define_module("AppMap");
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);
91
+
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);
95
+ }
@@ -0,0 +1,6 @@
1
+ require "mkmf"
2
+
3
+ $CFLAGS='-Werror'
4
+ extension_name = "appmap"
5
+ dir_config(extension_name)
6
+ create_makefile(File.join(extension_name, extension_name))
@@ -16,6 +16,9 @@ require 'appmap/metadata'
16
16
  require 'appmap/util'
17
17
  require 'appmap/open'
18
18
 
19
+ # load extension
20
+ require 'appmap/appmap'
21
+
19
22
  module AppMap
20
23
  class << self
21
24
  @configuration = nil
@@ -81,7 +84,7 @@ module AppMap
81
84
 
82
85
  # Builds a class map from a config and a list of Ruby methods.
83
86
  def class_map(methods)
84
- ClassMap.build_from_methods(configuration, methods)
87
+ ClassMap.build_from_methods(methods)
85
88
  end
86
89
 
87
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)
@@ -36,20 +36,38 @@ module AppMap
36
36
  '*Error inspecting variable*'
37
37
  end
38
38
 
39
- value_string = \
39
+ value_string = custom_display_string(value) || default_display_string(value)
40
+
41
+ (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
42
+ end
43
+
44
+ protected
45
+
46
+ def custom_display_string(value)
47
+ case value
48
+ when File
49
+ "#{value.class}[path=#{value.path}]"
50
+ when Net::HTTP
51
+ "#{value.class}[#{value.address}:#{value.port}]"
52
+ when Net::HTTPGenericRequest
53
+ "#{value.class}[#{value.method} #{value.path}]"
54
+ end
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ def default_display_string(value)
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
@@ -17,18 +20,7 @@ module AppMap
17
20
  # the given method.
18
21
  def qualify_method_name(method)
19
22
  if method.owner.singleton_class?
20
- # Singleton class names can take two forms:
21
- # #<Class:Foo> or
22
- # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
23
- # the class from the string.
24
- #
25
- # (There really isn't a better way to do this. The
26
- # singleton's reference to the class it was created
27
- # from is stored in an instance variable named
28
- # '__attached__'. It doesn't have the '@' prefix, so
29
- # it's internal only, and not accessible from user
30
- # code.)
31
- class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
23
+ class_name = singleton_method_owner_name(method)
32
24
  [ class_name, '.', method.name ]
33
25
  else
34
26
  [ method.owner.name, '#', method.name ]
@@ -56,22 +48,23 @@ module AppMap
56
48
  hook = lambda do |hook_cls|
57
49
  lambda do |method_id|
58
50
  method = hook_cls.public_instance_method(method_id)
59
- hook_method = Hook::Method.new(hook_cls, method)
60
51
 
61
- warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
52
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
62
53
 
63
54
  disasm = RubyVM::InstructionSequence.disasm(method)
64
55
  # Skip methods that have no instruction sequence, as they are obviously trivial.
65
56
  next unless disasm
66
57
 
58
+ next unless \
59
+ config.always_hook?(hook_cls, method.name) ||
60
+ config.included_by_location?(method)
61
+
62
+ hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
63
+
67
64
  # Don't try and trace the AppMap methods or there will be
68
65
  # a stack overflow in the defined hook method.
69
66
  next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
70
67
 
71
- next unless \
72
- config.always_hook?(hook_method.defined_class, method.name) ||
73
- config.included_by_location?(method)
74
-
75
68
  hook_method.activate
76
69
  end
77
70
  end
@@ -105,9 +98,9 @@ module AppMap
105
98
  end
106
99
 
107
100
  if method
108
- Hook::Method.new(cls, method).activate
101
+ Hook::Method.new(hook.package, cls, method).activate
109
102
  else
110
- warn "Method #{method_name} not found on #{cls.name}"
103
+ warn "Method #{method_name} not found on #{cls.name}"
111
104
  end
112
105
  end
113
106
  end