appmap 0.39.1 → 0.42.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +22 -0
- data/README.md +105 -50
- data/lib/appmap.rb +5 -0
- data/lib/appmap/class_map.rb +25 -8
- data/lib/appmap/command/record.rb +1 -1
- data/lib/appmap/config.rb +48 -28
- data/lib/appmap/event.rb +14 -4
- data/lib/appmap/hook.rb +7 -0
- data/lib/appmap/hook/method.rb +1 -1
- data/lib/appmap/middleware/remote_recording.rb +1 -1
- data/lib/appmap/minitest.rb +17 -14
- data/lib/appmap/rails/request_handler.rb +8 -3
- data/lib/appmap/railtie.rb +1 -5
- data/lib/appmap/rspec.rb +12 -78
- data/lib/appmap/version.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +2 -2
- data/spec/config_spec.rb +1 -0
- data/spec/fixtures/hook/exclude.rb +15 -0
- data/spec/fixtures/hook/labels.rb +6 -0
- data/spec/fixtures/rails5_users_app/Gemfile +2 -3
- data/spec/fixtures/rails5_users_app/appmap.yml +4 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/docker-compose.yml +3 -0
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +3 -3
- data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
- data/spec/fixtures/rails6_users_app/Gemfile +2 -3
- data/spec/fixtures/rails6_users_app/appmap.yml +4 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/docker-compose.yml +3 -0
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +3 -3
- data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
- data/spec/hook_spec.rb +37 -1
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/test/fixtures/gem_test/appmap.yml +1 -1
- data/test/fixtures/gem_test/test/parser_test.rb +12 -0
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
- data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
- data/test/gem_test.rb +4 -4
- data/test/minitest_test.rb +1 -2
- data/test/rspec_test.rb +1 -7
- metadata +6 -4
- data/spec/rspec_feature_metadata_spec.rb +0 -31
- data/test/fixtures/gem_test/test/to_param_test.rb +0 -14
data/lib/appmap/event.rb
CHANGED
@@ -38,6 +38,15 @@ module AppMap
|
|
38
38
|
|
39
39
|
protected
|
40
40
|
|
41
|
+
# Heuristic for dynamically defined class whose name can be nil
|
42
|
+
def best_class_name(value)
|
43
|
+
value_cls = value.class
|
44
|
+
while value_cls.name.nil?
|
45
|
+
value_cls = value_cls.superclass
|
46
|
+
end
|
47
|
+
value_cls.name
|
48
|
+
end
|
49
|
+
|
41
50
|
def custom_display_string(value)
|
42
51
|
case value
|
43
52
|
when File
|
@@ -77,6 +86,7 @@ module AppMap
|
|
77
86
|
|
78
87
|
class << self
|
79
88
|
def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
|
89
|
+
defined_class ||= 'Class'
|
80
90
|
mc.tap do
|
81
91
|
static = receiver.is_a?(Module)
|
82
92
|
mc.defined_class = defined_class
|
@@ -110,14 +120,14 @@ module AppMap
|
|
110
120
|
end
|
111
121
|
{
|
112
122
|
name: param_name,
|
113
|
-
class: value
|
123
|
+
class: best_class_name(value),
|
114
124
|
object_id: value.__id__,
|
115
125
|
value: display_string(value),
|
116
126
|
kind: param_type
|
117
127
|
}
|
118
128
|
end
|
119
129
|
mc.receiver = {
|
120
|
-
class: receiver
|
130
|
+
class: best_class_name(receiver),
|
121
131
|
object_id: receiver.__id__,
|
122
132
|
value: display_string(receiver)
|
123
133
|
}
|
@@ -172,7 +182,7 @@ module AppMap
|
|
172
182
|
mr.tap do |_|
|
173
183
|
if return_value
|
174
184
|
mr.return_value = {
|
175
|
-
class: return_value
|
185
|
+
class: best_class_name(return_value),
|
176
186
|
value: display_string(return_value),
|
177
187
|
object_id: return_value.__id__
|
178
188
|
}
|
@@ -183,7 +193,7 @@ module AppMap
|
|
183
193
|
while next_exception
|
184
194
|
exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
|
185
195
|
exceptions << {
|
186
|
-
class: next_exception
|
196
|
+
class: best_class_name(next_exception),
|
187
197
|
message: next_exception.message,
|
188
198
|
object_id: next_exception.__id__,
|
189
199
|
path: exception_backtrace&.path,
|
data/lib/appmap/hook.rb
CHANGED
@@ -63,6 +63,8 @@ module AppMap
|
|
63
63
|
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
64
64
|
next unless disasm
|
65
65
|
|
66
|
+
next if config.never_hook?(method)
|
67
|
+
|
66
68
|
next unless \
|
67
69
|
config.always_hook?(hook_cls, method.name) ||
|
68
70
|
config.included_by_location?(method)
|
@@ -84,6 +86,8 @@ module AppMap
|
|
84
86
|
tp.enable(&block)
|
85
87
|
end
|
86
88
|
|
89
|
+
# hook_builtins builds hooks for code that is built in to the Ruby standard library.
|
90
|
+
# No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
|
87
91
|
def hook_builtins
|
88
92
|
return unless self.class.lock_builtins
|
89
93
|
|
@@ -97,6 +101,7 @@ module AppMap
|
|
97
101
|
require hook.package.package_name if hook.package.package_name
|
98
102
|
Array(hook.method_names).each do |method_name|
|
99
103
|
method_name = method_name.to_sym
|
104
|
+
|
100
105
|
cls = class_from_string.(class_name)
|
101
106
|
method = \
|
102
107
|
begin
|
@@ -105,6 +110,8 @@ module AppMap
|
|
105
110
|
cls.method(method_name) rescue nil
|
106
111
|
end
|
107
112
|
|
113
|
+
next if config.never_hook?(method)
|
114
|
+
|
108
115
|
if method
|
109
116
|
Hook::Method.new(hook.package, cls, method).activate
|
110
117
|
else
|
data/lib/appmap/hook/method.rb
CHANGED
@@ -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, include_source: AppMap.include_source?),
|
71
71
|
metadata: metadata,
|
72
72
|
events: @events
|
73
73
|
|
data/lib/appmap/minitest.rb
CHANGED
@@ -7,20 +7,25 @@ module AppMap
|
|
7
7
|
# be activated around each test.
|
8
8
|
module Minitest
|
9
9
|
APPMAP_OUTPUT_DIR = 'tmp/appmap/minitest'
|
10
|
-
LOG =
|
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 source_location
|
25
|
+
test.method(test_name).source_location.join(':')
|
26
|
+
end
|
27
|
+
|
28
|
+
|
24
29
|
def finish
|
25
30
|
warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
|
26
31
|
|
@@ -31,7 +36,7 @@ module AppMap
|
|
31
36
|
|
32
37
|
AppMap::Minitest.add_event_methods @trace.event_methods
|
33
38
|
|
34
|
-
class_map = AppMap.class_map(@trace.event_methods)
|
39
|
+
class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
|
35
40
|
|
36
41
|
feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
|
37
42
|
feature_name = test.name.split('_')[1..-1].join(' ')
|
@@ -39,9 +44,8 @@ module AppMap
|
|
39
44
|
|
40
45
|
AppMap::Minitest.save scenario_name,
|
41
46
|
class_map,
|
42
|
-
|
43
|
-
|
44
|
-
feature_group_name: feature_group
|
47
|
+
source_location,
|
48
|
+
events: events
|
45
49
|
end
|
46
50
|
end
|
47
51
|
|
@@ -55,8 +59,8 @@ module AppMap
|
|
55
59
|
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
56
60
|
end
|
57
61
|
|
58
|
-
def begin_test(test)
|
59
|
-
@recordings_by_test[test.object_id] = Recording.new(test)
|
62
|
+
def begin_test(test, name)
|
63
|
+
@recordings_by_test[test.object_id] = Recording.new(test, name)
|
60
64
|
end
|
61
65
|
|
62
66
|
def end_test(test)
|
@@ -74,12 +78,11 @@ module AppMap
|
|
74
78
|
@event_methods += event_methods
|
75
79
|
end
|
76
80
|
|
77
|
-
def save(example_name, class_map,
|
81
|
+
def save(example_name, class_map, source_location, events: nil, labels: nil)
|
78
82
|
metadata = AppMap::Minitest.metadata.tap do |m|
|
79
83
|
m[:name] = example_name
|
84
|
+
m[:source_location] = source_location
|
80
85
|
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
86
|
m[:frameworks] ||= []
|
84
87
|
m[:frameworks] << {
|
85
88
|
name: 'minitest',
|
@@ -128,7 +131,7 @@ if AppMap::Minitest.enabled?
|
|
128
131
|
alias run_without_hook run
|
129
132
|
|
130
133
|
def run
|
131
|
-
AppMap::Minitest.begin_test self
|
134
|
+
AppMap::Minitest.begin_test self, name
|
132
135
|
begin
|
133
136
|
run_without_hook
|
134
137
|
ensure
|
@@ -47,9 +47,14 @@ module AppMap
|
|
47
47
|
|
48
48
|
private
|
49
49
|
|
50
|
-
def normalized_path(request)
|
51
|
-
|
52
|
-
|
50
|
+
def normalized_path(request, router = ::Rails.application.routes.router)
|
51
|
+
router.recognize request do |route, _|
|
52
|
+
app = route.app
|
53
|
+
next unless app.matches? request
|
54
|
+
return normalized_path request, app.rack_app.routes.router if app.engine?
|
55
|
+
|
56
|
+
return route.path.spec.to_s
|
57
|
+
end
|
53
58
|
end
|
54
59
|
end
|
55
60
|
|
data/lib/appmap/railtie.rb
CHANGED
@@ -5,13 +5,9 @@ module AppMap
|
|
5
5
|
class Railtie < ::Rails::Railtie
|
6
6
|
config.appmap = ActiveSupport::OrderedOptions.new
|
7
7
|
|
8
|
-
initializer 'appmap.init' do |_| # params: app
|
9
|
-
require 'appmap'
|
10
|
-
end
|
11
|
-
|
12
8
|
# appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
|
13
9
|
# AppMap events.
|
14
|
-
initializer 'appmap.subscribe'
|
10
|
+
initializer 'appmap.subscribe' do |_| # params: app
|
15
11
|
require 'appmap/rails/sql_handler'
|
16
12
|
require 'appmap/rails/request_handler'
|
17
13
|
ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
|
data/lib/appmap/rspec.rb
CHANGED
@@ -13,58 +13,9 @@ module AppMap
|
|
13
13
|
AppMap.detect_metadata
|
14
14
|
end
|
15
15
|
|
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
16
|
# ScopeExample and ScopeExampleGroup is a way to handle the weird way that RSpec
|
64
17
|
# stores the nested example names.
|
65
18
|
ScopeExample = Struct.new(:example) do
|
66
|
-
include FeatureAnnotations
|
67
|
-
|
68
19
|
alias_method :example_obj, :example
|
69
20
|
|
70
21
|
def description?
|
@@ -83,8 +34,6 @@ module AppMap
|
|
83
34
|
# As you can see here, the way that RSpec stores the example description and
|
84
35
|
# represents the example group hierarchy is pretty weird.
|
85
36
|
ScopeExampleGroup = Struct.new(:example_group) do
|
86
|
-
include FeatureAnnotations
|
87
|
-
|
88
37
|
alias_method :example_obj, :example_group
|
89
38
|
|
90
39
|
def description_args
|
@@ -133,11 +82,17 @@ module AppMap
|
|
133
82
|
page.driver.options[:http_client].instance_variable_get('@server_url').port
|
134
83
|
end
|
135
84
|
|
136
|
-
warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
|
85
|
+
warn "Starting recording of example #{example}@#{source_location}" if AppMap::RSpec::LOG
|
137
86
|
@trace = AppMap.tracing.trace
|
138
87
|
@webdriver_port = webdriver_port.()
|
139
88
|
end
|
140
89
|
|
90
|
+
def source_location
|
91
|
+
result = example.location_rerun_argument.split(':')[0]
|
92
|
+
result = result[2..-1] if result.index('./') == 0
|
93
|
+
result
|
94
|
+
end
|
95
|
+
|
141
96
|
def finish
|
142
97
|
warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
|
143
98
|
|
@@ -148,22 +103,16 @@ module AppMap
|
|
148
103
|
|
149
104
|
AppMap::RSpec.add_event_methods @trace.event_methods
|
150
105
|
|
151
|
-
class_map = AppMap.class_map(@trace.event_methods, include_source:
|
106
|
+
class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
|
152
107
|
|
153
108
|
description = []
|
154
109
|
scope = ScopeExample.new(example)
|
155
|
-
feature_group = feature = nil
|
156
110
|
|
157
|
-
labels = []
|
158
111
|
while scope
|
159
|
-
labels += scope.labels
|
160
112
|
description << scope.description
|
161
|
-
feature ||= scope.feature
|
162
|
-
feature_group ||= scope.feature_group
|
163
113
|
scope = scope.parent
|
164
114
|
end
|
165
115
|
|
166
|
-
labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
|
167
116
|
description.reject!(&:nil?).reject!(&:blank?)
|
168
117
|
default_description = description.last
|
169
118
|
description.reverse!
|
@@ -177,24 +126,10 @@ module AppMap
|
|
177
126
|
|
178
127
|
full_description = normalize.call(description.join(' '))
|
179
128
|
|
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
129
|
AppMap::RSpec.save full_description,
|
193
130
|
class_map,
|
194
|
-
|
195
|
-
|
196
|
-
feature_group_name: feature_group,
|
197
|
-
labels: labels.blank? ? nil : labels
|
131
|
+
source_location,
|
132
|
+
events: events
|
198
133
|
end
|
199
134
|
end
|
200
135
|
|
@@ -227,12 +162,11 @@ module AppMap
|
|
227
162
|
@event_methods += event_methods
|
228
163
|
end
|
229
164
|
|
230
|
-
def save(example_name, class_map,
|
165
|
+
def save(example_name, class_map, source_location, events: nil, labels: nil)
|
231
166
|
metadata = AppMap::RSpec.metadata.tap do |m|
|
232
167
|
m[:name] = example_name
|
168
|
+
m[:source_location] = source_location
|
233
169
|
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
170
|
m[:labels] = labels if labels
|
237
171
|
m[:frameworks] ||= []
|
238
172
|
m[:frameworks] << {
|
data/lib/appmap/version.rb
CHANGED
@@ -8,7 +8,7 @@ describe 'AbstractControllerBase' do
|
|
8
8
|
FileUtils.rm_rf tmpdir
|
9
9
|
FileUtils.mkdir_p tmpdir
|
10
10
|
cmd = <<~CMD.gsub "\n", ' '
|
11
|
-
docker-compose run --rm -e APPMAP=true
|
11
|
+
docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true
|
12
12
|
-v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
|
13
13
|
CMD
|
14
14
|
run_cmd cmd, chdir: fixture_dir
|
@@ -137,7 +137,7 @@ describe 'AbstractControllerBase' do
|
|
137
137
|
'name' => 'Renderer',
|
138
138
|
'children' => include(hash_including(
|
139
139
|
'name' => 'render',
|
140
|
-
'labels' => ['view']
|
140
|
+
'labels' => ['mvc.view']
|
141
141
|
))
|
142
142
|
))
|
143
143
|
))
|
data/spec/config_spec.rb
CHANGED
@@ -33,11 +33,10 @@ appmap_options = \
|
|
33
33
|
{ path: appmap_path }
|
34
34
|
else
|
35
35
|
{}
|
36
|
-
end.merge(require: %w[appmap
|
37
|
-
|
38
|
-
gem 'appmap', appmap_options
|
36
|
+
end.merge(require: %w[appmap])
|
39
37
|
|
40
38
|
group :development, :test do
|
39
|
+
gem 'appmap', appmap_options
|
41
40
|
gem 'cucumber-rails', require: false
|
42
41
|
gem 'rspec-rails'
|
43
42
|
# Required for Sequel, since without ActiveRecord, the Rails transactional fixture support
|