appmap 0.31.0 → 0.34.2

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rbenv-gemsets +1 -0
  4. data/CHANGELOG.md +22 -0
  5. data/README.md +38 -4
  6. data/Rakefile +10 -3
  7. data/appmap.gemspec +5 -0
  8. data/ext/appmap/appmap.c +26 -0
  9. data/ext/appmap/extconf.rb +6 -0
  10. data/lib/appmap.rb +23 -10
  11. data/lib/appmap/class_map.rb +13 -7
  12. data/lib/appmap/config.rb +54 -30
  13. data/lib/appmap/cucumber.rb +19 -2
  14. data/lib/appmap/event.rb +25 -16
  15. data/lib/appmap/hook.rb +52 -77
  16. data/lib/appmap/hook/method.rb +103 -0
  17. data/lib/appmap/open.rb +57 -0
  18. data/lib/appmap/rails/action_handler.rb +7 -7
  19. data/lib/appmap/rails/sql_handler.rb +10 -8
  20. data/lib/appmap/rspec.rb +1 -1
  21. data/lib/appmap/trace.rb +7 -7
  22. data/lib/appmap/util.rb +19 -0
  23. data/lib/appmap/version.rb +1 -1
  24. data/spec/abstract_controller4_base_spec.rb +1 -1
  25. data/spec/abstract_controller_base_spec.rb +9 -2
  26. data/spec/fixtures/hook/instance_method.rb +4 -0
  27. data/spec/fixtures/hook/singleton_method.rb +21 -12
  28. data/spec/hook_spec.rb +140 -44
  29. data/spec/open_spec.rb +19 -0
  30. data/spec/record_sql_rails_pg_spec.rb +56 -33
  31. data/test/cli_test.rb +12 -2
  32. data/test/fixtures/openssl_recorder/Gemfile +3 -0
  33. data/test/fixtures/openssl_recorder/appmap.yml +3 -0
  34. data/{spec/fixtures/hook/openssl_sign.rb → test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb} +11 -4
  35. data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
  36. data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
  37. data/test/openssl_test.rb +203 -0
  38. data/test/test_helper.rb +1 -0
  39. metadata +58 -4
@@ -4,6 +4,8 @@ require 'appmap/util'
4
4
 
5
5
  module AppMap
6
6
  module Cucumber
7
+ APPMAP_OUTPUT_DIR = 'tmp/appmap/cucumber'
8
+
7
9
  ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
8
10
 
9
11
  ProviderStruct = Struct.new(:scenario) do
@@ -38,18 +40,27 @@ module AppMap
38
40
  end
39
41
 
40
42
  class << self
43
+ def init
44
+ warn 'Configuring AppMap recorder for Cucumber'
45
+
46
+ FileUtils.mkdir_p APPMAP_OUTPUT_DIR
47
+ end
48
+
41
49
  def write_scenario(scenario, appmap)
42
50
  appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
43
51
  scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
44
52
 
45
- FileUtils.mkdir_p 'tmp/appmap/cucumber'
46
- File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
53
+ File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
47
54
  end
48
55
 
49
56
  def enabled?
50
57
  ENV['APPMAP'] == 'true'
51
58
  end
52
59
 
60
+ def run
61
+ init
62
+ end
63
+
53
64
  protected
54
65
 
55
66
  def cucumber_version
@@ -87,3 +98,9 @@ module AppMap
87
98
  end
88
99
  end
89
100
  end
101
+
102
+ if AppMap::Cucumber.enabled?
103
+ require 'appmap'
104
+
105
+ AppMap::Cucumber.run
106
+ end
@@ -15,21 +15,15 @@ module AppMap
15
15
  end
16
16
  end
17
17
 
18
- MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :thread_id)
18
+ MethodEventStruct = Struct.new(:id, :event, :thread_id)
19
19
 
20
20
  class MethodEvent < MethodEventStruct
21
21
  LIMIT = 100
22
22
 
23
23
  class << self
24
- def build_from_invocation(me, event_type, defined_class, method)
24
+ def build_from_invocation(me, event_type)
25
25
  me.id = AppMap::Event.next_id_counter
26
26
  me.event = event_type
27
- me.defined_class = defined_class
28
- me.method_id = method.name.to_s
29
- path = method.source_location[0]
30
- path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
31
- me.path = path
32
- me.lineno = method.source_location[1]
33
27
  me.thread_id = Thread.current.object_id
34
28
  end
35
29
 
