appmap 0.41.2 → 0.45.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 (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
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
@@ -13,58 +14,9 @@ module AppMap
13
14
  AppMap.detect_metadata
14
15
  end
15
16
 
16
- module FeatureAnnotations
17
- def feature
18
- return nil unless annotations
19
-
20
- annotations[:feature]
21
- end
22
-
23
- def labels
24
- labels = metadata[:appmap]
25
- if labels.is_a?(Array)
26
- labels
27
- elsif labels.is_a?(String) || labels.is_a?(Symbol)
28
- [ labels ]
29
- else
30
- []
31
- end
32
- end
33
-
34
- def feature_group
35
- return nil unless annotations
36
-
37
- annotations[:feature_group]
38
- end
39
-
40
- def annotations
41
- metadata.tap do |md|
42
- description_args_hashes.each do |h|
43
- md.merge! h
44
- end
45
- end
46
- end
47
-
48
- protected
49
-
50
- def metadata
51
- return {} unless example_obj.respond_to?(:metadata)
52
-
53
- example_obj.metadata
54
- end
55
-
56
- def description_args_hashes
57
- return [] unless example_obj.respond_to?(:metadata)
58
-
59
- (example_obj.metadata[:description_args] || []).select { |arg| arg.is_a?(Hash) }
60
- end
61
- end
62
-
63
17
  # ScopeExample and ScopeExampleGroup is a way to handle the weird way that RSpec
64
18
  # stores the nested example names.
65
19
  ScopeExample = Struct.new(:example) do
66
- include FeatureAnnotations
67
-
68
20
  alias_method :example_obj, :example
69
21
 
70
22
  def description?
@@ -83,8 +35,6 @@ module AppMap
83
35
  # As you can see here, the way that RSpec stores the example description and
84
36
  # represents the example group hierarchy is pretty weird.
85
37
  ScopeExampleGroup = Struct.new(:example_group) do
86
- include FeatureAnnotations
87
-
88
38
  alias_method :example_obj, :example_group
89
39
 
90
40
  def description_args
@@ -133,13 +83,20 @@ module AppMap
133
83
  page.driver.options[:http_client].instance_variable_get('@server_url').port
134
84
  end
135
85
 
136
- warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
86
+ warn "Starting recording of example #{example}@#{source_location}" if AppMap::RSpec::LOG
137
87
  @trace = AppMap.tracing.trace
138
88
  @webdriver_port = webdriver_port.()
139
89
  end
140
90
 
141
- def finish
91
+ def source_location
92
+ result = example.location_rerun_argument.split(':')[0]
93
+ result = result[2..-1] if result.index('./') == 0
94
+ result
95
+ end
96
+
97
+ def finish(exception)
142
98
  warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
99
+ warn "Exception: #{exception}" if exception && AppMap::RSpec::LOG
143
100
 
144
101
  events = []
145
102
  AppMap.tracing.delete @trace
@@ -148,22 +105,16 @@ module AppMap
148
105
 
149
106
  AppMap::RSpec.add_event_methods @trace.event_methods
150
107
 
151
- class_map = AppMap.class_map(@trace.event_methods, include_source: false)
108
+ class_map = AppMap.class_map(@trace.event_methods)
152
109
 
153
110
  description = []
154
111
  scope = ScopeExample.new(example)
155
- feature_group = feature = nil
156
112
 
157
- labels = []
158
113
  while scope
159
- labels += scope.labels
160
114
  description << scope.description
161
- feature ||= scope.feature
162
- feature_group ||= scope.feature_group
163
115
  scope = scope.parent
164
116
  end
165
117
 
166
- labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
167
118
  description.reject!(&:nil?).reject!(&:blank?)
168
119
  default_description = description.last
169
120
  description.reverse!
@@ -177,24 +128,12 @@ module AppMap
177
128
 
178
129
  full_description = normalize.call(description.join(' '))
179
130
 
180
- compute_feature_name = lambda do
181
- return 'unknown' if description.empty?
182
-
183
- feature_description = description.dup
184
- num_tokens = [2, feature_description.length - 1].min
185
- feature_description[0...num_tokens].map(&:strip).join(' ')
186
- end
187
-
188
- feature_group ||= normalize.call(default_description).underscore.gsub('/', '_').humanize
189
- feature_name = feature || compute_feature_name.call if feature_group
190
- feature_name = normalize.call(feature_name) if feature_name
191
-
192
- AppMap::RSpec.save full_description,
193
- class_map,
194
- events: events,
195
- feature_name: feature_name,
196
- feature_group_name: feature_group,
197
- labels: labels.blank? ? nil : labels
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,
136
+ events: events
198
137
  end
199
138
  end
200
139
 
@@ -212,11 +151,11 @@ module AppMap
212
151
  @recordings_by_example[example.object_id] = Recording.new(example)
213
152
  end
214
153
 
215
- def end_spec(example)
154
+ def end_spec(example, exception:)
216
155
  recording = @recordings_by_example.delete(example.object_id)
217
156
  return warn "No recording found for #{example}" unless recording
218
157
 
219
- recording.finish
158
+ recording.finish exception
220
159
  end
221
160
 
222
161
  def config
@@ -227,13 +166,11 @@ module AppMap
227
166
  @event_methods += event_methods
228
167
  end
229
168
 
230
- def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
169
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
231
170
  metadata = AppMap::RSpec.metadata.tap do |m|
232
- m[:name] = example_name
171
+ m[:name] = name
172
+ m[:source_location] = source_location
233
173
  m[:app] = AppMap.configuration.name
234
- m[:feature] = feature_name if feature_name
235
- m[:feature_group] = feature_group_name if feature_group_name
236
- m[:labels] = labels if labels
237
174
  m[:frameworks] ||= []
238
175
  m[:frameworks] << {
239
176
  name: 'rspec',
@@ -242,6 +179,13 @@ module AppMap
242
179
  m[:recorder] = {
243
180
  name: 'rspec'
244
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
245
189
  end
246
190
 
247
191
  appmap = {
@@ -250,14 +194,9 @@ module AppMap
250
194
  classMap: class_map,
251
195
  events: events
252
196
  }.compact
253
- fname = AppMap::Util.scenario_filename(example_name)
254
-
255
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
256
- end
197
+ fname = AppMap::Util.scenario_filename(name)
257
198
 
258
- def print_inventory
259
- class_map = AppMap.class_map(@event_methods)
260
- save 'Inventory', class_map, labels: %w[inventory]
199
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
261
200
  end
262
201
 
263
202
  def enabled?
@@ -266,9 +205,6 @@ module AppMap
266
205
 
267
206
  def run
268
207
  init
269
- at_exit do
270
- print_inventory
271
- end
272
208
  end
273
209
  end
274
210
  end
@@ -290,7 +226,7 @@ if AppMap::RSpec.enabled?
290
226
  begin
291
227
  instance_exec(&fn)
292
228
  ensure
293
- AppMap::RSpec.end_spec example
229
+ AppMap::RSpec.end_spec example, exception: $!
294
230
  end
295
231
  end
296
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,35 @@ 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
+ # Atomically writes AppMap data to +filename+.
97
+ def write_appmap(filename, appmap)
98
+ require 'fileutils'
99
+ require 'tmpdir'
100
+
101
+ # This is what Ruby Tempfile does; but we don't want the file to be unlinked.
102
+ mode = File::RDWR | File::CREAT | File::EXCL
103
+ ::Dir::Tmpname.create([ 'appmap_', '.json' ]) do |tmpname|
104
+ tempfile = File.open(tmpname, mode)
105
+ tempfile.write(appmap)
106
+ tempfile.close
107
+ # Atomically move the tempfile into place.
108
+ FileUtils.mv tempfile.path, filename
109
+ end
110
+ end
74
111
  end
75
112
  end
76
113
  end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.41.2'
6
+ VERSION = '0.45.0'
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,7 +175,7 @@ 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
180
  'name' => 'action_view',
134
181
  '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