appmap 0.41.2 → 0.45.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +23 -2
  4. data/CHANGELOG.md +40 -0
  5. data/README.md +36 -6
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +4 -2
  10. data/lib/appmap/class_map.rb +7 -10
  11. data/lib/appmap/config.rb +98 -28
  12. data/lib/appmap/cucumber.rb +1 -1
  13. data/lib/appmap/event.rb +18 -0
  14. data/lib/appmap/handler/function.rb +19 -0
  15. data/lib/appmap/handler/net_http.rb +107 -0
  16. data/lib/appmap/hook.rb +42 -22
  17. data/lib/appmap/hook/method.rb +5 -7
  18. data/lib/appmap/minitest.rb +35 -30
  19. data/lib/appmap/rails/request_handler.rb +30 -17
  20. data/lib/appmap/record.rb +1 -1
  21. data/lib/appmap/rspec.rb +32 -96
  22. data/lib/appmap/trace.rb +2 -1
  23. data/lib/appmap/util.rb +39 -2
  24. data/lib/appmap/version.rb +2 -2
  25. data/release.sh +17 -0
  26. data/spec/abstract_controller_base_spec.rb +76 -29
  27. data/spec/class_map_spec.rb +3 -11
  28. data/spec/config_spec.rb +33 -1
  29. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  30. data/spec/fixtures/hook/method_named_call.rb +11 -0
  31. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  32. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  33. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  34. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  35. data/spec/fixtures/rails5_users_app/create_app +8 -2
  36. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  37. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  38. data/spec/fixtures/rails5_users_app/spec/models/user_spec.rb +2 -12
  39. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  40. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  41. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  42. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  43. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  44. data/spec/fixtures/rails6_users_app/create_app +8 -2
  45. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +16 -3
  46. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  47. data/spec/fixtures/rails6_users_app/spec/models/user_spec.rb +2 -12
  48. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  49. data/spec/hook_spec.rb +135 -18
  50. data/spec/record_net_http_spec.rb +160 -0
  51. data/spec/record_sql_rails_pg_spec.rb +1 -1
  52. data/spec/spec_helper.rb +16 -0
  53. data/test/expectations/openssl_test_key_sign1.json +2 -4
  54. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +3 -3
  55. data/test/fixtures/rspec_recorder/spec/plain_hello_spec.rb +1 -1
  56. data/test/gem_test.rb +1 -1
  57. data/test/minitest_test.rb +1 -2
  58. data/test/rspec_test.rb +1 -20
  59. metadata +17 -13
  60. data/exe/appmap +0 -154
  61. data/spec/rspec_feature_metadata_spec.rb +0 -31
  62. data/test/cli_test.rb +0 -116
@@ -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
- File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
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
@@ -32,6 +32,7 @@ module AppMap
32
32
  end
33
33
 
34
34
  attr_reader :config
35
+
35
36
  def initialize(config)
36
37
  @config = config
37
38
  end
@@ -46,10 +47,23 @@ module AppMap
46
47
  cls = trace_point.self
47
48
 
48
49
  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
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
51
+ class_methods = begin
52
+ if cls.respond_to?(:singleton_class)
53
+ cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
54
+ else
55
+ []
56
+ end
57
+ rescue NameError
58
+ []
59
+ end
50
60
 
51
61
  hook = lambda do |hook_cls|
52
62
  lambda do |method_id|
63
+ # Don't try and trace the AppMap methods or there will be
64
+ # a stack overflow in the defined hook method.
65
+ return if (hook_cls&.name || '').split('::')[0] == AppMap.name
66
+
53
67
  method = begin
54
68
  hook_cls.public_instance_method(method_id)
55
69
  rescue NameError
@@ -69,18 +83,22 @@ module AppMap
69
83
  config.always_hook?(hook_cls, method.name) ||
70
84
  config.included_by_location?(method)
71
85
 
72
- hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
86
+ package = config.package_for_method(method)
73
87
 
74
- # Don't try and trace the AppMap methods or there will be
75
- # a stack overflow in the defined hook method.
76
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
88
+ hook_method = Hook::Method.new(package, hook_cls, method)
77
89
 
78
90
  hook_method.activate
79
91
  end
80
92
  end
81
93
 
