appmap 0.39.1 → 0.42.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/CONTRIBUTING.md +22 -0
  4. data/README.md +105 -50
  5. data/lib/appmap.rb +5 -0
  6. data/lib/appmap/class_map.rb +25 -8
  7. data/lib/appmap/command/record.rb +1 -1
  8. data/lib/appmap/config.rb +48 -28
  9. data/lib/appmap/event.rb +14 -4
  10. data/lib/appmap/hook.rb +7 -0
  11. data/lib/appmap/hook/method.rb +1 -1
  12. data/lib/appmap/middleware/remote_recording.rb +1 -1
  13. data/lib/appmap/minitest.rb +17 -14
  14. data/lib/appmap/rails/request_handler.rb +8 -3
  15. data/lib/appmap/railtie.rb +1 -5
  16. data/lib/appmap/rspec.rb +12 -78
  17. data/lib/appmap/version.rb +1 -1
  18. data/spec/abstract_controller_base_spec.rb +2 -2
  19. data/spec/config_spec.rb +1 -0
  20. data/spec/fixtures/hook/exclude.rb +15 -0
  21. data/spec/fixtures/hook/labels.rb +6 -0
  22. data/spec/fixtures/rails5_users_app/Gemfile +2 -3
  23. data/spec/fixtures/rails5_users_app/appmap.yml +4 -1
  24. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  25. data/spec/fixtures/rails5_users_app/docker-compose.yml +3 -0
  26. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +3 -3
  27. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  28. data/spec/fixtures/rails6_users_app/Gemfile +2 -3
  29. data/spec/fixtures/rails6_users_app/appmap.yml +4 -1
  30. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  31. data/spec/fixtures/rails6_users_app/docker-compose.yml +3 -0
  32. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +3 -3
  33. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  34. data/spec/hook_spec.rb +37 -1
  35. data/spec/record_sql_rails_pg_spec.rb +1 -1
  36. data/spec/spec_helper.rb +1 -0
  37. data/test/fixtures/gem_test/appmap.yml +1 -1
  38. data/test/fixtures/gem_test/test/parser_test.rb +12 -0
  39. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  40. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  41. data/test/gem_test.rb +4 -4
  42. data/test/minitest_test.rb +1 -2
  43. data/test/rspec_test.rb +1 -7
  44. metadata +6 -4
  45. data/spec/rspec_feature_metadata_spec.rb +0 -31
  46. 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.class.name,
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.class.name,
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.class.name,
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.class.name,
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
@@ -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
@@ -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
 
@@ -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 = 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 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
- events: events,
43
- feature_name: feature_name,
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, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
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
- route = ::Rails.application.routes.router.enum_for(:recognize, request).first
52
- route.first.path.spec.to_s if route
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
 
@@ -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', after: 'appmap.init' do |_| # params: app
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: false)
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
- events: events,
195
- feature_name: feature_name,
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, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
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] << {
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.39.1'
6
+ VERSION = '0.42.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.4'
9
9
  end
@@ -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
@@ -7,6 +7,7 @@ require 'appmap/config'
7
7
  describe AppMap::Config, docker: false do
8
8
  it 'loads from a Hash' do
9
9
  config_data = {
10
+ exclude: [],
10
11
  name: 'test',
11
12
  packages: [
12
13
  {
@@ -0,0 +1,15 @@
1
+ class ExcludeTest
2
+ def instance_method
3
+ 'instance_method'
4
+ end
5
+
6
+ class << self
7
+ def singleton_method
8
+ 'singleton_method'
9
+ end
10
+ end
11
+
12
+ def self.cls_method
13
+ 'class_method'
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # @label has-cls-label
2
+ class ClassWithLabel
3
+ # @label has-fn-label
4
+ def fn_with_label
5
+ end
6
+ end
@@ -33,11 +33,10 @@ appmap_options = \
33
33
  { path: appmap_path }
34
34
  else
35
35
  {}
36
- end.merge(require: %w[appmap appmap/railtie])
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