appmap 0.41.1 → 0.44.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +17 -2
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +54 -22
  5. data/appmap.gemspec +0 -2
  6. data/lib/appmap.rb +3 -2
  7. data/lib/appmap/class_map.rb +7 -10
  8. data/lib/appmap/config.rb +94 -34
  9. data/lib/appmap/cucumber.rb +1 -1
  10. data/lib/appmap/event.rb +18 -0
  11. data/lib/appmap/hook.rb +42 -22
  12. data/lib/appmap/hook/method.rb +1 -1
  13. data/lib/appmap/minitest.rb +35 -30
  14. data/lib/appmap/rails/request_handler.rb +41 -10
  15. data/lib/appmap/record.rb +1 -1
  16. data/lib/appmap/rspec.rb +32 -96
  17. data/lib/appmap/util.rb +16 -0
  18. data/lib/appmap/version.rb +1 -1
  19. data/patch +1447 -0
  20. data/spec/abstract_controller_base_spec.rb +69 -26
  21. data/spec/class_map_spec.rb +3 -11
  22. data/spec/config_spec.rb +31 -1
  23. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  24. data/spec/fixtures/hook/method_named_call.rb +11 -0
  25. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  26. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  27. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  28. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  29. data/spec/fixtures/rails5_users_app/create_app +8 -2
  30. data/spec/fixtures/rails5_users_app/docker-compose.yml +3 -0
  31. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  32. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  33. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  34. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  35. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  36. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  37. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  38. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  39. data/spec/fixtures/rails6_users_app/create_app +8 -2
  40. data/spec/fixtures/rails6_users_app/docker-compose.yml +3 -0
  41. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  42. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  43. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  44. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  45. data/spec/hook_spec.rb +134 -10
  46. data/spec/record_sql_rails_pg_spec.rb +1 -1
  47. data/spec/spec_helper.rb +6 -0
  48. data/test/expectations/openssl_test_key_sign1.json +2 -4
  49. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  50. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  51. data/test/gem_test.rb +1 -1
  52. data/test/minitest_test.rb +1 -2
  53. data/test/rspec_test.rb +1 -20
  54. metadata +7 -8
  55. data/exe/appmap +0 -154
  56. data/spec/rspec_feature_metadata_spec.rb +0 -31
  57. 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
data/lib/appmap/hook.rb CHANGED
@@ -32,6 +32,7 @@ module AppMap
32
32
  end
33
33
 
34
34
  attr_reader :config
35
+
35
36
  def initialize(config)
36
37
  @config = config
37
38
  end
@@ -46,10 +47,23 @@ module AppMap
46
47
  cls = trace_point.self
47
48
 
48
49
  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
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
51
+ class_methods = begin
52
+ if cls.respond_to?(:singleton_class)
53
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
54
+ else
55
+ []
56
+ end
57
+ rescue NameError
58
+ []
59
+ end
50
60
 
51
61
  hook = lambda do |hook_cls|
52
62
  lambda do |method_id|
63
+ # Don't try and trace the AppMap methods or there will be
64
+ # a stack overflow in the defined hook method.
65
+ return if (hook_cls&.name || '').split('::')[0] == AppMap.name
66
+
53
67
  method = begin
54
68
  hook_cls.public_instance_method(method_id)
55
69
  rescue NameError
@@ -69,18 +83,22 @@ module AppMap
69
83
  config.always_hook?(hook_cls, method.name) ||
70
84
  config.included_by_location?(method)
71
85
 
72
- hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
86
+ package = config.package_for_method(method)
73
87
 
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)
88
+ hook_method = Hook::Method.new(package, hook_cls, method)
77
89
 
78
90
  hook_method.activate
79
91
  end
80
92
  end
81
93
 
82
94
  instance_methods.each(&hook.(cls))
83
- class_methods.each(&hook.(cls.singleton_class))
95
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
96
+ begin
97
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
98
+ rescue NameError
99
+ # NameError:
100
+ # uninitialized constant Faraday::Connection
101
+ end
84
102
  end
85
103
 
86
104
  tp.enable(&block)
@@ -97,25 +115,27 @@ module AppMap
97
115
  end
98
116
  end