82
94
  instance_methods.each(&hook.(cls))
83
- class_methods.each(&hook.(cls.singleton_class))
95
+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
96
+ begin
97
+ class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
98
+ rescue NameError
99
+ # NameError:
100
+ # uninitialized constant Faraday::Connection
101
+ end
84
102
  end
85
103
 
86
104
  tp.enable(&block)
@@ -97,25 +115,27 @@ module AppMap
97
115
  end
98
116
  end
99
117
 
100
- Config::BUILTIN_METHODS.each do |class_name, hook|
101
- require hook.package.package_name if hook.package.package_name
102
- Array(hook.method_names).each do |method_name|
103
- method_name = method_name.to_sym
118
+ config.builtin_methods.each do |class_name, hooks|
119
+ Array(hooks).each do |hook|
120
+ require hook.package.package_name if hook.package.package_name
121
+ Array(hook.method_names).each do |method_name|
122
+ method_name = method_name.to_sym
104
123
 
105
- cls = class_from_string.(class_name)
106
- method = \
107
- begin
108
- cls.instance_method(method_name)
109
- rescue NameError
110
- cls.method(method_name) rescue nil
111
- end
124
+ cls = class_from_string.(class_name)
125
+ method = \
126
+ begin
127
+ cls.instance_method(method_name)
128
+ rescue NameError
129
+ cls.method(method_name) rescue nil
130
+ end
112
131
 
113
- next if config.never_hook?(method)
132
+ next if config.never_hook?(method)
114
133
 
115
- if method
116
- Hook::Method.new(hook.package, cls, method).activate
117
- else
118
- warn "Method #{method_name} not found on #{cls.name}"
134
+ if method
135
+ Hook::Method.new(hook.package, cls, method).activate
136
+ else
137
+ warn "Method #{method_name} not found on #{cls.name}"
138
+ end
119
139
  end
120
140
  end
121
141
  end
@@ -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
- require 'appmap/event'
91
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
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)
@@ -7,22 +7,28 @@ 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 finish
24
+ def source_location
25
+ test.method(test_name).source_location.join(':')
26
+ end
27
+
28
+
29
+ def finish(exception)
25
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
31
+ warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
26
32
 
27
33
  events = []
28
34
  AppMap.tracing.delete @trace
@@ -37,11 +43,12 @@ module AppMap
37
43
  feature_name = test.name.split('_')[1..-1].join(' ')
38
44
  scenario_name = [ feature_group, feature_name ].join(' ')
39
45
 
40
- AppMap::Minitest.save scenario_name,
41
- class_map,
42
- events: events,
43
- feature_name: feature_name,
44
- feature_group_name: feature_group
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,
51
+ events: events
45
52
  end
46
53
  end
47
54
 
@@ -55,15 +62,15 @@ module AppMap
55
62
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
56
63
  end
57
64
 
58
- def begin_test(test)
59
- @recordings_by_test[test.object_id] = Recording.new(test)
65
+ def begin_test(test, name)
66
+ @recordings_by_test[test.object_id] = Recording.new(test, name)
60
67
  end
61
68
 
62
- def end_test(test)
69
+ def end_test(test, exception:)
63
70
  recording = @recordings_by_test.delete(test.object_id)
64
71
  return warn "No recording found for #{test}" unless recording
65
72
 
66
- recording.finish
73
+ recording.finish exception
67
74
  end
68
75
 
69
76
  def config
@@ -74,12 +81,11 @@ module AppMap
74
81
  @event_methods += event_methods
75
82
  end
76
83
 
77
- def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
84
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
78
85
  metadata = AppMap::Minitest.metadata.tap do |m|
79
- m[:name] = example_name
86
+ m[:name] = name
87
+ m[:source_location] = source_location
80
88
  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
89
  m[:frameworks] ||= []
