appmap 0.42.0 → 0.45.1

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