appmap 0.42.0 → 0.45.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +23 -2
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +65 -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 -7
  10. data/lib/appmap/class_map.rb +7 -10
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +173 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +18 -0
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/hook.rb +112 -56
  18. data/lib/appmap/hook/method.rb +5 -7
  19. data/lib/appmap/middleware/remote_recording.rb +1 -1
  20. data/lib/appmap/minitest.rb +22 -20
  21. data/lib/appmap/rails/request_handler.rb +30 -17
  22. data/lib/appmap/record.rb +1 -1
  23. data/lib/appmap/rspec.rb +23 -21
  24. data/lib/appmap/trace.rb +2 -1
  25. data/lib/appmap/util.rb +47 -2
  26. data/lib/appmap/version.rb +2 -2
  27. data/release.sh +17 -0
  28. data/spec/abstract_controller_base_spec.rb +77 -30
  29. data/spec/class_map_spec.rb +3 -11
  30. data/spec/config_spec.rb +33 -1
  31. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  32. data/spec/fixtures/hook/method_named_call.rb +11 -0
  33. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  34. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  35. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  36. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  37. data/spec/fixtures/rails5_users_app/create_app +8 -2
  38. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  39. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  40. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  41. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  42. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  43. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  44. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  45. data/spec/fixtures/rails6_users_app/create_app +8 -2
  46. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  47. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  48. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  49. data/spec/hook_spec.rb +141 -20
  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/gem_test.rb +1 -1
  55. data/test/rspec_test.rb +0 -13
  56. metadata +17 -12
  57. data/exe/appmap +0 -154
  58. data/test/cli_test.rb +0 -116
@@ -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
data/lib/appmap/record.rb CHANGED
@@ -23,5 +23,5 @@ at_exit do
23
23
  'classMap' => AppMap.class_map(tracer.event_methods),
24
24
  'events' => events
25
25
  }
26
- File.write 'appmap.json', JSON.generate(appmap)
26
+ AppMap::Util.write_appmap('appmap.json', JSON.generate(appmap))
27
27
  end
data/lib/appmap/rspec.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/util'
4
+ require 'set'
4
5
 
5
6
  module AppMap
6
7
  # Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will
@@ -93,8 +94,9 @@ module AppMap
93
94
  result
94
95
  end
95
96
 
96
- def finish
97
+ def finish(exception)
97
98
  warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
99
+ warn "Exception: #{exception}" if exception && AppMap::RSpec::LOG
98
100
 
99
101
  events = []
100
102
  AppMap.tracing.delete @trace
@@ -103,7 +105,7 @@ module AppMap
103
105
 
104
106
  AppMap::RSpec.add_event_methods @trace.event_methods
105
107
 
106
- class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
108
+ class_map = AppMap.class_map(@trace.event_methods)
107
109
 
108
110
  description = []
109
111
  scope = ScopeExample.new(example)
@@ -126,9 +128,11 @@ module AppMap
126
128
 
127
129
  full_description = normalize.call(description.join(' '))
128
130
 
129
- AppMap::RSpec.save full_description,
130
- class_map,
131
- source_location,
131
+ AppMap::RSpec.save name: full_description,
132
+ class_map: class_map,
133
+ source_location: source_location,
134
+ test_status: exception ? 'failed' : 'succeeded',
135
+ exception: exception,
132
136
  events: events
133
137
  end
134
138
  end
@@ -147,11 +151,11 @@ module AppMap
147
151
  @recordings_by_example[example.object_id] = Recording.new(example)
148
152
  end
149
153
 
150
- def end_spec(example)
154
+ def end_spec(example, exception:)
151
155
  recording = @recordings_by_example.delete(example.object_id)
152
156
  return warn "No recording found for #{example}" unless recording
153
157
 
154
- recording.finish
158
+ recording.finish exception
155
159
  end
156
160
 
157
161
  def config
@@ -162,12 +166,11 @@ module AppMap
162
166
  @event_methods += event_methods
163
167
  end
164
168
 
165
- def save(example_name, class_map, source_location, events: nil, labels: nil)
169
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
166
170
  metadata = AppMap::RSpec.metadata.tap do |m|
167
- m[:name] = example_name
171
+ m[:name] = name
168
172
  m[:source_location] = source_location