99
117
 
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
118
+ config.builtin_methods.each do |class_name, hooks|
119
+ Array(hooks).each do |hook|
120
+ require hook.package.package_name if hook.package.package_name
121
+ Array(hook.method_names).each do |method_name|
122
+ method_name = method_name.to_sym
104
123
 
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
124
+ cls = class_from_string.(class_name)
125
+ method = \
126
+ begin
127
+ cls.instance_method(method_name)
128
+ rescue NameError
129
+ cls.method(method_name) rescue nil
130
+ end
112
131
 
113
- next if config.never_hook?(method)
132
+ next if config.never_hook?(method)
114
133
 
115
- if method
116
- Hook::Method.new(hook.package, cls, method).activate
117
- else
118
- warn "Method #{method_name} not found on #{cls.name}"
134
+ if method
135
+ Hook::Method.new(hook.package, cls, method).activate
136
+ else
137
+ warn "Method #{method_name} not found on #{cls.name}"
138
+ end
119
139
  end
120
140
  end
121
141
  end
@@ -35,7 +35,7 @@ module AppMap
35
35
  else
36
36
  "#{hook_method.name} (class resolution deferred)"
37
37
  end
38
- warn "AppMap: Hooking " + msg
38
+ warn "AppMap: Hooking #{msg} at line #{(hook_method.source_location || []).join(':')}"
39
39
  end
40
40
 
41
41
  defined_class = @defined_class
@@ -7,22 +7,28 @@ module AppMap
7
7
  # be activated around each test.
8
8
  module Minitest
9
9
  APPMAP_OUTPUT_DIR = 'tmp/appmap/minitest'
