appmap 0.39.1 → 0.42.0

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