169
173
  m[:app] = AppMap.configuration.name
170
- m[:labels] = labels if labels
171
174
  m[:frameworks] ||= []
172
175
  m[:frameworks] << {
173
176
  name: 'rspec',
@@ -176,6 +179,13 @@ module AppMap
176
179
  m[:recorder] = {
177
180
  name: 'rspec'
178
181
  }
182
+ m[:test_status] = test_status
183
+ if exception
184
+ m[:exception] = {
185
+ class: exception.class.name,
186
+ message: exception.to_s
187
+ }
188
+ end
179
189
  end
180
190
 
181
191
  appmap = {
@@ -184,14 +194,9 @@ module AppMap
184
194
  classMap: class_map,
185
195
  events: events
186
196
  }.compact
187
- fname = AppMap::Util.scenario_filename(example_name)
188
-
189
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
190
- end
197
+ fname = AppMap::Util.scenario_filename(name)
191
198
 
192
- def print_inventory
193
- class_map = AppMap.class_map(@event_methods)
194
- save 'Inventory', class_map, labels: %w[inventory]
199
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
195
200
  end
196
201
 
197
202
  def enabled?
@@ -200,9 +205,6 @@ module AppMap
200
205
 
201
206
  def run
202
207
  init
203
- at_exit do
204
- print_inventory
205
- end
206
208
  end
207
209
  end
208
210
  end
@@ -224,7 +226,7 @@ if AppMap::RSpec.enabled?
224
226
  begin
225
227
  instance_exec(&fn)
226
228
  ensure
227
- AppMap::RSpec.end_spec example
229
+ AppMap::RSpec.end_spec example, exception: $!
228
230
  end
229
231
  end
230
232
  end
data/lib/appmap/trace.rb CHANGED
@@ -82,7 +82,8 @@ module AppMap
82
82
 
83
83
  @last_package_for_thread[Thread.current.object_id] = package if package
84
84
  @events << event
85
- @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
85
+ static = event.static if event.respond_to?(:static)
86
+ @methods << Trace::ScopedMethod.new(package, defined_class, method, static) \
86
87
  if package && defined_class && method && (event.event == :call)
87
88
  end
88
89
 
data/lib/appmap/util.rb CHANGED
@@ -61,8 +61,16 @@ module AppMap
61
61
  delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
62
62
  delete_object_id.call(event[:receiver])
63
63
  delete_object_id.call(event[:return_value])
64
- (event[:parameters] || []).each(&delete_object_id)
65
- (event[:exceptions] || []).each(&delete_object_id)
64
+ %i[parameters exceptions message].each do |field|
65
+ (event[field] || []).each(&delete_object_id)
66
+ end
67
+ %i[http_client_request http_client_response http_server_request http_server_response].each do |field|
68
+ headers = event.dig(field, :headers)
69
+ next unless headers
70
+
71
+ headers['Date'] = '<instanceof date>' if headers['Date']
72
+ headers['Server'] = headers['Server'].match(/^(\w+)/)[0] if headers['Server']
73
+ end
66
74
 
67
75
  case event[:event]
68
76
  when :call
@@ -71,6 +79,43 @@ module AppMap
71
79
 
72
80
  event
73
81
  end
82
+
83
+ def select_headers(env)
84
+ # Rack prepends HTTP_ to all client-sent headers.
85
+ matching_headers = env
86
+ .select { |k,v| k.start_with? 'HTTP_'}
87
+ .reject { |k,v| v.blank? }
88
+ .each_with_object({}) do |kv, memo|
89
+ key = kv[0].sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
90
+ value = kv[1]
91
+ memo[key] = value
92
+ end
93
+ matching_headers.blank? ? nil : matching_headers
94
+ end
95
+
96
+ def normalize_path(path)
97
+ if path.index(Dir.pwd) == 0
98
+ path[Dir.pwd.length + 1..-1]
99
+ else
100
+ path
101
+ end
102
+ end
103
+
104
+ # Atomically writes AppMap data to +filename+.
105
+ def write_appmap(filename, appmap)
106
+ require 'fileutils'
107
+ require 'tmpdir'
108
+
109
+ # This is what Ruby Tempfile does; but we don't want the file to be unlinked.
110
+ mode = File::RDWR | File::CREAT | File::EXCL
111
+ ::Dir::Tmpname.create([ 'appmap_', '.json' ]) do |tmpname|
112
+ tempfile = File.open(tmpname, mode)
113
+ tempfile.write(appmap)
114
+ tempfile.close
115
+ # Atomically move the tempfile into place.
116
+ FileUtils.mv tempfile.path, filename
117
+ end
118
+ end
74
119
  end
75
120
  end
76
121
  end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.42.0'
6
+ VERSION = '0.45.1'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.4'
8
+ APPMAP_FORMAT_VERSION = '1.5.0'
9
9
  end
data/release.sh ADDED
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # using bash wrapper as Rake blows up in `require/extentiontask` (line 10)
3
+
4
+ RELEASE_FLAGS=""
5
+ if [ ! -z "$TRAVIS_REPO_SLUG" ]; then
6
+ RELEASE_FLAGS="-r git+https://github.com/${TRAVIS_REPO_SLUG}.git"
7
+ fi
8
+
9
+ if [ ! -z "$GEM_ALTERNATIVE_NAME" ]; then
10
+ echo "Release: GEM_ALTERNATIVE_NAME=$GEM_ALTERNATIVE_NAME"
11
+ else
12
+ echo "No GEM_ALTERNATIVE_NAME is provided, releasing gem with default name ('appmap')"
13
+ fi
14
+
15
+ set -x
16
+ exec semantic-release $RELEASE_FLAGS
17
+
@@ -1,12 +1,11 @@
1
1
  require 'rails_spec_helper'
2
2
 
3
- describe 'AbstractControllerBase' do
3
+ describe 'Rails' do
4
4
  %w[5 6].each do |rails_major_version| # rubocop:disable Metrics/BlockLength
5
- context "in Rails #{rails_major_version}" do
6
- include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app"
5
+ context "#{rails_major_version}" do
6
+ include_context 'Rails app pg database', "spec/fixtures/rails#{rails_major_version}_users_app" unless use_existing_data?
7
+
7
8
  def run_spec(spec_name)
8
- FileUtils.rm_rf tmpdir
9
- FileUtils.mkdir_p tmpdir
10
9
  cmd = <<~CMD.gsub "\n", ' '
11
10
  docker-compose run --rm -e RAILS_ENV=test -e APPMAP=true
12
11
  -v #{File.absolute_path tmpdir}:/app/tmp app ./bin/rspec #{spec_name}
@@ -18,25 +17,32 @@ describe 'AbstractControllerBase' do
18
17
  'tmp/spec/AbstractControllerBase'
19
18
  end
20
19
 
20
+ unless use_existing_data?
21
+ before(:all) do
22
+ FileUtils.rm_rf tmpdir
23
+ FileUtils.mkdir_p tmpdir
24
+ run_spec 'spec/controllers/users_controller_spec.rb'
25
+ run_spec 'spec/controllers/users_controller_api_spec.rb'
26
+ end
27
+ end
28
+
21
29
  let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
30
+ let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
31
+ let(:appmap) { JSON.parse File.read(appmap_json_path) }
22
32
  let(:events) { appmap['events'] }
23
33
 
24
- describe 'testing with rspec' do
25
- describe 'creating a user' do
26
- before(:all) { run_spec 'spec/controllers/users_controller_api_spec.rb:8' }
34
+ describe 'an API route' do
35
+ describe 'creating an object' do
27
36
  let(:appmap_json_file) do
28
37
  'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
29
38
  end
30
39
 
31
- it 'inventory file is printed' do
32
- expect(File).to exist(File.join(tmpdir, 'appmap/rspec/Inventory.appmap.json'))
33
- end
34
-
35
- it 'message fields are recorded in the appmap' do
40
+ it 'http_server_request is recorded in the appmap' do
36
41
  expect(events).to include(
37
42
  hash_including(
38
43
  'http_server_request' => hash_including(
39
44
  'request_method' => 'POST',
45
+ 'normalized_path_info' => '/api/users(.:format)',
40
46
  'path_info' => '/api/users'
41
47
  ),
42
48
  'message' => include(
@@ -53,12 +59,17 @@ describe 'AbstractControllerBase' do
53
59
  'object_id' => Integer
54
60
  )
55
61
  )
56
- ),
62
+ )
63
+ )
64
+ end
65
+
66
+ it 'http_server_response is recorded in the appmap' do
67
+ expect(events).to include(
57
68
  hash_including(
58
- 'http_server_response' => {
59
- 'status' => 201,
60
- 'mime_type' => 'application/json; charset=utf-8'
61
- }
69
+ 'http_server_response' => hash_including(
70
+ 'status_code' => 201,
71
+ 'mime_type' => 'application/json; charset=utf-8',
72
+ )
62
73
  )
63
74
  )
