appmap 0.42.0 → 0.45.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +23 -2
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +65 -6
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +4 -7
  10. data/lib/appmap/class_map.rb +7 -10
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +173 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +18 -0
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/hook.rb +112 -56
  18. data/lib/appmap/hook/method.rb +5 -7
  19. data/lib/appmap/middleware/remote_recording.rb +1 -1
  20. data/lib/appmap/minitest.rb +22 -20
  21. data/lib/appmap/rails/request_handler.rb +30 -17
  22. data/lib/appmap/record.rb +1 -1
  23. data/lib/appmap/rspec.rb +23 -21
  24. data/lib/appmap/trace.rb +2 -1
  25. data/lib/appmap/util.rb +47 -2
  26. data/lib/appmap/version.rb +2 -2
  27. data/release.sh +17 -0
  28. data/spec/abstract_controller_base_spec.rb +77 -30
  29. data/spec/class_map_spec.rb +3 -11
  30. data/spec/config_spec.rb +33 -1
  31. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  32. data/spec/fixtures/hook/method_named_call.rb +11 -0
  33. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  34. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  35. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  36. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  37. data/spec/fixtures/rails5_users_app/create_app +8 -2
  38. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  39. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  40. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  41. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  42. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  43. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  44. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  45. data/spec/fixtures/rails6_users_app/create_app +8 -2
  46. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  47. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  48. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  49. data/spec/hook_spec.rb +141 -20
  50. data/spec/record_net_http_spec.rb +160 -0
  51. data/spec/record_sql_rails_pg_spec.rb +1 -1
  52. data/spec/spec_helper.rb +16 -0
  53. data/test/expectations/openssl_test_key_sign1.json +2 -4
  54. data/test/gem_test.rb +1 -1
  55. data/test/rspec_test.rb +0 -13
  56. metadata +17 -12
  57. data/exe/appmap +0 -154
  58. data/test/cli_test.rb +0 -116
@@ -50,7 +50,7 @@ module AppMap
50
50
  appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
51
51
  scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
52
52
 
53
- File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
53
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
54
54
  end
55
55
 
56
56
  def enabled?
data/lib/appmap/event.rb CHANGED
@@ -36,6 +36,18 @@ module AppMap
36
36
  (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
37
  end
38
38
 
39
+ def object_properties(hash_like)
40
+ hash = hash_like.to_h
41
+ hash.keys.map do |key|
42
+ {
43
+ name: key,
44
+ class: hash[key].class.name,
45
+ }
46
+ end
47
+ rescue
48
+ nil
49
+ end
50
+
39
51
  protected
40
52
 
41
53
  # Heuristic for dynamically defined class whose name can be nil
@@ -79,6 +91,12 @@ module AppMap
79
91
  end
80
92
  end
81
93
  end
94
+
95
+ protected
96
+
97
+ def object_properties(hash_like)
98
+ self.class.object_properties(hash_like)
99
+ end
82
100
  end
83
101
 
84
102
  class MethodCall < MethodEvent
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+
5
+ module AppMap
6
+ module Handler
7
+ module Function
8
+ class << self
9
+ def handle_call(defined_class, hook_method, receiver, args)
10
+ AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
11
+ end
12
+
13
+ def handle_return(call_event_id, elapsed, return_value, exception)
14
+ AppMap::Event::MethodReturn.build_from_invocation(call_event_id, elapsed, return_value, exception)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/event'
4
+
5
+ module AppMap
6
+ module Handler
7
+ class HTTPClientRequest < AppMap::Event::MethodEvent
8
+ attr_accessor :request_method, :url, :params, :headers
9
+
10
+ def initialize(http, request)
11
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
12
+
13
+ path, query = request.path.split('?')
14
+ query ||= ''
15
+
16
+ protocol = http.use_ssl? ? 'https' : 'http'
17
+ port = if http.use_ssl? && http.port == 443
18
+ nil
19
+ elsif !http.use_ssl? && http.port == 80
20
+ nil
21
+ else
22
+ ":#{http.port}"
23
+ end
24
+
25
+ url = [ protocol, '://', http.address, port, path ].compact.join
26
+
27
+ self.request_method = request.method
28
+ self.url = url
29
+ self.headers = AppMap::Util.select_headers(NetHTTP.request_headers(request))
30
+ self.params = Rack::Utils.parse_nested_query(query)
31
+ end
32
+
33
+ def to_h
34
+ super.tap do |h|
35
+ h[:http_client_request] = {
36
+ request_method: request_method,
37
+ url: url,
38
+ headers: headers
39
+ }.compact
40
+
41
+ unless params.blank?
42
+ h[:message] = params.keys.map do |key|
43
+ val = params[key]
44
+ {
45
+ name: key,
46
+ class: val.class.name,
47
+ value: self.class.display_string(val),
48
+ object_id: val.__id__,
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ class HTTPClientResponse < AppMap::Event::MethodReturnIgnoreValue
57
+ attr_accessor :status, :mime_type, :headers
58
+
59
+ def initialize(response, parent_id, elapsed)
60
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
61
+
62
+ self.status = response.code.to_i
63
+ self.parent_id = parent_id
64
+ self.elapsed = elapsed
65
+ self.headers = AppMap::Util.select_headers(NetHTTP.response_headers(response))
66
+ end
67
+
68
+ def to_h
69
+ super.tap do |h|
70
+ h[:http_client_response] = {
71
+ status_code: status,
72
+ mime_type: mime_type,
73
+ headers: headers
74
+ }.compact
75
+ end
76
+ end
77
+ end
78
+
79
+ class NetHTTP
80
+ class << self
81
+ def request_headers(request)
82
+ {}.tap do |headers|
83
+ request.each_header do |k,v|
84
+ key = [ 'HTTP', k.underscore.upcase ].join('_')
85
+ headers[key] = v
86
+ end
87
+ end
88
+ end
89
+
90
+ alias response_headers request_headers
91
+
92
+ def handle_call(defined_class, hook_method, receiver, args)
93
+ # request will call itself again in a start block if it's not already started.
94
+ return unless receiver.started?
95
+
96
+ http = receiver
97
+ request = args.first
98
+ HTTPClientRequest.new(http, request)
99
+ end
100
+
101
+ def handle_return(call_event_id, elapsed, return_value, exception)
102
+ HTTPClientResponse.new(return_value, call_event_id, elapsed)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/appmap/hook.rb CHANGED
@@ -5,6 +5,7 @@ require 'English'
5
5
  module AppMap
6
6
  class Hook
7
7
  LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+ LOG_HOOK = (ENV['DEBUG_HOOK'] == 'true')
8
9
 
9
10
  OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self].freeze
10
11
  OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self].freeze
