appmap 0.73.0 → 0.74.0

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: df9ed913bb9ce4b945312ab2a50d8305b7c85c8f84a2004b9cf785362fcf2ad2
4
- data.tar.gz: 2f2a844c20263b62a1291b05396d883c205cdebf2ae32062206e7bf42c23e757
3
+ metadata.gz: eb314cb1f41f3735c773670cd8082bbc8cedb98d9b01c6c71a0c6a2d2b094da5
4
+ data.tar.gz: fb4f94b03bb6c50465d22db22f5cc1d25bfb95e3638543986306ca63ce5603e7
5
5
  SHA512:
6
- metadata.gz: 330bcbec5db0a2b92662898432b3cc4eb4e8ca98729f6cfc0577d6490a0f19a1908e19b6c58c512c9b5db7e98a9f90804c5f0fe4362f52b31a69849760cde6db
7
- data.tar.gz: 29544bb8ae0db136aad8a518511eb6723fe113e54bc4d585d9e06a857f9227f3e0fbf6daa2bcf1067df5092eef88b13034862d09b187e496cb6758eb8ec094eb
6
+ metadata.gz: 2da7b411bf29e42b6c81dad1c897021e67e85b4770037543508ea2562a6132d3e8c7d2fe8574971231c06f3ebaaa328264d9db461363f3f59166922d8327060f
7
+ data.tar.gz: 248770c6d301895621c72d6f46c6e6e1c18abaa368356c7c9deebd480e91ce8bb456374c04bd4547ae7423976d8911f1e627c137d2cffdb6801db50bef55a229
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # [0.74.0](https://github.com/applandinc/appmap-ruby/compare/v0.73.0...v0.74.0) (2022-03-14)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Apply special case hook handling to Kernel and instance_eval ([25823ff](https://github.com/applandinc/appmap-ruby/commit/25823ff0fb86beff3edc64da251a125ee198ef40))
7
+ * Only apply a method hook to a class that defines the method ([ede2236](https://github.com/applandinc/appmap-ruby/commit/ede22364bfcbf324e8db3aa6d64d5b032f36ace2))
8
+ * Optimize/improve string-ification of values ([c9b6cdb](https://github.com/applandinc/appmap-ruby/commit/c9b6cdb72dfc55cc3a166eda470eba19093e9090))
9
+
10
+
11
+ ### Features
12
+
13
+ * Improve hook performance by using bind_call ([e09fce9](https://github.com/applandinc/appmap-ruby/commit/e09fce9f5b3c0b18bc3b81083c1523df6a6932db))
14
+ * Label system.exec, string.pack, string.html_safe ([963c6dd](https://github.com/applandinc/appmap-ruby/commit/963c6ddfa0f607ad219ae8829cfb383b0d5988d0))
15
+ * Log initiation of each builtin hook ([902a736](https://github.com/applandinc/appmap-ruby/commit/902a7360d17c6b49de97f34643c733e8c47c294d))
16
+
1
17
  # [0.73.0](https://github.com/applandinc/appmap-ruby/compare/v0.72.5...v0.73.0) (2022-03-07)
2
18
 
3
19
 
@@ -0,0 +1,13 @@
1
+ - methods:
2
+ - Open3#capture2
3
+ - Open3#capture2e
4
+ - Open3#capture3
5
+ - Open3#pipeline
6
+ - Open3#pipeline_r
7
+ - Open3#pipeline_rw
8
+ - Open3#pipeline_start
9
+ - Open3#pipeline_w
10
+ - Open3#popen2
11
+ - Open3#popen2e
12
+ - Open3#popen3
13
+ label: system.exec
@@ -0,0 +1,39 @@
1
+ - methods:
2
+ - Marshal#load
3
+ - Marshal#restore
4
+ require_name: ruby
5
+ label: deserialize.unsafe
6
+ - method: Marshal#dump
7
+ require_name: ruby
8
+ label: serialize
9
+ - method: String#pack
10
+ require_name: ruby
11
+ label: string.pack
12
+ - methods:
13
+ - String#unpack
14
+ - String#unpack1
15
+ require_name: ruby
16
+ label: string.unpack
17
+ #- methods:
18
+ # TODO: eval does not happen in the right context, and therefore any new constants
19
+ # which are defined are placed on the wrong module/class.
20
+ # - Kernel#eval
21
+ # - Binding#eval
22
+ # - BasicObject#instance_eval
23
+ # These methods cannot be hooked as far as I can tell.
24
+ # Why? When calling one of these functions, the context at the point of
25
+ # definition is used. It's not possible to bind class_eval to a new context.
26
+ # - Module#class_eval
27
+ # - Module#module_eval
28
+ # require_name: ruby
29
+ # label: lang.eval
30
+ - methods:
31
+ - IO#popen
32
+ - Kernel#exec
33
+ - Kernel#spawn
34
+ - Kernel#syscall
35
+ - Kernel#system
36
+ - Process#exec
37
+ - Process#spawn
38
+ require_name: ruby
39
+ label: system.exec
@@ -51,7 +51,7 @@ module AppMap
51
51
  appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
52
52
  scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
53
53
 
54
- AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
54
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, scenario_filename), appmap)
55
55
  end
56
56
 
57
57
  def enabled?
data/lib/appmap/event.rb CHANGED
@@ -20,7 +20,9 @@ module AppMap
20
20
  MethodEventStruct = Struct.new(:id, :event, :thread_id)
21
21
 
22
22
  class MethodEvent < MethodEventStruct
23
- LIMIT = 100
23
+ MAX_ARRAY_ENUMERATION = 10
24
+ MAX_HASH_ENUMERATION = 10
25
+ MAX_STRING_LENGTH = 100
24
26
 
25
27
  class << self
26
28
  def build_from_invocation(event_type, event:)
@@ -48,14 +50,14 @@ module AppMap
48
50
  end
49
51
 
50
52
  start = Time.now
51
- value_string = custom_display_string(value) || default_display_string(value)
53
+ value_string, final = custom_display_string(value) || default_display_string(value)
52
54
 
53
55
  if @times
54
56
  elapsed = Time.now - start
55
57
  @times[best_class_name(value)] += elapsed
56
58
  end
57
59
 
58
- encode_display_string(value_string)
60
+ final ? value_string : encode_display_string(value_string)
59
61
  end
60
62
 
61
63
  def object_properties(hash_like)
@@ -80,21 +82,33 @@ module AppMap
80
82
  end
81
83
 
82
84
  def encode_display_string(value)
83
- (value||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
85
+ (value||'')[0...MAX_STRING_LENGTH].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
84
86
  end
85
87
 
86
88
  def custom_display_string(value)
87
89
  case value
88
90
  when NilClass, TrueClass, FalseClass, Numeric, Time, Date
89
- value.to_s
91
+ [ value.to_s, true ]
92
+ when Symbol
93
+ [ ":#{value}", true ]
90
94
  when String
91
- value[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
95
+ result = value[0...MAX_STRING_LENGTH].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
96
+ result << " (...#{value.length - MAX_STRING_LENGTH} more characters)" if value.length > MAX_STRING_LENGTH
97
+ [ result, true ]
98
+ when Array
99
+ result = value[0...MAX_ARRAY_ENUMERATION].map{|v| display_string(v)}.join(', ')
100
+ result << " (...#{value.length - MAX_ARRAY_ENUMERATION} more items)" if value.length > MAX_ARRAY_ENUMERATION
101
+ [ [ '[', result, ']' ].join, true ]
102
+ when Hash
103
+ result = value.keys[0...MAX_HASH_ENUMERATION].map{|key| "#{display_string(key)}=>#{display_string(value[key])}"}.join(', ')
104
+ result << " (...#{value.size - MAX_HASH_ENUMERATION} more entries)" if value.size > MAX_HASH_ENUMERATION
105
+ [ [ '{', result, '}' ].join, true ]
92
106
  when File
93
- "#{value.class}[path=#{value.path}]"
107
+ [ "#{value.class}[path=#{value.path}]", true ]
94
108
  when Net::HTTP
95
- "#{value.class}[#{value.address}:#{value.port}]"
109
+ [ "#{value.class}[#{value.address}:#{value.port}]", true ]
96
110
  when Net::HTTPGenericRequest
97
- "#{value.class}[#{value.method} #{value.path}]"
111
+ [ "#{value.class}[#{value.method} #{value.path}]", true ]
98
112
  end
99
113
  rescue StandardError
100
114
  nil
@@ -11,3 +11,7 @@
11
11
  label: mvc.template.resolver
12
12
  handler_class: AppMap::Handler::Rails::Template::ResolverHandler
13
13
  require_name: action_view
14
+ - methods:
15
+ - ActionView::Helpers::SanitizeHelper#sanitize
16
+ label: string.html_safe
17
+ require_name: action_view
@@ -69,28 +69,24 @@ module AppMap
69
69
  after_hook = self.method(:after_hook)
70
70
  with_disabled_hook = self.method(:with_disabled_hook)
71
71
 
72
- hook_method_def = Proc.new do |*args, &block|
73
- is_array_containing_empty_hash = ->(obj) {
74
- obj.is_a?(Array) && obj.length == 1 && obj[0].is_a?(Hash) && obj[0].size == 0
75
- }
76
-
77
- call_instance_method = -> {
78
- # https://github.com/applandinc/appmap-ruby/issues/153
79
- if NEW_RUBY && is_array_containing_empty_hash.(args) && hook_method.arity == 1
80
- if NEW_RUBY
81
- hook_method.bind_call(self, {}, &block)
82
- else
83
- hook_method.bind(self).call({}, &block)
84
- end
72
+ is_array_containing_empty_hash = ->(obj) {
73
+ obj.is_a?(Array) && obj.length == 1 && obj[0].is_a?(Hash) && obj[0].size == 0
74
+ }
75
+
76
+ call_instance_method = lambda do |receiver, args, &block|
77
+ # https://github.com/applandinc/appmap-ruby/issues/153
78
+ if NEW_RUBY && is_array_containing_empty_hash.(args) && hook_method.arity == 1
79
+ hook_method.bind_call(receiver, {}, &block)
80
+ else
81
+ if NEW_RUBY
82
+ hook_method.bind_call(receiver, *args, &block)
85
83
  else
86
- if NEW_RUBY
87
- hook_method.bind_call(self, *args, &block)
88
- else
89
- hook_method.bind(self).call(*args, &block)
90
- end
84
+ hook_method.bind(receiver).call(*args, &block)
91
85
  end
92
- }
86
+ end
87
+ end
93
88
 
89
+ hook_method_def = Proc.new do |*args, &block|
94
90
  # We may not have gotten the class for the method during
95
91
  # initialization (e.g. for a singleton method on an embedded
96
92
  # struct), so make sure we have it now.
@@ -102,7 +98,9 @@ module AppMap
102
98
 
103
99
  enabled = true if AppMap.tracing.enabled? && !reentrant && !disabled_by_shallow_flag.call
104
100
 
105
- return call_instance_method.call unless enabled
101
+ enabled = false if %i[instance_eval instance_exec].member?(hook_method.name) && args.empty?
102
+
103
+ return call_instance_method.call(self, args, &block) unless enabled
106
104
 
107
105
  call_event, start_time = with_disabled_hook.call do
108
106
  before_hook.call(self, defined_class, args)
@@ -110,7 +108,7 @@ module AppMap
110
108
  return_value = nil
111
109
  exception = nil
112
110
  begin
113
- return_value = call_instance_method.call
111
+ return_value = call_instance_method.call(self, args, &block)
114
112
  rescue
115
113
  exception = $ERROR_INFO
116
114
  raise
@@ -125,8 +123,18 @@ module AppMap
125
123
  hook_method_parameters = hook_method.parameters.dup.freeze
126
124
  SIGNATURES[[ hook_class, hook_method.name ]] = hook_method_parameters
127
125
 
128
- hook_class.ancestors.first.tap do |cls|
129
- cls.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
126
+ # irb(main):001:0> Kernel.public_instance_method(:system)
127
+ # (irb):1:in `public_instance_method': method `system' for module `Kernel' is private (NameError)
128
+ if hook_class == Kernel
129
+ hook_class.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
130
+ else
131
+ hook_class.ancestors.find { |cls| cls.method_defined?(hook_method.name, false) }.tap do |cls|
132
+ if cls
133
+ cls.define_method_with_arity(hook_method.name, hook_method.arity, hook_method_def)
134
+ else
135
+ warn "#{hook_method.name} not found on #{hook_class}"
136
+ end
137
+ end
130
138
  end
131
139
  end
132
140
 
data/lib/appmap/hook.rb CHANGED
@@ -105,6 +105,9 @@ module AppMap
105
105
 
106
106
  Array(hook.method_names).each do |method_name|
107
107
  method_name = method_name.to_sym
108
+
109
+ warn "AppMap: Initiating hook for builtin #{class_name} #{method_name}" if LOG
110
+
108
111
  base_cls = Util.class_from_string(class_name, must: false)
109
112
  next unless base_cls
110
113
 
@@ -116,6 +119,11 @@ module AppMap
116
119
  end
117
120
 
118
121
  methods = []
122
+ # irb(main):001:0> Kernel.public_instance_method(:system)
123
+ # (irb):1:in `public_instance_method': method `system' for module `Kernel' is private (NameError)
124
+ if base_cls == Kernel
125
+ methods << [ base_cls, base_cls.instance_method(method_name) ] rescue nil
126
+ end
119
127
  methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
120
128
  methods << [ base_cls, base_cls.protected_instance_method(method_name) ] rescue nil
121
129
  if base_cls.respond_to?(:singleton_class)
@@ -120,7 +120,7 @@ module AppMap
120
120
  }.compact
121
121
  fname = AppMap::Util.scenario_filename(name)
122
122
 
123
- AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
123
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), appmap)
124
124
  end
125
125
 
126
126
  def enabled?
data/lib/appmap/record.rb CHANGED
@@ -23,5 +23,5 @@ at_exit do
23
23
  'classMap' => AppMap.class_map(tracer.event_methods),
24
24
  'events' => events
25
25
  }
26
- AppMap::Util.write_appmap('appmap.json', JSON.generate(appmap))
26
+ AppMap::Util.write_appmap('appmap.json', appmap)
27
27
  end
data/lib/appmap/rspec.rb CHANGED
@@ -205,7 +205,7 @@ module AppMap
205
205
  }.compact
206
206
  fname = AppMap::Util.scenario_filename(name)
207
207
 
208
- AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
208
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), appmap)
209
209
  end
210
210
 
211
211
  def enabled?
data/lib/appmap/util.rb CHANGED
@@ -171,7 +171,7 @@ module AppMap
171
171
  mode = File::RDWR | File::CREAT | File::EXCL
172
172
  ::Dir::Tmpname.create([ 'appmap_', '.json' ]) do |tmpname|
173
173
  tempfile = File.open(tmpname, mode)
174
- tempfile.write(appmap)
174
+ tempfile.write(JSON.generate(appmap))
175
175
  tempfile.close
176
176
  # Atomically move the tempfile into place.
177
177
  FileUtils.mv tempfile.path, filename
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.73.0'
6
+ VERSION = '0.74.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'appmap/event'
5
+
6
+ include AppMap
7
+
8
+ describe 'display_string' do
9
+ def display_string(value)
10
+ Event::MethodEvent.display_string value
11
+ end
12
+
13
+ def compare_display_string(value, expected)
14
+ expect(display_string(value)).to eq(expected)
15
+ end
16
+
17
+ context 'for a' do
18
+ it 'String' do
19
+ compare_display_string 'foo', 'foo'
20
+ end
21
+ it 'long String' do
22
+ compare_display_string 'foo' * 100, 'foo' * 33 + 'f (...200 more characters)'
23
+ end
24
+ it 'Array' do
25
+ compare_display_string([ 1, 'my', :bar, [ 2, 3 ], { 4 => 5 } ], '[1, my, :bar, [2, 3], {4=>5}]')
26
+ end
27
+ it 'large Array' do
28
+ compare_display_string 50.times.map { |i| i }, '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9 (...40 more items)]'
29
+ end
30
+ it 'large Hash' do
31
+ compare_display_string(50.times.map { |i| [ i*2, i*2+1] }.to_h, '{0=>1, 2=>3, 4=>5, 6=>7, 8=>9, 10=>11, 12=>13, 14=>15, 16=>17, 18=>19 (...40 more entries)}')
32
+ end
33
+ it 'Hash' do
34
+ compare_display_string({ 1 => 2, 'my' => 'neighbor', bar: :baz, ary: [ 1, 2 ]}, '{1=>2, my=>neighbor, :bar=>:baz, :ary=>[1, 2]}')
35
+ end
36
+ it 'big Hash' do
37
+ compare_display_string(50.times.map { |i| [ i*2, i*2+1] }.to_h, '{0=>1, 2=>3, 4=>5, 6=>7, 8=>9, 10=>11, 12=>13, 14=>15, 16=>17, 18=>19 (...40 more entries)}')
38
+ end
39
+ end
40
+ end
data/spec/hook_spec.rb CHANGED
@@ -1136,7 +1136,7 @@ describe 'AppMap class Hooking', docker: false do
1136
1136
  :parameters:
1137
1137
  - :name: :args
1138
1138
  :class: Array
1139
- :value: '["foo"]'
1139
+ :value: "[foo]"
1140
1140
  :kind: :rest
1141
1141
  - :name: :kw1
1142
1142
  :class: String
@@ -78,7 +78,7 @@ describe 'Rails' do
78
78
  'name' => 'params',
79
79
  'class' => 'ActiveSupport::HashWithIndifferentAccess',
80
80
  'object_id' => Integer,
81
- 'value' => '{"login"=>"alice"}',
81
+ 'value' => '{login=>alice}',
82
82
  'kind' => 'req'
83
83
  ),
84
84
  'receiver' => anything
@@ -127,7 +127,7 @@ describe 'Net::HTTP handler' do
127
127
  :message:
128
128
  - :name: ary
129
129
  :class: Array
130
- :value: '["1", "2"]'
130
+ :value: "[1, 2]"
131
131
  EVENT
132
132
  end
133
133
 
data/test/gem_test.rb CHANGED
@@ -24,7 +24,7 @@ class GemTest < Minitest::Test
24
24
  appmap_file = 'tmp/appmap/minitest/Parser_parser.appmap.json'
25
25
  appmap = JSON.parse(File.read(appmap_file))
26
26
  events = appmap['events']
27
- assert_equal 2, events.size
27
+ assert_equal 6, events.size
28
28
  assert_equal 'call', events.first['event']
29
29
  assert_equal 'default_parser', events.first['method_id']
30
30
  assert_match /\lib\/parser\/base\.rb$/, events.first['path']
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.73.0
4
+ version: 0.74.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-07 00:00:00.000000000 Z
11
+ date: 2022-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -339,10 +339,11 @@ files:
339
339
  - lib/appmap/agent.rb
340
340
  - lib/appmap/builtin_hooks/json.yml
341
341
  - lib/appmap/builtin_hooks/logger.yml
342
- - lib/appmap/builtin_hooks/marshal.yml
343
342
  - lib/appmap/builtin_hooks/net/http.yml
343
+ - lib/appmap/builtin_hooks/open3.yml
344
344
  - lib/appmap/builtin_hooks/openssl.yml
345
345
  - lib/appmap/builtin_hooks/psych.yml
346
+ - lib/appmap/builtin_hooks/ruby.yml
346
347
  - lib/appmap/class_map.rb
347
348
  - lib/appmap/command/agent_setup/init.rb
348
349
  - lib/appmap/command/agent_setup/status.rb
@@ -409,6 +410,7 @@ files:
409
410
  - spec/config_spec.rb
410
411
  - spec/depends/api_spec.rb
411
412
  - spec/depends/spec_helper.rb
413
+ - spec/display_string_spec.rb
412
414
  - spec/fixtures/config/incomplete_config.yml
413
415
  - spec/fixtures/config/invalid_config.yml
414
416
  - spec/fixtures/config/invalid_yaml_config.yml
@@ -1,8 +0,0 @@
1
- - methods:
2
- - Marshal#load
3
- - Marshal#restore
4
- require_name: ruby
5
- label: deserialize.unsafe
6
- - method: Marshal#dump
7
- require_name: ruby
8
- label: serialize