appmap 0.42.1 → 0.46.0

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