@@ -32,93 +33,148 @@ module AppMap
32
33
  end
33
34
 
34
35
  attr_reader :config
36
+
35
37
  def initialize(config)
36
38
  @config = config
39
+ @trace_locations = []
40
+ # Paths that are known to be non-tracing
41
+ @notrace_paths = Set.new
37
42
  end
38
43
 
39
44
  # Observe class loading and hook all methods which match the config.
40
- def enable &block
45
+ def enable(&block)
41
46
  require 'appmap/hook/method'
42
47
 
43
48
  hook_builtins
44
49
 
45
- tp = TracePoint.new(:end) do |trace_point|
46
- cls = trace_point.self
47
-
48
- instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
49
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
50
-
51
- hook = lambda do |hook_cls|
52
- lambda do |method_id|
53
- method = begin
54
- hook_cls.public_instance_method(method_id)
55
- rescue NameError
56
- warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
57
- return
58
- end
50
+ @trace_begin = TracePoint.new(:class, &method(:trace_class))
51
+ @trace_end = TracePoint.new(:end, &method(:trace_end))
59
52
 
60
- warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
53
+ @trace_begin.enable(&block)
54
+ end
61
55
 
62
- disasm = RubyVM::InstructionSequence.disasm(method)
63
- # Skip methods that have no instruction sequence, as they are obviously trivial.
64
- next unless disasm
56
+ # hook_builtins builds hooks for code that is built in to the Ruby standard library.
57
+ # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
58
+ def hook_builtins
59
+ return unless self.class.lock_builtins
65
60
 
66
- next if config.never_hook?(method)
61
+ class_from_string = lambda do |fq_class|
62
+ fq_class.split('::').inject(Object) do |mod, class_name|
63
+ mod.const_get(class_name)
64
+ end
65
+ end
67
66
 
68
- next unless \
69
- config.always_hook?(hook_cls, method.name) ||
70
- config.included_by_location?(method)
67
+ config.builtin_methods.each do |class_name, hooks|
68
+ Array(hooks).each do |hook|
69
+ require hook.package.package_name if hook.package.package_name
70
+ Array(hook.method_names).each do |method_name|
71
+ method_name = method_name.to_sym
72
+ base_cls = class_from_string.(class_name)
71
73
 
72
- hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
74
+ hook_method = lambda do |entry|
75
+ cls, method = entry
76
+ return false if config.never_hook?(cls, method)
73
77
 
74
- # Don't try and trace the AppMap methods or there will be
75
- # a stack overflow in the defined hook method.
76
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
78
+ Hook::Method.new(hook.package, cls, method).activate
79
+ end
77
80
 
