appmap 0.28.1 → 0.31.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8619a5924ba51ee48d76ebfe5c72030629c5338217b4fe659cd0a580eaa6ddb9
4
- data.tar.gz: 0d8b1e8fcd41e0785b362350a53c95c5bc0ba5c628371b7b6216315b7ddcd85e
3
+ metadata.gz: 54b9a05aeb3eea84572b115a6de1106e6743b74046048e3c168f99a75aa9e669
4
+ data.tar.gz: eb8a690376d833c4bec9f0789c5499f413f7dcb3b1ef111257785bd344861f64
5
5
  SHA512:
6
- metadata.gz: c4da2cadb6a852b1213d1938b035ca147a939e7dc1378ef435d1e8b4d42d6f5b98e19191ecd2e1d1e48493817117123e0b9167080308dcc9ea5527bb676f436b
7
- data.tar.gz: 04c2a36c913a3eec42dcd8aa61e789b9b91fed1e43b4bfe84729c6a7b4451fb1064c6efdba39e3818674df8f9560caa486e1d4c8170f04682938775fbb2c4d7e
6
+ metadata.gz: 453f06041220ecd0a4fc563ae007e7f8247d3dfcccf49657cbf47ddc53d0175e6834d1dbd89a2338a032d6cfae866929216b58a11bb6f0fe3fd2ea69508d7b2a
7
+ data.tar.gz: 77f372ad255c3c664c690affa2962958cebd1e8f34579809e4d975fdf57404889d8506f9153d44a95afda297ffe7b1654529252a5293487477e803bdf0cc9608
@@ -1,7 +1,20 @@
1
+ # v0.31.0
2
+
3
+ * Add the ability to hook methods by default, and optionally add labels to them in the
4
+ classmap. Use it to hook `ActiveSupport::SecurityUtils.secure_compare`.
5
+
6
+ # v0.30.0
7
+
8
+ * Add support for Minitest.
9
+
10
+ # v0.29.0
11
+
12
+ * Add `lib/appmap/record.rb`, which can be `require`d to record the rest of the process.
13
+
1
14
  # v0.28.1
15
+
2
16
  * Fix the `defined_class` recorded in an appmap for an instance method included in a class
3
17
  at runtime.
4
-
5
18
  * Only include the `static` attribute on `call` events in an appmap. Determine its value
6
19
  based on the receiver of the method call.
