appmap 0.42.1 → 0.46.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +33 -2
  4. data/CHANGELOG.md +44 -0
  5. data/README.md +74 -16
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +3 -7
  10. data/lib/appmap/class_map.rb +11 -22
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +180 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +46 -27
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/handler/rails/request_handler.rb +124 -0
  18. data/lib/appmap/handler/rails/sql_handler.rb +152 -0
  19. data/lib/appmap/handler/rails/template.rb +149 -0
  20. data/lib/appmap/hook.rb +111 -70
  21. data/lib/appmap/hook/method.rb +6 -8
  22. data/lib/appmap/middleware/remote_recording.rb +1 -1
  23. data/lib/appmap/minitest.rb +22 -20
  24. data/lib/appmap/railtie.rb +5 -5
  25. data/lib/appmap/record.rb +1 -1
  26. data/lib/appmap/rspec.rb +22 -21
  27. data/lib/appmap/trace.rb +47 -6
  28. data/lib/appmap/util.rb +47 -2
  29. data/lib/appmap/version.rb +2 -2
  30. data/package-lock.json +3 -3
  31. data/release.sh +17 -0
  32. data/spec/abstract_controller_base_spec.rb +140 -34
  33. data/spec/class_map_spec.rb +5 -13
  34. data/spec/config_spec.rb +33 -1
  35. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  36. data/spec/fixtures/hook/method_named_call.rb +11 -0
  37. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  38. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  39. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  40. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  41. data/spec/fixtures/rails5_users_app/create_app +8 -2
  42. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  43. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  44. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  45. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  46. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  47. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  48. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  49. data/spec/fixtures/rails6_users_app/create_app +8 -2
  50. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  51. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  52. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  53. data/spec/hook_spec.rb +143 -22
  54. data/spec/record_net_http_spec.rb +160 -0
  55. data/spec/record_sql_rails_pg_spec.rb +1 -1
  56. data/spec/spec_helper.rb +16 -0
  57. data/test/expectations/openssl_test_key_sign1.json +2 -4
  58. data/test/gem_test.rb +1 -1
  59. data/test/rspec_test.rb +0 -13
  60. metadata +20 -14
  61. data/exe/appmap +0 -154
  62. data/lib/appmap/rails/request_handler.rb +0 -109
  63. data/lib/appmap/rails/sql_handler.rb +0 -150
  64. data/test/cli_test.rb +0 -116
data/lib/appmap/trace.rb CHANGED
@@ -2,14 +2,36 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- class ScopedMethod < SimpleDelegator
6
- attr_reader :package, :defined_class, :static
5
+ class RubyMethod
6
+ attr_reader :class_name, :static
7
7
 
8
- def initialize(package, defined_class, method, static)
8
+ def initialize(package, class_name, method, static)
9
9
  @package = package
10
- @defined_class = defined_class
10
+ @class_name = class_name
11
+ @method = method
11
12
  @static = static
12
- super(method)
13
+ end
14
+
15
+ def source_location
16
+ @method.source_location
17
+ end
18
+
19
+ def comment
20
+ @method.comment
21
+ rescue MethodSource::SourceNotFoundError
22
+ nil
23
+ end
24
+
25
+ def package
26
+ @package.name
27
+ end
28
+
29
+ def name
30
+ @method.name
31
+ end
32
+
33
+ def labels
34
+ @package.labels
13
35
  end
14
36
  end
15
37
 
@@ -43,6 +65,12 @@ module AppMap
43
65
  end
44
66
  end
45
67
 
68
+ def record_method(method)
69
+ @tracers.each do |tracer|
70
+ tracer.record_method(method)
71
+ end
72
+ end
73
+
46
74
  def delete(tracer)
47
75
  return unless @tracers.member?(tracer)
48
76
 
@@ -82,10 +110,23 @@ module AppMap
82
110
 
83
111
  @last_package_for_thread[Thread.current.object_id] = package if package
84
112
  @events << event
85
- @methods << Trace::ScopedMethod.new(package, defined_class, method, event.static) \
113
+ static = event.static if event.respond_to?(:static)
114
+ @methods << Trace::RubyMethod.new(package, defined_class, method, static) \
86
115
  if package && defined_class && method && (event.event == :call)
87
116
  end
88
117
 
118
+ # +method+ should be duck-typed to respond to the following:
119
+ # * package
120
+ # * defined_class
121
+ # * name
122
+ # * static
123
+ # * comment
124
+ # * labels
125
+ # * source_location
126
+ def record_method(method)
127
+ @methods << method
128
+ end
129
+
89
130
  # Gets the last package which was observed on the current thread.
90
131
  def last_package_for_current_thread
91
132
  @last_package_for_thread[Thread.current.object_id]
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.1'
6
+ VERSION = '0.46.0'
7
7
 
8
- APPMAP_FORMAT_VERSION = '1.4'
8
+ APPMAP_FORMAT_VERSION = '1.5.0'
9
9
  end
data/package-lock.json CHANGED
@@ -551,9 +551,9 @@
551
551
  }
552
552
  },
553
553
  "lodash": {
554
- "version": "4.17.19",
555
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
556
- "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
554
+ "version": "4.17.21",
555
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
556
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
557
557
  },