10
- LOG = false
10
+ LOG = ( ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true' )
11
11
 
12
12
  def self.metadata
13
13
  AppMap.detect_metadata
14
14
  end
15
15
 
16
- Recording = Struct.new(:test) do
17
- def initialize(test)
16
+ Recording = Struct.new(:test, :test_name) do
17
+ def initialize(test, test_name)
18
18
  super
19
19
 
20
- warn "Starting recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
20
+ warn "Starting recording of test #{test.class}.#{test.name}@#{source_location}" if AppMap::Minitest::LOG
21
21
  @trace = AppMap.tracing.trace
22
22
  end
23
23
 
24
- def finish
24
+ def source_location
25
+ test.method(test_name).source_location.join(':')
26
+ end
27
+
28
+
29
+ def finish(exception)
25
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
31
+ warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
26
32
 
27
33
  events = []
28
34
  AppMap.tracing.delete @trace
@@ -37,11 +43,12 @@ module AppMap
37
43
  feature_name = test.name.split('_')[1..-1].join(' ')
38
44
  scenario_name = [ feature_group, feature_name ].join(' ')
39
45
 
40
- AppMap::Minitest.save scenario_name,
41
- class_map,
42
- events: events,
43
- feature_name: feature_name,
44
- feature_group_name: feature_group
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,
51
+ events: events
45
52
  end
46
53
  end
47
54
 
@@ -55,15 +62,15 @@ module AppMap
55
62
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
56
63
  end
57
64
 
58
- def begin_test(test)
59
- @recordings_by_test[test.object_id] = Recording.new(test)
65
+ def begin_test(test, name)
66
+ @recordings_by_test[test.object_id] = Recording.new(test, name)
60
67
  end
61
68
 
62
- def end_test(test)
69
+ def end_test(test, exception:)
63
70
  recording = @recordings_by_test.delete(test.object_id)
64
71
  return warn "No recording found for #{test}" unless recording
65
72
 
66
- recording.finish
73
+ recording.finish exception
67
74
  end
68
75
 
69
76
  def config
@@ -74,12 +81,11 @@ module AppMap
74
81
  @event_methods += event_methods
75
82
  end
76
83
 
77
- def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
84
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
78
85
  metadata = AppMap::Minitest.metadata.tap do |m|
79
- m[:name] = example_name
86
+ m[:name] = name
87
+ m[:source_location] = source_location
80
88
  m[:app] = AppMap.configuration.name
81
- m[:feature] = feature_name if feature_name
82
- m[:feature_group] = feature_group_name if feature_group_name
83
89
  m[:frameworks] ||= []
84
90
  m[:frameworks] << {
85
91
  name: 'minitest',
@@ -88,6 +94,13 @@ module AppMap
88
94
  m[:recorder] = {
89
95
  name: 'minitest'
90
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
91
104
  end
92
105
 
93
106
  appmap = {
@@ -96,14 +109,9 @@ module AppMap
96
109
  classMap: class_map,
97
110
  events: events
98
111
  }.compact
99
- fname = AppMap::Util.scenario_filename(example_name)
112
+ fname = AppMap::Util.scenario_filename(name)
100
113
 
101
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
102
- end
103
-
104
- def print_inventory
105
- class_map = AppMap.class_map(@event_methods)
106
- save 'Inventory', class_map, labels: %w[inventory]
114
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
107
115
  end
108
116
 
109
117
  def enabled?
@@ -112,9 +120,6 @@ module AppMap
112
120
 
113
121
  def run
114
122
  init
115
- at_exit do
116
- print_inventory
117
- end
118
123
  end
119
124
  end
120
125
  end
@@ -128,11 +133,11 @@ if AppMap::Minitest.enabled?
128
133
  alias run_without_hook run
129
134
 
130
135
  def run
131
- AppMap::Minitest.begin_test self
136
+ AppMap::Minitest.begin_test self, name
132
137
  begin
133
138
  run_without_hook
134
139
  ensure
135
- AppMap::Minitest.end_test self
140
+ AppMap::Minitest.end_test self, exception: $!
136
141
  end
137
142
  end
138
143
  end
@@ -6,15 +6,38 @@ require 'appmap/hook'
6
6
  module AppMap
7
7
  module Rails
8
8
  module RequestHandler
9
+ # Host and User-Agent will just introduce needless variation.
10
+ # Content-Type and Authorization get their own fields in the request.
11
+ IGNORE_HEADERS = %w[host user_agent content_type authorization].map(&:upcase).map {|h| "HTTP_#{h}"}.freeze
12
+
13
+ class << self
14
+ def selected_headers(env)
15
+ # Rack prepends HTTP_ to all client-sent headers.
16
+ matching_headers = env
17
+ .select { |k,v| k.start_with? 'HTTP_'}
18
+ .reject { |k,v| IGNORE_HEADERS.member?(k) }
19
+ .reject { |k,v| v.blank? }
20
+ .each_with_object({}) do |kv, memo|
21
+ key = kv[0].sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
22
+ value = kv[1]
23
+ memo[key] = value
24
+ end
25
+ matching_headers.blank? ? nil : matching_headers
26
+ end
27
+ end
28
+
9
29
  class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :normalized_path_info, :request_method, :path_info, :params
30
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
11
31
 
12
32
  def initialize(request)
13
33
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
34
 
15
- @request_method = request.request_method
16
- @normalized_path_info = normalized_path request
17
- @path_info = request.path_info.split('?')[0]
35
+ self.request_method = request.request_method
36
+ self.normalized_path_info = normalized_path(request)
37
+ self.mime_type = request.headers['Content-Type']
38
+ self.headers = RequestHandler.selected_headers(request.env)
39
+ self.authorization = request.headers['Authorization']
40
+ self.path_info = request.path_info.split('?')[0]
18
41
  # ActionDispatch::Http::ParameterFilter is deprecated
19
42
  parameter_filter_cls = \
20
43
  if defined?(ActiveSupport::ParameterFilter)
@@ -22,7 +45,7 @@ module AppMap
22
45
  else
23
46
  ActionDispatch::Http::ParameterFilter
24
47
  end
25
- @params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
48
+ self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
26
49
  end
27
50
 
28
51
  def to_h
@@ -30,7 +53,10 @@ module AppMap
30
53
  h[:http_server_request] = {
31
54
  request_method: request_method,
32
55
  path_info: path_info,
33
- normalized_path_info: normalized_path_info
56
+ mime_type: mime_type,
57
+ normalized_path_info: normalized_path_info,
58
+ authorization: authorization,
59
+ headers: headers,
34
60
  }.compact
35
61
 
36
62
  h[:message] = params.keys.map do |key|
@@ -39,8 +65,11 @@ module AppMap
39
65
  name: key,
40
66
  class: val.class.name,
41
67
  value: self.class.display_string(val),
42
- object_id: val.__id__
43
- }
68
+ object_id: val.__id__,
69
+ }.tap do |message|
70
+ properties = object_properties(val)
71
+ message[:properties] = properties if properties
72
+ end
44
73
  end
45
74
  end
46
75
  end
@@ -59,7 +88,7 @@ module AppMap
59
88
  end
60
89
 
61
90
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
62
- attr_accessor :status, :mime_type
91
+ attr_accessor :status, :mime_type, :headers
63
92
 
64
93
  def initialize(response, parent_id, elapsed)
65
94
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
@@ -68,13 +97,15 @@ module AppMap
68
97
  self.mime_type = response.headers['Content-Type']
69
98
  self.parent_id = parent_id
70
99
  self.elapsed = elapsed
100
+ self.headers = RequestHandler.selected_headers(response.headers)
71
101
  end
72
102
 
73
103
  def to_h
74
104
  super.tap do |h|
75
105
  h[:http_server_response] = {
76
106
  status: status,
77
- mime_type: mime_type
107
+ mime_type: mime_type,
108
+ headers: headers
78
109
  }.compact
79
110
  end
80
111
  end
data/lib/appmap/record.rb CHANGED
@@ -23,5 +23,5 @@ at_exit do
23
23
  'classMap' => AppMap.class_map(tracer.event_methods),
24
24
  'events' => events
25
25
  }
