appmap 0.28.1 → 0.34.1

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