appmap 0.23.0 → 0.25.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +17 -8
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +19 -0
  6. data/README.md +29 -12
  7. data/Rakefile +3 -3
  8. data/appmap.gemspec +3 -1
  9. data/exe/appmap +6 -18
  10. data/lib/appmap.rb +47 -6
  11. data/lib/appmap/algorithm/prune_class_map.rb +2 -0
  12. data/lib/appmap/algorithm/stats.rb +4 -2
  13. data/lib/appmap/class_map.rb +143 -0
  14. data/lib/appmap/command/record.rb +8 -6
  15. data/lib/appmap/command/stats.rb +2 -0
  16. data/lib/appmap/command/upload.rb +4 -2
  17. data/lib/appmap/event.rb +168 -0
  18. data/lib/appmap/hook.rb +151 -0
  19. data/lib/appmap/middleware/remote_recording.rb +14 -20
  20. data/lib/appmap/rails/action_handler.rb +10 -6
  21. data/lib/appmap/rails/sql_handler.rb +10 -8
  22. data/lib/appmap/railtie.rb +31 -18
  23. data/lib/appmap/rspec.rb +238 -261
  24. data/lib/appmap/trace.rb +88 -0
  25. data/lib/appmap/version.rb +1 -1
  26. data/package-lock.json +90 -92
  27. data/spec/abstract_controller4_base_spec.rb +1 -1
  28. data/spec/abstract_controller_base_spec.rb +7 -3
  29. data/spec/config_spec.rb +25 -0
  30. data/spec/fixtures/hook/attr_accessor.rb +5 -0
  31. data/spec/fixtures/hook/class_method.rb +17 -0
  32. data/spec/fixtures/hook/constructor.rb +7 -0
  33. data/spec/fixtures/hook/exception_method.rb +11 -0
  34. data/spec/fixtures/hook/instance_method.rb +23 -0
  35. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
  36. data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
  37. data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
  38. data/spec/fixtures/rails_users_app/.ruby-version +1 -1
  39. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
  40. data/spec/fixtures/rails_users_app/config/database.yml +2 -1
  41. data/spec/fixtures/rails_users_app/create_app +1 -0
  42. data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
  43. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
  44. data/spec/hook_spec.rb +357 -0
  45. data/spec/rails_spec_helper.rb +25 -16
  46. data/spec/railtie_spec.rb +1 -1
  47. data/spec/record_sql_rails_pg_spec.rb +1 -2
  48. data/spec/remote_recording_spec.rb +117 -0
  49. data/spec/spec_helper.rb +1 -0
  50. data/test/cli_test.rb +7 -36
  51. data/test/fixtures/cli_record_test/appmap.yml +2 -1
  52. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
  53. data/test/test_helper.rb +0 -42
  54. metadata +46 -62
  55. data/exe/_appmap-record-self +0 -49
  56. data/lib/appmap/command/inspect.rb +0 -14
  57. data/lib/appmap/config.rb +0 -65
  58. data/lib/appmap/config/directory.rb +0 -65
  59. data/lib/appmap/config/file.rb +0 -13
  60. data/lib/appmap/config/named_function.rb +0 -21
  61. data/lib/appmap/config/package_dir.rb +0 -52
  62. data/lib/appmap/config/path.rb +0 -25
  63. data/lib/appmap/feature.rb +0 -262
  64. data/lib/appmap/inspect.rb +0 -91
  65. data/lib/appmap/inspect/inspector.rb +0 -99
  66. data/lib/appmap/inspect/parse_node.rb +0 -170
  67. data/lib/appmap/inspect/parser.rb +0 -15
  68. data/lib/appmap/parser.rb +0 -60
  69. data/lib/appmap/rspec/parse_node.rb +0 -41
  70. data/lib/appmap/rspec/parser.rb +0 -15
  71. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
  72. data/lib/appmap/trace/tracer.rb +0 -356
  73. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
  74. data/spec/rack_handler_webrick_spec.rb +0 -59
  75. data/test/config_test.rb +0 -149
  76. data/test/explict_inspect_test.rb +0 -29
  77. data/test/fixtures/active_record_like/active_record.rb +0 -2
  78. data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
  79. data/test/fixtures/active_record_like/active_record/association.rb +0 -4
  80. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
  81. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
  82. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
  83. data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
  84. data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
  85. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
  86. data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
  87. data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
  88. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
  89. data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
  90. data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
  91. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
  92. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
  93. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
  94. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
  95. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
  96. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
  97. data/test/fixtures/parse_file/defs_static_function.rb +0 -96
  98. data/test/fixtures/parse_file/function_within_class.rb +0 -36
  99. data/test/fixtures/parse_file/include_public_methods.rb +0 -127
  100. data/test/fixtures/parse_file/instance_function.rb +0 -17
  101. data/test/fixtures/parse_file/modules.rb +0 -71
  102. data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
  103. data/test/fixtures/parse_file/toplevel_class.rb +0 -13
  104. data/test/fixtures/parse_file/toplevel_function.rb +0 -14
  105. data/test/fixtures/trace_test/trace_program_1.rb +0 -44
  106. data/test/implicit_inspect_test.rb +0 -33
  107. data/test/include_exclude_test.rb +0 -48
  108. data/test/prerecorded_trace_test.rb +0 -76
  109. data/test/trace_test.rb +0 -92
