appmap 0.34.1 → 0.35.1

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: 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