84
90
  m[:frameworks] << {
85
91
  name: 'minitest',
@@ -88,6 +94,13 @@ module AppMap
88
94
  m[:recorder] = {
89
95
  name: 'minitest'
90
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
91
104
  end
92
105
 
93
106
  appmap = {
@@ -96,14 +109,9 @@ module AppMap
96
109
  classMap: class_map,
97
110
  events: events
98
111
  }.compact
99
- fname = AppMap::Util.scenario_filename(example_name)
112
+ fname = AppMap::Util.scenario_filename(name)
100
113
 
101
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
102
- end
103
-
104
- def print_inventory
105
- class_map = AppMap.class_map(@event_methods)
106
- save 'Inventory', class_map, labels: %w[inventory]
114
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
107
115
  end
108
116
 
109
117
  def enabled?
@@ -112,9 +120,6 @@ module AppMap
112
120
 
113
121
  def run
114
122
  init
115
- at_exit do
116
- print_inventory
117
- end
118
123
  end
119
124
  end
120
125
  end
@@ -128,11 +133,11 @@ if AppMap::Minitest.enabled?
128
133
  alias run_without_hook run
129
134
 
130
135
  def run
131
- AppMap::Minitest.begin_test self
136
+ AppMap::Minitest.begin_test self, name
132
137
  begin
133
138
  run_without_hook
134
139
  ensure
135
- AppMap::Minitest.end_test self
140
+ AppMap::Minitest.end_test self, exception: $!
136
141
  end
137
142
  end
138
143
  end
@@ -7,14 +7,17 @@ module AppMap
7
7
  module Rails
8
8
  module RequestHandler
9
9
  class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :normalized_path_info, :request_method, :path_info, :params
10
+ attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
11
11
 
12
12
  def initialize(request)
13
13
  super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
14
 
15
- @request_method = request.request_method
16
- @normalized_path_info = normalized_path request
17
- @path_info = request.path_info.split('?')[0]
15
+ self.request_method = request.request_method
16
+ self.normalized_path_info = normalized_path(request)
17
+ self.mime_type = request.headers['Content-Type']
18
+ self.headers = AppMap::Util.select_headers(request.env)
19
+ self.authorization = request.headers['Authorization']
20
+ self.path_info = request.path_info.split('?')[0]
18
21
  # ActionDispatch::Http::ParameterFilter is deprecated
19
22
  parameter_filter_cls = \
20
23
  if defined?(ActiveSupport::ParameterFilter)
@@ -22,7 +25,7 @@ module AppMap
22
25
  else
23
26
  ActionDispatch::Http::ParameterFilter
24
27
  end
25
- @params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
28
+ self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
26
29
  end
27
30
 
28
31
  def to_h
@@ -30,17 +33,25 @@ module AppMap
30
33
  h[:http_server_request] = {
31
34
  request_method: request_method,
32
35
  path_info: path_info,
33
- normalized_path_info: normalized_path_info
36
+ mime_type: mime_type,
37
+ normalized_path_info: normalized_path_info,
38
+ authorization: authorization,
39
+ headers: headers,
34
40
  }.compact
35
41
 
36
- h[:message] = params.keys.map do |key|
37
- val = params[key]
38
- {
39
- name: key,
40
- class: val.class.name,
41
- value: self.class.display_string(val),
42
- object_id: val.__id__
43
- }
42
+ unless params.blank?
43
+ h[:message] = params.keys.map do |key|
44
+ val = params[key]
45
+ {
46
+ name: key,
47
+ class: val.class.name,
48
+ value: self.class.display_string(val),
49
+ object_id: val.__id__,
50
+ }.tap do |message|
51
+ properties = object_properties(val)
52
+ message[:properties] = properties if properties
53
+ end
54
+ end
44
55
  end
45
56
  end
46
57
  end
@@ -59,7 +70,7 @@ module AppMap
59
70
  end
60
71
 
61
72
  class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
62
- attr_accessor :status, :mime_type
73
+ attr_accessor :status, :mime_type, :headers
63
74
 
64
75
  def initialize(response, parent_id, elapsed)
65
76
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
@@ -68,13 +79,15 @@ module AppMap
68
79
  self.mime_type = response.headers['Content-Type']
69
80
  self.parent_id = parent_id
70
81
  self.elapsed = elapsed
82
+ self.headers = AppMap::Util.select_headers(response.headers)
71
83
  end
72
84
 
73
85
  def to_h
74
86
  super.tap do |h|
75
87
  h[:http_server_response] = {
76
- status: status,
77
- mime_type: mime_type
88
+ status_code: status,
89
+ mime_type: mime_type,
90
+ headers: headers
78
91
  }.compact
79
92
  end
80
93
  end