7
20
 
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  - [Configuration](#configuration)
4
4
  - [Running](#running)
5
5
  - [RSpec](#rspec)
6
+ - [Minitest](#minitest)
6
7
  - [Cucumber](#cucumber)
7
8
  - [Remote recording](#remote-recording)
8
9
  - [Ruby on Rails](#ruby-on-rails)
@@ -125,6 +126,29 @@ If you include the `feature` and `feature_group` metadata, these attributes will
125
126
 
126
127
  If you don't explicitly declare `feature` and `feature_group`, then they will be inferred from the spec name and example descriptions.
127
128
 
129
+ ## Minitest
130
+
131
+ To record Minitest tests, follow these additional steps:
132
+
133
+ 1) Require `appmap/minitest` in `test_helper.rb`
134
+
135
+ ```ruby
136
+ require 'appmap/minitest'
137
+ ```
138
+
139
+ 2) Run the tests with the environment variable `APPMAP=true`:
140
+
141
+ ```sh-session
142
+ $ APPMAP=true bundle exec -Ilib -Itest test/*
143
+ ```
144
+
145
+ Each Minitest test will output an AppMap file into the directory `tmp/appmap/minitest`. For example:
146
+
147
+ ```
148
+ $ find tmp/appmap/minitest
149
+ Hello_says_hello_when_prompted.appmap.json
150
+ ```
151
+
128
152
  ## Cucumber
129
153
 
130
154
  To record Cucumber tests, follow these additional steps:
data/Rakefile CHANGED
@@ -122,7 +122,7 @@ end
122
122
  Rake::TestTask.new(:minitest) do |t|
123
123
  t.libs << 'test'
124
124
  t.libs << 'lib'
125
- t.test_files = FileList['test/**/*_test.rb']
125
+ t.test_files = FileList['test/*_test.rb']
126
126
  end
127
127
 
128
128
  task spec: "spec:all"
@@ -8,6 +8,11 @@ rescue NameError
8
8
  end
9
9
 
10
10
  require 'appmap/version'
11
+ require 'appmap/hook'
12
+ require 'appmap/config'
13
+ require 'appmap/trace'
14
+ require 'appmap/class_map'
15
+ require 'appmap/metadata'
11
16
 
12
17
  module AppMap
13
18
  class << self
@@ -34,14 +39,12 @@ module AppMap
34
39
  # the load events won't be seen and the hooks won't activate.
35
40
  def initialize(config_file_path = 'appmap.yml')
36
41
  warn "Configuring AppMap from path #{config_file_path}"
37
- require 'appmap/hook'
38
- self.configuration = Hook::Config.load_from_file(config_file_path)
39
- Hook.hook(configuration)
42
+ self.configuration = Config.load_from_file(config_file_path)
43
+ Hook.new(configuration).enable
40
44
  end
41
45
 
42
46
  # tracing can be used to start tracing, stop tracing, and record events.
43
47
  def tracing
44
- require 'appmap/trace'
45
48
  @tracing ||= Trace::Tracing.new
46
49
  end
47
50
 
@@ -68,14 +71,12 @@ module AppMap
68
71
 
69
72
  # class_map builds a class map from a config and a list of Ruby methods.
70
73
  def class_map(methods)
71
- require 'appmap/class_map'
72
74
  ClassMap.build_from_methods(configuration, methods)
73
75
  end
74
76
 
75
77
  # detect_metadata returns default metadata detected from the Ruby system and from the
76
78
  # filesystem.
77
79
  def detect_metadata
78
- require 'appmap/metadata'
79
80
  @metadata ||= Metadata.detect.freeze
80
81
  @metadata.deep_dup
81
82
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext'
4
-
5
3
  module AppMap
6
4
  class ClassMap
7
5
  module HasChildren
@@ -50,7 +48,7 @@ module AppMap
50
48
  end
51
49
  end
52
50
  Function = Struct.new(:name) do
53
- attr_accessor :static, :location
51
+ attr_accessor :static, :location, :labels
54
52
 
55
53
  def type
56
54
  'function'
@@ -61,8 +59,9 @@ module AppMap
61
59
  name: name,
62
60
  type: type,
63
61
  location: location,
64
- static: static
65
- }
62
+ static: static,
63
+ labels: labels
64
+ }.delete_if {|k,v| v.nil?}
66
65
  end
67
66
  end
68
67
  end
@@ -71,27 +70,17 @@ module AppMap
71
70
  def build_from_methods(config, methods)
72
71
  root = Types::Root.new
73
72
  methods.each do |method|
74
- package = package_for_method(config.packages, method)
75
- add_function root, package.path, method
73
+ package = config.package_for_method(method) \
74
+ or raise "No package found for method #{method}"
75
+ add_function root, package, method
76
76
  end
77
77
  root.children.map(&:to_h)
78
78
  end
79
79
 
80
80
  protected
81
81
 
82
- def package_for_method(packages, method)
83
- location = method.method.source_location
84
- location_file, = location
85
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
86
-
87
- packages.find do |pkg|
88
- (location_file.index(pkg.path) == 0) &&
89
- !pkg.exclude.find { |p| location_file.index(p) }
90
- end or raise "No package found for method #{method}"
91
- end
92
-
93
- def add_function(root, package_name, method)
94
- location = method.method.source_location
82
+ def add_function(root, package, method)
83
+ location = method.source_location
95
84
  location_file, lineno = location
96
85
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
97
86
 
@@ -99,7 +88,7 @@ module AppMap
99
88
 
100
89
  object_infos = [
101
90
  {
102
- name: package_name,
91
+ name: package.path,
103
92
  type: 'package'
104
93
  }
105
94
  ]
@@ -109,12 +98,15 @@ module AppMap
109
98
  type: 'class'
110
99
  }
111
100
  end
112
- object_infos << {
113
- name: method.method.name,
101
+ function_info = {
102
+ name: method.name,
114
103
  type: 'function',
115
104
  location: [ location_file, lineno ].join(':'),
116
105
  static: static
117
106
  }
107
+ function_info[:labels] = package.labels if package.labels
108
+ object_infos << function_info
109
+
118
110
  parent = root
119
111
  object_infos.each do |info|
120
112
  parent = find_or_create parent.children, info do
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ Package = Struct.new(:path, :exclude, :labels) do
5
+ def initialize(path, exclude, labels = nil)
6
+ super
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ path: path,
12
+ exclude: exclude.blank? ? nil : exclude,
13
+ labels: labels.blank? ? nil : labels
14
+ }.compact
15
+ end
16
+ end
17
+
18
+ class Config
19
+ # Methods that should always be hooked, with their containing
20
+ # package and labels that should be applied to them.
21
+ HOOKED_METHODS = {
22
+ 'ActiveSupport::SecurityUtils' => {
23
+ secure_compare: Package.new('active_support', nil, ['security'])
24
+ },
25
+ 'OpenSSL::X509::Certificate' => {
26
+ sign: Package.new('openssl', nil, ['security'])
27
+ }
28
+ }
29
+
30
+ attr_reader :name, :packages
31
+ def initialize(name, packages = [])
32
+ @name = name
33
+ @packages = packages
34
+ end
35
+
36
+ class << self
37
+ # Loads configuration data from a file, specified by the file name.
38
+ def load_from_file(config_file_name)
39
+ require 'yaml'
40
+ load YAML.safe_load(::File.read(config_file_name))
41
+ end
42
+
43
+ # Loads configuration from a Hash.
44
+ def load(config_data)
45
+ packages = (config_data['packages'] || []).map do |package|
46
+ Package.new(package['path'], package['exclude'] || [])
47
+ end
48
+ Config.new config_data['name'], packages
49
+ end
50
+ end
51
+
52
+ def to_h
53
+ {
54
+ name: name,
55
+ packages: packages.map(&:to_h)
56
+ }
57
+ end
58
+
59
+ def package_for_method(method)
60
+ location = method.source_location
61
+ location_file, = location
62
+ return unless location_file
63
+
64
+ defined_class,_,method_name = Hook.qualify_method_name(method)
65
+ hooked_method = find_hooked_method(defined_class, method_name)
66
+ return hooked_method if hooked_method
67
+
68
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
69
+ packages.find do |pkg|
70
+ (location_file.index(pkg.path) == 0) &&
71
+ !pkg.exclude.find { |p| location_file.index(p) }
72
+ end
73
+ end
74
+
75
+ def included_by_location?(method)
76
+ !!package_for_method(method)
77
+ end
78
+
79
+ def always_hook?(defined_class, method_name)
80
+ !!find_hooked_method(defined_class, method_name)
81
+ end
82
+
83
+ def find_hooked_method(defined_class, method_name)
84
+ find_hooked_class(defined_class)[method_name]
85
+ end
86
+
87
+ def find_hooked_class(defined_class)
88
+ HOOKED_METHODS[defined_class] || {}
89
+ end
90
+ end
91
+ end
@@ -6,161 +6,125 @@ module AppMap
6
6
  class Hook
7
7
  LOG = false
8
8
 
9
- Package = Struct.new(:path, :exclude) do
10
- def to_h
11
- {
12
- path: path,
13
- exclude: exclude.blank? ? nil : exclude
14
- }.compact
9
+ HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
10
+
11
+ class << self
12
+ # Return the class, separator ('.' or '#'), and method name for
13
+ # the given method.
14
+ def qualify_method_name(method)
15
+ 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']
28
+ [ class_name, '.', method.name ]
29
+ else
30
+ [ method.owner.name, '#', method.name ]
31
+ end
15
32
  end
16
33
  end
17
34
 
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
35
+ attr_reader :config
36
+ def initialize(config)
37
+ @config = config
38
+ end
25
39
 
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
40
+ # Observe class loading and hook all methods which match the config.
41
+ 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 ]
33
47
  end
34
48
 
35
- def initialize(name, packages = [])
36
- super name, packages || []
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
37
55
  end
38
56
 
39
- def to_h
40
- {
41
- name: name,
42
- packages: packages.map(&:to_h)
43
- }
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
44
66
  end
45
- end
46
67
 
47
- HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
68
+ tp = TracePoint.new(:end) do |tp|
69
+ hook = self
70
+ cls = tp.self
48
71
 
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
72
+ instance_methods = cls.public_instance_methods(false)
73
+ class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
58
74
 
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
75
+ hook_method = lambda do |cls|
76
+ lambda do |method_id|
77
+ next if method_id.to_s =~ /_hooked_by_appmap$/
65
78
 
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
79
+ method = cls.public_instance_method(method_id)
80
+ disasm = RubyVM::InstructionSequence.disasm(method)
81
+ # Skip methods that have no instruction sequence, as they are obviously trivial.
82
+ next unless disasm
73
83
 
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
82
- end
83
- end
84
+ defined_class, method_symbol, method_name = Hook.qualify_method_name(method)
85
+ method_display_name = [defined_class, method_symbol, method_name].join
84
86
 
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"
87
+ # Don't try and trace the AppMap methods or there will be
88
+ # a stack overflow in the defined hook method.
89
+ next if /\AAppMap[:\.]/.match?(method_display_name)
131
90
 
132
- warn "AppMap: Hooking #{method_display_name}" if LOG
91
+ next unless \
92
+ config.always_hook?(defined_class, method_name) ||
93
+ config.included_by_location?(method)
133
94
 
134
- cls.define_method method_id do |*args, &block|
135
- base_method = method.bind(self).to_proc
95
+ warn "AppMap: Hooking #{method_display_name}" if LOG
136
96
 
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
97
+ cls.define_method method_id do |*args, &block|
98
+ base_method = method.bind(self).to_proc
140
99
 
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
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)
155
117
  end
156
118
  end
157
119
  end
120
+ end
158
121
  end
159
122
 
160
123
  instance_methods.each(&hook_method.call(cls))
161
124
  class_methods.each(&hook_method.call(cls.singleton_class))
162
125
  end
163
- end
126
+
127
+ tp.enable(&block)
164
128
  end
165
129
  end
166
130
  end