appmap 0.41.1 → 0.44.0

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