26
- File.write 'appmap.json', JSON.generate(appmap)
26
+ AppMap::Util.write_appmap('appmap.json', JSON.generate(appmap))
27
27
  end
data/lib/appmap/rspec.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/util'
4
+ require 'set'
4
5
 
5
6
  module AppMap
6
7
  # Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will
@@ -13,58 +14,9 @@ module AppMap
13
14
  AppMap.detect_metadata
14
15
  end
15
16
 
16
- module FeatureAnnotations
17
- def feature
18
- return nil unless annotations
19
-
20
- annotations[:feature]
21
- end
22
-
23
- def labels
24
- labels = metadata[:appmap]
25
- if labels.is_a?(Array)
26
- labels
27
- elsif labels.is_a?(String) || labels.is_a?(Symbol)
28
- [ labels ]
29
- else
30
- []
31
- end
32
- end
33
-
34
- def feature_group
35
- return nil unless annotations
36
-
37
- annotations[:feature_group]
38
- end
39
-
40
- def annotations
41
- metadata.tap do |md|
42
- description_args_hashes.each do |h|
43
- md.merge! h
44
- end
45
- end
46
- end
47
-
48
- protected
49
-
50
- def metadata
51
- return {} unless example_obj.respond_to?(:metadata)
52
-
53
- example_obj.metadata
54
- end
55
-
56
- def description_args_hashes
57
- return [] unless example_obj.respond_to?(:metadata)
58
-
59
- (example_obj.metadata[:description_args] || []).select { |arg| arg.is_a?(Hash) }
60
- end
61
- end
62
-
63
17
  # ScopeExample and ScopeExampleGroup is a way to handle the weird way that RSpec
64
18
  # stores the nested example names.
65
19
  ScopeExample = Struct.new(:example) do
66
- include FeatureAnnotations
67
-
68
20
  alias_method :example_obj, :example
69
21
 
70
22
  def description?
@@ -83,8 +35,6 @@ module AppMap
83
35
  # As you can see here, the way that RSpec stores the example description and
84
36
  # represents the example group hierarchy is pretty weird.
85
37
  ScopeExampleGroup = Struct.new(:example_group) do
86
- include FeatureAnnotations
87
-
88
38
  alias_method :example_obj, :example_group
89
39
 
90
40
  def description_args
@@ -133,13 +83,20 @@ module AppMap
133
83
  page.driver.options[:http_client].instance_variable_get('@server_url').port
134
84
  end
135
85
 
136
- warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
86
+ warn "Starting recording of example #{example}@#{source_location}" if AppMap::RSpec::LOG
137
87
  @trace = AppMap.tracing.trace
138
88
  @webdriver_port = webdriver_port.()
139
89
  end
140
90
 
141
- def finish
91
+ def source_location
92
+ result = example.location_rerun_argument.split(':')[0]
93
+ result = result[2..-1] if result.index('./') == 0
94
+ result
95
+ end
96
+
97
+ def finish(exception)
142
98
  warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
99
+ warn "Exception: #{exception}" if exception && AppMap::RSpec::LOG
143
100
 
144
101
  events = []
145
102
  AppMap.tracing.delete @trace
@@ -148,22 +105,16 @@ module AppMap
148
105
 
149
106
  AppMap::RSpec.add_event_methods @trace.event_methods
150
107
 
151
- class_map = AppMap.class_map(@trace.event_methods, include_source: false)
108
+ class_map = AppMap.class_map(@trace.event_methods)
152
109
 