64
75
  end
@@ -70,7 +81,7 @@ describe 'AbstractControllerBase' do
70
81
  'defined_class' => 'Api::UsersController',
71
82
  'method_id' => 'build_user',
72
83
  'path' => 'app/controllers/api/users_controller.rb',
73
- 'lineno' => 23,
84
+ 'lineno' => Integer,
74
85
  'static' => false,
75
86
  'parameters' => include(
76
87
  'name' => 'params',
@@ -93,10 +104,47 @@ describe 'AbstractControllerBase' do
93
104
  'elapsed' => Numeric
94
105
  )
95
106
  end
107
+
108
+ context 'with an object-style message' do
109
+ let(:appmap_json_file) { 'Api_UsersController_POST_api_users_with_required_parameters_with_object-style_parameters_creates_a_user.appmap.json' }
110
+
111
+ it 'message properties are recorded in the appmap' do
112
+ expect(events).to include(
113
+ hash_including(
114
+ 'message' => include(
115
+ hash_including(
116
+ 'name' => 'user',
117
+ 'properties' => [
118
+ { 'name' => 'login', 'class' => 'String' },
119
+ { 'name' => 'password', 'class' => 'String' }
120
+ ]
121
+ )
122
+ )
123
+ )
124
+ )
125
+ end
126
+ end
127
+ end
128
+
129
+ describe 'listing objects' do
130
+ context 'with a custom header' do
131
+ let(:appmap_json_file) { 'Api_UsersController_GET_api_users_with_a_custom_header_lists_the_users.appmap.json' }
132
+
133
+ it 'custom header is recorded in the appmap' do
134
+ expect(events).to include(
135
+ hash_including(
136
+ 'http_server_request' => hash_including(
137
+ 'headers' => hash_including('X-Sandwich' => 'turkey')
138
+ )
139
+ )
140
+ )
141
+ end
142
+ end
96
143
  end
