appmap 0.28.1 → 0.31.0

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