78
- hook_method.activate
81
+ methods = []
82
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
83
+ if base_cls.respond_to?(:singleton_class)
84
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
85
+ end
86
+ methods.compact!
87
+ if methods.empty?
88
+ warn "Method #{method_name} not found on #{base_cls.name}"
89
+ else
90
+ methods.each(&hook_method)
91
+ end
79
92
  end
80
93
  end
94
+ end
95
+ end
96
+
97
+ protected
98
+
99
+ def trace_class(trace_point)
100
+ path = trace_point.path
81
101
 
82
- instance_methods.each(&hook.(cls))
83
- class_methods.each(&hook.(cls.singleton_class))
102
+ return if @notrace_paths.member?(path)
103
+
104
+ if config.path_enabled?(path)
105
+ location = trace_location(trace_point)
106
+ warn "Entering hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
107
+ @trace_locations << location
108
+ unless @trace_end.enabled?
109
+ warn "Enabling hooking" if Hook::LOG || Hook::LOG_HOOK
110
+ @trace_end.enable
111
+ end
112
+ else
113
+ @notrace_paths << path
84
114
  end
115
+ end
85
116
 
86
- tp.enable(&block)
117
+ def trace_location(trace_point)
118
+ [ trace_point.path, trace_point.lineno ].join(':')
87
119
  end
88
120
 
89
- # hook_builtins builds hooks for code that is built in to the Ruby standard library.
90
- # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
91
- def hook_builtins
92
- return unless self.class.lock_builtins
121
+ def trace_end(trace_point)
122
+ cls = trace_point.self
93
123
 
94
- class_from_string = lambda do |fq_class|
95
- fq_class.split('::').inject(Object) do |mod, class_name|
96
- mod.const_get(class_name)
124
+ instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
125
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
126
+ class_methods = begin
127
+ if cls.respond_to?(:singleton_class)
128
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
129
+ else
130
+ []
97
131
  end
132
+ rescue NameError
133
+ []
98
134
  end
99
135
 
100
- Config::BUILTIN_METHODS.each do |class_name, hook|
101
- require hook.package.package_name if hook.package.package_name
102
- Array(hook.method_names).each do |method_name|
103
- method_name = method_name.to_sym
104
-
105
- cls = class_from_string.(class_name)
106
- method = \
107
- begin
108
- cls.instance_method(method_name)
109
- rescue NameError
110
- cls.method(method_name) rescue nil
111
- end
136
+ hook = lambda do |hook_cls|
137
+ lambda do |method_id|
138
+ # Don't try and trace the AppMap methods or there will be
139
+ # a stack overflow in the defined hook method.
140
+ next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
141
+
142
+ method = begin
143
+ hook_cls.public_instance_method(method_id)
144
+ rescue NameError
145
+ warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
146
+ next
147
+ end
112
148
 
113
- next if config.never_hook?(method)
149
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
114
150
 
115
- if method
116
- Hook::Method.new(hook.package, cls, method).activate
117
- else
118
- warn "Method #{method_name} not found on #{cls.name}"
119
- end
151
+ disasm = RubyVM::InstructionSequence.disasm(method)
152
+ # Skip methods that have no instruction sequence, as they are obviously trivial.
153
+ next unless disasm
154
+
155
+ package = config.lookup_package(hook_cls, method)
156
+ next unless package
157
+
158
+ Hook::Method.new(package, hook_cls, method).activate
120
159
  end
121
160
  end
161
+
162
+ instance_methods.each(&hook.(cls))
163
+ begin
164
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
165
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
166
+ rescue NameError
167
+ # NameError:
168
+ # uninitialized constant Faraday::Connection
169
+ warn "NameError in #{__FILE__}: #{$!.message}"
170
+ end
171
+
172
+ location = @trace_locations.pop
173
+ warn "Leaving hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
174
+ if @trace_locations.empty?
175
+ warn "Disabling hooking" if Hook::LOG || Hook::LOG_HOOK
176
+ @trace_end.disable
177
+ end
122
178
  end
123
179
  end
124
180
  end
@@ -76,7 +76,7 @@ module AppMap
76
76
  raise
77
77
  ensure
78
78
  with_disabled_hook.call do
79
- after_hook.call(self, call_event, start_time, return_value, exception)
79
+ after_hook.call(self, call_event, start_time, return_value, exception) if call_event
80
80
  end
81
81
  end
82
82
  end
@@ -87,18 +87,16 @@ module AppMap
87
87
  protected
88
88
 
89
89
  def before_hook(receiver, defined_class, args)