558
558
  "longest": {
559
559
  "version": "1.0.1",
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,89 @@ 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
143
+ end
144
+ end
145
+
146
+ describe 'a UI route' do
147
+ describe 'rendering a page using a template file' do
148
+ let(:appmap_json_file) do
149
+ 'UsersController_GET_users_lists_the_users.appmap.json'
150
+ end
151
+
152
+ it 'records the template file' do
153
+ expect(events).to include hash_including(
154
+ 'event' => 'call',
155
+ 'defined_class' => 'app_views_users_index_html_haml',
156
+ 'method_id' => 'render',
157
+ 'path' => 'app/views/users/index.html.haml'
158
+ )
159
+
160
+ expect(appmap['classMap']).to include hash_including(
161
+ 'name' => 'app/views',
162
+ 'children' => include(hash_including(
163
+ 'name' => 'app_views_users_index_html_haml',
164
+ 'children' => include(hash_including(
165
+ 'name' => 'render',
166
+ 'type' => 'function',
167
+ 'location' => 'app/views/users/index.html.haml',
168
+ 'static' => true,
169
+ 'labels' => [ 'mvc.template' ]
170
+ ))
171
+ ))
172
+ )
173
+ expect(appmap['classMap']).to include hash_including(
174
+ 'name' => 'app/views',
175
+ 'children' => include(hash_including(
176
+ 'name' => 'app_views_layouts_application_html_haml',
177
+ 'children' => include(hash_including(
178
+ 'name' => 'render',
179
+ 'type' => 'function',
180
+ 'location' => 'app/views/layouts/application.html.haml',
181
+ 'static' => true,
182
+ 'labels' => [ 'mvc.template' ]
183
+ ))
184
+ ))
185
+ )
186
+ end
96
187
  end
97
188
 
98
- describe 'showing a user' do
99
- before(:all) { run_spec 'spec/controllers/users_controller_spec.rb:22' }
189
+ describe 'rendering a page using a text template' do
100
190
  let(:appmap_json_file) do
101
191
  'UsersController_GET_users_login_shows_the_user.appmap.json'
102
192
  end
@@ -107,30 +197,46 @@ describe 'AbstractControllerBase' do
107
197
  'http_server_request' => {
108
198
  'request_method' => 'GET',
109
199
  'path_info' => '/users/alice',
110
- 'normalized_path_info' => '/users/:id(.:format)'
200
+ 'normalized_path_info' => '/users/:id(.:format)',
201
+ 'headers' => {
202
+ 'Host' => 'test.host',
203
+ 'User-Agent' => 'Rails Testing'
204
+ }
111
205
  }
112
206
  )
113
207
  )
114
208
  end
115
- end
116
209
 
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' }
210
+ it 'ignores the text template' do
211
+ expect(events).to_not include hash_including(
212
+ 'event' => 'call',
213
+ 'method_id' => 'render',
214
+ 'render_template' => anything
215
+ )
216
+
217
+ expect(appmap['classMap']).to_not include hash_including(
218
+ 'name' => 'views',
219
+ 'children' => include(hash_including(
220
+ 'name' => 'ViewTemplate',
221
+ 'children' => include(hash_including(
222
+ 'name' => 'render',
223
+ 'type' => 'function',
224
+ 'location' => 'text template'
225
+ ))
226
+ ))
227
+ )
228
+ end
120
229
 
121
230
  it 'records and labels view rendering' do
122
231
  expect(events).to include hash_including(
123
232
  'event' => 'call',
124
233
  'thread_id' => Numeric,
125
- 'defined_class' => 'ActionView::Renderer',
126
- 'method_id' => 'render',
127
- 'path' => String,
128
- 'lineno' => Integer,
129
- 'static' => false
234
+ 'defined_class' => 'inline_template',
235
+ 'method_id' => 'render'
130
236
  )
131
-
237
+
132
238
  expect(appmap['classMap']).to include hash_including(
133
- 'name' => 'action_view',
239
+ 'name' => 'actionview',
134
240
  'children' => include(hash_including(
135
241
  'name' => 'ActionView',
136
242
  'children' => include(hash_including(
@@ -142,7 +248,7 @@ describe 'AbstractControllerBase' do
142
248
  ))
143
249
  ))
144
250
  )
145
- end
251
+ end
146
252
  end
147
253
  end
148
254
  end
@@ -4,18 +4,10 @@ require 'spec_helper'
4
4
 
5
5
  describe 'AppMap::ClassMap' do
6
6
  describe '.build_from_methods' do
7
- it 'includes source code if available' do
8
- map = AppMap.class_map([scoped_method(method(:test_method))])
7
+ it 'includes method comment' do
8
+ map = AppMap.class_map([ruby_method((method :test_method))])
9
9
  function = dig_map(map, 5)[0]
10
- expect(function[:source]).to include 'test method body'
11
- expect(function[:comment]).to include 'test method comment'
12
- end
13
-
14
- it 'can omit source code even if available' do
15
- map = AppMap.class_map([scoped_method((method :test_method))], include_source: false)
16
- function = dig_map(map, 5)[0]
17
- expect(function).to_not include(:source)
18
- expect(function).to_not include(:comment)
10
+ expect(function).to include(:comment)
19
11
  end
20
12
 
21
13
  # test method comment
@@ -23,8 +15,8 @@ describe 'AppMap::ClassMap' do
23
15
  'test method body'
24
16
  end
25
17
 
26
- def scoped_method(method)
27
- AppMap::Trace::ScopedMethod.new AppMap::Config::Package.new, method.receiver.class.name, method, false
18
+ def ruby_method(method)
19
+ AppMap::Trace::RubyMethod.new AppMap::Config::Package.new, method.receiver.class.name, method, false
28
20
  end
29
21
 
30
22
  def dig_map(map, depth)