153
110
  description = []
154
111
  scope = ScopeExample.new(example)
155
- feature_group = feature = nil
156
112
 
157
- labels = []
158
113
  while scope
159
- labels += scope.labels
160
114
  description << scope.description
161
- feature ||= scope.feature
162
- feature_group ||= scope.feature_group
163
115
  scope = scope.parent
164
116
  end
165
117
 
166
- labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
167
118
  description.reject!(&:nil?).reject!(&:blank?)
168
119
  default_description = description.last
169
120
  description.reverse!
@@ -177,24 +128,12 @@ module AppMap
177
128
 
178
129
  full_description = normalize.call(description.join(' '))
179
130
 
180
- compute_feature_name = lambda do
181
- return 'unknown' if description.empty?
182
-
183
- feature_description = description.dup
184
- num_tokens = [2, feature_description.length - 1].min
185
- feature_description[0...num_tokens].map(&:strip).join(' ')
186
- end
187
-
188
- feature_group ||= normalize.call(default_description).underscore.gsub('/', '_').humanize
189
- feature_name = feature || compute_feature_name.call if feature_group
190
- feature_name = normalize.call(feature_name) if feature_name
191
-
192
- AppMap::RSpec.save full_description,
193
- class_map,
194
- events: events,
195
- feature_name: feature_name,
196
- feature_group_name: feature_group,
197
- labels: labels.blank? ? nil : labels
131
+ AppMap::RSpec.save name: full_description,
132
+ class_map: class_map,
133
+ source_location: source_location,
134
+ test_status: exception ? 'failed' : 'succeeded',
135
+ exception: exception,
136
+ events: events
198
137
  end
199
138
  end
200
139
 
@@ -212,11 +151,11 @@ module AppMap
212
151
  @recordings_by_example[example.object_id] = Recording.new(example)
213
152
  end
214
153
 
215
- def end_spec(example)
154
+ def end_spec(example, exception:)
216
155
  recording = @recordings_by_example.delete(example.object_id)
217
156
  return warn "No recording found for #{example}" unless recording
218
157
 
219
- recording.finish
158
+ recording.finish exception
220
159
  end
221
160
 
222
161
  def config
@@ -227,13 +166,11 @@ module AppMap
227
166
  @event_methods += event_methods
228
167
  end
229
168
 
230
- def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
169
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
231
170
  metadata = AppMap::RSpec.metadata.tap do |m|
232
- m[:name] = example_name
171
+ m[:name] = name
172
+ m[:source_location] = source_location
233
173
  m[:app] = AppMap.configuration.name
234
- m[:feature] = feature_name if feature_name
235
- m[:feature_group] = feature_group_name if feature_group_name
236
- m[:labels] = labels if labels
237
174
  m[:frameworks] ||= []
238
175
  m[:frameworks] << {
239
176
  name: 'rspec',
@@ -242,6 +179,13 @@ module AppMap
242
179
  m[:recorder] = {
243
180
  name: 'rspec'
244
181
  }
182
+ m[:test_status] = test_status
183
+ if exception
184
+ m[:exception] = {
185
+ class: exception.class.name,
186
+ message: exception.to_s
187
+ }
188
+ end
245
189
  end
246
190
 
247
191
  appmap = {
@@ -250,14 +194,9 @@ module AppMap
250
194
  classMap: class_map,
251
195
  events: events
252
196
  }.compact
253
- fname = AppMap::Util.scenario_filename(example_name)
254
-
255
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
256
- end
197
+ fname = AppMap::Util.scenario_filename(name)
257
198
 
258
- def print_inventory
259
- class_map = AppMap.class_map(@event_methods)
260
- save 'Inventory', class_map, labels: %w[inventory]
199
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
261
200
  end
262
201
 
263
202
  def enabled?
@@ -266,9 +205,6 @@ module AppMap
266
205
 
267
206
  def run
268
207
  init
269
- at_exit do
270
- print_inventory
271
- end
272
208
  end
273
209
  end
274
210
  end
@@ -290,7 +226,7 @@ if AppMap::RSpec.enabled?
290
226
  begin
291
227
  instance_exec(&fn)
292
228
  ensure
293
- AppMap::RSpec.end_spec example
229
+ AppMap::RSpec.end_spec example, exception: $!
294
230
  end
295
231
  end
296
232
  end