90
- require 'appmap/event'
91
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
92
- AppMap.tracing.record_event call_event, package: hook_package, defined_class: defined_class, method: hook_method
90
+ call_event = hook_package.handler_class.handle_call(defined_class, hook_method, receiver, args)
91
+ AppMap.tracing.record_event(call_event, package: hook_package, defined_class: defined_class, method: hook_method) if call_event
93
92
  [ call_event, TIME_NOW.call ]
94
93
  end
95
94
 
96
95
  def after_hook(_receiver, call_event, start_time, return_value, exception)
97
- require 'appmap/event'
98
96
  elapsed = TIME_NOW.call - start_time
99
- return_event = \
100
- AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
97
+ return_event = hook_package.handler_class.handle_return(call_event.id, elapsed, return_value, exception)
101
98
  AppMap.tracing.record_event return_event
99
+ nil
102
100
  end
103
101
 
104
102
  def with_disabled_hook(&function)
@@ -67,7 +67,7 @@ module AppMap
67
67
 
68
68
  response = JSON.generate \
69
69
  version: AppMap::APPMAP_FORMAT_VERSION,
70
- classMap: AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
70
+ classMap: AppMap.class_map(tracer.event_methods),
71
71
  metadata: metadata,
72
72
  events: @events
73
73
 
@@ -26,8 +26,9 @@ module AppMap
26
26
  end
27
27
 
28
28
 
29
- def finish
29
+ def finish(exception)
30
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
31
+ warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
31
32
 
32
33
  events = []
33
34
  AppMap.tracing.delete @trace
@@ -36,15 +37,17 @@ module AppMap
36
37
 
37
38
  AppMap::Minitest.add_event_methods @trace.event_methods
38
39
 
39
- class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
40
+ class_map = AppMap.class_map(@trace.event_methods)
40
41
 
41
42
  feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
42
43
  feature_name = test.name.split('_')[1..-1].join(' ')
43
44
  scenario_name = [ feature_group, feature_name ].join(' ')
44
45
 
45
- AppMap::Minitest.save scenario_name,
46
- class_map,
47
- source_location,
46
+ AppMap::Minitest.save name: scenario_name,
47
+ class_map: class_map,
48
+ source_location: source_location,
49
+ test_status: exception ? 'failed' : 'succeeded',
50
+ exception: exception,
48
51
  events: events
49
52
  end
50
53
  end
@@ -63,11 +66,11 @@ module AppMap
63
66
  @recordings_by_test[test.object_id] = Recording.new(test, name)
64
67
  end
65
68
 
66
- def end_test(test)
69
+ def end_test(test, exception:)
67
70
  recording = @recordings_by_test.delete(test.object_id)
68
71
  return warn "No recording found for #{test}" unless recording
69
72
 
70
- recording.finish
73
+ recording.finish exception
71
74
  end
72
75
 
73
76
  def config
@@ -78,9 +81,9 @@ module AppMap
78
81
  @event_methods += event_methods
79
82
  end
80
83
 
81
- def save(example_name, class_map, source_location, events: nil, labels: nil)
84
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
82
85
  metadata = AppMap::Minitest.metadata.tap do |m|
83
- m[:name] = example_name
86
+ m[:name] = name
84
87
  m[:source_location] = source_location
85
88
  m[:app] = AppMap.configuration.name
86
89
  m[:frameworks] ||= []
@@ -91,6 +94,13 @@ module AppMap
91
94
  m[:recorder] = {
92
95
  name: 'minitest'
93
96
  }
97
+ m[:test_status] = test_status
98
+ if exception
99
+ m[:exception] = {
100
+ class: exception.class.name,
101
+ message: exception.to_s
102
+ }
103
+ end
94
104
  end
95
105
 
96
106
  appmap = {
@@ -99,14 +109,9 @@ module AppMap
99
109
  classMap: class_map,
100
110
  events: events
101
111
  }.compact
102
- fname = AppMap::Util.scenario_filename(example_name)
112
+ fname = AppMap::Util.scenario_filename(name)
103
113
 
104
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
105
- end
106
-
107
- def print_inventory
108
- class_map = AppMap.class_map(@event_methods)
109
- save 'Inventory', class_map, labels: %w[inventory]
114
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
110
115
  end
111
116
 
112
117
  def enabled?
@@ -115,9 +120,6 @@ module AppMap
115
120
 
116
121
  def run
117
122
  init
118
- at_exit do
119
- print_inventory
120
- end
121
123
  end
122
124
  end
123
125
  end
@@ -135,7 +137,7 @@ if AppMap::Minitest.enabled?
135
137
  begin
136
138
  run_without_hook
137
139
  ensure
138
- AppMap::Minitest.end_test self
140
+ AppMap::Minitest.end_test self, exception: $!
139
141
  end
140
142
  end
141
143
  end