appmap 0.102.2 → 0.103.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/.standard.yml +1 -0
- data/.travis.yml +2 -2
- data/CHANGELOG.md +17 -0
- data/appmap.gemspec +35 -36
- data/lib/appmap/agent.rb +28 -24
- data/lib/appmap/class_map.rb +36 -31
- data/lib/appmap/config.rb +132 -110
- data/lib/appmap/cucumber.rb +25 -25
- data/lib/appmap/detect_enabled.rb +30 -28
- data/lib/appmap/gem_hooks/activejob.yml +0 -1
- data/lib/appmap/hook/method/ruby2.rb +19 -8
- data/lib/appmap/hook/method/ruby3.rb +20 -13
- data/lib/appmap/hook/method.rb +27 -27
- data/lib/appmap/hook/record_around.rb +77 -0
- data/lib/appmap/hook.rb +74 -44
- data/lib/appmap/metadata.rb +6 -18
- data/lib/appmap/middleware/remote_recording.rb +26 -27
- data/lib/appmap/minitest.rb +27 -27
- data/lib/appmap/rspec.rb +37 -37
- data/lib/appmap/trace.rb +14 -12
- data/lib/appmap/util.rb +71 -61
- data/lib/appmap/version.rb +1 -1
- metadata +5 -16
data/lib/appmap/hook.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require_relative
|
5
|
-
|
3
|
+
require "English"
|
4
|
+
require_relative "hook_log"
|
5
|
+
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
8
|
+
# rubocop:disable Metrics/BlockLength
|
9
|
+
# rubocop:disable Metrics/AbcSize
|
10
|
+
# rubocop:disable Complexity/CyclomaticComplexity
|
11
|
+
# rubocop:disable Complexity/PerceivedComplexity
|
6
12
|
module AppMap
|
7
13
|
class Hook
|
8
14
|
OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup
|
9
|
-
|
15
|
+
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
16
|
OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr
|
11
|
-
|
17
|
+
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
|
12
18
|
SLOW_PACKAGE_THRESHOLD = 0.001
|
13
19
|
|
14
20
|
@unbound_method_arity = ::UnboundMethod.instance_method(:arity)
|
@@ -39,9 +45,9 @@ module AppMap
|
|
39
45
|
def qualify_method_name(method)
|
40
46
|
if method.owner.singleton_class?
|
41
47
|
class_name = singleton_method_owner_name(method)
|
42
|
-
[class_name,
|
48
|
+
[class_name, ".", method.name]
|
43
49
|
else
|
44
|
-
[method.owner.name,
|
50
|
+
[method.owner.name, "#", method.name]
|
45
51
|
end
|
46
52
|
end
|
47
53
|
end
|
@@ -55,7 +61,7 @@ module AppMap
|
|
55
61
|
|
56
62
|
# Observe class loading and hook all methods which match the config.
|
57
63
|
def enable(&block)
|
58
|
-
require
|
64
|
+
require "appmap/hook/method"
|
59
65
|
|
60
66
|
hook_builtins
|
61
67
|
|
@@ -66,7 +72,7 @@ module AppMap
|
|
66
72
|
@module_load_times = Hash.new { |memo, k| memo[k] = 0 }
|
67
73
|
@slow_packages = Set.new
|
68
74
|
|
69
|
-
if ENV[
|
75
|
+
if ENV["APPMAP_PROFILE_HOOK"] == "true"
|
70
76
|
dump_times = lambda do
|
71
77
|
@module_load_times
|
72
78
|
.keys
|
@@ -80,7 +86,7 @@ module AppMap
|
|
80
86
|
end
|
81
87
|
end
|
82
88
|
|
83
|
-
at_exit
|
89
|
+
at_exit(&dump_times)
|
84
90
|
Thread.new do
|
85
91
|
while true
|
86
92
|
dump_times.call
|
@@ -102,7 +108,7 @@ module AppMap
|
|
102
108
|
hooks_by_class.each do |class_name, hooks|
|
103
109
|
Array(hooks).each do |hook|
|
104
110
|
HookLog.builtin class_name do
|
105
|
-
if builtin && hook.package.require_name && hook.package.require_name !=
|
111
|
+
if builtin && hook.package.require_name && hook.package.require_name != "ruby"
|
106
112
|
begin
|
107
113
|
require hook.package.require_name
|
108
114
|
rescue
|
@@ -110,39 +116,59 @@ module AppMap
|
|
110
116
|
next
|
111
117
|
end
|
112
118
|
end
|
113
|
-
|
119
|
+
|
114
120
|
begin
|
115
121
|
base_cls = Object.const_get class_name
|
116
122
|
rescue NameError
|
117
123
|
HookLog.load_error class_name, "Class #{class_name} not found in global scope" if HookLog.enabled?
|
118
124
|
next
|
119
125
|
end
|
120
|
-
|
126
|
+
|
121
127
|
Array(hook.method_names).each do |method_name|
|
122
128
|
method_name = method_name.to_sym
|
123
|
-
|
129
|
+
|
124
130
|
hook_method = lambda do |entry|
|
125
131
|
cls, method = entry
|
126
132
|
next if config.never_hook?(cls, method)
|
127
|
-
|
133
|
+
|
128
134
|
hook.package.handler_class.new(hook.package, cls, method).activate
|
129
135
|
end
|
130
|
-
|
136
|
+
|
131
137
|
methods = []
|
132
138
|
# irb(main):001:0> Kernel.public_instance_method(:system)
|
133
139
|
# (irb):1:in `public_instance_method': method `system' for module `Kernel' is private (NameError)
|
134
140
|
if base_cls == Kernel
|
135
|
-
|
141
|
+
begin
|
142
|
+
methods << [base_cls, base_cls.instance_method(method_name)]
|
143
|
+
rescue
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
begin
|
148
|
+
methods << [base_cls, base_cls.public_instance_method(method_name)]
|
149
|
+
rescue
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
begin
|
153
|
+
methods << [base_cls, base_cls.protected_instance_method(method_name)]
|
154
|
+
rescue
|
155
|
+
nil
|
136
156
|
end
|
137
|
-
methods << [base_cls, base_cls.public_instance_method(method_name)] rescue nil
|
138
|
-
methods << [base_cls, base_cls.protected_instance_method(method_name)] rescue nil
|
139
157
|
if base_cls.respond_to?(:singleton_class)
|
140
|
-
|
141
|
-
|
158
|
+
begin
|
159
|
+
methods << [base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name)]
|
160
|
+
rescue
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
begin
|
164
|
+
methods << [base_cls.singleton_class, base_cls.singleton_class.protected_instance_method(method_name)]
|
165
|
+
rescue
|
166
|
+
nil
|
167
|
+
end
|
142
168
|
end
|
143
169
|
methods.compact!
|
144
170
|
if methods.empty?
|
145
|
-
HookLog.load_error [
|
171
|
+
HookLog.load_error [base_cls.name, method_name].join("[#.]"), "Method #{method_name} not found on #{base_cls.name}" if HookLog.enabled?
|
146
172
|
else
|
147
173
|
methods.each(&hook_method)
|
148
174
|
end
|
@@ -152,13 +178,13 @@ module AppMap
|
|
152
178
|
end
|
153
179
|
end
|
154
180
|
|
155
|
-
hook_loaded_code.(config.builtin_hooks, true)
|
181
|
+
hook_loaded_code.call(config.builtin_hooks, true)
|
156
182
|
end
|
157
183
|
|
158
184
|
protected
|
159
185
|
|
160
186
|
def trace_location(trace_point)
|
161
|
-
[trace_point.path, trace_point.lineno].join(
|
187
|
+
[trace_point.path, trace_point.lineno].join(":")
|
162
188
|
end
|
163
189
|
|
164
190
|
def trace_end(trace_point)
|
@@ -168,7 +194,7 @@ module AppMap
|
|
168
194
|
path = trace_point.path
|
169
195
|
enabled = !@notrace_paths.member?(path) && config.path_enabled?(path)
|
170
196
|
unless enabled
|
171
|
-
HookLog.log
|
197
|
+
HookLog.log "Not hooking - path is not enabled" if HookLog.enabled?
|
172
198
|
@notrace_paths << path
|
173
199
|
next
|
174
200
|
end
|
@@ -178,31 +204,31 @@ module AppMap
|
|
178
204
|
instance_methods = cls.public_instance_methods(false) + cls.protected_instance_methods(false) - OBJECT_INSTANCE_METHODS
|
179
205
|
# NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
|
180
206
|
class_methods = begin
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
[]
|
185
|
-
end
|
186
|
-
rescue NameError
|
207
|
+
if cls.respond_to?(:singleton_class)
|
208
|
+
cls.singleton_class.public_instance_methods(false) + cls.singleton_class.protected_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
|
209
|
+
else
|
187
210
|
[]
|
188
211
|
end
|
212
|
+
rescue NameError
|
213
|
+
[]
|
214
|
+
end
|
189
215
|
|
190
216
|
hook = lambda do |hook_cls|
|
191
217
|
lambda do |method_id|
|
192
218
|
method = begin
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
219
|
+
hook_cls.instance_method(method_id)
|
220
|
+
rescue NameError
|
221
|
+
HookLog.load_error [hook_cls, method_id].join("[#.]"), "Method #{hook_cls} #{method_id} is not accessible: #{$!}" if HookLog.enabled?
|
222
|
+
next
|
223
|
+
end
|
198
224
|
|
199
|
-
|
225
|
+
hook_config = config.lookup_hook_config(hook_cls, method)
|
200
226
|
# doing this check first returned early in 98.7% of cases in sample_app_6th_ed
|
201
|
-
next unless
|
227
|
+
next unless hook_config
|
202
228
|
|
203
229
|
# Don't try and trace the AppMap methods or there will be
|
204
230
|
# a stack overflow in the defined hook method.
|
205
|
-
next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name ||
|
231
|
+
next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || "").split("::")[0])
|
206
232
|
|
207
233
|
next if method_id == :call
|
208
234
|
|
@@ -215,15 +241,19 @@ module AppMap
|
|
215
241
|
# TODO: Figure out how to tell the difference?
|
216
242
|
next unless disasm
|
217
243
|
|
218
|
-
|
244
|
+
record_around = Config::RECORD_AROUND_LABELS.find { |label| hook_config.labels.include?(label) }
|
245
|
+
|
246
|
+
HookLog.log "Detected labels: #{hook_config.labels.join(", ")} (record_around: #{record_around})" if !hook_config.labels.empty? && HookLog.enabled?
|
247
|
+
|
248
|
+
hook_config.package.handler_class.new(hook_config.package, hook_cls, method, record_around: record_around).activate
|
219
249
|
end
|
220
250
|
end
|
221
251
|
|
222
252
|
start = Time.now
|
223
|
-
instance_methods.each(&hook.(cls))
|
253
|
+
instance_methods.each(&hook.call(cls))
|
224
254
|
begin
|
225
255
|
# NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
|
226
|
-
class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
|
256
|
+
class_methods.each(&hook.call(cls.singleton_class)) if cls.respond_to?(:singleton_class)
|
227
257
|
rescue NameError
|
228
258
|
# NameError:
|
229
259
|
# uninitialized constant Faraday::Connection
|
@@ -231,12 +261,12 @@ module AppMap
|
|
231
261
|
end
|
232
262
|
elapsed = Time.now - start
|
233
263
|
if location.index(Bundler.bundle_path.to_s) == 0
|
234
|
-
package_tokens = location[Bundler.bundle_path.to_s.length + 1..-1].split(
|
264
|
+
package_tokens = location[Bundler.bundle_path.to_s.length + 1..-1].split("/")
|
235
265
|
@module_load_times[package_tokens[1]] += elapsed
|
236
266
|
else
|
237
267
|
file_path = location[Dir.pwd.length + 1..-1]
|
238
268
|
if file_path
|
239
|
-
location = file_path.split(
|
269
|
+
location = file_path.split("/", 3)[0..1].join("/")
|
240
270
|
@module_load_times[location] += elapsed
|
241
271
|
end
|
242
272
|
end
|
data/lib/appmap/metadata.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "appmap/util"
|
4
4
|
|
5
5
|
module AppMap
|
6
6
|
module Metadata
|
@@ -9,12 +9,12 @@ module AppMap
|
|
9
9
|
{
|
10
10
|
app: AppMap.configuration.name,
|
11
11
|
language: {
|
12
|
-
name:
|
12
|
+
name: "ruby",
|
13
13
|
engine: RUBY_ENGINE,
|
14
14
|
version: RUBY_VERSION
|
15
15
|
},
|
16
16
|
client: {
|
17
|
-
name:
|
17
|
+
name: "appmap",
|
18
18
|
url: AppMap::URL,
|
19
19
|
version: AppMap::VERSION
|
20
20
|
}
|
@@ -22,7 +22,7 @@ module AppMap
|
|
22
22
|
if defined?(::Rails) && defined?(::Rails.version)
|
23
23
|
m[:frameworks] ||= []
|
24
24
|
m[:frameworks] << {
|
25
|
-
name:
|
25
|
+
name: "rails",
|
26
26
|
version: ::Rails.version
|
27
27
|
}
|
28
28
|
end
|
@@ -33,30 +33,18 @@ module AppMap
|
|
33
33
|
protected
|
34
34
|
|
35
35
|
def git_available
|
36
|
-
@git_available = system(
|
36
|
+
@git_available = system("git status 2>&1 > /dev/null") if @git_available.nil?
|
37
37
|
end
|
38
38
|
|
39
39
|
def git_metadata
|
40
40
|
git_repo = `git config --get remote.origin.url`.strip
|
41
41
|
git_branch = `git rev-parse --abbrev-ref HEAD`.strip
|
42
42
|
git_sha = `git rev-parse HEAD`.strip
|
43
|
-
git_status = `git status -s`.split("\n").map(&:strip)
|
44
|
-
git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
|
45
|
-
git_last_annotated_tag = nil if Util.blank?(git_last_annotated_tag)
|
46
|
-
git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
|
47
|
-
git_last_tag = nil if Util.blank?(git_last_tag)
|
48
|
-
git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
|
49
|
-
git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
|
50
43
|
|
51
44
|
{
|
52
45
|
repository: git_repo,
|
53
46
|
branch: git_branch,
|
54
|
-
commit: git_sha
|
55
|
-
status: git_status,
|
56
|
-
git_last_annotated_tag: git_last_annotated_tag,
|
57
|
-
git_last_tag: git_last_tag,
|
58
|
-
git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
|
59
|
-
git_commits_since_last_tag: git_commits_since_last_tag
|
47
|
+
commit: git_sha
|
60
48
|
}
|
61
49
|
end
|
62
50
|
end
|
@@ -6,7 +6,7 @@ module AppMap
|
|
6
6
|
# It can also be enabled to emit an AppMap for each request.
|
7
7
|
class RemoteRecording
|
8
8
|
def initialize(app)
|
9
|
-
require
|
9
|
+
require "json"
|
10
10
|
|
11
11
|
@app = app
|
12
12
|
end
|
@@ -23,18 +23,18 @@ module AppMap
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def ws_start_recording
|
26
|
-
return [
|
26
|
+
return [409, "Recording is already in progress"] if @tracer
|
27
27
|
|
28
28
|
@events = []
|
29
29
|
@tracer = AppMap.tracing.trace
|
30
30
|
@event_thread = Thread.new { event_loop }
|
31
31
|
@event_thread.abort_on_exception = true
|
32
32
|
|
33
|
-
[
|
33
|
+
[200]
|
34
34
|
end
|
35
35
|
|
36
36
|
def ws_stop_recording(req)
|
37
|
-
return [
|
37
|
+
return [404, "No recording is in progress"] unless @tracer
|
38
38
|
|
39
39
|
tracer = @tracer
|
40
40
|
@tracer = nil
|
@@ -50,7 +50,7 @@ module AppMap
|
|
50
50
|
is_control_command_event = lambda do |event|
|
51
51
|
event[:event] == :call &&
|
52
52
|
event[:http_server_request] &&
|
53
|
-
event[:http_server_request][:path_info] ==
|
53
|
+
event[:http_server_request][:path_info] == "/_appmap/record"
|
54
54
|
end
|
55
55
|
control_command_events = @events.select(&is_control_command_event)
|
56
56
|
|
@@ -63,8 +63,8 @@ module AppMap
|
|
63
63
|
|
64
64
|
metadata = AppMap.detect_metadata
|
65
65
|
metadata[:recorder] = {
|
66
|
-
name:
|
67
|
-
type:
|
66
|
+
name: "remote_recording",
|
67
|
+
type: "remote"
|
68
68
|
}
|
69
69
|
|
70
70
|
response = JSON.generate \
|
@@ -73,7 +73,7 @@ module AppMap
|
|
73
73
|
metadata: metadata,
|
74
74
|
events: @events
|
75
75
|
|
76
|
-
[
|
76
|
+
[200, response]
|
77
77
|
end
|
78
78
|
|
79
79
|
def call(env)
|
@@ -82,7 +82,7 @@ module AppMap
|
|
82
82
|
# 0
|
83
83
|
|
84
84
|
req = Rack::Request.new(env)
|
85
|
-
return handle_record_request(req) if AppMap.recording_enabled?(:remote) && req.path ==
|
85
|
+
return handle_record_request(req) if AppMap.recording_enabled?(:remote) && req.path == "/_appmap/record"
|
86
86
|
|
87
87
|
start_time = Time.now
|
88
88
|
# Support multi-threaded web server such as Puma by recording each thread
|
@@ -100,20 +100,19 @@ module AppMap
|
|
100
100
|
event_fields = events.map(&:keys).flatten.map(&:to_sym).uniq.sort
|
101
101
|
return unless %i[http_server_request http_server_response].all? { |field| event_fields.include?(field) }
|
102
102
|
|
103
|
-
path = req.path.gsub(/\/{2,}/,
|
104
|
-
appmap_name = "#{req.request_method} #{path} (#{status}) - #{start_time.strftime(
|
105
|
-
appmap_file_name = AppMap::Util.scenario_filename([
|
106
|
-
output_dir = File.join(AppMap
|
103
|
+
path = req.path.gsub(/\/{2,}/, "/") # Double slashes have been observed
|
104
|
+
appmap_name = "#{req.request_method} #{path} (#{status}) - #{start_time.strftime("%T.%L")}"
|
105
|
+
appmap_file_name = AppMap::Util.scenario_filename([start_time.to_f, req.url].join("_"))
|
106
|
+
output_dir = File.join(AppMap.output_dir, "requests")
|
107
107
|
appmap_file_path = File.join(output_dir, appmap_file_name)
|
108
108
|
|
109
109
|
metadata = AppMap.detect_metadata
|
110
110
|
metadata[:name] = appmap_name
|
111
|
-
metadata[:timestamp] = start_time.to_f
|
112
111
|
metadata[:recorder] = {
|
113
|
-
name:
|
114
|
-
type:
|
112
|
+
name: "rack",
|
113
|
+
type: "requests"
|
115
114
|
}
|
116
|
-
|
115
|
+
|
117
116
|
appmap = {
|
118
117
|
version: AppMap::APPMAP_FORMAT_VERSION,
|
119
118
|
classMap: AppMap.class_map(tracer.event_methods),
|
@@ -124,36 +123,36 @@ module AppMap
|
|
124
123
|
FileUtils.mkdir_p(output_dir)
|
125
124
|
File.write(appmap_file_path, JSON.generate(appmap))
|
126
125
|
|
127
|
-
headers[
|
128
|
-
headers[
|
126
|
+
headers["AppMap-Name"] = File.expand_path(appmap_name)
|
127
|
+
headers["AppMap-File-Name"] = File.expand_path(appmap_file_path)
|
129
128
|
end
|
130
129
|
|
131
130
|
@app.call(env).tap(&record_request)
|
132
131
|
end
|
133
132
|
|
134
133
|
def recording_state
|
135
|
-
[
|
134
|
+
[200, JSON.generate({enabled: recording?})]
|
136
135
|
end
|
137
136
|
|
138
137
|
def handle_record_request(req)
|
139
|
-
method = req.env[
|
138
|
+
method = req.env["REQUEST_METHOD"]
|
140
139
|
|
141
140
|
status, body = \
|
142
|
-
if method.eql?(
|
141
|
+
if method.eql?("GET")
|
143
142
|
recording_state
|
144
|
-
elsif method.eql?(
|
143
|
+
elsif method.eql?("POST")
|
145
144
|
ws_start_recording
|
146
|
-
elsif method.eql?(
|
145
|
+
elsif method.eql?("DELETE")
|
147
146
|
ws_stop_recording(req)
|
148
147
|
else
|
149
|
-
[
|
148
|
+
[404, ""]
|
150
149
|
end
|
151
150
|
|
152
|
-
[status, {
|
151
|
+
[status, {"Content-Type" => "application/json"}, [body || ""]]
|
153
152
|
end
|
154
153
|
|
155
154
|
def html_response?(headers)
|
156
|
-
headers[
|
155
|
+
headers["Content-Type"] && headers["Content-Type"] =~ /html/
|
157
156
|
end
|
158
157
|
|
159
158
|
def record_all_requests?
|
data/lib/appmap/minitest.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
3
|
+
require_relative "../appmap"
|
4
|
+
require_relative "util"
|
5
|
+
require_relative "detect_enabled"
|
6
|
+
require "fileutils"
|
7
|
+
require "active_support"
|
8
|
+
require "active_support/core_ext"
|
9
9
|
|
10
10
|
module AppMap
|
11
11
|
# Integration of AppMap with Minitest. When enabled with APPMAP=true, the AppMap tracer will
|
12
12
|
# be activated around each test.
|
13
13
|
module Minitest
|
14
|
-
APPMAP_OUTPUT_DIR =
|
15
|
-
LOG = (ENV[
|
14
|
+
APPMAP_OUTPUT_DIR = File.join(AppMap.output_dir, "minitest")
|
15
|
+
LOG = (ENV["APPMAP_DEBUG"] == "true" || ENV["DEBUG"] == "true")
|
16
16
|
|
17
17
|
def self.metadata
|
18
18
|
AppMap.detect_metadata
|
@@ -28,13 +28,13 @@ module AppMap
|
|
28
28
|
|
29
29
|
def source_location
|
30
30
|
location = test.method(test_name).source_location
|
31
|
-
[
|
31
|
+
[Util.normalize_path(location.first), location.last].join(":")
|
32
32
|
end
|
33
33
|
|
34
34
|
def finish(failures, exception)
|
35
35
|
failed = failures.any? || exception
|
36
36
|
if AppMap::Minitest::LOG
|
37
|
-
warn "Finishing recording of #{failed ?
|
37
|
+
warn "Finishing recording of #{failed ? "failed " : ""} test #{test.class}.#{test.name}"
|
38
38
|
end
|
39
39
|
warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
|
40
40
|
|
@@ -53,17 +53,17 @@ module AppMap
|
|
53
53
|
|
54
54
|
class_map = AppMap.class_map(@trace.event_methods)
|
55
55
|
|
56
|
-
feature_group = test.class.name.underscore.split(
|
57
|
-
feature_name = test.name.split(
|
58
|
-
scenario_name = [feature_group, feature_name].join(
|
56
|
+
feature_group = test.class.name.underscore.split("_")[0...-1].join("_").capitalize
|
57
|
+
feature_name = test.name.split("_")[1..-1].join(" ")
|
58
|
+
scenario_name = [feature_group, feature_name].join(" ")
|
59
59
|
|
60
60
|
AppMap::Minitest.save name: scenario_name,
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
class_map: class_map,
|
62
|
+
source_location: source_location,
|
63
|
+
test_status: failed ? "failed" : "succeeded",
|
64
|
+
test_failure: test_failure,
|
65
|
+
exception: exception,
|
66
|
+
events: events
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
@@ -96,7 +96,7 @@ module AppMap
|
|
96
96
|
end
|
97
97
|
|
98
98
|
def config
|
99
|
-
@config or raise
|
99
|
+
@config or raise "AppMap is not configured"
|
100
100
|
end
|
101
101
|
|
102
102
|
def add_event_methods(event_methods)
|
@@ -110,12 +110,12 @@ module AppMap
|
|
110
110
|
m[:app] = AppMap.configuration.name
|
111
111
|
m[:frameworks] ||= []
|
112
112
|
m[:frameworks] << {
|
113
|
-
name:
|
114
|
-
version: Gem.loaded_specs[
|
113
|
+
name: "minitest",
|
114
|
+
version: Gem.loaded_specs["minitest"]&.version&.to_s
|
115
115
|
}
|
116
116
|
m[:recorder] = {
|
117
|
-
name:
|
118
|
-
type:
|
117
|
+
name: "minitest",
|
118
|
+
type: "tests"
|
119
119
|
}
|
120
120
|
m[:test_status] = test_status
|
121
121
|
m[:test_failure] = test_failure if test_failure
|
@@ -145,11 +145,11 @@ module AppMap
|
|
145
145
|
end
|
146
146
|
|
147
147
|
if AppMap::Minitest.enabled?
|
148
|
-
require
|
149
|
-
require
|
148
|
+
require "appmap"
|
149
|
+
require "minitest/test"
|
150
150
|
|
151
151
|
class ::Minitest::Test
|
152
|
-
|
152
|
+
alias_method :run_without_hook, :run
|
153
153
|
|
154
154
|
def run
|
155
155
|
AppMap::Minitest.begin_test self, name
|