@@ -58,15 +52,25 @@ module AppMap
58
52
  (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
59
53
  end
60
54
  end
61
-
62
55
  end
63
56
 
64
57
  class MethodCall < MethodEvent
65
- attr_accessor :parameters, :receiver, :static
58
+ attr_accessor :defined_class, :method_id, :path, :lineno, :parameters, :receiver, :static
66
59
 
67
60
  class << self
68
61
  def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
69
62
  mc.tap do
63
+ static = receiver.is_a?(Module)
64
+ mc.defined_class = defined_class
65
+ mc.method_id = method.name.to_s
66
+ if method.source_location
67
+ path = method.source_location[0]
68
+ path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
69
+ mc.path = path
70
+ mc.lineno = method.source_location[1]
71
+ else
72
+ mc.path = [ defined_class, static ? '.' : '#', method.name ].join
73
+ end
70
74
  mc.parameters = method.parameters.map.with_index do |method_param, idx|
71
75
  param_type, param_name = method_param
72
76
  param_name ||= 'arg'
@@ -84,17 +88,22 @@ module AppMap
84
88
  object_id: receiver.__id__,
85
89
  value: display_string(receiver)
86
90
  }
87
- mc.static = receiver.is_a?(Module)
88
- MethodEvent.build_from_invocation(mc, :call, defined_class, method)
91
+ mc.static = static
92
+ MethodEvent.build_from_invocation(mc, :call)
89
93
  end
90
94
  end
91
95
  end
92
96
 
93
97
  def to_h
94
98
  super.tap do |h|
99
+ h[:defined_class] = defined_class
100
+ h[:method_id] = method_id
101
+ h[:path] = path
102
+ h[:lineno] = lineno
95
103
  h[:static] = static
96
104
  h[:parameters] = parameters
97
105
  h[:receiver] = receiver
106
+ h.delete_if { |_, v| v.nil? }
98
107
  end
99
108
  end
100
109
 
@@ -105,11 +114,11 @@ module AppMap
105
114
  attr_accessor :parent_id, :elapsed
106
115
 
107
116
  class << self
108
- def build_from_invocation(mr = MethodReturnIgnoreValue.new, defined_class, method, parent_id, elapsed)
117
+ def build_from_invocation(mr = MethodReturnIgnoreValue.new, parent_id, elapsed)
109
118
  mr.tap do |_|
110
119
  mr.parent_id = parent_id
111
120
  mr.elapsed = elapsed
112
- MethodEvent.build_from_invocation(mr, :return, defined_class, method)
121
+ MethodEvent.build_from_invocation(mr, :return)
113
122
  end
114
123
  end
115
124
  end
@@ -126,7 +135,7 @@ module AppMap
126
135
  attr_accessor :return_value, :exceptions
127
136
 
128
137
  class << self
129
- def build_from_invocation(mr = MethodReturn.new, defined_class, method, parent_id, elapsed, return_value, exception)
138
+ def build_from_invocation(mr = MethodReturn.new, parent_id, elapsed, return_value, exception)
130
139
  mr.tap do |_|
131
140
  if return_value
