appmap 0.31.0 → 0.32.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: 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