appmap 0.28.1 → 0.34.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/README.md +54 -2
  4. data/Rakefile +1 -1
  5. data/appmap.gemspec +2 -0
  6. data/lib/appmap.rb +25 -14
  7. data/lib/appmap/class_map.rb +25 -27
  8. data/lib/appmap/config.rb +115 -0
  9. data/lib/appmap/cucumber.rb +19 -2
  10. data/lib/appmap/event.rb +25 -16
  11. data/lib/appmap/hook.rb +89 -139
  12. data/lib/appmap/hook/method.rb +83 -0
  13. data/lib/appmap/metadata.rb +1 -1
  14. data/lib/appmap/minitest.rb +141 -0
  15. data/lib/appmap/open.rb +57 -0
  16. data/lib/appmap/rails/action_handler.rb +7 -7
  17. data/lib/appmap/rails/sql_handler.rb +10 -8
  18. data/lib/appmap/record.rb +27 -0
  19. data/lib/appmap/rspec.rb +2 -2
  20. data/lib/appmap/trace.rb +16 -8
  21. data/lib/appmap/util.rb +19 -0
  22. data/lib/appmap/version.rb +1 -1
  23. data/spec/abstract_controller4_base_spec.rb +1 -1
  24. data/spec/abstract_controller_base_spec.rb +9 -2
  25. data/spec/config_spec.rb +3 -3
  26. data/spec/fixtures/hook/compare.rb +7 -0
  27. data/spec/fixtures/hook/instance_method.rb +4 -0
  28. data/spec/hook_spec.rb +222 -37
  29. data/spec/open_spec.rb +19 -0
  30. data/spec/record_sql_rails_pg_spec.rb +56 -33
  31. data/spec/util_spec.rb +1 -1
  32. data/test/cli_test.rb +12 -2
  33. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  34. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  35. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  36. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  37. data/test/fixtures/openssl_recorder/Gemfile +3 -0
  38. data/test/fixtures/openssl_recorder/appmap.yml +3 -0
  39. data/test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb +94 -0
  40. data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
  41. data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
  42. data/test/fixtures/process_recorder/appmap.yml +3 -0
  43. data/test/fixtures/process_recorder/hello.rb +9 -0
  44. data/test/minitest_test.rb +38 -0
  45. data/test/openssl_test.rb +203 -0
  46. data/test/record_process_test.rb +35 -0
  47. data/test/test_helper.rb +1 -0
  48. metadata +51 -2
@@ -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,162 +4,112 @@ require 'English'
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
- LOG = false
8
-
9
- Package = Struct.new(:path, :exclude) do
10
- def to_h
11
- {
12
- path: path,
13
- exclude: exclude.blank? ? nil : exclude
14
- }.compact
15
- end
16
- end
7
+ LOG = (ENV['DEBUG'] == 'true')
17
8
 
18
- Config = Struct.new(:name, :packages) do
19
- class << self
20
- # Loads configuration data from a file, specified by the file name.
21
- def load_from_file(config_file_name)
22
- require 'yaml'
23
- load YAML.safe_load(::File.read(config_file_name))
24
- end
9
+ class << self
10
+ def lock_builtins
11
+ return if @builtins_hooked
25
12
 
26
- # Loads configuration from a Hash.
27
- def load(config_data)
28
- packages = (config_data['packages'] || []).map do |package|
29
- Package.new(package['path'], package['exclude'] || [])
30
- end
31
- Config.new config_data['name'], packages
32
- end
13
+ @builtins_hooked = true
33
14
  end
34
15
 
35
- def initialize(name, packages = [])
36
- super name, packages || []
16
+ # Return the class, separator ('.' or '#'), and method name for
17
+ # the given method.
18
+ def qualify_method_name(method)
19
+ 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']
32
+ [ class_name, '.', method.name ]
33
+ else
34
+ [ method.owner.name, '#', method.name ]
35
+ end
37
36
  end
37
+ end
38
38
 
39
- def to_h
40
- {
41
- name: name,
42
- packages: packages.map(&:to_h)
43
- }
44
- end
39
+ attr_reader :config
40
+ def initialize(config)
41
+ @config = config
45
42
  end
46
43
 
47
- HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
44
+ # Observe class loading and hook all methods which match the config.
45
+ def enable &block
46
+ require 'appmap/hook/method'
48
47
 