132
141
  mr.return_value = {
@@ -152,7 +161,7 @@ module AppMap
152
161
 
153
162
  mr.exceptions = exceptions
154
163
  end
155
- MethodReturnIgnoreValue.build_from_invocation(mr, defined_class, method, parent_id, elapsed)
164
+ MethodReturnIgnoreValue.build_from_invocation(mr, parent_id, elapsed)
156
165
  end
157
166
  end
158
167
  end
@@ -4,27 +4,20 @@ require 'English'
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
- LOG = false
8
-
9
- HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
7
+ LOG = (ENV['DEBUG'] == 'true')
10
8
 
11
9
  class << self
10
+ def lock_builtins
11
+ return if @builtins_hooked
12
+
13
+ @builtins_hooked = true
14
+ end
15
+
12
16
  # Return the class, separator ('.' or '#'), and method name for
13
17
  # the given method.
14
18
  def qualify_method_name(method)
15
19
  if method.owner.singleton_class?
16
- # Singleton class names can take two forms:
17
- # #<Class:Foo> or
18
- # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
19
- # the class from the string.
20
- #
21
- # (There really isn't a better way to do this. The
22
- # singleton's reference to the class it was created
23
- # from is stored in an instance variable named
24
- # '__attached__'. It doesn't have the '@' prefix, so
25
- # it's internal only, and not accessible from user
26
- # code.)
27
- class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
20
+ class_name = singleton_method_owner_name(method)
28
21
  [ class_name, '.', method.name ]
29
22
  else
30
23
  [ method.owner.name, '#', method.name ]
@@ -39,92 +32,74 @@ module AppMap
39
32
 
40
33
  # Observe class loading and hook all methods which match the config.
41
34
  def enable &block
42
- before_hook = lambda do |defined_class, method, receiver, args|
43
- require 'appmap/event'
44
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
45
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
46
- [ call_event, Time.now ]
47
- end
48
-
49
- after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
50
- require 'appmap/event'
51
- elapsed = Time.now - start_time
52
- return_event = AppMap::Event::MethodReturn.build_from_invocation \
53
- defined_class, method, call_event.id, elapsed, return_value, exception
54
- AppMap.tracing.record_event return_event
55
- end
35
+ require 'appmap/hook/method'
56
36
 
57
- with_disabled_hook = lambda do |&fn|
58
- # Don't record functions, such as to_s and inspect, that might be called
59
- # by the fn. Otherwise there can be a stack overflow.
60
- Thread.current[HOOK_DISABLE_KEY] = true
61
- begin
62
- fn.call
63
- ensure
64
- Thread.current[HOOK_DISABLE_KEY] = false
65
- end
66
- end
37
+ hook_builtins
67
38
 
68
- tp = TracePoint.new(:end) do |tp|
69
- hook = self
70
- cls = tp.self
39
+ tp = TracePoint.new(:end) do |trace_point|
40
+ cls = trace_point.self
71
41
 
72
42
  instance_methods = cls.public_instance_methods(false)
73
43
  class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
74
44
 
75
- hook_method = lambda do |cls|
45
+ hook = lambda do |hook_cls|
76
46
  lambda do |method_id|
77
- next if method_id.to_s =~ /_hooked_by_appmap$/
47
+ method = hook_cls.public_instance_method(method_id)
48
+ hook_method = Hook::Method.new(hook_cls, method)
49
+
50
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
78
51
 
79
- method = cls.public_instance_method(method_id)
80
52
  disasm = RubyVM::InstructionSequence.disasm(method)
81
53
  # Skip methods that have no instruction sequence, as they are obviously trivial.
82
54
  next unless disasm
83
55
 
84
- defined_class, method_symbol, method_name = Hook.qualify_method_name(method)
85
- method_display_name = [defined_class, method_symbol, method_name].join
86
-
87
56
  # Don't try and trace the AppMap methods or there will be
88
57
  # a stack overflow in the defined hook method.
89
- next if /\AAppMap[:\.]/.match?(method_display_name)
58
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
90
59
 
91
60
  next unless \
92
- config.always_hook?(defined_class, method_name) ||
61
+ config.always_hook?(hook_cls, method.name) ||
93
62
  config.included_by_location?(method)
94
63
 
95
- warn "AppMap: Hooking #{method_display_name}" if LOG
96
-
97
- cls.define_method method_id do |*args, &block|
98
- base_method = method.bind(self).to_proc
99
-
100
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
101
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
102
- return base_method.call(*args, &block) unless enabled
103
-
104
- call_event, start_time = with_disabled_hook.call do
105
- before_hook.call(defined_class, method, self, args)
106
- end
107
- return_value = nil
108
- exception = nil
109
- begin
110
- return_value = base_method.call(*args, &block)
111
- rescue
112
- exception = $ERROR_INFO
113
- raise
114
- ensure
115
- with_disabled_hook.call do
116
- after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
117
- end
118
- end
119
- end
64
+ hook_method.activate
120
65
  end
121
66
  end
122
67
 
123
- instance_methods.each(&hook_method.call(cls))
124
- class_methods.each(&hook_method.call(cls.singleton_class))
68
+ instance_methods.each(&hook.(cls))
69
+ class_methods.each(&hook.(cls.singleton_class))
125
70
  end
126
71
 
127
72
  tp.enable(&block)
128
73
  end
74
+
75
+ def hook_builtins
76
+ return unless self.class.lock_builtins
77
+
78
+ class_from_string = lambda do |fq_class|
79
+ fq_class.split('::').inject(Object) do |mod, class_name|
80
+ mod.const_get(class_name)
81
+ end
82
+ end
83
+
84
+ Config::BUILTIN_METHODS.each do |class_name, hook|
85
+ require hook.package.package_name if hook.package.package_name
86
+ Array(hook.method_names).each do |method_name|
87
+ method_name = method_name.to_sym
88
+ cls = class_from_string.(class_name)
89
+ method = \
90
+ begin
91
+ cls.instance_method(method_name)
92
+ rescue NameError
93
+ cls.method(method_name) rescue nil
94
+ end
95
+
96
+ if method
97
+ Hook::Method.new(cls, method).activate
98
+ else
99
+ warn "Method #{method_name} not found on #{cls.name}"
100
+ end
101
+ end
102
+ end
103
+ end
129
104
  end
130
105
  end
@@ -0,0 +1,103 @@
1
+ module AppMap
2
+ class Hook
3
+ class Method
4
+ attr_reader :hook_class, :hook_method
5
+
6
+ # +method_display_name+ may be nil if name resolution gets
7
+ # deferred until runtime (e.g. for a singleton method on an
8
+ # embedded Struct).
9
+ attr_reader :method_display_name
10
+
11
+ HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
12
+ private_constant :HOOK_DISABLE_KEY
13
+
14
+ # Grab the definition of Time.now here, to avoid interfering
15
+ # with the method we're hooking.
16
+ TIME_NOW = Time.method(:now)
17
+ private_constant :TIME_NOW
18
+
19
+ def initialize(hook_class, hook_method)
20
+ @hook_class = hook_class
21
+ @hook_method = hook_method
22
+
23
+ # Get the class for the method, if it's known.
24
+ @defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
25
+ @method_display_name = [@defined_class, method_symbol, @hook_method.name].join if @defined_class
26
+ end
27
+
28
+ def activate
29
+ if Hook::LOG
30
+ msg = if method_display_name
31
+ "#{method_display_name}"
32
+ else
33
+ "#{hook_method.name} (class resolution deferrred)"
34
+ end
35
+ warn "AppMap: Hooking " + msg
36
+ end
37
+
38
+ defined_class = @defined_class
39
+ hook_method = self.hook_method
40
+ before_hook = self.method(:before_hook)
41
+ after_hook = self.method(:after_hook)
42
+ with_disabled_hook = self.method(:with_disabled_hook)
43
+
44
+ hook_class.define_method hook_method.name do |*args, &block|
45
+ instance_method = hook_method.bind(self).to_proc
46
+
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
51
+
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
55
+
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)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def before_hook(receiver, defined_class, args)
77
+ require 'appmap/event'
78
+ 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
80
+ [ call_event, TIME_NOW.call ]
81
+ end
82
+
83
+ def after_hook(call_event, start_time, return_value, exception)
84
+ require 'appmap/event'
85
+ elapsed = TIME_NOW.call - start_time
86
+ return_event = \
87
+ AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
88
+ AppMap.tracing.record_event return_event
89
+ end
90
+
91
+ def with_disabled_hook(&fn)
92
+ # Don't record functions, such as to_s and inspect, that might be called
93
+ # by the fn. Otherwise there can be a stack overflow.
94
+ Thread.current[HOOK_DISABLE_KEY] = true
95
+ begin
96
+ fn.call
97
+ ensure
98
+ Thread.current[HOOK_DISABLE_KEY] = false
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ OpenStruct = Struct.new(:appmap)
5
+
6
+ class Open < OpenStruct
7
+ attr_reader :port
8
+
9
+ def perform
10
+ server = run_server
11
+ open_browser
12
+ server.kill
13
+ end
14
+
15
+ def page
16
+ require 'rack/utils'
17
+ <<~PAGE
18
+ <!DOCTYPE html>
19
+ <html>
20
+ <head>
21
+ <title>&hellip;</title>
22
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
23
+ <script type="text/javascript">
24
+ function dosubmit() { document.forms[0].submit(); }
25
+ </script>
26
+ </head>
27
+ <body onload="dosubmit();">
28
+ <form action="https://app.land/scenario_uploads" method="POST" accept-charset="utf-8">
29
+ <input type="hidden" name="data" value='#{Rack::Utils.escape_html appmap.to_json}'>
30
+ </form>
31
+ </body>
32
+ </html>
33
+ PAGE
34
+ end
35
+
36
+ def run_server
37
+ require 'rack'
38
+ Thread.new do
39
+ Rack::Handler::WEBrick.run(
40
+ lambda do |env|
41
+ return [200, { 'Content-Type' => 'text/html' }, [page]]
42
+ end,
43
+ :Port => 0
44
+ ) do |server|
45
+ @port = server.config[:Port]
46
+ end
47
+ end.tap do
48
+ sleep 1.0
49
+ end
50
+ end
51
+
52
+ def open_browser
53
+ system 'open', "http://localhost:#{@port}"
54
+ sleep 5.0
55
+ end
56
+ end
57
+ end