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.
- checksums.yaml +4 -4
- data/.releaserc.yml +11 -0
- data/.travis.yml +23 -2
- data/CHANGELOG.md +42 -0
- data/README.md +65 -6
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -3
- data/lib/appmap.rb +4 -7
- data/lib/appmap/class_map.rb +7 -10
- data/lib/appmap/command/record.rb +1 -1
- data/lib/appmap/config.rb +173 -67
- data/lib/appmap/cucumber.rb +1 -1
- data/lib/appmap/event.rb +18 -0
- data/lib/appmap/handler/function.rb +19 -0
- data/lib/appmap/handler/net_http.rb +107 -0
- data/lib/appmap/hook.rb +112 -56
- data/lib/appmap/hook/method.rb +5 -7
- data/lib/appmap/middleware/remote_recording.rb +1 -1
- data/lib/appmap/minitest.rb +22 -20
- data/lib/appmap/rails/request_handler.rb +30 -17
- data/lib/appmap/record.rb +1 -1
- data/lib/appmap/rspec.rb +23 -21
- data/lib/appmap/trace.rb +2 -1
- data/lib/appmap/util.rb +47 -2
- data/lib/appmap/version.rb +2 -2
- data/release.sh +17 -0
- data/spec/abstract_controller_base_spec.rb +77 -30
- data/spec/class_map_spec.rb +3 -11
- data/spec/config_spec.rb +33 -1
- data/spec/fixtures/hook/custom_instance_method.rb +11 -0
- data/spec/fixtures/hook/method_named_call.rb +11 -0
- data/spec/fixtures/rails5_users_app/Gemfile +7 -3
- data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
- data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/create_app +8 -2
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
- data/spec/fixtures/rails6_users_app/Gemfile +5 -4
- data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
- data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/create_app +8 -2
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
- data/spec/hook_spec.rb +141 -20
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- data/test/expectations/openssl_test_key_sign1.json +2 -4
- data/test/gem_test.rb +1 -1
- data/test/rspec_test.rb +0 -13
- metadata +17 -12
- data/exe/appmap +0 -154
- data/test/cli_test.rb +0 -116
data/lib/appmap/cucumber.rb
CHANGED
@@ -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
|
-
|
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
|
45
|
+
def enable(&block)
|
41
46
|
require 'appmap/hook/method'
|
42
47
|
|
43
48
|
hook_builtins
|
44
49
|
|
45
|
-
|
46
|
-
|
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
|
-
|
53
|
+
@trace_begin.enable(&block)
|
54
|
+
end
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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 =
|
74
|
+
hook_method = lambda do |entry|
|
75
|
+
cls, method = entry
|
76
|
+
return false if config.never_hook?(cls, method)
|
73
77
|
|
74
|
-
|
75
|
-
|
76
|
-
next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
|
78
|
+
Hook::Method.new(hook.package, cls, method).activate
|
79
|
+
end
|
77
80
|
|
78
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
117
|
+
def trace_location(trace_point)
|
118
|
+
[ trace_point.path, trace_point.lineno ].join(':')
|
87
119
|
end
|
88
120
|
|
89
|
-
|
90
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
method =
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
149
|
+
warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
|
114
150
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
data/lib/appmap/hook/method.rb
CHANGED
@@ -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
|
-
|
91
|
-
|
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
|
70
|
+
classMap: AppMap.class_map(tracer.event_methods),
|
71
71
|
metadata: metadata,
|
72
72
|
events: @events
|
73
73
|
|
data/lib/appmap/minitest.rb
CHANGED
@@ -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
|
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(
|
84
|
+
def save(name:, class_map:, source_location:, test_status:, exception:, events:)
|
82
85
|
metadata = AppMap::Minitest.metadata.tap do |m|
|
83
|
-
m[: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(
|
112
|
+
fname = AppMap::Util.scenario_filename(name)
|
103
113
|
|
104
|
-
|
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
|