appmap 0.31.0 → 0.34.2

Sign up to get free protection for your applications and to get access to all the features.
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