49
- class << self
50
- # Observe class loading and hook all methods which match the config.
51
- def hook(config = AppMap.configure)
52
- package_include_paths = config.packages.map(&:path)
53
- package_exclude_paths = config.packages.map do |pkg|
54
- pkg.exclude.map do |exclude|
55
- File.join(pkg.path, exclude)
56
- end
57
- end.flatten
48
+ hook_builtins
58
49
 
59
- before_hook = lambda do |defined_class, method, receiver, args|
60
- require 'appmap/event'
61
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
62
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
63
- [ call_event, Time.now ]
64
- end
50
+ tp = TracePoint.new(:end) do |trace_point|
51
+ cls = trace_point.self
65
52
 
66
- after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
67
- require 'appmap/event'
68
- elapsed = Time.now - start_time
69
- return_event = AppMap::Event::MethodReturn.build_from_invocation \
70
- defined_class, method, call_event.id, elapsed, return_value, exception
71
- AppMap.tracing.record_event return_event
72
- end
53
+ instance_methods = cls.public_instance_methods(false)
54
+ class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
55
+
56
+ hook = lambda do |hook_cls|
57
+ lambda do |method_id|
58
+ method = hook_cls.public_instance_method(method_id)
59
+ hook_method = Hook::Method.new(hook_cls, method)
60
+
61
+ warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
73
62
 
74
- with_disabled_hook = lambda do |&fn|
75
- # Don't record functions, such as to_s and inspect, that might be called
76
- # by the fn. Otherwise there can be a stack oveflow.
77
- Thread.current[HOOK_DISABLE_KEY] = true
78
- begin
79
- fn.call
80
- ensure
81
- Thread.current[HOOK_DISABLE_KEY] = false
63
+ disasm = RubyVM::InstructionSequence.disasm(method)
64
+ # Skip methods that have no instruction sequence, as they are obviously trivial.
65
+ next unless disasm
66
+
67
+ # Don't try and trace the AppMap methods or there will be
68
+ # a stack overflow in the defined hook method.
69
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
70
+
71
+ next unless \
72
+ config.always_hook?(hook_method.defined_class, method.name) ||
73
+ config.included_by_location?(method)
74
+
75
+ hook_method.activate
82
76
  end
83
77
  end
84
78
 
85
- TracePoint.trace(:end) do |tp|
86
- cls = tp.self
87
-
88
- instance_methods = cls.public_instance_methods(false)
89
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
90
-
91
- hook_method = lambda do |cls|
92
- lambda do |method_id|
93
- next if method_id.to_s =~ /_hooked_by_appmap$/
94
-
95
- method = cls.public_instance_method(method_id)
96
- location = method.source_location
97
- location_file, = location
98
- next unless location_file
99
-
100
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
101
- match = package_include_paths.find { |p| location_file.index(p) == 0 }
102
- match &&= !package_exclude_paths.find { |p| location_file.index(p) }
103
- next unless match
104
-
105
- disasm = RubyVM::InstructionSequence.disasm(method)
106
- # Skip methods that have no instruction sequence, as they are obviously trivial.
107
- next unless disasm
108
-
109
- defined_class, method_symbol = if method.owner.singleton_class?
110
- # Singleton class names can take two forms:
111
- # #<Class:Foo> or
112
- # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
113
- # the class from the string.
114
- #
115
- # (There really isn't a better way to do this. The
116
- # singleton's reference to the class it was created
117
- # from is stored in an instance variable named
118
- # '__attached__'. It doesn't have the '@' prefix, so
119
- # it's internal only, and not accessible from user
120
- # code.)
121
- class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
122
- [ class_name, '.' ]
123
- else
124
- [ method.owner.name, '#' ]
125
- end
126
-
127
- method_display_name = "#{defined_class}#{method_symbol}#{method.name}"
128
- # Don't try and trace the tracing method or there will be a stack overflow
129
- # in the defined hook method.
130
- next if method_display_name == "AppMap.tracing"
131
-
132
- warn "AppMap: Hooking #{method_display_name}" if LOG
133
-
134
- cls.define_method method_id do |*args, &block|
135
- base_method = method.bind(self).to_proc
136
-
137
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
138
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
139
- return base_method.call(*args, &block) unless enabled
140
-
141
- call_event, start_time = with_disabled_hook.call do
142
- before_hook.call(defined_class, method, self, args)
143
- end
144
- return_value = nil
145
- exception = nil
146
- begin
147
- return_value = base_method.call(*args, &block)
148
- rescue
149
- exception = $ERROR_INFO
150
- raise
151
- ensure
152
- with_disabled_hook.call do
153
- after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
154
- end
155
- end
156
- end
157
- end
158
- end
79
+ instance_methods.each(&hook.(cls))
80
+ class_methods.each(&hook.(cls.singleton_class))
81
+ end
82
+
83
+ tp.enable(&block)
84
+ end
159
85
 
