appmap 0.31.0 → 0.32.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: 54b9a05aeb3eea84572b115a6de1106e6743b74046048e3c168f99a75aa9e669
4
- data.tar.gz: eb8a690376d833c4bec9f0789c5499f413f7dcb3b1ef111257785bd344861f64
3
+ metadata.gz: f6becd9530caf01e14b8a7db69e74b40297d41e4724f25315cd7b44ea6ad3e02
4
+ data.tar.gz: 2863e714f0651d73fc9f2f87d0ea2c5f9fb7f9f553d1f109d9d8f10fff35f5be
5
5
  SHA512:
6
- metadata.gz: 453f06041220ecd0a4fc563ae007e7f8247d3dfcccf49657cbf47ddc53d0175e6834d1dbd89a2338a032d6cfae866929216b58a11bb6f0fe3fd2ea69508d7b2a
7
- data.tar.gz: 77f372ad255c3c664c690affa2962958cebd1e8f34579809e4d975fdf57404889d8506f9153d44a95afda297ffe7b1654529252a5293487477e803bdf0cc9608
6
+ metadata.gz: a5574478fee68b2531449df2b9ec135a60110950cdcd518cf5b85d10e6d0d756b3e22740a4ce0905d7fccb2e6e7d684ee38ac61fd11ad6fdfcff4355a7ae7b36
7
+ data.tar.gz: d15827a809ffde6b6ff98bfb62057f759cdc03e7e24452c1a8273801f94e37d61bab265ddb863ce8f60d83c639875fcf2d247f27abe9b27164817f0739b7c699
@@ -1,3 +1,7 @@
1
+ # v0.32.0
2
+
3
+ * Removes un-necessary fields from `return` events.
4
+
1
5
  # v0.31.0
2
6
 
3
7
  * Add the ability to hook methods by default, and optionally add labels to them in the
data/README.md CHANGED
@@ -33,7 +33,7 @@ There are several ways to record AppMaps of your Ruby program using the `appmap`
33
33
 
34
34
  Once you have recorded some AppMaps (for example, by running RSpec tests), you use the `appland upload` command
35
35
  to upload them to the AppLand server. This command, and some others, is provided
36
- by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases), to
36
+ by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases).
37
37
  Then, on the [AppLand website](https://app.land), you can
38
38
  visualize the design of your code and share links with collaborators.
39
39
 
@@ -87,12 +87,25 @@ Each entry in the `packages` list is a YAML object which has the following keys:
87
87
 
88
88
  To record RSpec tests, follow these additional steps:
89
89
 
90
- 1) Require `appmap/rspec` in your `spec_helper.rb`.
90
+ 1) Require `appmap/rspec` in your `spec_helper.rb` before any other classes are loaded.
91
91
 
92
92
  ```ruby
93
93
  require 'appmap/rspec'
94
94
  ```
95
95
 
96
+ Note that `spec_helper.rb` in a Rails project typically loads the application's classes this way:
97
+
98
+ ```ruby
99
+ require File.expand_path("../../config/environment", __FILE__)
100
+ ```
101
+
102
+ and `appmap/rspec` must be required before this:
103
+
104
+ ```ruby
105
+ require 'appmap/rspec'
106
+ require File.expand_path("../../config/environment", __FILE__)
107
+ ```
108
+
96
109
  2) *Optional* Add `feature: '<feature name>'` and `feature_group: '<feature group name>'` annotations to your
97
110
  examples.
98
111
 
@@ -136,6 +149,19 @@ To record Minitest tests, follow these additional steps:
136
149
  require 'appmap/minitest'
137
150
  ```
138
151
 
152
+ Note that `test_helper.rb` in a Rails project typically loads the application's classes this way:
153
+
154
+ ```ruby
155
+ require_relative '../config/environment'
156
+ ```
157
+
158
+ and `appmap/rspec` must be required before this:
159
+
160
+ ```ruby
161
+ require 'appmap/rspec'
162
+ require_relative '../config/environment'
163
+ ```
164
+
139
165
  2) Run the tests with the environment variable `APPMAP=true`:
140
166
 
141
167
  ```sh-session
@@ -159,6 +185,8 @@ To record Cucumber tests, follow these additional steps:
159
185
  require 'appmap/cucumber'
