appmap 0.42.0 → 0.45.1

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