@@ -1,15 +0,0 @@
1
- require 'appmap/parser'
2
- require 'appmap/rspec/parse_node'
3
-
4
- module AppMap
5
- module RSpec
6
- # Parser processes a Ruby into a list of parse nodes and a list of comments.
7
- class Parser < ::AppMap::Parser
8
- protected
9
-
10
- def build_parse_node(node, file_path, ancestors)
11
- ParseNode.from_node(node, file_path, ancestors)
12
- end
13
- end
14
- end
15
- end
@@ -1,65 +0,0 @@
1
- module AppMap
2
- module Trace
3
- module EventHandler
4
- # Use the `req` and `res` parameters on Rack::Handler::WEBrick to populate the
5
- # `http_server_request` and `http_server_response` info on the trace event.
6
- #
7
- # See https://github.com/rack/rack/blob/b72bfc9435c118c54019efae1fedd119521b76df/lib/rack/handler/webrick.rb#L26
8
- module RackHandlerWebrick
9
- class Call < MethodEvent
10
- attr_accessor :http_server_request
11
-
12
- class << self
13
- def build_from_tracepoint(mc = Call.new, tp, path)
14
- mc.tap do |_|
15
- req = value_in_binding(tp, :req)
16
-
17
- # Don't try and grab 'parameters', because:
18
- # a) They aren't needed.
19
- # b) We want to avoid triggering side effects like reading the request body.
20
-
21
- mc.http_server_request = {
22
- request_method: req.request_method,
23
- path_info: req.path_info,
24
- protocol: "HTTP/#{req.http_version}"
25
- }
26
-
27
- MethodEvent.build_from_tracepoint(mc, tp, path)
28
- end
29
- end
30
- end
31
-
32
- def to_h
33
- super.tap do |h|
34
- h[:http_server_request] = http_server_request
35
- end
36
- end
37
- end
38
-
39
- class Return < MethodReturnIgnoreValue
40
- attr_accessor :http_server_response
41
-
42
- class << self
43
- def build_from_tracepoint(mr = Return.new, tp, path, parent_id, elapsed)
44
- mr.tap do |_|
45
- res = value_in_binding(tp, :res)
46
-
47
- mr.http_server_response = {
48
- status: res.status
49
- }
50
-
51
- MethodReturnIgnoreValue.build_from_tracepoint(mr, tp, path, parent_id, elapsed)
52
- end
53
- end
54
- end
55
-
56
- def to_h
57
- super.tap do |h|
58
- h[:http_server_response] = http_server_response
59
- end
60
- end
61
- end
62
- end
63
- end
64
- end
65
- end
@@ -1,356 +0,0 @@
1
- module AppMap
2
- module Trace
3
- MethodEventStruct =
4
- Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :static, :thread_id)
5
-
6
- class Tracers
7
- def initialize
8
- @tracers = []
9
- end
10
-
11
- def empty?
12
- @tracers.empty?
13
- end
14
-
15
- def trace(functions, enable: true)
16
- AppMap::Trace::Tracer.new(functions).tap do |tracer|
17
- @tracers << tracer
18
- tracer.enable if enable
19
- end
20
- end
21
-
22
- def record_event(event)
23
- @tracers.each do |tracer|
24
- tracer.record_event(event)
25
- end
26
- end
27
-
28
- def delete(tracer)
29
- return unless @tracers.member?(tracer)
30
-
31
- @tracers.delete(tracer)
32
- tracer.disable
33
- end
34
- end
35
-
36
- class << self
37
- def tracers
38
- @tracers ||= Tracers.new
39
- end
40
- end
41
-
42
- # @appmap
43
- class MethodEvent < MethodEventStruct
44
- LIMIT = 100
45
-
46
- COUNTER_LOCK = Mutex.new # :nodoc:
47
- @@id_counter = 0
48
-
49
- class << self
50
- # Build a new instance from a TracePoint.
51
- def build_from_tracepoint(me, tp, path)
52
- me.id = next_id
53
- me.event = tp.event
54
-
55
- if tp.defined_class.singleton_class?
56
- me.defined_class = (tp.self.is_a?(Class) || tp.self.is_a?(Module)) ? tp.self.name : tp.self.class.name
57
- else
58
- me.defined_class = tp.defined_class.name
59
- end
60
-
61
- me.method_id = tp.method_id
62
- me.path = path
63
- me.lineno = tp.lineno
64
- me.static = tp.defined_class.name.nil?
65
- me.thread_id = Thread.current.object_id
66
- end
67
-
68
- # Gets the next serial id.
69
- #
70
- # This method is thread-safe.
71
- # @appmap
72
- def next_id
73
- COUNTER_LOCK.synchronize do
74
- @@id_counter += 1
75
- end
76
- end
77
-
78
- # Gets a value, by key, from the trace point binding.
79
- # If the method raises an error, it can be handled by the optional block.
80
- def value_in_binding(tp, key, &block)
81
- tp.binding.eval(key.to_s)
82
- rescue NameError, ArgumentError
83
- yield if block_given?
84
- end
85
-
86
- # Gets a display string for a value. This is not meant to be a machine deserializable value.
87
- def display_string(value)
88
- return nil unless value
89
-
90
- last_resort_string = lambda do
91
- warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
92
- '*Error inspecting variable*'
93
- end
94
-
95
- value_string = \
96
- begin
97
- value.to_s
98
- rescue NoMethodError
99
- begin
100
- value.inspect
101
- rescue StandardError
102
- last_resort_string.call
103
- end
104
- rescue StandardError
105
- last_resort_string.call
106
- end
107
-
108
- value_string[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
109
- end
110
- end
111
-
112
- alias static? static
113
- end
114
-
115
- # @appmap
116
- class MethodCall < MethodEvent
117
- attr_accessor :parameters, :receiver
118
-
119
- class << self
120
- # @appmap
121
- def build_from_tracepoint(mc = MethodCall.new, tp, path)
122
- mc.tap do |_|
123
- mc.parameters = collect_parameters(tp)
124
- mc.receiver = collect_self(tp)
125
- MethodEvent.build_from_tracepoint(mc, tp, path)
126
- end
127
- end
128
-
129
- def collect_self(tp)
130
- {
131
- class: tp.self.class.name,
132
- object_id: tp.self.__id__,
133
- value: display_string(tp.self)
134
- }
135
- end
136
-
137
- def collect_parameters(tp)
138
- -> { tp.self.method(tp.method_id).parameters rescue [] }.call.collect do |pinfo|
139
- kind, key = pinfo
140
- value = value_in_binding(tp, key)
141
- {
142
- name: key,
143
- class: value.class.name,
144
- object_id: value.__id__,
145
- value: display_string(value),
146
- kind: kind # :req, :rest, :key, :keyrest, :block
147
- }
148
- end
149
- end
150
- end
151
-
152
- def to_h
153
- super.tap do |h|
154
- h[:parameters] = parameters
155
- h[:receiver] = receiver
156
- end
157
- end
158
- end
159
-
160
- class MethodReturnIgnoreValue < MethodEvent
161
- attr_accessor :parent_id, :elapsed
162
-
163
- class << self
164
- def build_from_tracepoint(mr = MethodReturnIgnoreValue.new, tp, path, parent_id, elapsed)
165
- mr.tap do |_|
166
- mr.parent_id = parent_id
167
- mr.elapsed = elapsed
168
- MethodEvent.build_from_tracepoint(mr, tp, path)
169
- end
170
- end
171
- end
172
-
173
- def to_h
174
- super.tap do |h|
175
- h[:parent_id] = parent_id
176
- h[:elapsed] = elapsed
177
- end
178
- end
179
- end
180
-
181
- class MethodReturn < MethodReturnIgnoreValue
182
- attr_accessor :return_value
183
-
184
- class << self
185
- def build_from_tracepoint(mr = MethodReturn.new, tp, path, parent_id, elapsed)
186
- mr.tap do |_|
187
- mr.return_value = {
188
- class: tp.return_value.class.name,
189
- value: display_string(tp.return_value),
190
- object_id: tp.return_value.__id__
191
- }
192
- MethodReturnIgnoreValue.build_from_tracepoint(mr, tp, path, parent_id, elapsed)
193
- end
194
- end
195
- end
196
-
197
- def to_h
198
- super.tap do |h|
199
- h[:return_value] = return_value
200
- end
201
- end
202
- end
203
-
204
- # Processes a series of calls into recorded events.
205
- # Each call to the handle should provide a TracePoint (or duck-typed object) as the argument.
206
- # On each call, a MethodEvent is constructed according to the nature of the TracePoint, and then
207
- # stored using the record_event method.
208
- # @appmap
209
- class TracePointHandler
210
- attr_accessor :call_constructor, :return_constructor
211
-
212
- DEFAULT_HANDLER_CLASSES = {
213
- call: MethodCall,
214
- return: MethodReturn
215
- }.freeze
216
-
217
- # @appmap
218
- def initialize(tracer)
219
- @pwd = Dir.pwd
220
- @tracer = tracer
221
- @call_stack = Hash.new { |h, k| h[k] = [] }
222
- @handler_classes = {}
223
- end
224
-
225
- # @appmap
226
- def handle(tp)
227
- # Absoute paths which are within the current working directory are normalized
228
- # to be relative paths.
229
- path = tp.path
230
- if path.index(@pwd) == 0
231
- path = path[@pwd.length+1..-1]
232
- end
233
-
234
- method_event = \
235
- if tp.event == :call && (function = @tracer.lookup_function(path, tp.lineno))
236
- call_constructor = handler_class(function, tp.event)
237
- call_constructor.build_from_tracepoint(tp, path).tap do |c|
238
- @call_stack[Thread.current.object_id] << [ tp.defined_class, tp.method_id, c.id, Time.now, function ]
239
- end
240
- elsif (c = @call_stack[Thread.current.object_id].last) &&
241
- c[0] == tp.defined_class &&
242
- c[1] == tp.method_id
243
- function = c[4]
244
- @call_stack[Thread.current.object_id].pop
245
- return_constructor = handler_class(function, tp.event)
246
- return_constructor.build_from_tracepoint(tp, path, c[2], Time.now - c[3])
247
- end
248
-
249
- @tracer.record_event method_event if method_event
250
-
251
- method_event
252
- rescue
253
- puts $!.message
254
- puts $!.backtrace.join("\n")
255
- # XXX If this exception doesn't get reraised, internal errors
256
- # (e.g. a missing method on TracePoint) get silently
257
- # ignored. This allows tests to pass that should fail, which
258
- # is bad, but is it desirable otherwise?
259
- raise
260
- end
261
-
262
- protected
263
-
264
- # Figure out which handler class should be used for a trace event. It may be
265
- # a custom handler, e.g. in case we are processing a special named function such as a
266
- # web server entry point, or it may be the standard :call or :return handler.
267
- def handler_class(function, event)
268
- cache_key = [function.location, event]
269
- cached_handler = @handler_classes[cache_key]
270
- return cached_handler if cached_handler
271
-
272
- return default_handler_class(event) unless function.handler_id
273
-
274
- require "appmap/trace/event_handler/#{function.handler_id}"
275
-
276
- AppMap::Trace::EventHandler
277
- .const_get(function.handler_id.to_s.camelize)
278
- .const_get(event.to_s.capitalize).tap do |handler|
279
- @handler_classes[cache_key] = handler
280
- end
281
- end
282
-
283
- def default_handler_class(event)
284
- DEFAULT_HANDLER_CLASSES[event] or raise "No handler class for #{event.inspect}"
285
- end
286
- end
287
-
288
- # @appmap
289
- class Tracer
290
- # Trace a specified set of functions.
291
- #
292
- # functions Array of AppMap::Feature::Function.
293
- # @appmap
294
- def initialize(functions)
295
- @functions = functions
296
-
297
- @functions_by_location = functions.each_with_object({}) do |m, memo|
298
- path, lineno = m.location.split(':', 2)
299
- memo[path] ||= {}
300
- memo[path][lineno.to_i] = m
301
- memo
302
- end
303
-
304
- @events_mutex = Mutex.new
305
- @events = []
306
- end
307
-
308
- def enable
309
- handler = TracePointHandler.new(self)
310
- @trace_point = TracePoint.trace(:call, :return, &handler.method(:handle))
311
- end
312
-
313
- # Private function. Use AppMap.tracers#delete.
314
- def disable # :nodoc:
315
- @trace_point.disable
316
- end
317
-
318
- # Whether the indicated file path and lineno is a breakpoint on which
319
- # execution should interrupted.
320
- # @appmap
321
- def lookup_function(path, lineno)
322
- (methods_by_path = @functions_by_location[path]) && methods_by_path[lineno]
323
- end
324
-
325
- # Record a program execution event.
326
- #
327
- # The event should be one of the MethodEvent subclasses.
328
- #
329
- # This method is thread-safe.
330
- # @appmap
331
- def record_event(event)
332
- @events_mutex.synchronize do
333
- @events << event
334
- end
335
- end
336
-
337
- # Whether there is an event available for processing.
338
- #
339
- # This method is thread-safe.
340
- def event?
341
- @events_mutex.synchronize do
342
- !@events.empty?
343
- end
344
- end
345
-
346
- # Gets the next available event, if any.
347
- #
348
- # This method is thread-safe.
349
- def next_event
350
- @events_mutex.synchronize do
351
- @events.shift
352
- end
353
- end
354
- end
355
- end
356
- end
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- #
5
- # This file was generated by Bundler.
6
- #
7
- # The application '_appmap-record-self' is installed as part of a gem, and
8
- # this file is here to facilitate running it.
9
- #
10
-
11
- require "pathname"
12
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
- Pathname.new(__FILE__).realpath)
14
-
15
- bundle_binstub = File.expand_path("../bundle", __FILE__)
16
-
17
- if File.file?(bundle_binstub)
18
- if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
- load(bundle_binstub)
20
- else
21
- abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
- Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
- end
24
- end
25
-
26
- require "rubygems"
27
- require "bundler/setup"
28
-
29
- load Gem.bin_path("appmap", "_appmap-record-self")