160
186
  ```
161
187
 
188
+ Be sure to require it before `config/environment` is required.
189
+
162
190
  2) Create an `Around` hook in `support/hooks.rb` to record the scenario:
163
191
 
164
192
 
@@ -13,6 +13,7 @@ require 'appmap/config'
13
13
  require 'appmap/trace'
14
14
  require 'appmap/class_map'
15
15
  require 'appmap/metadata'
16
+ require 'appmap/util'
16
17
 
17
18
  module AppMap
18
19
  class << self
@@ -80,10 +80,6 @@ module AppMap
80
80
  protected
81
81
 
82
82
  def add_function(root, package, method)
83
- location = method.source_location
84
- location_file, lineno = location
85
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
86
-
87
83
  static = method.static
88
84
 
89
85
  object_infos = [
@@ -101,12 +97,17 @@ module AppMap
101
97
  function_info = {
102
98
  name: method.name,
103
99
  type: 'function',
104
- location: [ location_file, lineno ].join(':'),
105
100
  static: static
106
101
  }
102
+ location = method.source_location
103
+ if location
104
+ location_file, lineno = location
105
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
106
+ function_info[:location] = [ location_file, lineno ].join(':')
107
+ end
107
108
  function_info[:labels] = package.labels if package.labels
108
109
  object_infos << function_info
109
-
110
+
110
111
  parent = root
111
112
  object_infos.each do |info|
112
113
  parent = find_or_create parent.children, info do
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppMap
4
- Package = Struct.new(:path, :exclude, :labels) do
5
- def initialize(path, exclude, labels = nil)
4
+ Package = Struct.new(:path, :package_name, :exclude, :labels) do
5
+ def initialize(path, package_name, exclude, labels = nil)
6
6
  super
7
7
  end
8
-
8
+
9
9
  def to_h
10
10
  {
11
11
  path: path,
12
+ package_name: package_name,
12
13
  exclude: exclude.blank? ? nil : exclude,
13
14
  labels: labels.blank? ? nil : labels
14
15
  }.compact
@@ -20,10 +21,7 @@ module AppMap
20
21
  # package and labels that should be applied to them.
21
22
  HOOKED_METHODS = {
22
23
  'ActiveSupport::SecurityUtils' => {
23
- secure_compare: Package.new('active_support', nil, ['security'])
24
- },
25
- 'OpenSSL::X509::Certificate' => {
26
- sign: Package.new('openssl', nil, ['security'])
24
+ secure_compare: Package.new('active_support', nil, nil, ['security'])
27
25
  }
28
26
  }
29
27
 
@@ -43,7 +41,7 @@ module AppMap
43
41
  # Loads configuration from a Hash.
44
42
  def load(config_data)
45
43
  packages = (config_data['packages'] || []).map do |package|
46
- Package.new(package['path'], package['exclude'] || [])
44
+ Package.new(package['path'], nil, package['exclude'] || [])
47
45
  end
48
46
  Config.new config_data['name'], packages
49
47
  end
@@ -57,14 +55,14 @@ module AppMap
57
55
  end
58
56
 
59
57
  def package_for_method(method)
58
+ defined_class, _, method_name = Hook.qualify_method_name(method)
59
+ hooked_method = find_hooked_method(defined_class, method_name)
60
+ return hooked_method if hooked_method
61
+
60
62
  location = method.source_location
61
63
  location_file, = location
62
64
  return unless location_file
63
65
 
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
66
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
69
67
  packages.find do |pkg|
70
68
  (location_file.index(pkg.path) == 0) &&
@@ -83,7 +81,7 @@ module AppMap
83
81
  def find_hooked_method(defined_class, method_name)
84
82
  find_hooked_class(defined_class)[method_name]
85
83
  end
86
-
84
+
87
85
  def find_hooked_class(defined_class)
88
86
  HOOKED_METHODS[defined_class] || {}
89
87
  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,9 +4,7 @@ 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
12
10
  # Return the class, separator ('.' or '#'), and method name for
@@ -17,7 +15,7 @@ module AppMap
17
15
  # #<Class:Foo> or
18
16
  # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
19
17
  # the class from the string.
20
- #
18
+ #
21
19
  # (There really isn't a better way to do this. The
22
20
  # singleton's reference to the class it was created
23
21
  # from is stored in an instance variable named
@@ -39,89 +37,41 @@ module AppMap
39
37
 
40
38
  # Observe class loading and hook all methods which match the config.
41
39
  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
56
-
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
40
+ require 'appmap/hook/method'
67
41
 
68
- tp = TracePoint.new(:end) do |tp|
69
- hook = self
70
- cls = tp.self
42
+ tp = TracePoint.new(:end) do |trace_point|
43
+ cls = trace_point.self
71
44
 
72
45
  instance_methods = cls.public_instance_methods(false)
73
46
  class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
74
47
 
75
- hook_method = lambda do |cls|
48
+ hook = lambda do |hook_cls|
76
49
  lambda do |method_id|
77
50
  next if method_id.to_s =~ /_hooked_by_appmap$/
78
51
 
79
- method = cls.public_instance_method(method_id)
52
+ method = hook_cls.public_instance_method(method_id)
53
+ hook_method = Hook::Method.new(hook_cls, method)
54
+
55
+ warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
56
+
80
57
  disasm = RubyVM::InstructionSequence.disasm(method)
81
58
  # Skip methods that have no instruction sequence, as they are obviously trivial.
82
59
  next unless disasm
83
60
 
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
61
  # Don't try and trace the AppMap methods or there will be
88
62
  # a stack overflow in the defined hook method.
89
- next if /\AAppMap[:\.]/.match?(method_display_name)
63
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
90
64
 
91
65
  next unless \
92
- config.always_hook?(defined_class, method_name) ||
66
+ config.always_hook?(hook_method.defined_class, method.name) ||
93
67
  config.included_by_location?(method)
94
68
 
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
69
+ hook_method.activate
120
70
  end
121
71
  end
122
72
 
123
- instance_methods.each(&hook_method.call(cls))
124
- class_methods.each(&hook_method.call(cls.singleton_class))
73
+ instance_methods.each(&hook.(cls))
74
+ class_methods.each(&hook.(cls.singleton_class))
125
75
  end
126
76
 
127
77
  tp.enable(&block)
@@ -0,0 +1,78 @@
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
+ def initialize(hook_class, hook_method)
10
+ @hook_class = hook_class
11
+ @hook_method = hook_method
12
+ @defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
13
+ @method_display_name = [@defined_class, method_symbol, @hook_method.name].join
14
+ end
15
+
16
+ def activate
17
+ warn "AppMap: Hooking #{method_display_name}" if Hook::LOG
18
+
19
+ hook_method = self.hook_method
20
+ before_hook = self.method(:before_hook)
21
+ after_hook = self.method(:after_hook)
22
+ with_disabled_hook = self.method(:with_disabled_hook)
23
+
24
+ hook_class.define_method hook_method.name do |*args, &block|
25
+ instance_method = hook_method.bind(self).to_proc
26
+
27
+ hook_disabled = Thread.current[HOOK_DISABLE_KEY]
28
+ enabled = true if !hook_disabled && AppMap.tracing.enabled?
29
+ return instance_method.call(*args, &block) unless enabled
30
+
31
+ call_event, start_time = with_disabled_hook.() do
32
+ before_hook.(self, args)
33
+ end
34
+ return_value = nil
35
+ exception = nil
36
+ begin
37
+ return_value = instance_method.(*args, &block)
38
+ rescue
39
+ exception = $ERROR_INFO
40
+ raise
41
+ ensure
42
+ with_disabled_hook.() do
43
+ after_hook.(call_event, start_time, return_value, exception)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def before_hook(receiver, args)
52
+ require 'appmap/event'
53
+ call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
54
+ AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
55
+ [ call_event, Time.now ]
56
+ end
57
+
58
+ def after_hook(call_event, start_time, return_value, exception)
59
+ require 'appmap/event'
60
+ elapsed = Time.now - start_time
61
+ return_event = \
62
+ AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
63
+ AppMap.tracing.record_event return_event
64
+ end
65
+
66
+ def with_disabled_hook(&fn)
67
+ # Don't record functions, such as to_s and inspect, that might be called
68
+ # by the fn. Otherwise there can be a stack overflow.
69
+ Thread.current[HOOK_DISABLE_KEY] = true
70
+ begin
71
+ fn.call
72
+ ensure
73
+ Thread.current[HOOK_DISABLE_KEY] = false
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -16,11 +16,11 @@ module AppMap
16
16
  class HTTPServerRequest
17
17
  include ContextKey
18
18
 
19
- class Call < AppMap::Event::MethodEvent
19
+ class Call < AppMap::Event::MethodCall
20
20
  attr_accessor :payload
21
21
 
22
- def initialize(path, lineno, payload)
23
- super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, Thread.current.object_id
22
+ def initialize(payload)
23
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
24
24
 
25
25
  self.payload = payload
26
26
  end
@@ -47,7 +47,7 @@ module AppMap
47
47
  end
48
48
 
49
49
  def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
50
- event = Call.new(__FILE__, __LINE__, payload)
50
+ event = Call.new(payload)
51
51
  Thread.current[context_key] = Context.new(event.id, Time.now)
52
52
  AppMap.tracing.record_event(event)
53
53
  end
@@ -59,8 +59,8 @@ module AppMap
59
59
  class Call < AppMap::Event::MethodReturnIgnoreValue
60
60
  attr_accessor :payload
61
61
 
62
- def initialize(path, lineno, payload, parent_id, elapsed)
63
- super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, Thread.current.object_id
62
+ def initialize(payload, parent_id, elapsed)
63
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
64
64
 
65
65
  self.payload = payload
66
66
  self.parent_id = parent_id
@@ -82,7 +82,7 @@ module AppMap
82
82
  context = Thread.current[context_key]
83
83
  Thread.current[context_key] = nil
84
84
 
85
- event = Call.new(__FILE__, __LINE__, payload, context.id, Time.now - context.start_time)
85
+ event = Call.new(payload, context.id, Time.now - context.start_time)
86
86
  AppMap.tracing.record_event(event)
87
87
  end
88
88
  end
@@ -5,11 +5,11 @@ require 'appmap/event'
5
5
  module AppMap
6
6
  module Rails
7
7
  class SQLHandler
8
- class SQLCall < AppMap::Event::MethodEvent
8
+ class SQLCall < AppMap::Event::MethodCall
9
9
  attr_accessor :payload
10
10
 
11
- def initialize(path, lineno, payload)
12
- super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, Thread.current.object_id
11
+ def initialize(payload)
12
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
13
13
 
14
14
  self.payload = payload
15
15
  end
@@ -20,7 +20,7 @@ module AppMap
20
20
  sql: payload[:sql],
21
21
  database_type: payload[:database_type]
22
22
  }.tap do |sql_query|
23
- %i[server_version explain_sql].each do |attribute|
23
+ %i[server_version].each do |attribute|
24
24
  sql_query[attribute] = payload[attribute] if payload[attribute]
25
25
  end
26
26
  end
@@ -29,8 +29,8 @@ module AppMap
29
29
  end
30
30
 
31
31
  class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
32
- def initialize(path, lineno, parent_id, elapsed)
33
- super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, Thread.current.object_id
32
+ def initialize(parent_id, elapsed)
33
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
34
34
 
35
35
  self.parent_id = parent_id
36
36
  self.elapsed = elapsed
@@ -76,6 +76,8 @@ module AppMap
76
76
  case database_type
77
77
  when :postgres
78
78
  ActiveRecord::Base.connection.postgresql_version
79
+ when :sqlite
80
+ ActiveRecord::Base.connection.database_version.to_s
79
81
  else
80
82
  warn "Unable to determine database version for #{database_type.inspect}"
81
83
  end
@@ -133,9 +135,9 @@ module AppMap
133
135
 
134
136
  SQLExaminer.examine payload, sql: sql
135
137
 
136
- call = SQLCall.new(__FILE__, __LINE__, payload)
138
+ call = SQLCall.new(payload)
137
139
  AppMap.tracing.record_event(call)
138
- AppMap.tracing.record_event(SQLReturn.new(__FILE__, __LINE__, call.id, finished - started))
140
+ AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
139
141
  ensure
140
142
  Thread.current[reentry_key] = nil
141
143
  end
@@ -14,34 +14,34 @@ module AppMap
14
14
 
15
15
  class Tracing
16
16
  def initialize
17
- @Tracing = []
17
+ @tracing = []
18
18
  end
19
19
 
20
20
  def empty?
21
- @Tracing.empty?
21
+ @tracing.empty?
22
22
  end
23
23
 
24
24
  def trace(enable: true)
25
25
  Tracer.new.tap do |tracer|
26
- @Tracing << tracer
26
+ @tracing << tracer
27
27
  tracer.enable if enable
28
28
  end
29
29
  end
30
30
 
31
31
  def enabled?
32
- @Tracing.any?(&:enabled?)
32
+ @tracing.any?(&:enabled?)
33
33
  end
34
34
 
35
35
  def record_event(event, defined_class: nil, method: nil)
36
- @Tracing.each do |tracer|
36
+ @tracing.each do |tracer|
37
37
  tracer.record_event(event, defined_class: defined_class, method: method)
38
38
  end
39
39
  end
40
40
 
41
41
  def delete(tracer)
42
- return unless @Tracing.member?(tracer)
42
+ return unless @tracing.member?(tracer)
43
43
 
44
- @Tracing.delete(tracer)
44
+ @tracing.delete(tracer)
45
45
  tracer.disable
46
46
  end
47
47
  end
@@ -35,6 +35,25 @@ module AppMap
35
35
 
36
36
  [ fname, extension ].join
37
37
  end
38
+
39
+ # sanitize_event removes ephemeral values from an event, making
40
+ # events easier to compare across runs.
41
+ def sanitize_event(event, &block)
42
+ event.delete(:thread_id)
43
+ event.delete(:elapsed)
44
+ delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
45
+ delete_object_id.call(event[:receiver])
46
+ delete_object_id.call(event[:return_value])
47
+ (event[:parameters] || []).each(&delete_object_id)
48
+ (event[:exceptions] || []).each(&delete_object_id)
49
+
50
+ case event[:event]
51
+ when :call
52
+ event[:path] = event[:path].gsub(Gem.dir + '/', '')
53
+ end
54
+
55
+ event
56
+ end
38
57
  end
39
58
  end
40
59
  end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.31.0'
6
+ VERSION = '0.32.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -48,11 +48,11 @@ describe 'AbstractControllerBase' do
48
48
 
49
49
  expect(appmap).to match(<<-CREATE_CALL.strip)
50
50
  event: call
51
+ thread_id: .*
51
52
  defined_class: Api::UsersController
52
53
  method_id: build_user
53
54
  path: app/controllers/api/users_controller.rb
54
55
  lineno: 23
55
- thread_id: .*
56
56
  static: false
57
57
  parameters:
58
58
  - name: params
@@ -47,17 +47,17 @@ describe 'AbstractControllerBase' do
47
47
  SERVER_REQUEST
48
48
  end
49
49
 
50
- it 'Properly captures method parameters in the appmap' do
50
+ it 'properly captures method parameters in the appmap' do
51
51
  expect(File).to exist(appmap_json)
52
52
  appmap = JSON.parse(File.read(appmap_json)).to_yaml
53
53
 
54
54
  expect(appmap).to match(<<-CREATE_CALL.strip)
55
55
  event: call
56
+ thread_id: .*
56
57
  defined_class: Api::UsersController
57
58
  method_id: build_user
58
59
  path: app/controllers/api/users_controller.rb
59
60
  lineno: 23
60
- thread_id: .*
61
61
  static: false
62
62
  parameters:
63
63
  - name: params
@@ -68,5 +68,12 @@ describe 'AbstractControllerBase' do
68
68
  receiver:
69
69
  CREATE_CALL
70
70
  end
71
+
72
+ it 'returns a minimal event' do
73
+ expect(File).to exist(appmap_json)
74
+ appmap = JSON.parse(File.read(appmap_json))
75
+ event = appmap['events'].find { |event| event['event'] == 'return' && event['return_value'] }
76
+ expect(event.keys).to eq(%w[id event thread_id parent_id elapsed return_value])
77
+ end
71
78
  end
72
79
  end
@@ -16,36 +16,18 @@ end
16
16
  Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
17
17
 
18
18
  describe 'AppMap class Hooking', docker: false do
19
+ require 'appmap/util'
19
20
  def collect_events(tracer)
20
21
  [].tap do |events|
21
22
  while tracer.event?
22
23
  events << tracer.next_event.to_h
23
24
  end
24
- end.map do |event|
25
- event.delete(:thread_id)
26
- event.delete(:elapsed)
27
- delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
28
- delete_object_id.call(event[:receiver])
29
- delete_object_id.call(event[:return_value])
30
- (event[:parameters] || []).each(&delete_object_id)
31
- (event[:exceptions] || []).each(&delete_object_id)
32
-
33
- case event[:event]
34
- when :call
35
- event[:path] = event[:path].gsub(Gem.dir + '/', '')
36
- when :return
37
- # These should be removed from the appmap spec
38
- %i[defined_class method_id path lineno static].each do |obsolete_field|
39
- event.delete(obsolete_field)
40
- end
41
- end
42
- event
43
- end.to_yaml
25
+ end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
44
26
  end
45
27
 
46
28
  def invoke_test_file(file, setup: nil, &block)
47
29
  AppMap.configuration = nil
48
- package = AppMap::Package.new(file, [])
30
+ package = AppMap::Package.new(file, nil, [])
49
31
  config = AppMap::Config.new('hook_spec', [ package ])
50
32
  AppMap.configuration = config
51
33
  tracer = nil
@@ -99,7 +81,7 @@ describe 'AppMap class Hooking', docker: false do
99
81
  :class: String
100
82
  :value: default
101
83
  YAML
102
- config, tracer = test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
84
+ test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
103
85
  expect(InstanceMethod.new.say_default).to eq('default')
104
86
  end
105
87
  end
@@ -456,20 +438,6 @@ describe 'AppMap class Hooking', docker: false do
456
438
  end
457
439
  end
458
440
 
459
- context 'OpenSSL::X509::Certificate.sign' do
460
- # OpenSSL::X509 is not being hooked.
461
- # This might be because the class is being loaded before AppMap, and so the TracePoint
462
- # set by AppMap doesn't see it.
463
- xit 'is hooked' do
464
- events_yaml = <<~YAML
465
- ---
466
- YAML
467
- test_hook_behavior 'spec/fixtures/hook/openssl_sign.rb', events_yaml do
468
- expect(OpenSSLExample.example).to be_truthy
469
- end
470
- end
471
- end
472
-
473
441
  context 'ActiveSupport::SecurityUtils.secure_compare' do
474
442
  it 'is hooked' do
475
443
  events_yaml = <<~YAML
@@ -560,7 +528,7 @@ describe 'AppMap class Hooking', docker: false do
560
528
  :labels:
561
529
  - security
562
530
  YAML
563
-
531
+
564
532
  config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
565
533
  expect(Compare.compare('string', 'string')).to be_truthy
566
534
  end
@@ -1,6 +1,6 @@
1
1
  require 'rails_spec_helper'
2
2
 
3
- describe 'Record SQL queries in a Rails app' do
3
+ describe 'SQL events' do
4
4
  before(:all) { @fixture_dir = 'spec/fixtures/rails_users_app' }
5
5
  include_context 'Rails app pg database'
6
6
 
@@ -14,54 +14,77 @@ describe 'Record SQL queries in a Rails app' do
14
14
  end
15
15
 
16
16
  let(:tmpdir) { "tmp/spec/record_sql_rails_pg_spec" }
17
- let(:appmap) { JSON.parse(File.read(appmap_json)).to_yaml }
18
17
 
19
- context 'while creating a new record' do
18
+ describe 'fields' do
20
19
  let(:test_line_number) { 8 }
21
20
  let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json') }
21
+ let(:orm_module) { 'sequel' }
22
+ let(:appmap) { JSON.parse(File.read(appmap_json)) }
23
+ describe 'on a call event' do
24
+ let(:event) do
25
+ appmap['events'].find do |event|
26
+ event['event'] == 'call' &&
27
+ event.keys.include?('sql_query')
28
+ end
29
+ end
30
+ it 'do not include function-only fields' do
31
+ expect(event.keys).to_not include('defined_class')
32
+ expect(event.keys).to_not include('method_id')
33
+ expect(event.keys).to_not include('path')
34
+ expect(event.keys).to_not include('lineno')
35
+ end
36
+ end
37
+ end
38
+
39
+ describe 'in a Rails app' do
40
+ let(:appmap) { JSON.parse(File.read(appmap_json)).to_yaml }
41
+ context 'while creating a new record' do
42
+ let(:test_line_number) { 8 }
43
+ let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json') }
22
44
 
23
- context 'using Sequel ORM' do
24
- let(:orm_module) { 'sequel' }
25
- it 'detects the sql INSERT' do
26
- expect(appmap).to include(<<-SQL_QUERY.strip)
45
+ context 'using Sequel ORM' do
46
+ let(:orm_module) { 'sequel' }
47
+ it 'detects the sql INSERT' do
48
+ expect(appmap).to include(<<-SQL_QUERY.strip)
27
49
  sql_query:
28
50
  sql: INSERT INTO "users" ("login") VALUES ('alice') RETURNING *
29
- SQL_QUERY
51
+ SQL_QUERY
52
+ end
30
53
  end
31
- end
32
- context 'using ActiveRecord ORM' do
33
- let(:orm_module) { 'activerecord' }
34
- it 'detects the sql INSERT' do
35
- expect(appmap).to include(<<-SQL_QUERY.strip)
54
+ context 'using ActiveRecord ORM' do
55
+ let(:orm_module) { 'activerecord' }
56
+ it 'detects the sql INSERT' do
57
+ expect(appmap).to include(<<-SQL_QUERY.strip)
36
58
  sql_query:
37
59
  sql: INSERT INTO "users" ("login") VALUES ($1) RETURNING "id"
38
- SQL_QUERY
60
+ SQL_QUERY
61
+ end
39
62
  end
40
63
  end
41
- end
42
-
43
- context 'while listing records' do
44
- let(:test_line_number) { 23 }
45
- let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_GET_api_users_lists_the_users.appmap.json') }
46
-
47
- context 'using Sequel ORM' do
48
- let(:orm_module) { 'sequel' }
49
- it 'detects the sql SELECT' do
50
- expect(appmap).to include(<<-SQL_QUERY.strip)
64
+
65
+ context 'while listing records' do
66
+ let(:test_line_number) { 23 }
67
+ let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_GET_api_users_lists_the_users.appmap.json') }
68
+
69
+ context 'using Sequel ORM' do
70
+ let(:orm_module) { 'sequel' }
71
+ it 'detects the sql SELECT' do
72
+ expect(appmap).to include(<<-SQL_QUERY.strip)
51
73
  sql_query:
52
74
  sql: SELECT * FROM "users"
53
- SQL_QUERY
54
-
55
- expect(appmap).to include('sql:')
75
+ SQL_QUERY
76
+
77
+ expect(appmap).to include('sql:')
78
+ end
56
79
  end
57
- end
58
- context 'using ActiveRecord ORM' do
59
- let(:orm_module) { 'activerecord' }
60
- it 'detects the sql SELECT' do
61
- expect(appmap).to include(<<-SQL_QUERY.strip)
80
+ context 'using ActiveRecord ORM' do
81
+ let(:orm_module) { 'activerecord' }
82
+ it 'detects the sql SELECT' do
83
+ expect(appmap).to include(<<-SQL_QUERY.strip)
62
84
  sql_query:
63
85
  sql: SELECT "users".* FROM "users"
64
- SQL_QUERY
86
+ SQL_QUERY
87
+ end
65
88
  end
66
89
  end
67
90
  end
@@ -55,7 +55,7 @@ class CLITest < Minitest::Test
55
55
  assert_equal <<~OUTPUT.strip, output.strip
56
56
  Class frequency:
57
57
  ----------------
58
- 2 Main
58
+ 1 Main
59
59
 
60
60
  Method frequency:
61
61
  ----------------
@@ -79,7 +79,7 @@ class CLITest < Minitest::Test
79
79
  "class_frequency": [
80
80
  {
81
81
  "name": "Main",
82
- "count": 2
82
+ "count": 1
83
83
  }
84
84
  ],
85
85
  "method_frequency": [
@@ -2,3 +2,4 @@ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2
2
  require 'appmap'
3
3
 
4
4
  require 'minitest/autorun'
5
+ require 'diffy'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.0
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-21 00:00:00.000000000 Z
11
+ date: 2020-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -273,6 +273,7 @@ files:
273
273
  - lib/appmap/cucumber.rb
274
274
  - lib/appmap/event.rb
275
275
  - lib/appmap/hook.rb
276
+ - lib/appmap/hook/method.rb
276
277
  - lib/appmap/metadata.rb
277
278
  - lib/appmap/middleware/remote_recording.rb
278
279
  - lib/appmap/minitest.rb
@@ -311,7 +312,6 @@ files:
311
312
  - spec/fixtures/hook/constructor.rb
312
313
  - spec/fixtures/hook/exception_method.rb
313
314
  - spec/fixtures/hook/instance_method.rb
314
- - spec/fixtures/hook/openssl_sign.rb
315
315
  - spec/fixtures/hook/singleton_method.rb
316
316
  - spec/fixtures/rack_users_app/.dockerignore
317
317
  - spec/fixtures/rack_users_app/.gitignore
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # From the manual page https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL.html
4
-
5
- require 'openssl'
6
-
7
- module OpenSSLExample
8
- def OpenSSLExample.example
9
- ca_key = OpenSSL::PKey::RSA.new 2048
10
- pass_phrase = 'my secure pass phrase goes here'
11
-
12
- cipher = OpenSSL::Cipher.new 'AES-256-CBC'
13
-
14
- open 'tmp/ca_key.pem', 'w', 0644 do |io|
15
- io.write ca_key.export(cipher, pass_phrase)
16
- end
17
-
18
- ca_name = OpenSSL::X509::Name.parse '/CN=ca/DC=example'
19
-
20
- ca_cert = OpenSSL::X509::Certificate.new
21
- ca_cert.serial = 0
22
- ca_cert.version = 2
23
- ca_cert.not_before = Time.now
24
- ca_cert.not_after = Time.now + 86400
25
-
26
- ca_cert.public_key = ca_key.public_key
27
- ca_cert.subject = ca_name
28
- ca_cert.issuer = ca_name
29
-
30
- extension_factory = OpenSSL::X509::ExtensionFactory.new
31
- extension_factory.subject_certificate = ca_cert
32
- extension_factory.issuer_certificate = ca_cert
33
-
34
- ca_cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
35
- ca_cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
36
-
37
- ca_cert.add_extension extension_factory.create_extension(
38
- 'keyUsage', 'cRLSign,keyCertSign', true)
39
-
40
- ca_cert.sign ca_key, OpenSSL::Digest::SHA1.new
41
-
42
- open 'tmp/ca_cert.pem', 'w' do |io|
43
- io.write ca_cert.to_pem
44
- end
45
-
46
- csr = OpenSSL::X509::Request.new
47
- csr.version = 0
48
- csr.subject = OpenSSL::X509::Name.new([ ['CN', 'the name to sign', OpenSSL::ASN1::UTF8STRING] ])
49
- csr.public_key = ca_key.public_key
50
- csr.sign ca_key, OpenSSL::Digest::SHA1.new
51
-
52
- open 'tmp/csr.pem', 'w' do |io|
53
- io.write csr.to_pem
54
- end
55
-
56
- csr = OpenSSL::X509::Request.new File.read 'tmp/csr.pem'
57
-
58
- raise 'CSR can not be verified' unless csr.verify csr.public_key
59
-
60
- csr_cert = OpenSSL::X509::Certificate.new
61
- csr_cert.serial = 0
62
- csr_cert.version = 2
63
- csr_cert.not_before = Time.now
64
- csr_cert.not_after = Time.now + 600
65
-
66
- csr_cert.subject = csr.subject
67
- csr_cert.public_key = csr.public_key
68
- csr_cert.issuer = ca_cert.subject
69
-
70
- extension_factory = OpenSSL::X509::ExtensionFactory.new
71
- extension_factory.subject_certificate = csr_cert
72
- extension_factory.issuer_certificate = ca_cert
73
-
74
- csr_cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:FALSE')
75
-
76
- csr_cert.add_extension extension_factory.create_extension(
77
- 'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
78
-
79
- csr_cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
80
-
81
- csr_cert.sign ca_key, OpenSSL::Digest::SHA1.new
82
-
83
- open 'tmp/csr_cert.pem', 'w' do |io|
84
- io.write csr_cert.to_pem
85
- end
86
- end
87
- end