144
+ end
97
145
 
98
- describe 'showing a user' do
99
- before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:22' }
146
+ describe 'a UI route' do
147
+ describe 'rendering a page' do
100
148
  let(:appmap_json_file) do
101
149
  'UsersController_GET_users_login_shows_the_user.appmap.json'
102
150
  end
@@ -107,16 +155,15 @@ describe 'AbstractControllerBase' do
107
155
  'http_server_request' => {
108
156
  'request_method' => 'GET',
109
157
  'path_info' => '/users/alice',
110
- 'normalized_path_info' => '/users/:id(.:format)'
158
+ 'normalized_path_info' => '/users/:id(.:format)',
159
+ 'headers' => {
160
+ 'Host' => 'test.host',
161
+ 'User-Agent' => 'Rails Testing'
162
+ }
111
163
  }
112
164
  )
113
165
  )
114
166
  end
115
- end
116
-
117
- describe 'listing users' do
118
- before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:11' }
119
- let(:appmap_json_file) { 'UsersController_GET_users_lists_the_users.appmap.json' }
120
167
 
121
168
  it 'records and labels view rendering' do
122
169
  expect(events).to include hash_including(
@@ -128,9 +175,9 @@ describe 'AbstractControllerBase' do
128
175
  'lineno' => Integer,
129
176
  'static' => false
130
177
  )
131
-
178
+
132
179
  expect(appmap['classMap']).to include hash_including(
133
- 'name' => 'action_view',
180
+ 'name' => 'actionview',
134
181
  'children' => include(hash_including(
135
182
  'name' => 'ActionView',
136
183
  'children' => include(hash_including(
@@ -142,7 +189,7 @@ describe 'AbstractControllerBase' do
142
189
  ))
143
190
  ))
144
191
  )
145
- end
192
+ end
146
193
  end
147
194
  end
148
195
  end