160
- instance_methods.each(&hook_method.call(cls))
161
- class_methods.each(&hook_method.call(cls.singleton_class))
86
+ def hook_builtins
87
+ return unless self.class.lock_builtins
88
+
89
+ class_from_string = lambda do |fq_class|
90
+ fq_class.split('::').inject(Object) do |mod, class_name|
91
+ mod.const_get(class_name)
92
+ end
162
93
  end
94
+
95
+ Config::BUILTIN_METHODS.each do |class_name, hook|
96
+ require hook.package.package_name if hook.package.package_name
97
+ Array(hook.method_names).each do |method_name|
98
+ method_name = method_name.to_sym
99
+ cls = class_from_string.(class_name)
100
+ method = \
101
+ begin
102
+ cls.instance_method(method_name)
103
+ rescue NameError
104
+ cls.method(method_name) rescue nil
105
+ end
106
+
107
+ if method
108
+ Hook::Method.new(cls, method).activate
109
+ else
110
+ warn "Method #{method_name} not found on #{cls.name}"
111
+ end
112
+ end
163
113
  end
164
114
  end
165
115
  end
@@ -0,0 +1,83 @@
1
+ module AppMap
2
+ class Hook
3
+ class Method
4
+ attr_reader :hook_class, :hook_method, :defined_class, :method_display_name
5
+
6
+ HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
7
+ private_constant :HOOK_DISABLE_KEY
8
+
9
+ # Grab the definition of Time.now here, to avoid interfering
10
+ # with the method we're hooking.
11
+ TIME_NOW = Time.method(:now)
12
+ private_constant :TIME_NOW
13
+
14
+ def initialize(hook_class, hook_method)
15
+ @hook_class = hook_class
16
+ @hook_method = hook_method
17
+ @defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
18
+ @method_display_name = [@defined_class, method_symbol, @hook_method.name].join
19
+ end
20
+
21
+ def activate
22
+ warn "AppMap: Hooking #{method_display_name}" if Hook::LOG
23
+
24
+ hook_method = self.hook_method
25
+ before_hook = self.method(:before_hook)
26
+ after_hook = self.method(:after_hook)
27
+ with_disabled_hook = self.method(:with_disabled_hook)
28
+
29
+ hook_class.define_method hook_method.name do |*args, &block|
30
+ instance_method = hook_method.bind(self).to_proc
31
+
32
+ hook_disabled = Thread.current[HOOK_DISABLE_KEY]
33
+ enabled = true if !hook_disabled && AppMap.tracing.enabled?
34
+ return instance_method.call(*args, &block) unless enabled
35
+
36
+ call_event, start_time = with_disabled_hook.() do
37
+ before_hook.(self, args)
38
+ end
39
+ return_value = nil
40
+ exception = nil
41
+ begin
42
+ return_value = instance_method.(*args, &block)
43
+ rescue
44
+ exception = $ERROR_INFO
45
+ raise
46
+ ensure
47
+ with_disabled_hook.() do
48
+ after_hook.(call_event, start_time, return_value, exception)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ protected
55
+
56
+ def before_hook(receiver, args)
57
+ require 'appmap/event'
58
+ call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
59
+ AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
60
+ [ call_event, TIME_NOW.call ]
61
+ end
62
+
63
+ def after_hook(call_event, start_time, return_value, exception)
64
+ require 'appmap/event'
65
+ elapsed = TIME_NOW.call - start_time
66
+ return_event = \
67
+ AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
68
+ AppMap.tracing.record_event return_event
69
+ end
70
+
71
+ def with_disabled_hook(&fn)
72
+ # Don't record functions, such as to_s and inspect, that might be called
73
+ # by the fn. Otherwise there can be a stack overflow.
74
+ Thread.current[HOOK_DISABLE_KEY] = true
75
+ begin
76
+ fn.call
77
+ ensure
78
+ Thread.current[HOOK_DISABLE_KEY] = false
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end